From 8ec8da0491ad89604700b3e29a227966f6d84ba1 Mon Sep 17 00:00:00 2001 From: Perl Tidy Date: Wed, 5 Dec 2018 15:38:52 -0500 Subject: no bug - reformat all the code using the new perltidy rules --- .circleci/deploy.pl | 51 +- Bugzilla.pm | 1054 +-- Bugzilla/Attachment.pm | 838 +-- Bugzilla/Attachment/Archive.pm | 154 +- Bugzilla/Attachment/Database.pm | 52 +- Bugzilla/Attachment/FileSystem.pm | 52 +- Bugzilla/Attachment/PatchReader.pm | 536 +- Bugzilla/Attachment/S3.pm | 56 +- Bugzilla/Auth.pm | 451 +- Bugzilla/Auth/Login.pm | 18 +- Bugzilla/Auth/Login/APIKey.pm | 45 +- Bugzilla/Auth/Login/CGI.pm | 118 +- Bugzilla/Auth/Login/Cookie.pm | 221 +- Bugzilla/Auth/Login/Env.pm | 29 +- Bugzilla/Auth/Login/Stack.pm | 126 +- Bugzilla/Auth/Persist/Cookie.pm | 284 +- Bugzilla/Auth/Verify.pm | 202 +- Bugzilla/Auth/Verify/DB.pm | 152 +- Bugzilla/Auth/Verify/LDAP.pm | 253 +- Bugzilla/Auth/Verify/RADIUS.pm | 58 +- Bugzilla/Auth/Verify/Stack.pm | 107 +- Bugzilla/Bloomfilter.pm | 56 +- Bugzilla/Bug.pm | 7665 ++++++++++---------- Bugzilla/BugMail.pm | 1088 +-- Bugzilla/BugUrl.pm | 228 +- Bugzilla/BugUrl/Aha.pm | 22 +- Bugzilla/BugUrl/Bugzilla.pm | 52 +- Bugzilla/BugUrl/Bugzilla/Local.pm | 121 +- Bugzilla/BugUrl/Chromium.pm | 41 +- Bugzilla/BugUrl/Debian.pm | 48 +- Bugzilla/BugUrl/Edge.pm | 16 +- Bugzilla/BugUrl/GitHub.pm | 26 +- Bugzilla/BugUrl/Google.pm | 58 +- Bugzilla/BugUrl/JIRA.pm | 25 +- Bugzilla/BugUrl/Launchpad.pm | 44 +- Bugzilla/BugUrl/MantisBT.pm | 18 +- Bugzilla/BugUrl/MozSupport.pm | 20 +- Bugzilla/BugUrl/ServiceNow.pm | 12 +- Bugzilla/BugUrl/SourceForge.pm | 43 +- Bugzilla/BugUrl/Splat.pm | 12 +- Bugzilla/BugUrl/Trac.pm | 25 +- Bugzilla/BugUrl/WebCompat.pm | 24 +- Bugzilla/BugUserLastVisit.pm | 22 +- Bugzilla/CGI.pm | 1351 ++-- Bugzilla/CGI/ContentSecurityPolicy.pm | 154 +- Bugzilla/CPAN.pm | 153 +- Bugzilla/Chart.pm | 671 +- Bugzilla/Classification.pm | 153 +- Bugzilla/Comment.pm | 647 +- Bugzilla/Comment/TagWeights.pm | 10 +- Bugzilla/Component.pm | 599 +- Bugzilla/Config.pm | 488 +- Bugzilla/Config/Admin.pm | 107 +- Bugzilla/Config/Advanced.pm | 30 +- Bugzilla/Config/Attachment.pm | 106 +- Bugzilla/Config/Auth.pm | 400 +- Bugzilla/Config/BugChange.pm | 98 +- Bugzilla/Config/BugFields.pm | 138 +- Bugzilla/Config/Common.pm | 451 +- Bugzilla/Config/DependencyGraph.pm | 18 +- Bugzilla/Config/Elastic.pm | 22 +- Bugzilla/Config/General.pm | 77 +- Bugzilla/Config/GroupSecurity.pm | 129 +- Bugzilla/Config/LDAP.pm | 50 +- Bugzilla/Config/MTA.pm | 97 +- Bugzilla/Config/PatchViewer.pm | 38 +- Bugzilla/Config/Query.pm | 88 +- Bugzilla/Config/RADIUS.pm | 38 +- Bugzilla/Config/Reports.pm | 28 +- Bugzilla/Config/ShadowDB.pm | 49 +- Bugzilla/Config/UserMatch.pm | 44 +- Bugzilla/Constants.pm | 753 +- Bugzilla/DB.pm | 1954 ++--- Bugzilla/DB/Mysql.pm | 1515 ++-- Bugzilla/DB/Oracle.pm | 992 +-- Bugzilla/DB/Pg.pm | 436 +- Bugzilla/DB/Schema.pm | 4208 ++++++----- Bugzilla/DB/Schema/Mysql.pm | 603 +- Bugzilla/DB/Schema/Oracle.pm | 772 +- Bugzilla/DB/Schema/Pg.pm | 286 +- Bugzilla/DB/Schema/Sqlite.pm | 414 +- Bugzilla/DB/Sqlite.pm | 305 +- Bugzilla/DaemonControl.pm | 386 +- Bugzilla/DuoAPI.pm | 136 +- Bugzilla/DuoWeb.pm | 146 +- Bugzilla/Elastic.pm | 68 +- Bugzilla/Elastic/Indexer.pm | 277 +- Bugzilla/Elastic/Role/HasClient.pm | 12 +- Bugzilla/Elastic/Role/Object.pm | 39 +- Bugzilla/Elastic/Search.pm | 541 +- Bugzilla/Elastic/Search/FakeCGI.pm | 33 +- Bugzilla/Error.pm | 399 +- Bugzilla/Error/Base.pm | 6 +- Bugzilla/Error/Template.pm | 12 +- Bugzilla/Extension.pm | 227 +- Bugzilla/Field.pm | 1414 ++-- Bugzilla/Field/Choice.pm | 304 +- Bugzilla/Field/ChoiceInterface.pm | 227 +- Bugzilla/Flag.pm | 1531 ++-- Bugzilla/FlagType.pm | 775 +- Bugzilla/Group.pm | 727 +- Bugzilla/Hook.pm | 28 +- Bugzilla/Install.pm | 795 +- Bugzilla/Install/DB.pm | 6695 +++++++++-------- Bugzilla/Install/Filesystem.pm | 1279 ++-- Bugzilla/Install/Localconfig.pm | 577 +- Bugzilla/Install/Requirements.pm | 382 +- Bugzilla/Install/Util.pm | 907 +-- Bugzilla/Job/BugMail.pm | 4 +- Bugzilla/Job/Mailer.pm | 40 +- Bugzilla/JobQueue.pm | 163 +- Bugzilla/JobQueue/Runner.pm | 285 +- Bugzilla/JobQueue/Worker.pm | 20 +- Bugzilla/Keyword.pm | 126 +- Bugzilla/Logging.pm | 166 +- Bugzilla/MFA.pm | 188 +- Bugzilla/MFA/Dummy.pm | 10 +- Bugzilla/MFA/Duo.pm | 71 +- Bugzilla/MFA/TOTP.pm | 75 +- Bugzilla/Mailer.pm | 450 +- Bugzilla/Markdown/GFM.pm | 80 +- Bugzilla/Markdown/GFM/Node.pm | 34 +- Bugzilla/Markdown/GFM/Parser.pm | 129 +- Bugzilla/Markdown/GFM/SyntaxExtension.pm | 36 +- Bugzilla/Markdown/GFM/SyntaxExtensionList.pm | 20 +- Bugzilla/Memcached.pm | 495 +- Bugzilla/Migrate.pm | 1201 +-- Bugzilla/Migrate/Gnats.pm | 1024 +-- Bugzilla/Milestone.pm | 297 +- Bugzilla/Object.pm | 1374 ++-- Bugzilla/PSGI.pm | 38 +- Bugzilla/PatchReader/AddCVSContext.pm | 81 +- Bugzilla/PatchReader/Base.pm | 3 +- Bugzilla/PatchReader/CVSClient.pm | 45 +- Bugzilla/PatchReader/DiffPrinter/raw.pm | 6 +- Bugzilla/PatchReader/DiffPrinter/template.pm | 72 +- Bugzilla/PatchReader/FilterPatch.pm | 2 +- Bugzilla/PatchReader/FixPatchRoot.pm | 50 +- Bugzilla/PatchReader/NarrowPatch.pm | 7 +- Bugzilla/PatchReader/PatchInfoGrabber.pm | 13 +- Bugzilla/PatchReader/Raw.pm | 86 +- Bugzilla/Product.pm | 1234 ++-- Bugzilla/Quantum.pm | 7 +- Bugzilla/Quantum/CGI.pm | 27 +- Bugzilla/Quantum/Home.pm | 3 +- Bugzilla/Quantum/Plugin/BasicAuth.pm | 40 +- Bugzilla/Quantum/Plugin/Glue.pm | 3 +- Bugzilla/Quantum/Plugin/Hostage.pm | 107 +- Bugzilla/Quantum/SES.pm | 364 +- Bugzilla/RNG.pm | 245 +- Bugzilla/Report/SecurityRisk.pm | 409 +- Bugzilla/S3.pm | 479 +- Bugzilla/S3/Bucket.pm | 299 +- Bugzilla/Search.pm | 5289 +++++++------- Bugzilla/Search/Clause.pm | 170 +- Bugzilla/Search/ClauseGroup.pm | 121 +- Bugzilla/Search/Condition.pm | 70 +- Bugzilla/Search/Quicksearch.pm | 1148 +-- Bugzilla/Search/Recent.pm | 116 +- Bugzilla/Search/Saved.pm | 369 +- Bugzilla/Send/Sendmail.pm | 148 +- Bugzilla/Series.pm | 391 +- Bugzilla/Status.pm | 254 +- Bugzilla/Template.pm | 1874 ++--- Bugzilla/Template/Context.pm | 118 +- Bugzilla/Template/Plugin/Bugzilla.pm | 14 +- Bugzilla/Template/Plugin/Hook.pm | 109 +- Bugzilla/Template/Plugin/User.pm | 14 +- Bugzilla/Template/PreloadProvider.pm | 144 +- Bugzilla/Test/MockDB.pm | 179 +- Bugzilla/Test/MockLocalconfig.pm | 6 +- Bugzilla/Test/MockParams.pm | 82 +- Bugzilla/Test/Util.pm | 65 +- Bugzilla/Token.pm | 764 +- Bugzilla/Types.pm | 17 +- Bugzilla/Update.pm | 274 +- Bugzilla/User.pm | 3948 +++++----- Bugzilla/User/APIKey.pm | 94 +- Bugzilla/User/Session.pm | 36 +- Bugzilla/User/Setting.pm | 437 +- Bugzilla/User/Setting/Lang.pm | 6 +- Bugzilla/User/Setting/Skin.pm | 43 +- Bugzilla/User/Setting/Timezone.pm | 22 +- Bugzilla/UserAgent.pm | 355 +- Bugzilla/Util.pm | 1434 ++-- Bugzilla/Version.pm | 278 +- Bugzilla/WebService.pm | 7 +- Bugzilla/WebService/Bug.pm | 2661 +++---- Bugzilla/WebService/BugUserLastVisit.pm | 118 +- Bugzilla/WebService/Bugzilla.pm | 120 +- Bugzilla/WebService/Classification.pm | 89 +- Bugzilla/WebService/Constants.pm | 478 +- Bugzilla/WebService/Elastic.pm | 32 +- Bugzilla/WebService/Group.pm | 325 +- Bugzilla/WebService/JSON.pm | 2 +- Bugzilla/WebService/Product.pm | 428 +- Bugzilla/WebService/Server.pm | 147 +- Bugzilla/WebService/Server/JSONRPC.pm | 714 +- Bugzilla/WebService/Server/REST.pm | 769 +- Bugzilla/WebService/Server/REST/Resources/Bug.pm | 331 +- .../Server/REST/Resources/BugUserLastVisit.pm | 46 +- .../WebService/Server/REST/Resources/Bugzilla.pm | 52 +- .../Server/REST/Resources/Classification.pm | 27 +- .../WebService/Server/REST/Resources/Elastic.pm | 13 +- Bugzilla/WebService/Server/REST/Resources/Group.pm | 55 +- .../WebService/Server/REST/Resources/Product.pm | 78 +- Bugzilla/WebService/Server/REST/Resources/User.pm | 107 +- Bugzilla/WebService/Server/XMLRPC.pm | 488 +- Bugzilla/WebService/User.pm | 690 +- Bugzilla/WebService/Util.pm | 474 +- Bugzilla/Whine.pm | 22 +- Bugzilla/Whine/Query.pm | 20 +- Bugzilla/Whine/Schedule.pm | 76 +- Log/Log4perl/Layout/Mozilla.pm | 100 +- admin.cgi | 12 +- attachment.cgi | 1172 +-- auth.cgi | 174 +- buglist.cgi | 1222 ++-- bugzilla.pl | 6 +- chart.cgi | 402 +- checksetup.pl | 232 +- clean-bug-user-last-visit.pl | 6 +- colchange.cgi | 204 +- collectstats.pl | 693 +- config.cgi | 120 +- contrib/clear-memcached.pl | 9 +- contrib/clear-templates.pl | 7 +- createaccount.cgi | 25 +- describecomponents.cgi | 68 +- describekeywords.cgi | 13 +- docs/lib/Pod/Simple/HTML/Bugzilla.pm | 48 +- docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm | 120 +- docs/makedocs.pl | 136 +- duplicates.cgi | 264 +- editclassifications.cgi | 211 +- editcomponents.cgi | 274 +- editfields.cgi | 232 +- editflagtypes.cgi | 839 ++- editgroups.cgi | 692 +- editkeywords.cgi | 152 +- editmilestones.cgi | 203 +- editparams.cgi | 231 +- editproducts.cgi | 497 +- editsettings.cgi | 49 +- editusers.cgi | 1333 ++-- editvalues.cgi | 129 +- editversions.cgi | 177 +- editwhines.cgi | 550 +- editworkflow.cgi | 193 +- email_in.pl | 582 +- enter_bug.cgi | 508 +- extensions/AntiSpam/Config.pm | 9 +- extensions/AntiSpam/Extension.pm | 476 +- extensions/AntiSpam/lib/Config.pm | 116 +- extensions/BMO/Config.pm | 21 +- extensions/BMO/Extension.pm | 4648 ++++++------ extensions/BMO/bin/bug_1022707.pl | 6 +- extensions/BMO/bin/bug_1093952.pl | 52 +- extensions/BMO/bin/bug_1141452.pl | 110 +- extensions/BMO/bin/migrate-github-pull-requests.pl | 67 +- extensions/BMO/lib/Constants.pm | 6 +- extensions/BMO/lib/Data.pm | 403 +- extensions/BMO/lib/FakeBug.pm | 28 +- extensions/BMO/lib/Reports/Groups.pm | 485 +- extensions/BMO/lib/Reports/Internship.pm | 135 +- extensions/BMO/lib/Reports/ProductSecurity.pm | 76 +- extensions/BMO/lib/Reports/Recruiting.pm | 126 +- extensions/BMO/lib/Reports/ReleaseTracking.pm | 779 +- extensions/BMO/lib/Reports/Triage.pm | 516 +- extensions/BMO/lib/Reports/UserActivity.pm | 382 +- extensions/BMO/lib/Util.pm | 111 +- extensions/BMO/lib/WebService.pm | 68 +- extensions/BMO/t/bounty_attachment.t | 93 +- extensions/Bitly/Config.pm | 2 +- extensions/Bitly/Extension.pm | 13 +- extensions/Bitly/lib/WebService.pm | 190 +- extensions/BugModal/Config.pm | 6 +- extensions/BugModal/Extension.pm | 551 +- extensions/BugModal/lib/ActivityStream.pm | 567 +- extensions/BugModal/lib/MonkeyPatches.pm | 21 +- extensions/BugModal/lib/Util.pm | 28 +- extensions/BugModal/lib/WebService.pm | 618 +- extensions/BugmailFilter/Config.pm | 2 +- extensions/BugmailFilter/Extension.pm | 807 +-- extensions/BugmailFilter/lib/Constants.pm | 120 +- extensions/BugmailFilter/lib/FakeField.pm | 50 +- extensions/BugmailFilter/lib/Filter.pm | 217 +- extensions/BzAPI/Extension.pm | 339 +- extensions/BzAPI/bin/rest.cgi | 16 +- extensions/BzAPI/lib/Constants.pm | 230 +- extensions/BzAPI/lib/Resources/Bug.pm | 1385 ++-- extensions/BzAPI/lib/Resources/Bugzilla.pm | 208 +- extensions/BzAPI/lib/Resources/User.pm | 75 +- extensions/BzAPI/lib/Util.pm | 652 +- extensions/ComponentWatching/Extension.pm | 891 ++- extensions/ComponentWatching/lib/WebService.pm | 128 +- extensions/ContributorEngagement/Config.pm | 6 +- extensions/ContributorEngagement/Extension.pm | 147 +- extensions/ContributorEngagement/lib/Constants.pm | 15 +- extensions/EditComments/Config.pm | 2 +- extensions/EditComments/Extension.pm | 355 +- extensions/EditComments/lib/WebService.pm | 89 +- extensions/EditTable/Config.pm | 2 +- extensions/EditTable/Extension.pm | 227 +- extensions/Ember/Extension.pm | 6 +- extensions/Ember/lib/FakeBug.pm | 92 +- extensions/Ember/lib/WebService.pm | 1229 ++-- extensions/Example/Config.pm | 21 +- extensions/Example/Extension.pm | 1344 ++-- 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/FlagDefaultRequestee/Extension.pm | 198 +- extensions/FlagDefaultRequestee/lib/Constants.pm | 8 +- extensions/FlagTypeComment/Extension.pm | 264 +- extensions/FlagTypeComment/lib/Constants.pm | 32 +- extensions/GitHubAuth/Extension.pm | 88 +- extensions/GitHubAuth/lib/Client.pm | 134 +- extensions/GitHubAuth/lib/Client/Error.pm | 42 +- extensions/GitHubAuth/lib/Config.pm | 24 +- extensions/GitHubAuth/lib/Login.pm | 329 +- extensions/GitHubAuth/lib/Verify.pm | 6 +- extensions/GoogleAnalytics/Extension.pm | 6 +- extensions/GoogleAnalytics/lib/Config.pm | 41 +- extensions/Gravatar/Config.pm | 2 +- extensions/Gravatar/Extension.pm | 52 +- extensions/Gravatar/lib/Data.pm | 6 +- extensions/GuidedBugEntry/Config.pm | 6 +- extensions/GuidedBugEntry/Extension.pm | 182 +- extensions/InlineHistory/Extension.pm | 384 +- extensions/LastResolved/Config.pm | 6 +- extensions/LastResolved/Extension.pm | 130 +- extensions/LimitedEmail/Config.pm | 14 +- extensions/LimitedEmail/Extension.pm | 62 +- extensions/MozProjectReview/Config.pm | 6 +- extensions/MozProjectReview/Extension.pm | 254 +- extensions/MyDashboard/Extension.pm | 317 +- extensions/MyDashboard/lib/BugInterest.pm | 54 +- extensions/MyDashboard/lib/Queries.pm | 566 +- extensions/MyDashboard/lib/Util.pm | 32 +- extensions/MyDashboard/lib/WebService.pm | 220 +- extensions/Needinfo/Config.pm | 6 +- extensions/Needinfo/Extension.pm | 435 +- extensions/OldBugMove/Extension.pm | 248 +- extensions/OldBugMove/lib/Params.pm | 22 +- extensions/OpenGraph/Config.pm | 6 +- extensions/OrangeFactor/Extension.pm | 59 +- extensions/PhabBugz/Extension.pm | 81 +- extensions/PhabBugz/bin/phabbugz_feed.pl | 4 +- extensions/PhabBugz/lib/Config.pm | 76 +- extensions/PhabBugz/lib/Constants.pm | 14 +- extensions/PhabBugz/lib/Daemon.pm | 89 +- extensions/PhabBugz/lib/Feed.pm | 1206 ++- extensions/PhabBugz/lib/Policy.pm | 133 +- extensions/PhabBugz/lib/Project.pm | 355 +- extensions/PhabBugz/lib/Revision.pm | 544 +- extensions/PhabBugz/lib/Types.pm | 17 +- extensions/PhabBugz/lib/User.pm | 202 +- extensions/PhabBugz/lib/Util.pm | 251 +- extensions/PhabBugz/lib/WebService.pm | 187 +- extensions/PhabBugz/t/basic.t | 252 +- extensions/PhabBugz/t/feed-daemon-guts.t | 191 +- extensions/PhabBugz/t/review-flags.t | 246 +- extensions/ProdCompSearch/Config.pm | 2 +- extensions/ProdCompSearch/Extension.pm | 6 +- extensions/ProdCompSearch/lib/WebService.pm | 237 +- extensions/Profanivore/Config.pm | 12 +- extensions/Profanivore/Extension.pm | 247 +- extensions/Push/Config.pm | 39 +- extensions/Push/Extension.pm | 876 +-- extensions/Push/bin/bugzilla-pushd.pl | 4 +- extensions/Push/bin/nagios_push_checker.pl | 33 +- extensions/Push/lib/Admin.pm | 171 +- extensions/Push/lib/BacklogMessage.pm | 135 +- extensions/Push/lib/BacklogQueue.pm | 138 +- extensions/Push/lib/Backoff.pm | 82 +- extensions/Push/lib/Config.pm | 298 +- extensions/Push/lib/Connector.disabled/AMQP.pm | 344 +- .../Push/lib/Connector.disabled/ServiceNow.pm | 690 +- extensions/Push/lib/Connector/Base.pm | 99 +- extensions/Push/lib/Connector/File.pm | 61 +- extensions/Push/lib/Connector/Phabricator.pm | 153 +- extensions/Push/lib/Connector/Spark.pm | 228 +- extensions/Push/lib/Connectors.pm | 129 +- extensions/Push/lib/Constants.pm | 28 +- extensions/Push/lib/Daemon.pm | 87 +- extensions/Push/lib/Log.pm | 32 +- extensions/Push/lib/LogEntry.pm | 42 +- extensions/Push/lib/Logger.pm | 50 +- extensions/Push/lib/Message.pm | 72 +- extensions/Push/lib/Option.pm | 32 +- extensions/Push/lib/Push.pm | 434 +- extensions/Push/lib/Queue.pm | 71 +- extensions/Push/lib/Serialise.pm | 427 +- extensions/Push/lib/Util.pm | 165 +- .../Push/template/en/default/setup/strings.txt.pl | 4 +- extensions/REMO/Config.pm | 6 +- extensions/REMO/Extension.pm | 547 +- extensions/RequestNagger/Config.pm | 6 +- extensions/RequestNagger/Extension.pm | 552 +- extensions/RequestNagger/bin/send-request-nags.pl | 481 +- extensions/RequestNagger/lib/Bug.pm | 34 +- extensions/RequestNagger/lib/Constants.pm | 69 +- extensions/RequestNagger/lib/Settings.pm | 75 +- extensions/RestrictComments/Config.pm | 2 +- extensions/RestrictComments/Extension.pm | 98 +- extensions/RestrictComments/lib/Config.pm | 40 +- extensions/Review/Config.pm | 2 +- extensions/Review/Extension.pm | 1651 ++--- .../Review/bin/migrate_mentor_from_whiteboard.pl | 268 +- extensions/Review/bin/review_requests_rebuild.pl | 6 +- extensions/Review/lib/FlagStateActivity.pm | 110 +- extensions/Review/lib/Util.pm | 71 +- extensions/Review/lib/WebService.pm | 468 +- extensions/SecureMail/Config.pm | 32 +- extensions/SecureMail/Extension.pm | 1046 +-- extensions/SecureMail/lib/TCT.pm | 112 +- extensions/ShadowBugs/Config.pm | 2 +- extensions/ShadowBugs/Extension.pm | 114 +- extensions/SiteMapIndex/Config.pm | 10 +- extensions/SiteMapIndex/Extension.pm | 116 +- extensions/SiteMapIndex/lib/Constants.pm | 8 +- extensions/SiteMapIndex/lib/Util.pm | 207 +- extensions/Splinter/Extension.pm | 217 +- extensions/Splinter/lib/Config.pm | 14 +- extensions/Splinter/lib/Util.pm | 208 +- extensions/TagNewUsers/Config.pm | 6 +- extensions/TagNewUsers/Extension.pm | 346 +- extensions/TrackingFlags/Config.pm | 12 +- extensions/TrackingFlags/Extension.pm | 1224 ++-- extensions/TrackingFlags/bin/bug_825946.pl | 25 +- extensions/TrackingFlags/bin/bulk_flag_clear.pl | 80 +- .../TrackingFlags/bin/migrate_tracking_flags.pl | 407 +- extensions/TrackingFlags/lib/Admin.pm | 721 +- extensions/TrackingFlags/lib/Constants.pm | 44 +- extensions/TrackingFlags/lib/Flag.pm | 632 +- extensions/TrackingFlags/lib/Flag/Bug.pm | 168 +- extensions/TrackingFlags/lib/Flag/Value.pm | 129 +- extensions/TrackingFlags/lib/Flag/Visibility.pm | 195 +- extensions/TypeSniffer/Config.pm | 14 +- extensions/TypeSniffer/Extension.pm | 103 +- extensions/UserProfile/Config.pm | 6 +- extensions/UserProfile/Extension.pm | 796 +- extensions/UserProfile/bin/migrate.pl | 14 +- extensions/UserProfile/bin/update.pl | 62 +- extensions/UserProfile/lib/Util.pm | 365 +- extensions/UserStory/Config.pm | 9 +- extensions/UserStory/Extension.pm | 110 +- extensions/UserStory/lib/Constants.pm | 2 +- extensions/Voting/Config.pm | 6 +- extensions/Voting/Extension.pm | 1367 ++-- extensions/ZPushNotify/Config.pm | 2 +- extensions/ZPushNotify/Extension.pm | 183 +- extensions/create.pl | 38 +- gen-cpanfile.pl | 68 +- github.cgi | 183 +- heartbeat.cgi | 47 +- importxml.pl | 2028 +++--- index.cgi | 86 +- jobqueue-worker.pl | 21 +- jobqueue.pl | 8 +- jsonrpc.cgi | 7 +- long_list.cgi | 5 +- migrate.pl | 16 +- new_bug.cgi | 161 +- page.cgi | 79 +- post_bug.cgi | 242 +- process_bug.cgi | 562 +- qa/config/generate_test_data.pl | 1126 +-- qa/extensions/QA/Config.pm | 6 +- qa/extensions/QA/Extension.pm | 74 +- qa/extensions/QA/lib/Util.pm | 10 +- qa/t/archived/test_email_preferences.t | 367 +- qa/t/lib/QA/REST.pm | 61 +- qa/t/lib/QA/RPC.pm | 454 +- qa/t/lib/QA/RPC/JSONRPC.pm | 192 +- qa/t/lib/QA/RPC/XMLRPC.pm | 2 +- qa/t/lib/QA/Tests.pm | 114 +- qa/t/lib/QA/Util.pm | 593 +- qa/t/rest_bugzilla.t | 31 +- qa/t/rest_classification.t | 76 +- qa/t/selenium_server_start.t | 76 +- qa/t/selenium_server_stop.t | 2 +- qa/t/test_bmo_autolinkification.t | 12 +- qa/t/test_bmo_enter_new_bug.t | 384 +- qa/t/test_bmo_retire_values.t | 141 +- qa/t/test_bug_edit.t | 354 +- qa/t/test_choose_priority.t | 9 +- qa/t/test_classifications.t | 48 +- qa/t/test_config.t | 14 +- qa/t/test_create_user_accounts.t | 119 +- qa/t/test_custom_fields.t | 149 +- qa/t/test_custom_fields_admin.t | 45 +- qa/t/test_default_groups.t | 62 +- qa/t/test_dependencies.t | 13 +- qa/t/test_edit_products_properties.t | 254 +- qa/t/test_enter_new_bug.t | 22 +- qa/t/test_flags.t | 208 +- qa/t/test_flags2.t | 110 +- qa/t/test_groups.t | 123 +- qa/t/test_keywords.t | 35 +- qa/t/test_login.t | 3 +- qa/t/test_milestones.t | 117 +- qa/t/test_private_attachments.t | 98 +- qa/t/test_qa_contact.t | 81 +- qa/t/test_require_login.t | 64 +- qa/t/test_sanity_check.t | 42 +- qa/t/test_saved_searches.t | 42 +- qa/t/test_search.t | 14 +- qa/t/test_security.t | 61 +- qa/t/test_shared_searches.t | 81 +- qa/t/test_show_all_products.t | 17 +- qa/t/test_status_whiteboard.t | 10 +- qa/t/test_sudo_sessions.t | 68 +- qa/t/test_target_milestones.t | 34 +- qa/t/test_time_summary.t | 16 +- qa/t/test_user_groups.t | 155 +- qa/t/test_user_matching.t | 86 +- qa/t/test_user_preferences.t | 90 +- qa/t/test_user_privs.t | 37 +- qa/t/test_votes.t | 99 +- qa/t/webservice_bug_add_attachment.t | 411 +- qa/t/webservice_bug_add_comment.t | 301 +- qa/t/webservice_bug_attachments.t | 213 +- qa/t/webservice_bug_comments.t | 228 +- qa/t/webservice_bug_create.t | 379 +- qa/t/webservice_bug_fields.t | 341 +- qa/t/webservice_bug_get.t | 187 +- qa/t/webservice_bug_get_bugs.t | 190 +- qa/t/webservice_bug_history.t | 13 +- qa/t/webservice_bug_legal_values.t | 120 +- qa/t/webservice_bug_search.t | 246 +- qa/t/webservice_bug_update.t | 1406 ++-- qa/t/webservice_bug_update_see_also.t | 71 +- qa/t/webservice_bugzilla.t | 43 +- qa/t/webservice_group_create.t | 157 +- qa/t/webservice_jsonp.t | 25 +- qa/t/webservice_product.t | 138 +- qa/t/webservice_product_create.t | 321 +- qa/t/webservice_product_get.t | 138 +- qa/t/webservice_user_create.t | 170 +- qa/t/webservice_user_get.t | 404 +- qa/t/webservice_user_login_logout.t | 179 +- qa/t/webservice_user_offer_account_by_email.t | 65 +- query.cgi | 249 +- quips.cgi | 187 +- relogin.cgi | 347 +- report.cgi | 346 +- reports.cgi | 321 +- request.cgi | 443 +- reset_password.cgi | 133 +- rest.cgi | 9 +- robots.cgi | 6 +- runtests.pl | 12 +- sanitycheck.cgi | 1074 +-- sanitycheck.pl | 34 +- scripts/1298978.pl | 50 +- scripts/addcustomfield.pl | 44 +- scripts/attachment-data.pl | 127 +- scripts/block-ip.pl | 7 +- scripts/build-bmo-push-data.pl | 195 +- scripts/bulk_index.pl | 84 +- scripts/cereal.pl | 39 +- scripts/clear-memcached.pl | 11 +- scripts/clear-templates.pl | 8 +- scripts/convert_datetime.pl | 125 +- scripts/cpanfile_fixed_versions.pl | 174 +- scripts/create_app_id.pl | 2 +- scripts/delete_comments_csv.pl | 33 +- scripts/eject-users-from-groups.pl | 26 +- scripts/entrypoint.pl | 361 +- scripts/fix-attachment-sizes.pl | 15 +- scripts/fix_all_open_status_queries.pl | 121 +- scripts/fixgroupqueries.pl | 107 +- scripts/fixperms.pl | 1 - scripts/fixqueries.pl | 117 +- scripts/generate_bmo_data.pl | 1213 ++-- scripts/generate_conduit_data.pl | 250 +- scripts/group-set-members.pl | 74 +- scripts/issue-api-key.pl | 20 +- scripts/merge-users.pl | 211 +- scripts/migrate-attachments.pl | 165 +- scripts/migrate-cab-review.pl | 71 +- scripts/migrate_whiteboard_keyword.pl | 123 +- scripts/move_flag_types.pl | 103 +- scripts/move_os.pl | 42 +- scripts/movebugs.pl | 158 +- scripts/movecomponent.pl | 124 +- scripts/nagios_blocker_checker.pl | 287 +- scripts/nagios_push_checker.pl | 54 +- scripts/nuke-bugs.pl | 8 +- scripts/reassign_open_bugs.pl | 40 +- scripts/remove-non-public-data.pl | 364 +- scripts/remove_idle_group_members.pl | 141 +- scripts/reset_default_user.pl | 145 +- scripts/resolve_bugs.pl | 67 +- scripts/rewrite2mojo.pl | 91 +- scripts/sanitizeme.pl | 299 +- scripts/search.pl | 10 +- scripts/secbugsreport.pl | 69 +- scripts/security_remove.pl | 117 +- scripts/sendmail.pl | 23 +- scripts/sendunsentbugmail.pl | 39 +- scripts/suggest-user.pl | 8 +- scripts/syncflags.pl | 38 +- scripts/syncmsandversions.pl | 32 +- scripts/triage_owners_csv.pl | 66 +- scripts/undo.pl | 171 +- scripts/update-bug-groups.pl | 62 +- scripts/update-crash-signatures.pl | 264 +- scripts/update_localconfig.pl | 32 +- scripts/update_params.pl | 13 +- scripts/user-prefs.pl | 60 +- search_plugin.cgi | 4 +- show_activity.cgi | 10 +- show_bug.cgi | 114 +- showattachment.cgi | 6 +- showdependencygraph.cgi | 405 +- showdependencytree.cgi | 118 +- summarize_time.cgi | 497 +- t/001compile.t | 141 +- t/002goodperl.t | 288 +- t/004template.t | 161 +- t/005whitespace.t | 66 +- t/006spellcheck.t | 104 +- t/007util.t | 66 +- t/008filter.t | 288 +- t/009bugwords.t | 74 +- t/010dependencies.t | 76 +- t/011pod.t | 40 +- t/012throwables.t | 281 +- t/901-secure-mail-loop.t | 20 +- t/902-crypt-openpgp-random.t | 18 +- t/903-passwdqc-conf.t | 30 +- t/Support/Files.pm | 60 +- t/Support/Templates.pm | 91 +- t/bmo/comments.t | 75 +- t/bmo/passwords.t | 407 +- t/critic.t | 2 +- t/css.t | 27 +- t/daemon-control-catch-signal.t | 88 +- t/docker.t | 24 +- t/extract-nicks.t | 29 +- t/hash-sig.t | 9 +- t/json-boxes.t | 2 +- t/markdown.t | 36 +- t/mock-db.t | 24 +- t/mock-params.t | 6 +- t/mojo-example.t | 29 +- t/security-risk.t | 231 +- t/sqlite-memory.t | 95 +- template/en/default/filterexceptions.pl | 649 +- template/en/default/setup/strings.txt.pl | 192 +- testserver.pl | 361 +- token.cgi | 575 +- userprefs.cgi | 1611 ++-- vagrant_support/re.pl | 6 +- view_job_queue.cgi | 106 +- votes.cgi | 13 +- whine.pl | 918 +-- whineatnews.pl | 67 +- xml.cgi | 2 +- xt/lib/Bugzilla/Test/Search.pm | 1492 ++-- xt/lib/Bugzilla/Test/Search/AndTest.pm | 30 +- xt/lib/Bugzilla/Test/Search/Constants.pm | 1857 ++--- xt/lib/Bugzilla/Test/Search/CustomTest.pm | 81 +- xt/lib/Bugzilla/Test/Search/FieldTest.pm | 806 +- xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm | 121 +- xt/lib/Bugzilla/Test/Search/InjectionTest.pm | 82 +- xt/lib/Bugzilla/Test/Search/NotTest.pm | 37 +- xt/lib/Bugzilla/Test/Search/OperatorTest.pm | 105 +- xt/lib/Bugzilla/Test/Search/OrTest.pm | 139 +- xt/search.t | 3 +- 675 files changed, 94316 insertions(+), 90614 deletions(-) diff --git a/.circleci/deploy.pl b/.circleci/deploy.pl index 391b9b660..5c7d648a6 100755 --- a/.circleci/deploy.pl +++ b/.circleci/deploy.pl @@ -3,50 +3,49 @@ use 5.10.1; use strict; use warnings; -my ($repo, $user, $pass) = check_env(qw(DOCKERHUB_REPO DOCKER_USER DOCKER_PASS)); +my ($repo, $user, $pass) + = check_env(qw(DOCKERHUB_REPO DOCKER_USER DOCKER_PASS)); run("docker", "login", "-u", $user, "-p", $pass); my @docker_tags = ($ENV{CIRCLE_SHA1}); if ($ENV{CIRCLE_TAG}) { - push @docker_tags, $ENV{CIRCLE_TAG}; + push @docker_tags, $ENV{CIRCLE_TAG}; } elsif ($ENV{CIRCLE_BRANCH}) { - if ($ENV{CIRCLE_BRANCH} eq 'master') { - push @docker_tags, 'latest'; - } - else { - push @docker_tags, $ENV{CIRCLE_BRANCH}; - } + if ($ENV{CIRCLE_BRANCH} eq 'master') { + push @docker_tags, 'latest'; + } + else { + push @docker_tags, $ENV{CIRCLE_BRANCH}; + } } say "Pushing tags..."; say " $_" for @docker_tags; foreach my $tag (@docker_tags) { - run("docker", "tag", "bmo", "$repo:$tag"); - run("docker", "push", "$repo:$tag"); + run("docker", "tag", "bmo", "$repo:$tag"); + run("docker", "push", "$repo:$tag"); } sub run { - my (@cmd) = @_; - my $rv = system(@cmd); - exit 1 if $rv != 0; + my (@cmd) = @_; + my $rv = system(@cmd); + exit 1 if $rv != 0; } sub check_env { - my (@missing, @found); - foreach my $name (@_) { - push @missing, $name unless $ENV{$name}; - push @found, $ENV{$name}; - } - - if (@missing) { - warn "Missing environmental variables: ", join(", ", @missing), "\n"; - exit; - } - return @found; + my (@missing, @found); + foreach my $name (@_) { + push @missing, $name unless $ENV{$name}; + push @found, $ENV{$name}; + } + + if (@missing) { + warn "Missing environmental variables: ", join(", ", @missing), "\n"; + exit; + } + return @found; } - - diff --git a/Bugzilla.pm b/Bugzilla.pm index 27ed0876c..85a2e4b48 100644 --- a/Bugzilla.pm +++ b/Bugzilla.pm @@ -64,33 +64,34 @@ use constant request_cache => Bugzilla::Install::Util::_cache(); # Note that this is a raw subroutine, not a method, so $class isn't available. sub init_page { - # This is probably not needed, but bugs resulting from a dirty - # request cache are very annoying (see bug 1347335) - # and this is not an expensive operation. - clear_request_cache(); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - init_console(); - } - elsif (Bugzilla->params->{'utf8'}) { - binmode STDOUT, ':utf8'; - } - if (i_am_cgi()) { - Bugzilla::Logging->fields->{remote_ip} = remote_ip(); - } + # This is probably not needed, but bugs resulting from a dirty + # request cache are very annoying (see bug 1347335) + # and this is not an expensive operation. + clear_request_cache(); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + init_console(); + } + elsif (Bugzilla->params->{'utf8'}) { + binmode STDOUT, ':utf8'; + } - # 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; + if (i_am_cgi()) { + Bugzilla::Logging->fields->{remote_ip} = remote_ip(); + } - my $script = basename($0); + # 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; - # Because of attachment_base, attachment.cgi handles this itself. - if ($script ne 'attachment.cgi') { - do_ssl_redirect_if_required(); - } + my $script = basename($0); + + # Because of attachment_base, attachment.cgi handles this itself. + if ($script ne 'attachment.cgi') { + do_ssl_redirect_if_required(); + } } ##################################################################### @@ -98,618 +99,647 @@ sub init_page { ##################################################################### my $preload_templates = 0; + sub preload_templates { - $preload_templates = 1; + $preload_templates = 1; - delete request_cache->{template}; - template(); - return 1; + delete request_cache->{template}; + template(); + return 1; } sub template { - request_cache->{template} //= Bugzilla::Template->create(preload => $preload_templates); - request_cache->{template}->{_is_main} = 1; + request_cache->{template} + //= Bugzilla::Template->create(preload => $preload_templates); + request_cache->{template}->{_is_main} = 1; - return request_cache->{template}; + return request_cache->{template}; } sub template_inner { - my (undef, $lang) = @_; - my $cache = request_cache; - my $current_lang = $cache->{template_current_lang}->[0]; - $lang ||= $current_lang || ''; - my %options = (language => $lang, preload => $preload_templates); - return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(%options); + my (undef, $lang) = @_; + my $cache = request_cache; + my $current_lang = $cache->{template_current_lang}->[0]; + $lang ||= $current_lang || ''; + my %options = (language => $lang, preload => $preload_templates); + return $cache->{"template_inner_$lang"} + ||= Bugzilla::Template->create(%options); } sub extensions { - # Guard against extensions querying the extension list during initialization - # (through this method or has_extension). - # The extension list is not fully populated at that point, - # so the results would not be meaningful. - state $recursive = 0; - die "Recursive attempt to load/query extensions" if $recursive; - $recursive = 1; - - my $cache = request_cache; - if (!$cache->{extensions}) { - my $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; + + # Guard against extensions querying the extension list during initialization + # (through this method or has_extension). + # The extension list is not fully populated at that point, + # so the results would not be meaningful. + state $recursive = 0; + die "Recursive attempt to load/query extensions" if $recursive; + $recursive = 1; + + my $cache = request_cache; + if (!$cache->{extensions}) { + my $extension_packages = Bugzilla::Extension->load_all(); + my @extensions; + foreach my $package (@$extension_packages) { + my $extension = $package->new(); + if ($extension->enabled) { + push(@extensions, $extension); + } } - $recursive = 0; - return $cache->{extensions}; + $cache->{extensions} = \@extensions; + } + $recursive = 0; + return $cache->{extensions}; } sub has_extension { - my ($class, $name) = @_; - my $cache = $class->request_cache; - if (!$cache->{extensions_hash}) { - my %extensions = map { $_->NAME => 1 } @{ Bugzilla->extensions }; - $cache->{extensions_hash} = \%extensions; - } - return exists $cache->{extensions_hash}{$name}; + my ($class, $name) = @_; + my $cache = $class->request_cache; + if (!$cache->{extensions_hash}) { + my %extensions = map { $_->NAME => 1 } @{Bugzilla->extensions}; + $cache->{extensions_hash} = \%extensions; + } + return exists $cache->{extensions_hash}{$name}; } sub cgi { - return request_cache->{cgi} ||= Bugzilla::CGI->new; + return request_cache->{cgi} ||= Bugzilla::CGI->new; } sub input_params { - my ($class, $params) = @_; - my $cache = 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 = 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}; - # 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}; + # 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 urlbase { - my ($class) = @_; + my ($class) = @_; - # Since this could be modified, we have to return a new one every time. - return URI->new($class->localconfig->{urlbase}); + # Since this could be modified, we have to return a new one every time. + return URI->new($class->localconfig->{urlbase}); } sub params { - return request_cache->{params} ||= Bugzilla::Config::read_param_file(); + return request_cache->{params} ||= Bugzilla::Config::read_param_file(); } sub get_param_with_override { - my ($class, $name) = @_; - return $class->localconfig->{param_override}{$name} // $class->params->{$name}; + my ($class, $name) = @_; + return $class->localconfig->{param_override}{$name} // $class->params->{$name}; } sub user { - return request_cache->{user} ||= new Bugzilla::User; + return request_cache->{user} ||= new Bugzilla::User; } sub set_user { - my (undef, $new_user, %option) = @_; - - if ($option{scope_guard}) { - my $old_user = request_cache->{user}; - request_cache->{user} = $new_user; - return Scope::Guard->new( - sub { - request_cache->{user} = $old_user; - } - ) - } - else { - request_cache->{user} = $new_user; - } + my (undef, $new_user, %option) = @_; + + if ($option{scope_guard}) { + my $old_user = request_cache->{user}; + request_cache->{user} = $new_user; + return Scope::Guard->new(sub { + request_cache->{user} = $old_user; + }); + } + else { + request_cache->{user} = $new_user; + } } sub sudoer { - return request_cache->{sudoer}; + return request_cache->{sudoer}; } sub sudo_request { - my (undef, $new_user, $new_sudoer) = @_; - request_cache->{user} = $new_user; - request_cache->{sudoer} = $new_sudoer; - # NOTE: If you want to log the start of an sudo session, do it here. + my (undef, $new_user, $new_sudoer) = @_; + request_cache->{user} = $new_user; + 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 request_cache->{page_requires_login}; + return request_cache->{page_requires_login}; } sub github_secret { - my ($class) = @_; - my $cache = request_cache; - my $cgi = $class->cgi; + my ($class) = @_; + my $cache = request_cache; + my $cgi = $class->cgi; - $cache->{github_secret} //= $cgi->cookie('github_secret') // generate_random_password(256); + $cache->{github_secret} //= $cgi->cookie('github_secret') + // generate_random_password(256); - return $cache->{github_secret}; + return $cache->{github_secret}; } sub passwdqc { - my ($class) = @_; - require Data::Password::passwdqc; + my ($class) = @_; + require Data::Password::passwdqc; - my $cache = request_cache; - my $params = $class->params; + my $cache = request_cache; + my $params = $class->params; - return $cache->{passwdqc} if $cache->{passwdqc}; + return $cache->{passwdqc} if $cache->{passwdqc}; - my @min = map { $_ eq 'undef' ? undef : $_ } - split( /\s*,\s*/, $params->{passwdqc_min} ); + my @min = map { $_ eq 'undef' ? undef : $_ } + split(/\s*,\s*/, $params->{passwdqc_min}); - return $cache->{passwdqc} = Data::Password::passwdqc->new( - min => \@min, - max => $params->{passwdqc_max}, - passphrase_words => $params->{passwdqc_passphrase_words}, - match_length => $params->{passwdqc_match_length}, - random_bits => $params->{passwdqc_random_bits}, - ); + return $cache->{passwdqc} = Data::Password::passwdqc->new( + min => \@min, + max => $params->{passwdqc_max}, + passphrase_words => $params->{passwdqc_passphrase_words}, + match_length => $params->{passwdqc_match_length}, + random_bits => $params->{passwdqc_random_bits}, + ); } sub assert_password_is_secure { - my ( $class, $password1 ) = @_; + my ($class, $password1) = @_; - my $pwqc = $class->passwdqc; - ThrowUserError( 'password_insecure', { reason => $pwqc->reason } ) - unless $pwqc->validate_password($password1); + my $pwqc = $class->passwdqc; + ThrowUserError('password_insecure', {reason => $pwqc->reason}) + unless $pwqc->validate_password($password1); } sub assert_passwords_match { - my ( $class, $password1, $password2 ) = @_; + my ($class, $password1, $password2) = @_; - ThrowUserError('password_mismatch') if $password1 ne $password2; + ThrowUserError('password_mismatch') if $password1 ne $password2; } sub login { - my ($class, $type) = @_; + my ($class, $type) = @_; - return $class->user if $class->user->id; + return $class->user if $class->user->id; - 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) { - request_cache->{page_requires_login} = 1; - } + # Allow templates to know that we're in a page that always requires + # login. + if ($type == LOGIN_REQUIRED) { + request_cache->{page_requires_login} = 1; + } - my $authenticated_user = $authorizer->login($type); + my $authenticated_user = $authorizer->login($type); - if (i_am_cgi() && $authenticated_user->id) { - Bugzilla::Logging->fields->{user_id} = $authenticated_user->id; - } + if (i_am_cgi() && $authenticated_user->id) { + Bugzilla::Logging->fields->{user_id} = $authenticated_user->id; + } - # At this point, we now know if a real person is logged in. - - # Check if a password reset is required - my $cgi = Bugzilla->cgi; - my $script_name = $cgi->script_name; - my $do_logout = $cgi->param('logout'); - - if ( $authenticated_user->password_change_required ) { - # We cannot show the password reset UI for API calls, so treat those as - # a disabled account. - if ( i_am_webservice() ) { - ThrowUserError( "account_disabled", { disabled_reason => $authenticated_user->password_change_reason } ); - } - - # only allow the reset-password and token pages to handle requests - # (tokens handles the 'forgot password' process) - # otherwise redirect user to the reset-password page. - if ( $script_name !~ m#/(?:reset_password|token)\.cgi$# && !$do_logout ) { - my $self_url = trim($cgi->self_url); - my $sig_type = 'prev_url:' . $authenticated_user->id; - my $self_url_sig = issue_hash_sig($sig_type, $self_url); - my $redir_url = URI->new( Bugzilla->localconfig->{urlbase} . "reset_password.cgi" ); - $redir_url->query_form(prev_url => $self_url, prev_url_sig => $self_url_sig); - print $cgi->redirect($redir_url); - exit; - } - } - elsif ( !i_am_webservice() && $authenticated_user->in_mfa_group && !$authenticated_user->mfa ) { - - # decide if the user needs a warning or to be blocked. - my $date = $authenticated_user->mfa_required_date('UTC'); - my $grace_period = Bugzilla->params->{mfa_group_grace_period}; - my $expired = defined $date && $date < DateTime->now; - my $on_mfa_page = $script_name eq '/userprefs.cgi' && $cgi->param('tab') eq 'mfa'; - my $on_token_page = $script_name eq '/token.cgi'; - - Bugzilla->request_cache->{mfa_warning} = 1; - Bugzilla->request_cache->{mfa_grace_period_expired} = $expired; - Bugzilla->request_cache->{on_mfa_page} = $on_mfa_page; - - if ( $grace_period == 0 || $expired) { - if ( !( $on_mfa_page || $on_token_page || $do_logout ) ) { - print Bugzilla->cgi->redirect("userprefs.cgi?tab=mfa"); - exit; - } - } - else { - my $dbh = Bugzilla->dbh_main; - my $date = $dbh->sql_date_math( 'NOW()', '+', '?', 'DAY' ); - my ($mfa_required_date) = $dbh->selectrow_array( "SELECT $date", undef, $grace_period ); - $authenticated_user->set_mfa_required_date($mfa_required_date); - $authenticated_user->update(); - } + # At this point, we now know if a real person is logged in. + + # Check if a password reset is required + my $cgi = Bugzilla->cgi; + my $script_name = $cgi->script_name; + my $do_logout = $cgi->param('logout'); + + if ($authenticated_user->password_change_required) { + + # We cannot show the password reset UI for API calls, so treat those as + # a disabled account. + if (i_am_webservice()) { + ThrowUserError("account_disabled", + {disabled_reason => $authenticated_user->password_change_reason}); } - # 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); - 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 }); - } + # only allow the reset-password and token pages to handle requests + # (tokens handles the 'forgot password' process) + # otherwise redirect user to the reset-password page. + if ($script_name !~ m#/(?:reset_password|token)\.cgi$# && !$do_logout) { + my $self_url = trim($cgi->self_url); + my $sig_type = 'prev_url:' . $authenticated_user->id; + my $self_url_sig = issue_hash_sig($sig_type, $self_url); + my $redir_url + = URI->new(Bugzilla->localconfig->{urlbase} . "reset_password.cgi"); + $redir_url->query_form(prev_url => $self_url, prev_url_sig => $self_url_sig); + print $cgi->redirect($redir_url); + exit; + } + } + elsif (!i_am_webservice() + && $authenticated_user->in_mfa_group + && !$authenticated_user->mfa) + { + + # decide if the user needs a warning or to be blocked. + my $date = $authenticated_user->mfa_required_date('UTC'); + my $grace_period = Bugzilla->params->{mfa_group_grace_period}; + my $expired = defined $date && $date < DateTime->now; + my $on_mfa_page + = $script_name eq '/userprefs.cgi' && $cgi->param('tab') eq 'mfa'; + my $on_token_page = $script_name eq '/token.cgi'; + + Bugzilla->request_cache->{mfa_warning} = 1; + Bugzilla->request_cache->{mfa_grace_period_expired} = $expired; + Bugzilla->request_cache->{on_mfa_page} = $on_mfa_page; + + if ($grace_period == 0 || $expired) { + if (!($on_mfa_page || $on_token_page || $do_logout)) { + print Bugzilla->cgi->redirect("userprefs.cgi?tab=mfa"); + exit; + } } else { - $class->set_user($authenticated_user); + my $dbh = Bugzilla->dbh_main; + my $date = $dbh->sql_date_math('NOW()', '+', '?', 'DAY'); + my ($mfa_required_date) + = $dbh->selectrow_array("SELECT $date", undef, $grace_period); + $authenticated_user->set_mfa_required_date($mfa_required_date); + $authenticated_user->update(); } + } - if (Bugzilla->sudoer) { - Bugzilla->sudoer->update_last_seen_date(); - } else { - $class->user->update_last_seen_date(); + # 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'); } - if ($type == LOGIN_REQUIRED && ! $class->user->id) { - FATAL("Detected failure to throw login_required when login was required and user is not logged in."); - ThrowUserError('login_required'); + 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); + 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}); + } + } + else { + $class->set_user($authenticated_user); + } + + if (Bugzilla->sudoer) { + Bugzilla->sudoer->update_last_seen_date(); + } + else { + $class->user->update_last_seen_date(); + } + + if ($type == LOGIN_REQUIRED && !$class->user->id) { + FATAL( + "Detected failure to throw login_required when login was required and user is not logged in." + ); + ThrowUserError('login_required'); + } - return $class->user; + 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 request_cache->{user}; - delete 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 request_cache->{user}; + delete 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 request_cache->{job_queue} ||= Bugzilla::JobQueue->new(); + require Bugzilla::JobQueue; + return request_cache->{job_queue} ||= Bugzilla::JobQueue->new(); } sub dbh { - my ($class) = @_; - # If we're not connected, then we must want the main db - return request_cache->{dbh} ||= $class->dbh_main; + my ($class) = @_; + + # If we're not connected, then we must want the main db + return request_cache->{dbh} ||= $class->dbh_main; } sub dbh_main { - return request_cache->{dbh_main} ||= Bugzilla::DB::connect_main(); + return 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 request_cache->{current_language} ||= (include_languages())[0]; + return request_cache->{current_language} ||= (include_languages())[0]; } sub error_mode { - my (undef, $newval) = @_; - if (defined $newval) { - request_cache->{error_mode} = $newval; - } - return request_cache->{error_mode} - || (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE); + my (undef, $newval) = @_; + if (defined $newval) { + request_cache->{error_mode} = $newval; + } + return request_cache->{error_mode} + || (i_am_cgi() ? ERROR_MODE_WEBPAGE : ERROR_MODE_DIE); } # This is used only by Bugzilla::Error to throw errors. sub _json_server { - my (undef, $newval) = @_; - if (defined $newval) { - request_cache->{_json_server} = $newval; - } - return request_cache->{_json_server}; + my (undef, $newval) = @_; + if (defined $newval) { + request_cache->{_json_server} = $newval; + } + return 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); - } - elsif ($newval == USAGE_MODE_MOJO) { - $class->error_mode(ERROR_MODE_MOJO); - } - else { - ThrowCodeError('usage_mode_invalid', - {'invalid_usage_mode', $newval}); - } - request_cache->{usage_mode} = $newval; + 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); } - return request_cache->{usage_mode} - || (i_am_cgi()? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); + elsif ($newval == USAGE_MODE_MOJO) { + $class->error_mode(ERROR_MODE_MOJO); + } + else { + ThrowCodeError('usage_mode_invalid', {'invalid_usage_mode', $newval}); + } + request_cache->{usage_mode} = $newval; + } + return request_cache->{usage_mode} + || (i_am_cgi() ? USAGE_MODE_BROWSER : USAGE_MODE_CMDLINE); } sub installation_mode { - my (undef, $newval) = @_; - (request_cache->{installation_mode} = $newval) if defined $newval; - return request_cache->{installation_mode} - || INSTALLATION_MODE_INTERACTIVE; + my (undef, $newval) = @_; + (request_cache->{installation_mode} = $newval) if defined $newval; + return request_cache->{installation_mode} || INSTALLATION_MODE_INTERACTIVE; } sub installation_answers { - my (undef, $filename) = @_; - if ($filename) { - my $s = new Safe; - $s->rdo($filename); + my (undef, $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 - request_cache->{installation_answers} = $s->varglob('answer'); - } - return request_cache->{installation_answers} || {}; + # Now read the param back out from the sandbox + request_cache->{installation_answers} = $s->varglob('answer'); + } + return request_cache->{installation_answers} || {}; } sub switch_to_shadow_db { - my $class = shift; - - if (!request_cache->{dbh_shadow}) { - if ($class->get_param_with_override('shadowdb')) { - request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow(); - } else { - request_cache->{dbh_shadow} = $class->dbh_main; - } + my $class = shift; + + if (!request_cache->{dbh_shadow}) { + if ($class->get_param_with_override('shadowdb')) { + request_cache->{dbh_shadow} = Bugzilla::DB::connect_shadow(); } + else { + request_cache->{dbh_shadow} = $class->dbh_main; + } + } + + request_cache->{dbh} = request_cache->{dbh_shadow}; - request_cache->{dbh} = 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; - request_cache->{dbh} = $class->dbh_main; - return $class->dbh_main; + request_cache->{dbh} = $class->dbh_main; + return $class->dbh_main; } sub log_user_request { - my ($class, $bug_id, $attach_id, $action) = @_; - - return unless (i_am_cgi() || i_am_webservice()) - && Bugzilla->params->{log_user_requests}; - - my $cgi = $class->cgi; - my $user_id = $class->user->id; - my $request_url = $cgi->request_uri // ''; - my $method = $cgi->request_method; - my $user_agent = $cgi->user_agent // ''; - my $script_name = $cgi->script_name; - my $server = "web"; - - if ($script_name =~ /rest\.cgi/) { - $server = $script_name =~ /BzAPI/ ? "bzapi" : "rest"; - } - elsif ($script_name =~ /xmlrpc\.cgi/) { - $server = "xmlrpc"; - } - elsif ($script_name =~ /jsonrpc\.cgi/) { - $server = "jsonrpc"; - } + my ($class, $bug_id, $attach_id, $action) = @_; + + return + unless (i_am_cgi() || i_am_webservice()) + && Bugzilla->params->{log_user_requests}; + + my $cgi = $class->cgi; + my $user_id = $class->user->id; + my $request_url = $cgi->request_uri // ''; + my $method = $cgi->request_method; + my $user_agent = $cgi->user_agent // ''; + my $script_name = $cgi->script_name; + my $server = "web"; + + if ($script_name =~ /rest\.cgi/) { + $server = $script_name =~ /BzAPI/ ? "bzapi" : "rest"; + } + elsif ($script_name =~ /xmlrpc\.cgi/) { + $server = "xmlrpc"; + } + elsif ($script_name =~ /jsonrpc\.cgi/) { + $server = "jsonrpc"; + } - my @params = ($user_id, remote_ip(), $user_agent, $request_url, $method, $bug_id, $attach_id, $action, $server); - foreach my $param (@params) { - trick_taint($param) if defined $param; - } + my @params = ( + $user_id, remote_ip(), $user_agent, $request_url, $method, + $bug_id, $attach_id, $action, $server + ); + foreach my $param (@params) { + trick_taint($param) if defined $param; + } - eval { - local request_cache->{dbh}; - $class->switch_to_main_db(); - $class->dbh->do("INSERT INTO user_request_log + eval { + local request_cache->{dbh}; + $class->switch_to_main_db(); + $class->dbh->do( + "INSERT INTO user_request_log (user_id, ip_address, user_agent, request_url, method, timestamp, bug_id, attach_id, action, server) - VALUES (?, ?, ?, ?, ?, NOW(), ?, ?, ?, ?)", undef, @params); - }; - warn $@ if $@; + VALUES (?, ?, ?, ?, ?, NOW(), ?, ?, ?, ?)", undef, @params + ); + }; + warn $@ if $@; } sub is_shadow_db { - my $class = shift; - return request_cache->{dbh} != $class->dbh_main; + my $class = shift; + return request_cache->{dbh} != $class->dbh_main; } sub fields { - my (undef, $criteria) = @_; - $criteria ||= {}; - my $cache = 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 (undef, $criteria) = @_; + $criteria ||= {}; + my $cache = 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 $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}; - # 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 ($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 (undef, $params) = @_; - my $cache_id = 'active_custom_fields'; - if ($params) { - $cache_id .= ($params->{product} ? '_p' . $params->{product}->id : '') . - ($params->{component} ? '_c' . $params->{component}->id : ''); - $cache_id .= ':noext' if $params->{skip_extensions}; - } - if (!exists request_cache->{$cache_id}) { - my $fields = Bugzilla::Field->match({ custom => 1, obsolete => 0, skip_extensions => 1 }); - Bugzilla::Hook::process('active_custom_fields', - { fields => \$fields, params => $params }); - request_cache->{$cache_id} = $fields; - } - return @{request_cache->{$cache_id}}; + my (undef, $params) = @_; + my $cache_id = 'active_custom_fields'; + if ($params) { + $cache_id .= ($params->{product} ? '_p' . $params->{product}->id : '') + . ($params->{component} ? '_c' . $params->{component}->id : ''); + $cache_id .= ':noext' if $params->{skip_extensions}; + } + if (!exists request_cache->{$cache_id}) { + my $fields + = Bugzilla::Field->match({custom => 1, obsolete => 0, skip_extensions => 1}); + Bugzilla::Hook::process('active_custom_fields', + {fields => \$fields, params => $params}); + request_cache->{$cache_id} = $fields; + } + return @{request_cache->{$cache_id}}; } sub has_flags { - if (!defined request_cache->{has_flags}) { - request_cache->{has_flags} = Bugzilla::Flag->any_exist; - } - return request_cache->{has_flags}; + if (!defined request_cache->{has_flags}) { + request_cache->{has_flags} = Bugzilla::Flag->any_exist; + } + return 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'); } # Send messages to syslog for the auditing systems (eg. mozdef) to pick up. sub audit { - my (undef, $message) = @_; - state $logger = Log::Log4perl->get_logger("audit"); - $logger->notice(encode_utf8($message)); + my (undef, $message) = @_; + state $logger = Log::Log4perl->get_logger("audit"); + $logger->notice(encode_utf8($message)); } sub clear_request_cache { - my (undef, %option) = @_; - my $request_cache = request_cache(); - my @except = $option{except} ? @{ $option{except} } : (); + my (undef, %option) = @_; + my $request_cache = request_cache(); + my @except = $option{except} ? @{$option{except}} : (); - %{ $request_cache } = map { $_ => $request_cache->{$_} } @except; + %{$request_cache} = map { $_ => $request_cache->{$_} } @except; } # This is a per-process cache. Under mod_cgi it's identical to the @@ -718,72 +748,74 @@ sub clear_request_cache { our $_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 request_cache->{memcached} ||= Bugzilla::Memcached->_new(); + return request_cache->{memcached} ||= Bugzilla::Memcached->_new(); } # Connector to the Datadog metrics collection daemon. sub datadog { - my ($class, $namespace) = @_; - my $host = $class->localconfig->{datadog_host}; - my $port = $class->localconfig->{datadog_port}; - - $namespace //= ''; - - if ($class->has_feature('datadog') && $host) { - require DataDog::DogStatsd; - return request_cache->{datadog}{$namespace} //= DataDog::DogStatsd->new( - host => $host, - port => $port, - namespace => $namespace ? "$namespace." : '', - ); - } - else { - return undef; - } + my ($class, $namespace) = @_; + my $host = $class->localconfig->{datadog_host}; + my $port = $class->localconfig->{datadog_port}; + + $namespace //= ''; + + if ($class->has_feature('datadog') && $host) { + require DataDog::DogStatsd; + return request_cache->{datadog}{$namespace} //= DataDog::DogStatsd->new( + host => $host, + port => $port, + namespace => $namespace ? "$namespace." : '', + ); + } + else { + return undef; + } } sub elastic { - my ($class) = @_; - $class->process_cache->{elastic} //= Bugzilla::Elastic->new(); + my ($class) = @_; + $class->process_cache->{elastic} //= Bugzilla::Elastic->new(); } sub check_rate_limit { - my ($class, $name, $ip) = @_; - my $params = Bugzilla->params; - if ($params->{rate_limit_active}) { - my $rules = decode_json($params->{rate_limit_rules}); - my $limit = $rules->{$name}; - unless ($limit) { - warn "no rules for $name!"; - return 0; - } - if (Bugzilla->memcached->should_rate_limit("$name:$ip", @$limit)) { - my $action = 'block'; - my $filter = Bugzilla::Bloomfilter->lookup("rate_limit_whitelist"); - if ($filter && $filter->test($ip)) { - $action = 'ignore'; - } - my $limit = join("/", @$limit); - Bugzilla->audit("[rate_limit] action=$action, ip=$ip, limit=$limit, name=$name"); - if ($action eq 'block') { - $Bugzilla::Quantum::CGI::C->block_ip($ip); - ThrowUserError("rate_limit"); - } - } + my ($class, $name, $ip) = @_; + my $params = Bugzilla->params; + if ($params->{rate_limit_active}) { + my $rules = decode_json($params->{rate_limit_rules}); + my $limit = $rules->{$name}; + unless ($limit) { + warn "no rules for $name!"; + return 0; + } + if (Bugzilla->memcached->should_rate_limit("$name:$ip", @$limit)) { + my $action = 'block'; + my $filter = Bugzilla::Bloomfilter->lookup("rate_limit_whitelist"); + if ($filter && $filter->test($ip)) { + $action = 'ignore'; + } + my $limit = join("/", @$limit); + Bugzilla->audit( + "[rate_limit] action=$action, ip=$ip, limit=$limit, name=$name"); + if ($action eq 'block') { + $Bugzilla::Quantum::CGI::C->block_ip($ip); + ThrowUserError("rate_limit"); + } } + } } sub markdown_parser { - require Bugzilla::Markdown::GFM; - require Bugzilla::Markdown::GFM::Parser; - return request_cache->{markdown_parser} - ||= Bugzilla::Markdown::GFM::Parser->new( {extensions => [qw( autolink tagfilter table strikethrough)] } ); + require Bugzilla::Markdown::GFM; + require Bugzilla::Markdown::GFM::Parser; + return request_cache->{markdown_parser} + ||= Bugzilla::Markdown::GFM::Parser->new( + {extensions => [qw( autolink tagfilter table strikethrough)]}); } # Private methods @@ -791,29 +823,33 @@ sub markdown_parser { # Per-process cleanup. Note that this is a plain subroutine, not a method, # so we don't have $class available. *cleanup = \&_cleanup; + sub _cleanup { - return if $^C; + return if $^C; - # BMO - allow "end of request" processing - Bugzilla::Hook::process('request_cleanup'); - Bugzilla::Bug->CLEANUP; + # BMO - allow "end of request" processing + Bugzilla::Hook::process('request_cleanup'); + Bugzilla::Bug->CLEANUP; - my $main = Bugzilla->request_cache->{dbh_main}; - my $shadow = Bugzilla->request_cache->{dbh_shadow}; - foreach my $dbh ($main, $shadow) { - next if !$dbh; - $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; - } - clear_request_cache(); + my $main = Bugzilla->request_cache->{dbh_main}; + my $shadow = Bugzilla->request_cache->{dbh_shadow}; + foreach my $dbh ($main, $shadow) { + next if !$dbh; + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction; + } + clear_request_cache(); - Log::Log4perl::MDC->remove(); + Log::Log4perl::MDC->remove(); } our ($caller_package, $caller_file) = caller; -init_page() if $caller_package eq 'main' && $caller_package !~ /^Test/ && $caller_file =~ /\.t$/; +init_page() + if $caller_package eq 'main' + && $caller_package !~ /^Test/ + && $caller_file =~ /\.t$/; END { - cleanup() if $caller_package eq 'main'; + cleanup() if $caller_package eq 'main'; } 1; diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm index 9eac3a147..f6b65d368 100644 --- a/Bugzilla/Attachment.pm +++ b/Bugzilla/Attachment.pm @@ -58,56 +58,52 @@ use base 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_size + attach_id + bug_id + creation_ts + description + filename + isobsolete + ispatch + isprivate + mimetype + modification_time + submitter_id + attach_size ); -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 ###### @@ -128,7 +124,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 @@ -142,12 +138,12 @@ the bug object to which the attachment is attached =cut sub bug { - my ($self) = @_; - require Bugzilla::Bug; - return $self->{bug} if defined $self->{bug}; - my $bug = $self->{bug} = Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 }); - weaken($self->{bug}); - return $bug; + my ($self) = @_; + require Bugzilla::Bug; + return $self->{bug} if defined $self->{bug}; + my $bug = $self->{bug} = Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1}); + weaken($self->{bug}); + return $bug; } =over @@ -161,7 +157,7 @@ user-provided text describing the attachment =cut sub description { - return $_[0]->{description}; + return $_[0]->{description}; } =over @@ -175,7 +171,7 @@ the attachment's MIME media type =cut sub contenttype { - return $_[0]->{mimetype}; + return $_[0]->{mimetype}; } =over @@ -189,8 +185,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 @@ -204,7 +200,7 @@ the date and time on which the attacher attached the attachment =cut sub attached { - return $_[0]->{creation_ts}; + return $_[0]->{creation_ts}; } =over @@ -218,7 +214,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 @@ -232,7 +228,7 @@ the name of the file the attacher attached =cut sub filename { - return $_[0]->{filename}; + return $_[0]->{filename}; } =over @@ -246,7 +242,7 @@ whether or not the attachment is a patch =cut sub ispatch { - return $_[0]->{ispatch}; + return $_[0]->{ispatch}; } =over @@ -260,7 +256,7 @@ whether or not the attachment is obsolete =cut sub isobsolete { - return $_[0]->{isobsolete}; + return $_[0]->{isobsolete}; } =over @@ -274,7 +270,7 @@ whether or not the attachment is private =cut sub isprivate { - return $_[0]->{isprivate}; + return $_[0]->{isprivate}; } =over @@ -291,21 +287,21 @@ 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)\//); - # Modern browsers support PDF as well. - return 1 if ($contenttype eq 'application/pdf'); + # Modern browsers support PDF as well. + return 1 if ($contenttype eq 'application/pdf'); - # 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 @@ -319,8 +315,8 @@ the content of the attachment =cut sub data { - my $self = shift; - return $self->{data} //= current_storage()->retrieve($self->id); + my $self = shift; + return $self->{data} //= current_storage()->retrieve($self->id); } =over @@ -334,7 +330,7 @@ the length (in bytes) of the attachment content =cut sub datasize { - return $_[0]->{attach_size}; + return $_[0]->{attach_size}; } =over @@ -348,8 +344,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 @@ -364,151 +361,163 @@ 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, - active_or_has_flags => $self->bug_id }; + my $vars = { + target_type => 'attachment', + product_id => $self->bug->product_id, + component_id => $self->bug->component_id, + attach_id => $self->id, + active_or_has_flags => $self->bug_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 }); - } - trick_taint($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); - return $content_type; + return $content_type; } sub _check_data { - my ($invocant, $params) = @_; + my ($invocant, $params) = @_; - my $data = $params->{data}; - $params->{attach_size} = ref $data ? -s $data : length($data); + my $data = $params->{data}; + $params->{attach_size} = 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->{attach_size} || ThrowUserError('zero_length_file'); - # Make sure the attachment does not exceed the maximum permitted size. - if ($params->{attach_size} > Bugzilla->params->{'maxattachmentsize'} * 1024) { - ThrowUserError('file_too_large', { filesize => sprintf("%.0f", $params->{attach_size}/1024) }); - } + $params->{attach_size} || ThrowUserError('zero_length_file'); + + # Make sure the attachment does not exceed the maximum permitted size. + if ($params->{attach_size} > Bugzilla->params->{'maxattachmentsize'} * 1024) { + ThrowUserError('file_too_large', + {filesize => sprintf("%.0f", $params->{attach_size} / 1024)}); + } - return $data; + 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) = @_; + my ($invocant, $filename) = @_; - $filename = clean_text($filename); - if (!$filename) { - if (ref $invocant) { - ThrowUserError('filename_not_specified'); - } - else { - ThrowUserError('file_not_specified'); - } - } + $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/^.*[\/\\]//; + # 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 100 characters, counting from the end of the - # string to make sure we keep the filename extension. - $filename = substr($filename, -100, 100); - trick_taint($filename); + # Truncate the filename to 100 characters, counting from the end of the + # string to make sure we keep the filename extension. + $filename = substr($filename, -100, 100); + trick_taint($filename); - return $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 @@ -530,61 +539,66 @@ 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); + } + + # BMO - allow loading of just non-obsolete attachments + if ($vars->{exclude_obsolete}) { + $and_restriction .= ' AND (isobsolete = 0)'; + } + + 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); - # BMO - allow loading of just non-obsolete attachments - if ($vars->{exclude_obsolete}) { - $and_restriction .= ' AND (isobsolete = 0)'; + # To avoid $attachment->flags to run SQL queries itself for each + # attachment listed here, we collect all the data at once and + # populate $attachment->{flags} ourselves. + 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); - - # To avoid $attachment->flags to run SQL queries itself for each - # attachment listed here, we collect all the data at once and - # populate $attachment->{flags} ourselves. - 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 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}}; } - return $attachments; + } + return $attachments; } =pod @@ -603,22 +617,22 @@ Returns: 1 on success, 0 otherwise. =cut sub validate_can_edit { - my $self = shift; - my $user = Bugzilla->user; + my $self = shift; + my $user = Bugzilla->user; - # The submitter can edit their attachments. - return 1 if $self->attacher->id == $user->id; + # The submitter can edit their attachments. + return 1 if $self->attacher->id == $user->id; - # Private attachments - return 0 if $self->isprivate && !$user->is_insider; + # Private attachments + return 0 if $self->isprivate && !$user->is_insider; - # BMO: if you can edit the bug, then you can also edit any of its attachments - return 1 if $self->bug->user->{canedit}; + # BMO: if you can edit the bug, then you can also edit any of its attachments + return 1 if $self->bug->user->{canedit}; - # If you are in editbugs for this product - return 1 if $user->in_group('editbugs', $self->bug->product_id); + # If you are in editbugs for this product + return 1 if $user->in_group('editbugs', $self->bug->product_id); - return 0; + return 0; } =item C @@ -637,37 +651,37 @@ Returns: The list of attachment objects to mark as obsolete. =cut sub validate_obsolete { - 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; - - detaint_natural($attachid) - || ThrowCodeError('invalid_attach_id_to_obsolete', $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 }); - - if ($attachment->bug_id != $bug->bug_id) { - $vars->{'my_bug_id'} = $bug->bug_id; - ThrowCodeError('mismatched_bug_ids_on_obsolete', $vars); - } + 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; + + detaint_natural($attachid) + || ThrowCodeError('invalid_attach_id_to_obsolete', $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}); + + if ($attachment->bug_id != $bug->bug_id) { + $vars->{'my_bug_id'} = $bug->bug_id; + ThrowCodeError('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; } ############################### @@ -702,85 +716,88 @@ 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 $attachment = $class->insert_create_data($params); - $attachment->{bug} = $bug; - - # store attachment data - if (ref($data)) { - local $/; - my $tmp = <$data>; - close($data); - $data = $tmp; - } - current_storage()->store($attachment->id, $data); + my $class = shift; + my $dbh = Bugzilla->dbh; - # Return the new attachment object - return $attachment; -} + $class->check_required_create_fields(@_); + my $params = $class->run_create_validators(@_); -sub run_create_validators { - my ($class, $params) = @_; + # Extract everything which is not a valid column name. + my $bug = delete $params->{bug}; + $params->{bug_id} = $bug->id; + my $data = delete $params->{data}; - # 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); + my $attachment = $class->insert_create_data($params); + $attachment->{bug} = $bug; - $params->{creation_ts} ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $params->{modification_time} = $params->{creation_ts}; - $params->{submitter_id} = Bugzilla->user->id || ThrowCodeError('invalid_user'); + # store attachment data + if (ref($data)) { + local $/; + my $tmp = <$data>; + close($data); + $data = $tmp; + } + current_storage()->store($attachment->id, $data); - 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)'); - - 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]; - } +sub run_create_validators { + my ($class, $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); - } + # 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); - 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 }); - } + $params->{creation_ts} + ||= Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $params->{modification_time} = $params->{creation_ts}; + $params->{submitter_id} = Bugzilla->user->id || ThrowCodeError('invalid_user'); - Bugzilla::Hook::process('attachment_end_of_update', - { object => $self, old_object => $old_self, changes => $changes }); + 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}); + } + + Bugzilla::Hook::process('attachment_end_of_update', + {object => $self, old_object => $old_self, changes => $changes}); + + return $changes; } =pod @@ -798,25 +815,28 @@ 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('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ?, attach_size = ? - WHERE attach_id = ?', undef, ('text/plain', 0, 1, 0, $self->id)); - $dbh->bz_commit_transaction(); - current_storage()->remove($self->id); - - # 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( + 'UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ?, attach_size = ? + WHERE attach_id = ?', undef, ('text/plain', 0, 1, 0, $self->id) + ); + $dbh->bz_commit_transaction(); + current_storage()->remove($self->id); + + # 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}); + } } ############################### @@ -825,83 +845,87 @@ 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 (!defined $method) { - ThrowUserError("missing_content_type_method"); - } - elsif ($method eq 'autodetect') { - 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"); - - # Set the ispatch flag to 1 if the content type - # is text/x-diff or text/x-patch - if ($content_type =~ m{text/x-(?:diff|patch)}) { - $cgi->param('ispatch', 1); - $content_type = 'text/plain'; - } - - # 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'; - } - } - elsif ($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'); + if (!defined $method) { + ThrowUserError("missing_content_type_method"); + } + elsif ($method eq 'autodetect') { + 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"); + + # Set the ispatch flag to 1 if the content type + # is text/x-diff or text/x-patch + if ($content_type =~ m{text/x-(?:diff|patch)}) { + $cgi->param('ispatch', 1); + $content_type = 'text/plain'; } - else { - ThrowCodeError("illegal_content_type_method", { contenttypemethod => $method }); + + # 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; + } + elsif ($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 { + ThrowCodeError("illegal_content_type_method", {contenttypemethod => $method}); + } + return $content_type; } sub current_storage { - return state $storage //= get_storage_by_name(Bugzilla->params->{attachment_storage}); + return state $storage + //= get_storage_by_name(Bugzilla->params->{attachment_storage}); } sub get_storage_names { - require Bugzilla::Config::Attachment; - foreach my $param (Bugzilla::Config::Attachment->get_param_list) { - next unless $param->{name} eq 'attachment_storage'; - return @{ $param->{choices} }; - } - return []; + require Bugzilla::Config::Attachment; + foreach my $param (Bugzilla::Config::Attachment->get_param_list) { + next unless $param->{name} eq 'attachment_storage'; + return @{$param->{choices}}; + } + return []; } sub get_storage_by_name { - my ($name) = @_; - # all options for attachment_storage need to be handled here - if ($name eq 'database') { - require Bugzilla::Attachment::Database; - return Bugzilla::Attachment::Database->new(); - } - elsif ($name eq 'filesystem') { - require Bugzilla::Attachment::FileSystem; - return Bugzilla::Attachment::FileSystem->new(); - } - elsif ($name eq 's3') { - require Bugzilla::Attachment::S3; - return Bugzilla::Attachment::S3->new(); - } - else { - return undef; - } + my ($name) = @_; + + # all options for attachment_storage need to be handled here + if ($name eq 'database') { + require Bugzilla::Attachment::Database; + return Bugzilla::Attachment::Database->new(); + } + elsif ($name eq 'filesystem') { + require Bugzilla::Attachment::FileSystem; + return Bugzilla::Attachment::FileSystem->new(); + } + elsif ($name eq 's3') { + require Bugzilla::Attachment::S3; + return Bugzilla::Attachment::S3->new(); + } + else { + return undef; + } } 1; diff --git a/Bugzilla/Attachment/Archive.pm b/Bugzilla/Attachment/Archive.pm index ccedf1da4..1b22fc50e 100644 --- a/Bugzilla/Attachment/Archive.pm +++ b/Bugzilla/Attachment/Archive.pm @@ -16,108 +16,110 @@ use IO::File; use constant HEADER_SIZE => 45; use constant HEADER_FORMAT => 'ANNNH64'; -has 'file' => ( is => 'ro', required => 1 ); -has 'input_fh' => ( is => 'lazy', predicate => 'has_input_fh' ); -has 'output_fh' => ( is => 'lazy', predicate => 'has_output_fh' ); -has 'checksum' => ( is => 'lazy', clearer => 'reset_checksum' ); +has 'file' => (is => 'ro', required => 1); +has 'input_fh' => (is => 'lazy', predicate => 'has_input_fh'); +has 'output_fh' => (is => 'lazy', predicate => 'has_output_fh'); +has 'checksum' => (is => 'lazy', clearer => 'reset_checksum'); sub read_member { - my ($self) = @_; - my $header = $self->_read_header(); - my ($type, $bug_id, $attach_id, $data_len, $hash) = unpack HEADER_FORMAT, $header; - if ( $type eq 'D' ) { - $self->checksum->add($header); - my $data = $self->_read_data( $data_len, $hash ); - return { - bug_id => $bug_id, - attach_id => $attach_id, - data_len => $data_len, - hash => $hash, - data => $data, - }; - } - elsif ($type eq 'C') { - die "bad overall checksum\n" unless $hash eq $self->checksum->hexdigest; - $self->reset_checksum; - return undef; - } - else { - die "unknown member type: $type\n"; - } + my ($self) = @_; + my $header = $self->_read_header(); + my ($type, $bug_id, $attach_id, $data_len, $hash) = unpack HEADER_FORMAT, + $header; + if ($type eq 'D') { + $self->checksum->add($header); + my $data = $self->_read_data($data_len, $hash); + return { + bug_id => $bug_id, + attach_id => $attach_id, + data_len => $data_len, + hash => $hash, + data => $data, + }; + } + elsif ($type eq 'C') { + die "bad overall checksum\n" unless $hash eq $self->checksum->hexdigest; + $self->reset_checksum; + return undef; + } + else { + die "unknown member type: $type\n"; + } } sub write_attachment { - my ( $self, $attachment ) = @_; - my $data = $attachment->data; - my $bug_id = $attachment->bug_id; - my $attach_id = $attachment->id; - - if (defined $data && length($data) == $attachment->datasize) { - my $header = pack HEADER_FORMAT, 'D', $bug_id, $attach_id, length($data), sha256_hex($data); - $self->checksum->add($header); - $self->output_fh->print($header, $data); - } + my ($self, $attachment) = @_; + my $data = $attachment->data; + my $bug_id = $attachment->bug_id; + my $attach_id = $attachment->id; + + if (defined $data && length($data) == $attachment->datasize) { + my $header = pack HEADER_FORMAT, 'D', $bug_id, $attach_id, length($data), + sha256_hex($data); + $self->checksum->add($header); + $self->output_fh->print($header, $data); + } } sub write_checksum { - my ($self) = @_; - my $header = pack HEADER_FORMAT, 'C', 0, 0, 0, $self->checksum->hexdigest; - $self->output_fh->print($header); - $self->reset_checksum; - $self->output_fh->flush; + my ($self) = @_; + my $header = pack HEADER_FORMAT, 'C', 0, 0, 0, $self->checksum->hexdigest; + $self->output_fh->print($header); + $self->reset_checksum; + $self->output_fh->flush; } sub _build_checksum { - my ($self) = @_; - return Digest::SHA->new(256); + my ($self) = @_; + return Digest::SHA->new(256); } sub _build_input_fh { - my ($self) = @_; - if ($self->has_output_fh) { - croak "I will not read and write a file at the same time"; - } - my $file = $self->file; - return IO::File->new( $self->file, '<:bytes' ) or die "cannot read $file: $!"; + my ($self) = @_; + if ($self->has_output_fh) { + croak "I will not read and write a file at the same time"; + } + my $file = $self->file; + return IO::File->new($self->file, '<:bytes') or die "cannot read $file: $!"; } sub _build_output_fh { - my ($self) = @_; - if ($self->has_input_fh) { - croak "I will not read and write a file at the same time"; - } - my $file = $self->file; - if (-e $file) { - croak "I will not overwrite a file (file $file already exists)"; - } - return IO::File->new( $file, '>:bytes' ) or die "cannot write $file: $!"; + my ($self) = @_; + if ($self->has_input_fh) { + croak "I will not read and write a file at the same time"; + } + my $file = $self->file; + if (-e $file) { + croak "I will not overwrite a file (file $file already exists)"; + } + return IO::File->new($file, '>:bytes') or die "cannot write $file: $!"; } sub _read_header { - my ($self) = @_; - my $header = '' x HEADER_SIZE; - my $header_len = $self->input_fh->read($header, HEADER_SIZE); - if ( !$header_len || $header_len != HEADER_SIZE ) { - die "bad header\n"; - } - return $header; + my ($self) = @_; + my $header = '' x HEADER_SIZE; + my $header_len = $self->input_fh->read($header, HEADER_SIZE); + if (!$header_len || $header_len != HEADER_SIZE) { + die "bad header\n"; + } + return $header; } sub _read_data { - my ($self, $data_len, $hash) = @_; + my ($self, $data_len, $hash) = @_; - my $data = '' x $data_len; - my $read_data_len = $self->input_fh->read($data, $data_len); + my $data = '' x $data_len; + my $read_data_len = $self->input_fh->read($data, $data_len); - unless ( $read_data_len == $data_len ) { - die "bad data\n"; - } + unless ($read_data_len == $data_len) { + die "bad data\n"; + } - unless ( $hash eq sha256_hex($data) ) { - die "bad checksum:\n\t$hash\n\t" . sha226_hex($data) . "\n"; - } + unless ($hash eq sha256_hex($data)) { + die "bad checksum:\n\t$hash\n\t" . sha226_hex($data) . "\n"; + } - return $data; + return $data; } -1; \ No newline at end of file +1; diff --git a/Bugzilla/Attachment/Database.pm b/Bugzilla/Attachment/Database.pm index 42cdd2d6f..661ac9131 100644 --- a/Bugzilla/Attachment/Database.pm +++ b/Bugzilla/Attachment/Database.pm @@ -14,48 +14,40 @@ use warnings; use Bugzilla::Util qw(trick_taint); sub new { - return bless({}, shift); + return bless({}, shift); } sub store { - my ($self, $attach_id, $data) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare("INSERT INTO attach_data (id, thedata) VALUES ($attach_id, ?)"); - trick_taint($data); - $sth->bind_param(1, $data, $dbh->BLOB_TYPE); - $sth->execute(); + my ($self, $attach_id, $data) = @_; + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare( + "INSERT INTO attach_data (id, thedata) VALUES ($attach_id, ?)"); + trick_taint($data); + $sth->bind_param(1, $data, $dbh->BLOB_TYPE); + $sth->execute(); } sub retrieve { - my ($self, $attach_id) = @_; - my $dbh = Bugzilla->dbh; - my ($data) = $dbh->selectrow_array( - "SELECT thedata FROM attach_data WHERE id = ?", - undef, - $attach_id - ); - return $data; + my ($self, $attach_id) = @_; + my $dbh = Bugzilla->dbh; + my ($data) + = $dbh->selectrow_array("SELECT thedata FROM attach_data WHERE id = ?", + undef, $attach_id); + return $data; } sub remove { - my ($self, $attach_id) = @_; - my $dbh = Bugzilla->dbh; - $dbh->do( - "DELETE FROM attach_data WHERE id = ?", - undef, - $attach_id - ); + my ($self, $attach_id) = @_; + my $dbh = Bugzilla->dbh; + $dbh->do("DELETE FROM attach_data WHERE id = ?", undef, $attach_id); } sub exists { - my ($self, $attach_id) = @_; - my $dbh = Bugzilla->dbh; - my ($exists) = $dbh->selectrow_array( - "SELECT 1 FROM attach_data WHERE id = ?", - undef, - $attach_id - ); - return !!$exists; + my ($self, $attach_id) = @_; + my $dbh = Bugzilla->dbh; + my ($exists) = $dbh->selectrow_array("SELECT 1 FROM attach_data WHERE id = ?", + undef, $attach_id); + return !!$exists; } 1; diff --git a/Bugzilla/Attachment/FileSystem.pm b/Bugzilla/Attachment/FileSystem.pm index a9fdf83b6..f10a994fd 100644 --- a/Bugzilla/Attachment/FileSystem.pm +++ b/Bugzilla/Attachment/FileSystem.pm @@ -14,50 +14,50 @@ use warnings; use Bugzilla::Constants qw(bz_locations); sub new { - return bless({}, shift); + return bless({}, shift); } sub store { - my ($self, $attach_id, $data) = @_; - my $path = _local_path($attach_id); - mkdir($path, 0770) unless -d $path; - open(my $fh, '>', _local_file($attach_id)); - binmode($fh); - print $fh $data; - close($fh); + my ($self, $attach_id, $data) = @_; + my $path = _local_path($attach_id); + mkdir($path, 0770) unless -d $path; + open(my $fh, '>', _local_file($attach_id)); + binmode($fh); + print $fh $data; + close($fh); } sub retrieve { - my ($self, $attach_id) = @_; - if (open(my $fh, '<', _local_file($attach_id))) { - local $/; - binmode($fh); - my $data = <$fh>; - close($fh); - return $data; - } - return undef; + my ($self, $attach_id) = @_; + if (open(my $fh, '<', _local_file($attach_id))) { + local $/; + binmode($fh); + my $data = <$fh>; + close($fh); + return $data; + } + return undef; } sub remove { - my ($self, $attach_id) = @_; - unlink(_local_file($attach_id)); + my ($self, $attach_id) = @_; + unlink(_local_file($attach_id)); } sub exists { - my ($self, $attach_id) = @_; - return -e _local_file($attach_id); + my ($self, $attach_id) = @_; + return -e _local_file($attach_id); } sub _local_path { - my ($attach_id) = @_; - my $hash = sprintf('group.%03d', $attach_id % 1000); - return bz_locations()->{attachdir} . '/' . $hash; + my ($attach_id) = @_; + my $hash = sprintf('group.%03d', $attach_id % 1000); + return bz_locations()->{attachdir} . '/' . $hash; } sub _local_file { - my ($attach_id) = @_; - return _local_path($attach_id) . '/attachment.' . $attach_id; + my ($attach_id) = @_; + return _local_path($attach_id) . '/attachment.' . $attach_id; } 1; diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm index 8025f5b82..0ae70d3f4 100644 --- a/Bugzilla/Attachment/PatchReader.pm +++ b/Bugzilla/Attachment/PatchReader.pm @@ -20,148 +20,160 @@ use Bugzilla::Attachment; use Bugzilla::Util; sub process_diff { - my ($attachment, $format, $context) = @_; - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $lc = Bugzilla->localconfig; - my $vars = {}; - - my ($reader, $last_reader) = setup_patch_readers(undef, $context); - - if ($format eq 'raw') { - require Bugzilla::PatchReader::DiffPrinter::raw; - $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw()); - - Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get") - if Bugzilla->user->id; - # Actually print out the patch. - print $cgi->header(-type => 'text/plain', - -expires => '+3M'); - 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; - } - } - } + my ($attachment, $format, $context) = @_; + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $lc = Bugzilla->localconfig; + my $vars = {}; - $vars->{'bugid'} = $attachment->bug_id; - $vars->{'attachid'} = $attachment->id; - $vars->{'description'} = $attachment->description; - $vars->{'other_patches'} = \@other_patches; - - setup_template_patch_reader($last_reader, $format, $context, $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}); - } - $reader->iterate_string('Attachment ' . $attachment->id, $attachment->data); - } -} + my ($reader, $last_reader) = setup_patch_readers(undef, $context); -sub process_interdiff { - my ($old_attachment, $new_attachment, $format, $context) = @_; - my $cgi = Bugzilla->cgi; - my $lc = Bugzilla->localconfig; - my $vars = {}; - - if (Bugzilla->user->id) { - foreach my $attachment ($old_attachment, $new_attachment) { - Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get"); + if ($format eq 'raw') { + require Bugzilla::PatchReader::DiffPrinter::raw; + $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw()); + + Bugzilla->log_user_request($attachment->bug_id, $attachment->id, + "attachment-get") + if Bugzilla->user->id; + + # Actually print out the patch. + print $cgi->header(-type => 'text/plain', -expires => '+3M'); + 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; + } + } } - # 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); + $vars->{'bugid'} = $attachment->bug_id; + $vars->{'attachid'} = $attachment->id; + $vars->{'description'} = $attachment->description; + $vars->{'other_patches'} = \@other_patches; - my $warning = warn_if_interdiff_might_fail($old_file_list, $new_file_list); + setup_template_patch_reader($last_reader, $format, $context, $vars); - # Send through interdiff, send output directly to template. - # Must hack path so that interdiff will work. - $ENV{'PATH'} = $lc->{diffpath}; - - my ($pid, $interdiff_stdout, $interdiff_stderr); - $interdiff_stderr = gensym; - $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, - $lc->{interdiffbin}, $old_filename, $new_filename); - binmode $interdiff_stdout; - - # Check for errors - { - local $/ = undef; - my $error = <$interdiff_stderr>; - if ($error) { - warn($error); - $warning = 'interdiff3'; - } + # 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); + } +} - my ($reader, $last_reader) = setup_patch_readers("", $context); - - if ($format eq 'raw') { - require Bugzilla::PatchReader::DiffPrinter::raw; - $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw()); - # Actually print out the patch. - print $cgi->header(-type => 'text/plain', - -expires => '+3M'); - disable_utf8(); +sub process_interdiff { + my ($old_attachment, $new_attachment, $format, $context) = @_; + my $cgi = Bugzilla->cgi; + my $lc = Bugzilla->localconfig; + my $vars = {}; + + if (Bugzilla->user->id) { + foreach my $attachment ($old_attachment, $new_attachment) { + Bugzilla->log_user_request($attachment->bug_id, $attachment->id, + "attachment-get"); } - else { - # In case the HTML page is displayed with the UTF-8 encoding. - binmode $interdiff_stdout, ':utf8' if Bugzilla->params->{'utf8'}; - - $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($last_reader, $format, $context, $vars); + } + + # 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. + $ENV{'PATH'} = $lc->{diffpath}; + + my ($pid, $interdiff_stdout, $interdiff_stderr); + $interdiff_stderr = gensym; + $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr, $lc->{interdiffbin}, + $old_filename, $new_filename); + binmode $interdiff_stdout; + + # Check for errors + { + local $/ = undef; + my $error = <$interdiff_stderr>; + if ($error) { + warn($error); + $warning = 'interdiff3'; } - $reader->iterate_fh($interdiff_stdout, 'interdiff #' . $old_attachment->id . - ' #' . $new_attachment->id); - waitpid($pid, 0) if $pid; - $ENV{'PATH'} = ''; - - # Delete temporary files. - unlink($old_filename) or warn "Could not unlink $old_filename: $!"; - unlink($new_filename) or warn "Could not unlink $new_filename: $!"; + } + + my ($reader, $last_reader) = setup_patch_readers("", $context); + + if ($format eq 'raw') { + require Bugzilla::PatchReader::DiffPrinter::raw; + $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::raw()); + + # Actually print out the patch. + print $cgi->header(-type => 'text/plain', -expires => '+3M'); + disable_utf8(); + } + else { + # In case the HTML page is displayed with the UTF-8 encoding. + binmode $interdiff_stdout, ':utf8' if Bugzilla->params->{'utf8'}; + + $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($last_reader, $format, $context, $vars); + } + $reader->iterate_fh($interdiff_stdout, + 'interdiff #' . $old_attachment->id . ' #' . $new_attachment->id); + waitpid($pid, 0) if $pid; + $ENV{'PATH'} = ''; + + # Delete temporary files. + unlink($old_filename) or warn "Could not unlink $old_filename: $!"; + unlink($new_filename) or warn "Could not unlink $new_filename: $!"; } ###################### @@ -169,144 +181,154 @@ sub process_interdiff { ###################### sub get_unified_diff { - my ($attachment, $format) = @_; - - # Bring in the modules we need. - require Bugzilla::PatchReader::Raw; - require Bugzilla::PatchReader::FixPatchRoot; - require Bugzilla::PatchReader::DiffPrinter::raw; - require Bugzilla::PatchReader::PatchInfoGrabber; - require File::Temp; - - $attachment->ispatch - || ThrowCodeError('must_be_patch', { 'attach_id' => $attachment->id }); - - # Reads in the patch, converting to unified diff in a temp file. - my $reader = new Bugzilla::PatchReader::Raw; - my $last_reader = $reader; - - # Fixes patch root (makes canonical if possible). - if (Bugzilla->params->{'cvsroot'}) { - my $fix_patch_root = - new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}); - $last_reader->sends_data_to($fix_patch_root); - $last_reader = $fix_patch_root; - } - - # Grabs the patch file info. - my $patch_info_grabber = new Bugzilla::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 Bugzilla::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}); + my ($attachment, $format) = @_; + + # Bring in the modules we need. + require Bugzilla::PatchReader::Raw; + require Bugzilla::PatchReader::FixPatchRoot; + require Bugzilla::PatchReader::DiffPrinter::raw; + require Bugzilla::PatchReader::PatchInfoGrabber; + require File::Temp; + + $attachment->ispatch + || ThrowCodeError('must_be_patch', {'attach_id' => $attachment->id}); + + # Reads in the patch, converting to unified diff in a temp file. + my $reader = new Bugzilla::PatchReader::Raw; + my $last_reader = $reader; + + # Fixes patch root (makes canonical if possible). + if (Bugzilla->params->{'cvsroot'}) { + my $fix_patch_root + = new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}); + $last_reader->sends_data_to($fix_patch_root); + $last_reader = $fix_patch_root; + } + + # Grabs the patch file info. + my $patch_info_grabber = new Bugzilla::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 Bugzilla::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 ( + $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 ($old_file_list->{$file}{old_revision} ne - $new_file_list->{$file}{old_revision}) - { - return 'interdiff2'; - } + return 'interdiff2'; } - return undef; + } + return undef; } sub setup_patch_readers { - my ($diff_root, $context) = @_; - - # Parameters: - # format=raw|html - # context=patch|file|0-n - # collapsed=0|1 - # headers=0|1 - - # Define the patch readers. - # The reader that reads the patch in (whatever its format). - require Bugzilla::PatchReader::Raw; - my $reader = new Bugzilla::PatchReader::Raw; - my $last_reader = $reader; - # Fix the patch root if we have a cvs root. - if (Bugzilla->params->{'cvsroot'}) { - require Bugzilla::PatchReader::FixPatchRoot; - $last_reader->sends_data_to(new Bugzilla::PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'})); - $last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root); - $last_reader = $last_reader->sends_data_to; - } - - # Add in cvs context if we have the necessary info to do it - if ($context ne 'patch' && Bugzilla->localconfig->{cvsbin} - && Bugzilla->params->{'cvsroot_get'}) - { - require Bugzilla::PatchReader::AddCVSContext; - # We need to set $cvsbin as global, because PatchReader::CVSClient - # needs it in order to find 'cvs'. - $main::cvsbin = Bugzilla->localconfig->{cvsbin}; - $last_reader->sends_data_to( - new Bugzilla::PatchReader::AddCVSContext($context, Bugzilla->params->{'cvsroot_get'})); - $last_reader = $last_reader->sends_data_to; - } - - return ($reader, $last_reader); + my ($diff_root, $context) = @_; + + # Parameters: + # format=raw|html + # context=patch|file|0-n + # collapsed=0|1 + # headers=0|1 + + # Define the patch readers. + # The reader that reads the patch in (whatever its format). + require Bugzilla::PatchReader::Raw; + my $reader = new Bugzilla::PatchReader::Raw; + my $last_reader = $reader; + + # Fix the patch root if we have a cvs root. + if (Bugzilla->params->{'cvsroot'}) { + require Bugzilla::PatchReader::FixPatchRoot; + $last_reader->sends_data_to(new Bugzilla::PatchReader::FixPatchRoot( + Bugzilla->params->{'cvsroot'})); + $last_reader->sends_data_to->diff_root($diff_root) if defined($diff_root); + $last_reader = $last_reader->sends_data_to; + } + + # Add in cvs context if we have the necessary info to do it + if ( $context ne 'patch' + && Bugzilla->localconfig->{cvsbin} + && Bugzilla->params->{'cvsroot_get'}) + { + require Bugzilla::PatchReader::AddCVSContext; + + # We need to set $cvsbin as global, because PatchReader::CVSClient + # needs it in order to find 'cvs'. + $main::cvsbin = Bugzilla->localconfig->{cvsbin}; + $last_reader->sends_data_to(new Bugzilla::PatchReader::AddCVSContext( + $context, Bugzilla->params->{'cvsroot_get'} + )); + $last_reader = $last_reader->sends_data_to; + } + + return ($reader, $last_reader); } sub setup_template_patch_reader { - my ($last_reader, $format, $context, $vars) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - - require Bugzilla::PatchReader::DiffPrinter::template; - - # Define the vars for templates. - if (defined $cgi->param('headers')) { - $vars->{'headers'} = $cgi->param('headers'); - } - else { - $vars->{'headers'} = 1; + my ($last_reader, $format, $context, $vars) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + + require Bugzilla::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'); + $vars->{'context'} = $context; + $vars->{'do_context'} + = Bugzilla->localconfig->{cvsbin} + && Bugzilla->params->{'cvsroot_get'} + && !$vars->{'newid'}; + + # Print everything out. + print $cgi->header(-type => 'text/html'); + + $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::template( + $template, + "attachment/diff-header.$format.tmpl", + "attachment/diff-file.$format.tmpl", + "attachment/diff-footer.$format.tmpl", + { + %{$vars}, + bonsai_url => Bugzilla->params->{'bonsai_url'}, + lxr_url => Bugzilla->params->{'lxr_url'}, + lxr_root => Bugzilla->params->{'lxr_root'}, } - - $vars->{'collapsed'} = $cgi->param('collapsed'); - $vars->{'context'} = $context; - $vars->{'do_context'} = Bugzilla->localconfig->{cvsbin} - && Bugzilla->params->{'cvsroot_get'} && !$vars->{'newid'}; - - # Print everything out. - print $cgi->header(-type => 'text/html'); - - $last_reader->sends_data_to(new Bugzilla::PatchReader::DiffPrinter::template($template, - "attachment/diff-header.$format.tmpl", - "attachment/diff-file.$format.tmpl", - "attachment/diff-footer.$format.tmpl", - { %{$vars}, - bonsai_url => Bugzilla->params->{'bonsai_url'}, - lxr_url => Bugzilla->params->{'lxr_url'}, - lxr_root => Bugzilla->params->{'lxr_root'}, - })); + )); } 1; diff --git a/Bugzilla/Attachment/S3.pm b/Bugzilla/Attachment/S3.pm index e1c7269a5..7f4755720 100644 --- a/Bugzilla/Attachment/S3.pm +++ b/Bugzilla/Attachment/S3.pm @@ -15,44 +15,48 @@ use Bugzilla::Error; use Bugzilla::S3; sub new { - my $s3 = Bugzilla::S3->new({ - aws_access_key_id => Bugzilla->params->{aws_access_key_id}, - aws_secret_access_key => Bugzilla->params->{aws_secret_access_key}, - secure => 1, - }); - return bless({ - s3 => $s3, - bucket => $s3->bucket(Bugzilla->params->{s3_bucket}), - }, shift); + my $s3 = Bugzilla::S3->new({ + aws_access_key_id => Bugzilla->params->{aws_access_key_id}, + aws_secret_access_key => Bugzilla->params->{aws_secret_access_key}, + secure => 1, + }); + return + bless({s3 => $s3, bucket => $s3->bucket(Bugzilla->params->{s3_bucket}),}, + shift); } sub store { - my ($self, $attach_id, $data) = @_; - unless ($self->{bucket}->add_key($attach_id, $data)) { - warn "Failed to add attachment $attach_id to S3: " . $self->{bucket}->errstr . "\n"; - ThrowCodeError('s3_add_failed', { attach_id => $attach_id, reason => $self->{bucket}->errstr }); - } + my ($self, $attach_id, $data) = @_; + unless ($self->{bucket}->add_key($attach_id, $data)) { + warn "Failed to add attachment $attach_id to S3: " + . $self->{bucket}->errstr . "\n"; + ThrowCodeError('s3_add_failed', + {attach_id => $attach_id, reason => $self->{bucket}->errstr}); + } } sub retrieve { - my ($self, $attach_id) = @_; - my $response = $self->{bucket}->get_key($attach_id); - if (!$response) { - warn "Failed to retrieve attachment $attach_id from S3: " . $self->{bucket}->errstr . "\n"; - ThrowCodeError('s3_get_failed', { attach_id => $attach_id, reason => $self->{bucket}->errstr }); - } - return $response->{value}; + my ($self, $attach_id) = @_; + my $response = $self->{bucket}->get_key($attach_id); + if (!$response) { + warn "Failed to retrieve attachment $attach_id from S3: " + . $self->{bucket}->errstr . "\n"; + ThrowCodeError('s3_get_failed', + {attach_id => $attach_id, reason => $self->{bucket}->errstr}); + } + return $response->{value}; } sub remove { - my ($self, $attach_id) = @_; - $self->{bucket}->delete_key($attach_id) - or warn "Failed to remove attachment $attach_id from S3: " . $self->{bucket}->errstr . "\n"; + my ($self, $attach_id) = @_; + $self->{bucket}->delete_key($attach_id) + or warn "Failed to remove attachment $attach_id from S3: " + . $self->{bucket}->errstr . "\n"; } sub exists { - my ($self, $attach_id) = @_; - return !!$self->{bucket}->head_key($attach_id); + my ($self, $attach_id) = @_; + return !!$self->{bucket}->head_key($attach_id); } 1; diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm index 58ac248c5..c40b3582e 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; @@ -30,276 +30,281 @@ use URI; use URI::QueryParam; 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) = @_; - my $dbh = Bugzilla->dbh; - - # Get login info from the cookie, form, environment variables, etc. - my $login_info = $self->{_info_getter}->get_login_info(); + my ($self, $type) = @_; + my $dbh = Bugzilla->dbh; - 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 his 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 his 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); - } - - # 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); - - # trigger multi-factor auth - if ($self->{_info_getter}->{successful}->requires_verification - && $user->mfa - && !Bugzilla->sudoer - && !i_am_webservice() - ) { - my $params = Bugzilla->input_params; - my $cgi = Bugzilla->cgi; - my $uri = URI->new($cgi->self_url); - foreach my $param (qw( Bugzilla_remember Bugzilla_restrictlogin GoAheadAndLogIn )) { - $uri->query_param_delete($param); - } - $user->mfa_provider->verify_prompt({ - user => $user, - type => $type, - reason => 'Logging in as ' . $user->identity, - restrictlogin => $params->{Bugzilla_restrictlogin}, - remember => $params->{Bugzilla_remember}, - url => $uri->as_string, - postback => { - action => 'token.cgi', - token_field => 't', - fields => { - a => 'mfa_l', - }, - } - }); + 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); + + # trigger multi-factor auth + if ( $self->{_info_getter}->{successful}->requires_verification + && $user->mfa + && !Bugzilla->sudoer + && !i_am_webservice()) + { + my $params = Bugzilla->input_params; + my $cgi = Bugzilla->cgi; + my $uri = URI->new($cgi->self_url); + foreach + my $param (qw( Bugzilla_remember Bugzilla_restrictlogin GoAheadAndLogIn )) + { + $uri->query_param_delete($param); + } + $user->mfa_provider->verify_prompt({ + user => $user, + type => $type, + reason => 'Logging in as ' . $user->identity, + restrictlogin => $params->{Bugzilla_restrictlogin}, + remember => $params->{Bugzilla_remember}, + url => $uri->as_string, + postback => + {action => 'token.cgi', token_field => 't', fields => {a => 'mfa_l',},} + }); + } + + + return $self->_handle_login_result($login_info, $type); } sub mfa_verified { - my ($self, $user, $event) = @_; - require Bugzilla::Auth::Login::CGI; + my ($self, $user, $event) = @_; + require Bugzilla::Auth::Login::CGI; - my $params = Bugzilla->input_params; - $self->{_info_getter}->{successful} = Bugzilla::Auth::Login::CGI->new(); - $params->{Bugzilla_restrictlogin} = $event->{restrictlogin} if defined $event->{restrictlogin}; - $params->{Bugzilla_remember} = $event->{remember} if defined $event->{remember}; + my $params = Bugzilla->input_params; + $self->{_info_getter}->{successful} = Bugzilla::Auth::Login::CGI->new(); + $params->{Bugzilla_restrictlogin} = $event->{restrictlogin} + if defined $event->{restrictlogin}; + $params->{Bugzilla_remember} = $event->{remember} if defined $event->{remember}; - $self->_handle_login_result({ user => $user }, $event->{type}); + $self->_handle_login_result({user => $user}, $event->{type}); } sub successful_info_getter { - my ($self) = @_; + my ($self) = @_; - return $self->{_info_getter}->{successful}; + return $self->{_info_getter}->{successful}; } 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; + return $_[0]->user_can_create_account; } 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} - || Bugzilla->request_cache->{dont_persist_session} - ) - ) { - $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) { + + # 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} + || Bugzilla->request_cache->{dont_persist_session} + ) + ) + { + $user->{_login_token} = $self->{_persister}->persist_login($user); } - 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; + } + elsif ($fail_code == AUTH_ERROR) { + if ($result->{user_error}) { + ThrowUserError($result->{user_error}, $result->{details}); } - # 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_username_or_password", - { remaining => $remaining_attempts }); - } - # 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) { - my $host = gethostbyaddr($n, AF_INET); - $address = "$host ($address)" if $host; - } - 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); - Bugzilla->audit(sprintf( - '<%s> triggered lockout of %s after %s attempts', - $address, $user->login, scalar(@$attempts) - )); - } - - $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_username_or_password", + {remaining => $remaining_attempts}); + } + + # 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) { + my $host = gethostbyaddr($n, AF_INET); + $address = "$host ($address)" if $host; + } + 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); + Bugzilla->audit(sprintf( + '<%s> triggered lockout of %s after %s attempts', + $address, $user->login, scalar(@$attempts) + )); + } + + $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; + return $user; } 1; diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm index 49d670d93..6b0810035 100644 --- a/Bugzilla/Auth/Login.pm +++ b/Bugzilla/Auth/Login.pm @@ -16,18 +16,18 @@ 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 is_automatic => 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; diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm index 25c2a3555..43032c584 100644 --- a/Bugzilla/Auth/Login/APIKey.pm +++ b/Bugzilla/Auth/Login/APIKey.pm @@ -26,41 +26,42 @@ use constant can_logout => 0; use fields qw(app_id); sub set_app_id { - my ($self, $app_id) = @_; - $self->{app_id} = $app_id; + my ($self, $app_id) = @_; + $self->{app_id} = $app_id; } sub app_id { - my ($self) = @_; - return $self->{app_id}; + my ($self) = @_; + return $self->{app_id}; } # 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 $api_key = Bugzilla::User::APIKey->new({name => $api_key_text}); - 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'); - } + if (!$api_key or $api_key->api_key ne $api_key_text) { - $api_key->update_last_used(); - $self->set_app_id($api_key->app_id); + # The second part checks the correct capitalisation. Silly MySQL + ThrowUserError("api_key_not_valid"); + } + elsif ($api_key->revoked) { + ThrowUserError('api_key_revoked'); + } - return { user_id => $api_key->user_id }; + $api_key->update_last_used(); + $self->set_app_id($api_key->app_id); + + return {user_id => $api_key->user_id}; } 1; diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm index a813529d5..1b3b1f69e 100644 --- a/Bugzilla/Auth/Login/CGI.pm +++ b/Bugzilla/Auth/Login/CGI.pm @@ -21,65 +21,71 @@ 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 = Bugzilla->localconfig->{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 = Bugzilla->localconfig->{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}; } 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 9a94fe019..79456c383 100644 --- a/Bugzilla/Auth/Login/Cookie.pm +++ b/Bugzilla/Auth/Login/Cookie.pm @@ -23,145 +23,148 @@ use List::Util qw(first); use constant requires_persistence => 0; use constant requires_verification => 0; -use constant can_login => 0; +use constant can_login => 0; sub is_automatic { return $_[0]->login_token ? 0 : 1; } # 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, $is_internal); - - 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; - } - trick_taint($login_cookie) if $login_cookie; - $self->cookie($login_cookie); - - # If the call is for a web service, and an api token is provided, check - # it is valid. - if (i_am_webservice()) { - if (exists 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 }); - } - $is_internal = 1; - } - elsif ($login_cookie && Bugzilla->usage_mode == USAGE_MODE_REST) { - # REST requires an api-token when using cookie authentication - # fall back to a non-authenticated request - $login_cookie = ''; - } + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my ($user_id, $login_cookie, $is_internal); + + 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; + } + trick_taint($login_cookie) if $login_cookie; + $self->cookie($login_cookie); + + # If the call is for a web service, and an api token is provided, check + # it is valid. + if (i_am_webservice()) { + if (exists 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}); } + $is_internal = 1; + } + elsif ($login_cookie && Bugzilla->usage_mode == USAGE_MODE_REST) { + + # REST requires an api-token when using cookie authentication + # fall back to a non-authenticated request + $login_cookie = ''; + } } + } - # 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'}); - } + # 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'}); + } + + my $ip_addr = remote_ip(); - my $ip_addr = remote_ip(); + if ($login_cookie && $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); + # 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); - my $db_cookie = - $dbh->selectrow_array('SELECT cookie + my $db_cookie = $dbh->selectrow_array( + 'SELECT cookie FROM logincookies WHERE cookie = ? AND userid = ? AND (restrict_ipaddr = 0 OR ipaddr = ?)', - 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) { - - # forbid logging in with a cookie if only api-keys are allowed - if (i_am_webservice() && !$is_internal) { - my $user = Bugzilla::User->new({ id => $user_id, cache => 1 }); - if ($user->settings->{api_key_only}->{value} eq 'on') { - ThrowUserError('invalid_cookies_or_token'); - } - } - - # 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'); + 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) { + + # forbid logging in with a cookie if only api-keys are allowed + if (i_am_webservice() && !$is_internal) { + my $user = Bugzilla::User->new({id => $user_id, cache => 1}); + if ($user->settings->{api_key_only}->{value} eq 'on') { + ThrowUserError('invalid_cookies_or_token'); } + } + + # 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}; } - - # 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 }; + elsif (i_am_webservice()) { + ThrowUserError('invalid_cookies_or_token'); + } + } + + # 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 - }; + return $self->{'_login_token'} + = {user_id => $user_id, login_token => $login_token}; } sub cookie { - my ($self, $val) = @_; - $self->{_cookie} = $val if @_ > 1; + my ($self, $val) = @_; + $self->{_cookie} = $val if @_ > 1; - return $self->{_cookie}; + return $self->{_cookie}; } 1; diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm index a4de8c638..edcc269bb 100644 --- a/Bugzilla/Auth/Login/Env.pm +++ b/Bugzilla/Auth/Login/Env.pm @@ -16,29 +16,32 @@ use base 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 is_automatic => 1; +use constant extern_id_used => 1; sub get_login_info { - my ($self) = @_; - my $dbh = Bugzilla->dbh; + my ($self) = @_; + my $dbh = Bugzilla->dbh; - 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 d44ebbd46..7786f26c8 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,87 @@ 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; + 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; } - return $result; + $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 57473f551..65e6a1541 100644 --- a/Bugzilla/Auth/Persist/Cookie.pm +++ b/Bugzilla/Auth/Persist/Cookie.pm @@ -21,150 +21,166 @@ use List::Util qw(first); use List::MoreUtils qw(any); 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; - - $dbh->bz_start_transaction(); - - my $login_cookie = - Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); - - my $ip_addr = remote_ip(); - trick_taint($ip_addr); - my $restrict = $input_params->{Bugzilla_restrictlogin} ? 1 : 0; - - $dbh->do("INSERT INTO logincookies (cookie, userid, ipaddr, lastused, restrict_ipaddr) - VALUES (?, ?, ?, NOW(), ?)", - undef, $login_cookie, $user->id, $ip_addr, $restrict); - - # 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(); - - # 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->remove_cookie('github_secret'); - $cgi->remove_cookie('Bugzilla_login_request_cookie'); - $cgi->send_cookie(-name => 'Bugzilla_login', - -value => $user->id, - %cookieargs); - $cgi->send_cookie(-name => 'Bugzilla_logincookie', - -value => $login_cookie, - %cookieargs); - - my $securemail_groups = Bugzilla->can('securemail_groups') ? Bugzilla->securemail_groups : [ 'admin' ]; - - if (any { $user->in_group($_) } 'mozilla-employee-confidential', @$securemail_groups) { - my $auth_method = eval { ref($user->authorizer->successful_info_getter) } // 'unknown'; - - Bugzilla->audit(sprintf "successful login of %s from %s using \"%s\", authenticated by %s", - $user->login, $ip_addr, $cgi->user_agent // '', $auth_method); - } - - return $login_cookie; + my ($self, $user) = @_; + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $input_params = Bugzilla->input_params; + + $dbh->bz_start_transaction(); + + my $login_cookie + = Bugzilla::Token::GenerateUniqueToken('logincookies', 'cookie'); + + my $ip_addr = remote_ip(); + trick_taint($ip_addr); + my $restrict = $input_params->{Bugzilla_restrictlogin} ? 1 : 0; + + $dbh->do( + "INSERT INTO logincookies (cookie, userid, ipaddr, lastused, restrict_ipaddr) + VALUES (?, ?, ?, NOW(), ?)", undef, $login_cookie, $user->id, + $ip_addr, $restrict + ); + + # 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(); + + # 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->remove_cookie('github_secret'); + $cgi->remove_cookie('Bugzilla_login_request_cookie'); + $cgi->send_cookie(-name => 'Bugzilla_login', -value => $user->id, %cookieargs); + $cgi->send_cookie( + -name => 'Bugzilla_logincookie', + -value => $login_cookie, + %cookieargs + ); + + my $securemail_groups + = Bugzilla->can('securemail_groups') + ? Bugzilla->securemail_groups + : ['admin']; + + if (any { $user->in_group($_) } 'mozilla-employee-confidential', + @$securemail_groups) + { + my $auth_method + = eval { ref($user->authorizer->successful_info_getter) } // 'unknown'; + + Bugzilla->audit( + sprintf "successful login of %s from %s using \"%s\", authenticated by %s", + $user->login, $ip_addr, $cgi->user_agent // '', $auth_method); + } + + return $login_cookie; } 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); - } - else { - push(@login_cookies, $cgi->cookie("Bugzilla_logincookie")); - } - - # 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); + } + else { + push(@login_cookies, $cgi->cookie("Bugzilla_logincookie")); + } + + # 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 5895534cd..20782e633 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} }; - # 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({ id => $user_id, cache => 1 }); - - # Now that we have a valid User, we need to see if any data has to be - # updated. - my $user_updated = 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); - $user_updated = 1; + # 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 ($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); - $user_updated = 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} + }; + + # 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}); } - $user->update() if $user_updated; - 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({id => $user_id, cache => 1}); + + # Now that we have a valid User, we need to see if any data has to be + # updated. + my $user_updated = 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); + $user_updated = 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); + $user_updated = 1; + } + $user->update() if $user_updated; + + return {user => $user}; } 1; diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm index e46d1cd82..9251fa893 100644 --- a/Bugzilla/Auth/Verify/DB.pm +++ b/Bugzilla/Auth/Verify/DB.pm @@ -19,97 +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}; - return { failure => AUTH_NODATA } unless defined $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 ($user->account_is_locked_out) { + return {failure => AUTH_LOCKOUT, user => $user}; + } - if ($entered_password_crypted ne $real_password_crypted) { - # Record the login failure - $user->note_login_failure(); + my $password = $login_data->{password}; + return {failure => AUTH_NODATA} unless defined $login_data->{password}; + my $real_password_crypted = $user->cryptpassword; - # Immediately check if we are locked out - if ($user->account_is_locked_out) { - return { failure => AUTH_LOCKOUT, user => $user, - just_locked_out => 1 }; - } + # Using the internal crypted password as the salt, + # crypt the password the user entered. + my $entered_password_crypted = bz_crypt($password, $real_password_crypted); - return { failure => AUTH_LOGINFAILED, - failure_count => scalar(@{ $user->account_ip_login_failures }), - }; - } + if ($entered_password_crypted ne $real_password_crypted) { - # 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 $pwqc = Bugzilla->passwdqc; - unless ($pwqc->validate_password($password)) { - my $reason = $pwqc->reason; - Bugzilla->audit(sprintf "%s logged in with a weak password (reason: %s)", $user->login, $reason); - $user->set_password_change_required(1); - $user->set_password_change_reason( - "You must change your password for the following reason: $reason" - ); - $user->update(); - } - } + # Record the login failure + $user->note_login_failure(); - # 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(); - - # 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; - if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) { - # 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(); + # Immediately check if we are locked out + if ($user->account_is_locked_out) { + return {failure => AUTH_LOCKOUT, user => $user, just_locked_out => 1}; } - if (i_am_webservice() && $user->settings->{api_key_only}->{value} eq 'on') { - # api-key verification happens in Auth/Login/APIKey - # token verification happens in Auth/Login/Cookie - # if we get here from an api call then we must be using user/pass - return { - failure => AUTH_ERROR, - user_error => 'invalid_auth_method', - }; + 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 $pwqc = Bugzilla->passwdqc; + unless ($pwqc->validate_password($password)) { + my $reason = $pwqc->reason; + Bugzilla->audit(sprintf "%s logged in with a weak password (reason: %s)", + $user->login, $reason); + $user->set_password_change_required(1); + $user->set_password_change_reason( + "You must change your password for the following reason: $reason"); + $user->update(); } - - return $login_data; + } + + # 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(); + + # 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; + if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) { + + # 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 (i_am_webservice() && $user->settings->{api_key_only}->{value} eq 'on') { + + # api-key verification happens in Auth/Login/APIKey + # token verification happens in Auth/Login/Cookie + # if we get here from an api call then we must be using user/pass + return {failure => AUTH_ERROR, user_error => 'invalid_auth_method',}; + } + + 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 de88f9a43..e9aca17e8 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_retrieve_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_retrieve_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 his 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 ad0778e39..a2a54b944 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/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/Bloomfilter.pm b/Bugzilla/Bloomfilter.pm index ba1d6d6c3..fb8bcc9ac 100644 --- a/Bugzilla/Bloomfilter.pm +++ b/Bugzilla/Bloomfilter.pm @@ -17,45 +17,45 @@ use File::Slurper qw(write_binary read_binary read_lines); use File::Spec::Functions qw(catfile); sub _new_bloom_filter { - my ($n) = @_; - my $p = 0.01; - my $m = $n * abs(log $p) / log(2) ** 2; - my $k = $m / $n * log(2); - return Algorithm::BloomFilter->new($m, $k); + my ($n) = @_; + my $p = 0.01; + my $m = $n * abs(log $p) / log(2)**2; + my $k = $m / $n * log(2); + return Algorithm::BloomFilter->new($m, $k); } sub _filename { - my ($name, $type) = @_; + my ($name, $type) = @_; - my $datadir = bz_locations->{datadir}; + my $datadir = bz_locations->{datadir}; - return catfile($datadir, "$name.$type"); + return catfile($datadir, "$name.$type"); } sub populate { - my ($class, $name) = @_; - my $memcached = Bugzilla->memcached; - my @items = read_lines(_filename($name, 'list')); - my $filter = _new_bloom_filter(@items + 0); - - $filter->add($_) foreach @items; - write_binary(_filename($name, 'bloom'), $filter->serialize); - $memcached->clear_bloomfilter({name => $name}); + my ($class, $name) = @_; + my $memcached = Bugzilla->memcached; + my @items = read_lines(_filename($name, 'list')); + my $filter = _new_bloom_filter(@items + 0); + + $filter->add($_) foreach @items; + write_binary(_filename($name, 'bloom'), $filter->serialize); + $memcached->clear_bloomfilter({name => $name}); } sub lookup { - my ($class, $name) = @_; - my $memcached = Bugzilla->memcached; - my $filename = _filename($name, 'bloom'); - my $filter_data = $memcached->get_bloomfilter( { name => $name } ); - - if (!$filter_data && -f $filename) { - $filter_data = read_binary($filename); - $memcached->set_bloomfilter({ name => $name, filter => $filter_data }); - } - - return Algorithm::BloomFilter->deserialize($filter_data) if $filter_data; - return undef; + my ($class, $name) = @_; + my $memcached = Bugzilla->memcached; + my $filename = _filename($name, 'bloom'); + my $filter_data = $memcached->get_bloomfilter({name => $name}); + + if (!$filter_data && -f $filename) { + $filter_data = read_binary($filename); + $memcached->set_bloomfilter({name => $name, filter => $filter_data}); + } + + return Algorithm::BloomFilter->deserialize($filter_data) if $filter_data; + return undef; } 1; diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm index ee48ed7a2..5673ab6e3 100644 --- a/Bugzilla/Bug.pm +++ b/Bugzilla/Bug.pm @@ -41,9 +41,9 @@ use Role::Tiny::With; use base qw(Bugzilla::Object Exporter); @Bugzilla::Bug::EXPORT = qw( - bug_alias_to_id - LogActivityEntry - editable_bug_fields + bug_alias_to_id + LogActivityEntry + editable_bug_fields ); my %CLEANUP; @@ -56,192 +56,196 @@ use constant DB_TABLE => 'bugs'; use constant ID_FIELD => 'bug_id'; use constant NAME_FIELD => 'alias'; 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({skip_extensions => 1}); - my @custom_names = map {$_->name} @custom; - - my @columns = (qw( - alias - 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 } + Bugzilla->active_custom_fields({skip_extensions => 1}); + my @custom_names = map { $_->name } @custom; + + my @columns = ( + qw( + alias + 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, - 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, - 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_INTEGER) { - $validator = \&_check_integer_field; - } - else { - $validator = \&_check_default_field; - } - $validators->{$field->name} = $validator; + my $validators = { + alias => \&_check_alias, + assigned_to => \&_check_assigned_to, + 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, + 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_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'], - bug_status => ['product', 'comment', 'target_milestone'], - cc => ['component'], - comment => ['creation_ts'], - component => ['product'], - dup_id => ['bug_status', 'resolution'], - groups => ['product'], - keywords => ['product'], - resolution => ['bug_status'], - 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'], + bug_status => ['product', 'comment', 'target_milestone'], + cc => ['component'], + comment => ['creation_ts'], + component => ['product'], + dup_id => ['bug_status', 'resolution'], + groups => ['product'], + keywords => ['product'], + resolution => ['bug_status'], + 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; +} sub UPDATE_COLUMNS { - my @custom = grep {$_->type != FIELD_TYPE_MULTI_SELECT } - Bugzilla->active_custom_fields({skip_extensions => 1}); - my @custom_names = map {$_->name} @custom; - my @columns = qw( - alias - 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 } + Bugzilla->active_custom_fields({skip_extensions => 1}); + my @custom_names = map { $_->name } @custom; + my @columns = qw( + alias + 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( + estimated_time + remaining_time ); sub DATE_COLUMNS { - my @fields = (@{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) }, - @{ Bugzilla->fields({ type => FIELD_TYPE_DATE }) }); - return map { $_->name } @fields; + my @fields = ( + @{Bugzilla->fields({type => FIELD_TYPE_DATETIME})}, + @{Bugzilla->fields({type => FIELD_TYPE_DATE})} + ); + return map { $_->name } @fields; } # Used in LogActivityEntry(). Gives the max length of lines in the @@ -253,32 +257,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', - cc_accessible => 'cclist_accessible', - 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', - comment_count => 'longdescs.count', - platform => 'rep_platform', - severity => 'bug_severity', - status => 'bug_status', - summary => 'short_desc', - url => 'bug_file_loc', - whiteboard => 'status_whiteboard', + blocks => 'blocked', + cc_accessible => 'cclist_accessible', + 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', + comment_count => 'longdescs.count', + platform => 'rep_platform', + severity => 'bug_severity', + status => 'bug_status', + summary => 'short_desc', + url => 'bug_file_loc', + whiteboard => 'status_whiteboard', }; -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) @@ -296,7 +298,8 @@ 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); +use constant EXTRA_REQUIRED_FIELDS => + qw(creation_ts target_milestone cc qa_contact groups); with 'Bugzilla::Elastic::Role::Object'; @@ -305,118 +308,97 @@ sub ES_TYPE {'bug'} sub ES_INDEX { Bugzilla->params->{elasticsearch_index} } sub ES_SETTINGS { - return { - number_of_shards => 2, - analysis => { - filter => { - asciifolding_original => { - type => "asciifolding", - preserve_original => \1, - }, - }, - analyzer => { - autocomplete => { - type => 'custom', - tokenizer => 'keyword', - filter => [ 'lowercase', 'asciifolding_original' ], - }, - folding => { - tokenizer => 'standard', - filter => [ 'standard', 'lowercase', 'asciifolding_original' ], - }, - bz_text_analyzer => { - type => 'standard', - filter => [ 'lowercase', 'stop' ], - max_token_length => '20' - }, - bz_equals_analyzer => { - type => 'custom', - filter => ['lowercase'], - tokenizer => 'keyword', - }, - whiteboard_words => { - type => 'custom', - tokenizer => 'whiteboard_words_pattern', - filter => ['stop'] - }, - whiteboard_shingle_words => { - type => 'custom', - tokenizer => 'whiteboard_words_pattern', - filter => [ 'stop', 'shingle', 'lowercase' ] - }, - whiteboard_tokens => { - type => 'custom', - tokenizer => 'whiteboard_tokens_pattern', - filter => [ 'stop', 'lowercase' ] - }, - whiteboard_shingle_tokens => { - type => 'custom', - tokenizer => 'whiteboard_tokens_pattern', - filter => [ 'stop', 'shingle', 'lowercase' ] - } - }, - tokenizer => { - whiteboard_tokens_pattern => { - type => 'pattern', - pattern => '\\s*([,;]*\\[|\\][\\s\\[]*|[;,])\\s*' - }, - whiteboard_words_pattern => { - type => 'pattern', - pattern => '[\\[\\];,\\s]+' - }, - }, + return { + number_of_shards => 2, + analysis => { + filter => { + asciifolding_original => {type => "asciifolding", preserve_original => \1,}, + }, + analyzer => { + autocomplete => { + type => 'custom', + tokenizer => 'keyword', + filter => ['lowercase', 'asciifolding_original'], }, - }; + folding => { + tokenizer => 'standard', + filter => ['standard', 'lowercase', 'asciifolding_original'], + }, + bz_text_analyzer => { + type => 'standard', + filter => ['lowercase', 'stop'], + max_token_length => '20' + }, + bz_equals_analyzer => + {type => 'custom', filter => ['lowercase'], tokenizer => 'keyword',}, + whiteboard_words => { + type => 'custom', + tokenizer => 'whiteboard_words_pattern', + filter => ['stop'] + }, + whiteboard_shingle_words => { + type => 'custom', + tokenizer => 'whiteboard_words_pattern', + filter => ['stop', 'shingle', 'lowercase'] + }, + whiteboard_tokens => { + type => 'custom', + tokenizer => 'whiteboard_tokens_pattern', + filter => ['stop', 'lowercase'] + }, + whiteboard_shingle_tokens => { + type => 'custom', + tokenizer => 'whiteboard_tokens_pattern', + filter => ['stop', 'shingle', 'lowercase'] + } + }, + tokenizer => { + whiteboard_tokens_pattern => + {type => 'pattern', pattern => '\\s*([,;]*\\[|\\][\\s\\[]*|[;,])\\s*'}, + whiteboard_words_pattern => {type => 'pattern', pattern => '[\\[\\];,\\s]+'}, + }, + }, + }; } sub _bz_field { - my ($field, @fields) = @_; - - return ( - $field => { - type => 'string', - analyzer => 'bz_text_analyzer', - fields => { - eq => { - type => 'string', - analyzer => 'bz_equals_analyzer', - }, - @fields, - }, - }, - ); + my ($field, @fields) = @_; + + return ( + $field => { + type => 'string', + analyzer => 'bz_text_analyzer', + fields => + {eq => {type => 'string', analyzer => 'bz_equals_analyzer',}, @fields,}, + }, + ); } sub ES_PROPERTIES { - return { - _bz_field('priority'), - _bz_field('bug_severity'), - _bz_field('bug_status'), - _bz_field('resolution'), - status_whiteboard => { type => 'string', analyzer => 'whiteboard_shingle_tokens' }, - delta_ts => { type => 'string', index => 'not_analyzed' }, - _bz_field('product'), - _bz_field('component'), - _bz_field('classification'), - _bz_field('short_desc'), - _bz_field('assigned_to'), - _bz_field('reporter'), - }; + return { + _bz_field('priority'), _bz_field('bug_severity'), _bz_field('bug_status'), + _bz_field('resolution'), + status_whiteboard => + {type => 'string', analyzer => 'whiteboard_shingle_tokens'}, + delta_ts => {type => 'string', index => 'not_analyzed'}, + _bz_field('product'), _bz_field('component'), _bz_field('classification'), + _bz_field('short_desc'), _bz_field('assigned_to'), _bz_field('reporter'), + }; } -sub ES_OBJECTS_AT_ONCE { 4000 } +sub ES_OBJECTS_AT_ONCE {4000} sub ES_SELECT_UPDATED_SQL { - my ($class, $mtime) = @_; + my ($class, $mtime) = @_; - my @fields = ( - 'keywords', 'short_desc', 'product', 'component', - 'cf_crash_signature', 'alias', 'status_whiteboard', - 'bug_status', 'resolution', 'priority', 'assigned_to' - ); - my $fields = join(', ', ("?") x @fields); + my @fields = ( + 'keywords', 'short_desc', 'product', 'component', + 'cf_crash_signature', 'alias', 'status_whiteboard', 'bug_status', + 'resolution', 'priority', 'assigned_to' + ); + my $fields = join(', ', ("?") x @fields); - my $sql = qq{ + my $sql = qq{ SELECT DISTINCT bug_id FROM @@ -468,368 +450,378 @@ sub ES_SELECT_UPDATED_SQL { AND field = 'name' AND at_time > FROM_UNIXTIME(?) }; - return ($sql, [$mtime, @fields, $mtime, $mtime, $mtime, $mtime]); + return ($sql, [$mtime, @fields, $mtime, $mtime, $mtime, $mtime]); } sub es_document { - my ($self) = @_; - return { - bug_id => $self->id, - product => $self->product_obj->name, - alias => $self->alias, - keywords => [ map { $_->name } @{$self->keyword_objects} ], - priority => $self->priority, - bug_status => $self->bug_status, - resolution => $self->resolution, - component => $self->component_obj->name, - classification => $self->product_obj->classification->name, - status_whiteboard => $self->status_whiteboard, - short_desc => $self->short_desc, - assigned_to => $self->assigned_to->login, - reporter => $self->reporter->login, - delta_ts => $self->delta_ts, - bug_severity => $self->bug_severity, - }; + my ($self) = @_; + return { + bug_id => $self->id, + product => $self->product_obj->name, + alias => $self->alias, + keywords => [map { $_->name } @{$self->keyword_objects}], + priority => $self->priority, + bug_status => $self->bug_status, + resolution => $self->resolution, + component => $self->component_obj->name, + classification => $self->product_obj->classification->name, + status_whiteboard => $self->status_whiteboard, + short_desc => $self->short_desc, + assigned_to => $self->assigned_to->login, + reporter => $self->reporter->login, + delta_ts => $self->delta_ts, + bug_severity => $self->bug_severity, + }; } ##################################################################### 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 =~ /^#(\d+)$/) { - $param = $1; + 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 =~ /^#(\d+)$/) { + $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 || $param =~ /\D/)) + || (ref($param) && (!$param->{id} || $param->{id} =~ /\D/))) + { + # But only if aliases are enabled. + if (Bugzilla->params->{'usebugaliases'} && $param) { + $param = { + name => ref($param) ? $param->{id} : $param, + cache => ref($param) ? $param->{cache} : 0 + }; } - - # If we get something that looks like a word (not a number), - # make it the "name" param. - if (!defined $param - || (!ref($param) && (!$param || $param =~ /\D/)) - || (ref($param) && (!$param->{id} || $param->{id} =~ /\D/))) - { - # But only if aliases are enabled. - if (Bugzilla->params->{'usebugaliases'} && $param) { - $param = { name => ref($param) ? $param->{id} : $param, - cache => ref($param) ? $param->{cache} : 0 }; - } - else { - # Aliases are off, and 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; - } + else { + # Aliases are off, and 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'; } - - 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; + else { + $error_self->{bug_id} = $param; + $error_self->{error} = 'NotFound'; } + bless $error_self, $class; + return $error_self; + } - $CLEANUP{$self->id} = $self; - weaken($CLEANUP{$self->id}); + $CLEANUP{$self->id} = $self; + weaken($CLEANUP{$self->id}); - return $self; + 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; } sub CLEANUP { - foreach my $bug (values %CLEANUP) { - next unless $bug; - delete $bug->{depends_on_obj}; - delete $bug->{blocks_obj}; - } - %CLEANUP = (); + foreach my $bug (values %CLEANUP) { + 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; + 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; + } + return $self; } sub check_is_visible { - my $self = shift; - 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 ($user->id) { - ThrowUserError("bug_access_denied", { bug_id => $self->id }); - } else { - ThrowUserError("bug_access_query", { bug_id => $self->id }); - } - } -} + my $self = shift; + my $user = Bugzilla->user; -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; - } - } + if (!$user->can_see_bug($self->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}; + # 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 ($user->id) { + ThrowUserError("bug_access_denied", {bug_id => $self->id}); } - foreach my $field (qw(product component)) { - delete $params->{$field}; - $params->{"${field}_id"} = $translated{$field} - if exists $translated{$field}; + else { + ThrowUserError("bug_access_query", {bug_id => $self->id}); } + } +} - return $class->SUPER::match(@_); +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; + } + } + + # 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(@_); } # 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 }); - } - @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 lists, during get_bug_link in Bugzilla::Template. - $user->visible_bugs(\@all_dep_ids); - - # We preload comments here in order to allow us to compare the time it - # takes to load comments from the database with the template rendering - # time. - foreach my $bug (@$bugs) { - $bug->comments(); - } - - foreach my $bug (@$bugs) { - $bug->_preload_referenced_bugs(); - } + 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}); + } + @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 lists, during get_bug_link in Bugzilla::Template. + $user->visible_bugs(\@all_dep_ids); + + # We preload comments here in order to allow us to compare the time it + # takes to load comments from the database with the template rendering + # time. + foreach my $bug (@$bugs) { + $bug->comments(); + } + + foreach my $bug (@$bugs) { + $bug->_preload_referenced_bugs(); + } } # 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 @referenced_bug_ids; - - # 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 - require Bugzilla::Template; - foreach my $comment (@{ $self->comments }) { - if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { - # duplicate bugs that aren't currently in $self->duplicates - push @referenced_bug_ids, $comment->extra_data - unless Bugzilla::Bug->object_cache_get($comment->extra_data); - } - else { - # bugs referenced in comments - Bugzilla::Template::quoteUrls($comment->body, undef, undef, undef, - sub { - my $bug_id = $_[0]; - push @referenced_bug_ids, $bug_id - unless Bugzilla::Bug->object_cache_get($bug_id); - }); + my $self = shift; + my @referenced_bug_ids; + + # 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 + require Bugzilla::Template; + foreach my $comment (@{$self->comments}) { + if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) { + + # duplicate bugs that aren't currently in $self->duplicates + push @referenced_bug_ids, $comment->extra_data + unless Bugzilla::Bug->object_cache_get($comment->extra_data); + } + else { + # bugs referenced in comments + Bugzilla::Template::quoteUrls( + $comment->body, + undef, undef, undef, + sub { + my $bug_id = $_[0]; + push @referenced_bug_ids, $bug_id + unless Bugzilla::Bug->object_cache_get($bug_id); } + ); } + } - # inject into object-cache - my $referenced_bugs = Bugzilla::Bug->new_from_list( - [ uniq @referenced_bug_ids ]); - foreach my $bug (@$referenced_bugs) { - $bug->object_cache_set(); - } + # inject into object-cache + my $referenced_bugs = Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids]); + foreach my $bug (@$referenced_bugs) { + $bug->object_cache_set(); + } - # preload bug visibility - Bugzilla->user->visible_bugs(\@referenced_bug_ids); + # preload bug visibility + Bugzilla->user->visible_bugs(\@referenced_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 + 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) { - $word =~ s/(?:^\W+|\W+$)//g; + my ($term, $rel_term) + = $dbh->sql_fulltext_search('bugs_fulltext.short_desc', $word); + push(@where, $term); + push(@relevance, $rel_term || $term); } - # 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); - } - $where_sql = join(' OR ', @where); - $relevance_sql = join(' + ', @relevance); - } + $where_sql = join(' OR ', @where); + $relevance_sql = join(' + ', @relevance); + } - my $product_ids = join(',', map { $_->id } @$products); - my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; + my $product_ids = join(',', map { $_->id } @$products); + my $product_sql = $product_ids ? "AND product_id IN ($product_ids)" : ""; - # Because we collapse duplicates, we want to get slightly more bugs - # than were actually asked for. - my $sql_limit = $limit + 5; + # 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, + 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 @@ -871,654 +863,699 @@ sub possible_duplicates { # C - For time-tracking. Will be ignored for the same # reasons as C. sub create { - my ($class, $params) = @_; - my $dbh = Bugzilla->dbh; - - # BMO - allow parameter alteration before creation. also add support for - # fields which are not bug columns (eg bug_mentors). extensions should move - # fields from $params to $stash, then use the bug_end_of_create hook to - # update the database - my $stash = {}; - Bugzilla::Hook::process('bug_before_create', { params => $params, - stash => $stash }); - - $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}; - - # BMO - per-product hw/os defaults - if (!defined $params->{rep_platform} || !defined $params->{op_sys}) { - if (my $product = Bugzilla::Product->new({ name => $params->{product}, cache => 1 })) { - $params->{rep_platform} //= $product->default_platform; - $params->{op_sys} //= $product->default_op_sys; - } - } - - # 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 $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}; - my $comment_tags = delete $params->{comment_tags}; - - # 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 (?, ?)'); - - 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); - _update_delta_ts($depends_on_id, $timestamp); - } - 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); - _update_delta_ts($blocked_id, $timestamp); - } - - # 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. - my $comment = Bugzilla::Comment->insert_create_data($creation_comment); - - # Add comment tags - if (defined $comment_tags && Bugzilla->user->can_tag_comments) { - $comment_tags = ref $comment_tags ? $comment_tags : [ $comment_tags ]; - foreach my $tag (@{$comment_tags}) { - $comment->add_tag($tag) if defined $tag; - } - $comment->update(); - } - - # BMO - add the stash param from bug_start_of_create - Bugzilla::Hook::process('bug_end_of_create', { bug => $bug, - timestamp => $timestamp, - stash => $stash, - }); - - - $bug->_sync_fulltext( new_bug => 1 ); - - $dbh->bz_commit_transaction(); - - # BMO - some work should happen outside of the transaction block - Bugzilla::Hook::process('bug_after_create', { bug => $bug, timestamp => $timestamp }); - - return $bug; -} - -sub 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; - - 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 ($class, $params) = @_; + my $dbh = Bugzilla->dbh; + + # BMO - allow parameter alteration before creation. also add support for + # fields which are not bug columns (eg bug_mentors). extensions should move + # fields from $params to $stash, then use the bug_end_of_create hook to + # update the database + my $stash = {}; + Bugzilla::Hook::process('bug_before_create', + {params => $params, stash => $stash}); + + $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}; + + # BMO - per-product hw/os defaults + if (!defined $params->{rep_platform} || !defined $params->{op_sys}) { + if (my $product + = Bugzilla::Product->new({name => $params->{product}, cache => 1})) + { + $params->{rep_platform} //= $product->default_platform; + $params->{op_sys} //= $product->default_op_sys; + } + } + + # 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 $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}; + my $comment_tags = delete $params->{comment_tags}; + + # 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); - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - my $component = delete $params->{component}; - $params->{component_id} = $component->id; + # Update the bug instance as well + $bug->{creation_ts} = $timestamp; - # Callers cannot set reporter, creation_ts, or delta_ts. - $params->{reporter} = $class->_check_reporter(); - $params->{delta_ts} = $params->{creation_ts}; + # 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); + } - if ($params->{estimated_time}) { - $params->{remaining_time} = $params->{estimated_time}; - } + # 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); + } - $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, - $params->{qa_contact}, $product); + # Set up dependencies (blocked/dependson) + my $sth_deps = $dbh->prepare( + 'INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)'); - ($params->{dependson}, $params->{blocked}) = - $class->_check_dependencies($params->{dependson}, $params->{blocked}, - $product); + foreach my $depends_on_id (@$depends_on) { + $sth_deps->execute($bug->bug_id, $depends_on_id); - # You can't set these fields on bug creation (or sometimes ever). - delete $params->{resolution}; - delete $params->{lastdiffed}; - delete $params->{bug_id}; - delete $params->{classification}; + # Log the reverse action on the other bug. + LogActivityEntry($depends_on_id, 'blocked', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + _update_delta_ts($depends_on_id, $timestamp); + } + foreach my $blocked_id (@$blocked) { + $sth_deps->execute($blocked_id, $bug->bug_id); - Bugzilla::Hook::process('bug_end_of_create_validators', - { params => $params }); + # Log the reverse action on the other bug. + LogActivityEntry($blocked_id, 'dependson', '', $bug->bug_id, + $bug->{reporter_id}, $timestamp); + _update_delta_ts($blocked_id, $timestamp); + } - return $params; -} - -sub update { - 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)'); - - $dbh->bz_start_transaction(); - - 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]; - } + # 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); } + } - # 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); + # 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 $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]; - } - - # 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 $value (@$see_also_array) { + $bug->add_see_also($value); } - foreach my $keyword_id (@$added_kw) { - $dbh->do('INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)', - undef, $self->id, $keyword_id); + foreach my $see_also (@{$bug->see_also}) { + $see_also->insert_create_data($see_also); } - # 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]; + foreach my $ref_bug (@{$bug->{_update_ref_bugs} || []}) { + $ref_bug->update(); } + delete $bug->{_update_ref_bugs}; + } - # Dependencies - foreach my $pair ([qw(dependson blocked)], [qw(blocked dependson)]) { - my ($type, $other) = @$pair; - my $old = $old_bug->$type; - my $new = $self->$type; + # Comment #0 handling... - 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); + # We now have a bug id so we can fill this out + $creation_comment->{'bug_id'} = $bug->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. - _update_delta_ts($removed_id, $delta_ts); - } - 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. - _update_delta_ts($added_id, $delta_ts); - } + # Insert the comment. We always insert a comment on bug creation, + # but sometimes it's blank. + my $comment = Bugzilla::Comment->insert_create_data($creation_comment); - if (scalar(@$removed) || scalar(@$added)) { - $changes->{$type} = [join(', ', @$removed), join(', ', @$added)]; - } + # Add comment tags + if (defined $comment_tags && Bugzilla->user->can_tag_comments) { + $comment_tags = ref $comment_tags ? $comment_tags : [$comment_tags]; + foreach my $tag (@{$comment_tags}) { + $comment->add_tag($tag) if defined $tag; } + $comment->update(); + } - # 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)]; - - # we only audit when bugs protected with a secure-mail enabled group - # are made public - if (!scalar @{ $self->groups_in } && any { $old_groups{$_}->secure_mail } @$removed_gr) { - Bugzilla->audit(sprintf('%s made Bug %s public (%s)', $user->login, $self->id, $self->short_desc)); - } - } + # BMO - add the stash param from bug_start_of_create + Bugzilla::Hook::process('bug_end_of_create', + {bug => $bug, timestamp => $timestamp, stash => $stash,}); - # Comments and comment tags - 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); - } - foreach my $tag (@{$self->{added_comment_tags} || []}) { - $comment->add_tag($tag) if defined $tag; - } - $comment->update() if @{$self->{added_comment_tags} || []}; - } - # Comment Privacy - foreach my $comment (@{$self->{comment_isprivate} || []}) { - $comment->update(); + $bug->_sync_fulltext(new_bug => 1); - my ($from, $to) - = $comment->is_private ? (0, 1) : (1, 0); - LogActivityEntry($self->id, "longdescs.isprivate", $from, $to, - $user->id, $delta_ts, $comment->id); - } + $dbh->bz_commit_transaction(); - # Clear the cache of comments - delete $self->{comments}; + # BMO - some work should happen outside of the transaction block + Bugzilla::Hook::process('bug_after_create', + {bug => $bug, timestamp => $timestamp}); - # 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)]; + return $bug; +} - $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); - } - } - } +sub run_create_validators { + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); - # See Also + # Add classification for checking mandatory fields which depend on it + $params->{classification} = $params->{product}->classification->name; - my ($removed_see, $added_see) = - diff_arrays($old_bug->see_also, $self->see_also, '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); + } - $_->remove_from_db foreach @$removed_see; - $_->insert_create_data($_) foreach @$added_see; + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + my $component = delete $params->{component}; + $params->{component_id} = $component->id; - # 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)]; - } + # Callers cannot set reporter, creation_ts, or delta_ts. + $params->{reporter} = $class->_check_reporter(); + $params->{delta_ts} = $params->{creation_ts}; - $_->update foreach @{ $self->{_update_ref_bugs} || [] }; - delete $self->{_update_ref_bugs}; + if ($params->{estimated_time}) { + $params->{remaining_time} = $params->{estimated_time}; + } - # Flags - my ($removed, $added) = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); - if ($removed || $added) { - $changes->{'flagtypes.name'} = [$removed, $added]; - } + $class->_check_strict_isolation($params->{cc}, $params->{assigned_to}, + $params->{qa_contact}, $product); - # BMO - allow extensions to alter what is logged into bugs_activity - Bugzilla::Hook::process('bug_update_before_logging', - { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug }); + ($params->{dependson}, $params->{blocked}) + = $class->_check_dependencies($params->{dependson}, $params->{blocked}, + $product); - # 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); - } + # You can't set these fields on bug creation (or sometimes ever). + delete $params->{resolution}; + delete $params->{lastdiffed}; + delete $params->{bug_id}; + delete $params->{classification}; - # 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(); - } - } + Bugzilla::Hook::process('bug_end_of_create_validators', {params => $params}); - $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}) - { - _update_delta_ts($self->id, $delta_ts); - $self->{delta_ts} = $delta_ts; - } + return $params; +} - # Update last-visited - if ($user->is_involved_in_bug($self)) { - $self->update_user_last_visit($user, $delta_ts); - } +sub update { + my $self = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - # 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); - } + # XXX This is just a temporary hack until all updating happens + # inside this function. + my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - # 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; + $dbh->bz_start_transaction(); - } - 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; - } + my ($changes, $old_bug) = $self->SUPER::update(@_); - $self->_sync_fulltext( - update_short_desc => $changes->{short_desc}, - update_comments => $self->{added_comments} || $self->{comment_isprivate} + 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]; + } + + # 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. + _update_delta_ts($removed_id, $delta_ts); + } + 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. + _update_delta_ts($added_id, $delta_ts); + } + + 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)]; + + # we only audit when bugs protected with a secure-mail enabled group + # are made public + if ( + !scalar @{$self->groups_in} && any { $old_groups{$_}->secure_mail } + @$removed_gr + ) + { + Bugzilla->audit(sprintf( + '%s made Bug %s public (%s)', + $user->login, $self->id, $self->short_desc + )); + } + } + + # Comments and comment tags + 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); + } + foreach my $tag (@{$self->{added_comment_tags} || []}) { + $comment->add_tag($tag) if defined $tag; + } + $comment->update() if @{$self->{added_comment_tags} || []}; + } + + # 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) + ]; + } + + $_->update foreach @{$self->{_update_ref_bugs} || []}; + delete $self->{_update_ref_bugs}; + + # Flags + my ($removed, $added) + = Bugzilla::Flag->update_flags($self, $old_bug, $delta_ts); + if ($removed || $added) { + $changes->{'flagtypes.name'} = [$removed, $added]; + } + + # BMO - allow extensions to alter what is logged into bugs_activity + Bugzilla::Hook::process( + 'bug_update_before_logging', + { + bug => $self, + timestamp => $delta_ts, + changes => $changes, + old_bug => $old_bug + } + ); + + # 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}) + { + _update_delta_ts($self->id, $delta_ts); + $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; + } + + $self->_sync_fulltext( + update_short_desc => $changes->{short_desc}, + update_comments => $self->{added_comments} || $self->{comment_isprivate} + ); + + $dbh->bz_commit_transaction(); + + # 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}; + + # BMO - some work should happen outside of the transaction block + Bugzilla::Hook::process( + 'bug_after_update', + { + bug => $self, + timestamp => $delta_ts, + changes => $changes, + old_bug => $old_bug + } + ); - $dbh->bz_commit_transaction(); - - # 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}; - - # BMO - some work should happen outside of the transaction block - Bugzilla::Hook::process('bug_after_update', - { bug => $self, timestamp => $delta_ts, changes => $changes, old_bug => $old_bug }); - - return $changes; + return $changes; } # 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; - - 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, + 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 + ); + } + } } # Update a bug's delta_ts without requiring the full object to be loaded. sub _update_delta_ts { - my ($bug_id, $timestamp) = @_; - Bugzilla->dbh->do( - "UPDATE bugs SET delta_ts = ? WHERE bug_id = ?", - undef, - $timestamp, $bug_id - ); - Bugzilla::Hook::process('bug_end_of_update_delta_ts', - { bug_id => $bug_id, timestamp => $timestamp }); + my ($bug_id, $timestamp) = @_; + Bugzilla->dbh->do("UPDATE bugs SET delta_ts = ? WHERE bug_id = ?", + undef, $timestamp, $bug_id); + Bugzilla::Hook::process('bug_end_of_update_delta_ts', + {bug_id => $bug_id, timestamp => $timestamp}); } # This is the correct way to delete bugs from the DB. # No bug should be deleted from anywhere else except from here. # sub remove_from_db { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - if ($self->{'error'}) { - ThrowCodeError("bug_error", { bug => $self }); - } - - my $bug_id = $self->{'bug_id'}; - - # tables having 'bugs.bug_id' as a foreign key: - # - attachments - # - bug_group_map - # - bugs - # - bugs_activity - # - bugs_fulltext - # - cc - # - dependencies - # - duplicates - # - flags - # - keywords - # - longdescs - - # Also, the attach_data table uses attachments.attach_id as a foreign - # key, and so indirectly depends on a bug deletion too. - - $dbh->bz_start_transaction(); - - $dbh->do("DELETE FROM bug_group_map WHERE bug_id = ?", undef, $bug_id); - $dbh->do("DELETE FROM bugs_activity WHERE bug_id = ?", undef, $bug_id); - $dbh->do("DELETE FROM cc WHERE bug_id = ?", undef, $bug_id); - $dbh->do("DELETE FROM dependencies WHERE blocked = ? OR dependson = ?", - undef, ($bug_id, $bug_id)); - $dbh->do("DELETE FROM duplicates WHERE dupe = ? OR dupe_of = ?", - undef, ($bug_id, $bug_id)); - $dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id); - $dbh->do("DELETE FROM keywords WHERE bug_id = ?", undef, $bug_id); - - # The attach_data table doesn't depend on bugs.bug_id directly. - my $attach_ids = - $dbh->selectcol_arrayref("SELECT attach_id FROM attachments - WHERE bug_id = ?", undef, $bug_id); - - if (scalar(@$attach_ids)) { - $dbh->do("DELETE FROM attach_data WHERE " - . $dbh->sql_in('id', $attach_ids)); - } - - # Several of the previous tables also depend on attach_id. - $dbh->do("DELETE FROM attachments WHERE bug_id = ?", undef, $bug_id); - $dbh->do("DELETE FROM bugs WHERE bug_id = ?", undef, $bug_id); - $dbh->do("DELETE FROM longdescs WHERE bug_id = ?", undef, $bug_id); - - $dbh->bz_commit_transaction(); - - # The bugs_fulltext table doesn't support transactions. - $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id); - - undef $self; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + if ($self->{'error'}) { + ThrowCodeError("bug_error", {bug => $self}); + } + + my $bug_id = $self->{'bug_id'}; + + # tables having 'bugs.bug_id' as a foreign key: + # - attachments + # - bug_group_map + # - bugs + # - bugs_activity + # - bugs_fulltext + # - cc + # - dependencies + # - duplicates + # - flags + # - keywords + # - longdescs + + # Also, the attach_data table uses attachments.attach_id as a foreign + # key, and so indirectly depends on a bug deletion too. + + $dbh->bz_start_transaction(); + + $dbh->do("DELETE FROM bug_group_map WHERE bug_id = ?", undef, $bug_id); + $dbh->do("DELETE FROM bugs_activity WHERE bug_id = ?", undef, $bug_id); + $dbh->do("DELETE FROM cc WHERE bug_id = ?", undef, $bug_id); + $dbh->do("DELETE FROM dependencies WHERE blocked = ? OR dependson = ?", + undef, ($bug_id, $bug_id)); + $dbh->do("DELETE FROM duplicates WHERE dupe = ? OR dupe_of = ?", + undef, ($bug_id, $bug_id)); + $dbh->do("DELETE FROM flags WHERE bug_id = ?", undef, $bug_id); + $dbh->do("DELETE FROM keywords WHERE bug_id = ?", undef, $bug_id); + + # The attach_data table doesn't depend on bugs.bug_id directly. + my $attach_ids = $dbh->selectcol_arrayref( + "SELECT attach_id FROM attachments + WHERE bug_id = ?", undef, $bug_id + ); + + if (scalar(@$attach_ids)) { + $dbh->do("DELETE FROM attach_data WHERE " . $dbh->sql_in('id', $attach_ids)); + } + + # Several of the previous tables also depend on attach_id. + $dbh->do("DELETE FROM attachments WHERE bug_id = ?", undef, $bug_id); + $dbh->do("DELETE FROM bugs WHERE bug_id = ?", undef, $bug_id); + $dbh->do("DELETE FROM longdescs WHERE bug_id = ?", undef, $bug_id); + + $dbh->bz_commit_transaction(); + + # The bugs_fulltext table doesn't support transactions. + $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id = ?", undef, $bug_id); + + undef $self; } ##################################################################### @@ -1526,91 +1563,91 @@ sub remove_from_db { ##################################################################### sub send_changes { - my ($self, $changes) = @_; - my @results; - 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, - ); - - push @results, _send_bugmail( - { id => $self->id, type => 'bug', forced => \%forced }); - - # 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) { - push @results, _send_bugmail( - { forced => { changer => $user }, type => "dupe", id => $new_dup_id }); - } - - # 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; - push @results, _send_bugmail($params); - } - } - } - - # 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)) { - push @results, _send_bugmail( - { forced => { changer => $user }, type => "dep", id => $id }); - } - - # Sending emails for the referenced bugs. - foreach my $ref_bug_id (uniq @{ $self->{see_also_changes} || [] }) { - push @results, _send_bugmail( - { forced => { changer => $user }, id => $ref_bug_id }); - } - - return \@results; + my ($self, $changes) = @_; + my @results; + 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, + ); + + push @results, + _send_bugmail({id => $self->id, type => 'bug', forced => \%forced}); + + # 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) { + push @results, + _send_bugmail( + {forced => {changer => $user}, type => "dupe", id => $new_dup_id}); + } + + # 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; + push @results, _send_bugmail($params); + } + } + } + + # 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)) { + push @results, + _send_bugmail({forced => {changer => $user}, type => "dep", id => $id}); + } + + # Sending emails for the referenced bugs. + foreach my $ref_bug_id (uniq @{$self->{see_also_changes} || []}) { + push @results, _send_bugmail({forced => {changer => $user}, id => $ref_bug_id}); + } + + return \@results; } sub _send_bugmail { - my ($params) = @_; + my ($params) = @_; - require Bugzilla::BugMail; + require Bugzilla::BugMail; - my $sent_bugmail = - Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); + my $sent_bugmail + = Bugzilla::BugMail::Send($params->{'id'}, $params->{'forced'}, $params); - return { params => $params, sent_bugmail => $sent_bugmail }; + return {params => $params, sent_bugmail => $sent_bugmail}; } ##################################################################### @@ -1618,902 +1655,925 @@ sub _send_bugmail { ##################################################################### sub _check_alias { - my ($invocant, $alias) = @_; - $alias = trim($alias); - return undef if (!Bugzilla->params->{'usebugaliases'} || !$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 }); - } + my ($invocant, $alias) = @_; + $alias = trim($alias); + return undef if (!Bugzilla->params->{'usebugaliases'} || !$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}); + } - return $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 $alias; } 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)) - { - $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; - } - return $id; + 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; + } + 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; + } + return $id; } sub _check_bug_file_loc { - my ($invocant, $url) = @_; - $url = '' if !defined($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); + + # 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::Status->can_change_to()}; - if (!$product->allows_unconfirmed) { - @valid_statuses = grep {$_->name ne 'UNCONFIRMED'} @valid_statuses; - } - } - - # 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::Status->can_change_to()}; + if (!$product->allows_unconfirmed) { + @valid_statuses = grep { $_->name ne 'UNCONFIRMED' } @valid_statuses; + } + } + + # 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]; } + } } - - # 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, - new => $new_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 }); - } - - if (!blessed $invocant) { - $params->{everconfirmed} = $new_status->name eq 'UNCONFIRMED' ? 0 : 1; - } - - return $new_status->name; + 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, new => $new_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}); + } + + 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; + 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; + # 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 %cc_ids; + foreach my $person (@$ccs) { + $person = trim($person); + next unless $person; + my $id = login_to_id($person, THROW_ERROR); + $cc_ids{$id} = 1; + } - # Enforce Default CC - $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); + # Enforce Default CC + $cc_ids{$_->id} = 1 foreach (@{$component->initial_cc}); - 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_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, $depends_on, $blocks, $product) = @_; - - if (!ref $invocant) { - # Only editbugs users can set dependencies on bug entry. - return ([], []) unless Bugzilla->user->in_group('editbugs', - $product->id); - } - - my %deps_in = (dependson => $depends_on || '', blocked => $blocks || ''); - - 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; - # We do this up here to make sure all aliases are converted to IDs. - @bug_ids = map { $invocant->check($_, $type)->id } @bug_ids; - - my $user = Bugzilla->user; - 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); - - # If a user has editbugs they are allowed to add dependencies on - # bugs that they cannot see -- only check access for bugs that are - # removed. - if ($user->in_group('editbugs')) { - @check_access = @$removed; - } - else { - @check_access = (@$added, @$removed); - } - - # Check field permissions if we've changed anything. - if (@$added || @$removed) { - my $privs; - if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { - ThrowUserError('illegal_change', { field => $type, - privs => $privs }); - } - } - } + my ($invocant, $depends_on, $blocks, $product) = @_; - foreach my $modified_id (@check_access) { - # Check that the user has access to the other bug. - 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}); - } - } - } + if (!ref $invocant) { - $deps_in{$type} = \@bug_ids; - } + # Only editbugs users can set dependencies on bug entry. + return ([], []) unless Bugzilla->user->in_group('editbugs', $product->id); + } - # 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); + my %deps_in = (dependson => $depends_on || '', blocked => $blocks || ''); - return ($deps{'dependson'}, $deps{'blocked'}); -} + foreach my $type (qw(dependson blocked)) { + my @bug_ids + = ref($deps_in{$type}) + ? @{$deps_in{$type}} + : split(/[\s,]+/, $deps_in{$type}); -sub _check_dup_id { - my ($self, $dupe_of) = @_; - my $dbh = Bugzilla->dbh; + # Eliminate nulls. + @bug_ids = grep {$_} @bug_ids; + + # We do this up here to make sure all aliases are converted to IDs. + @bug_ids = map { $invocant->check($_, $type)->id } @bug_ids; - $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; + my $user = Bugzilla->user; + my @check_access = @bug_ids; - # If the dupe is unchanged, we have nothing more to check. - return $dupe_of if ($self->dup_id && $self->dup_id == $dupe_of); + # 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); + + # If a user has editbugs they are allowed to add dependencies on + # bugs that they cannot see -- only check access for bugs that are + # removed. + if ($user->in_group('editbugs')) { + @check_access = @$removed; + } + else { + @check_access = (@$added, @$removed); + } + + # Check field permissions if we've changed anything. + if (@$added || @$removed) { + my $privs; + if (!$invocant->check_can_change_field($type, 0, 1, \$privs)) { + ThrowUserError('illegal_change', {field => $type, privs => $privs}); + } + } + } - # 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; + foreach my $modified_id (@check_access) { - # Make sure a loop isn't created when marking this bug - # as duplicate. - _resolve_ultimate_dup_id($self->id, $dupe_of, 1); + # Check that the user has access to the other bug. + my $delta_bug = $invocant->check($modified_id); - 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 he can see the bug... - if ($self->reporter->can_see_bug($dupe_of)) { - # We only add him if he's 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 he user is already the reporter - # of the dupe_of bug, since we already checked if he 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 he's 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; + # 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}); } + } } - return $dupe_of; + $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); + + return ($deps{'dependson'}, $deps{'blocked'}); } -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}; - } +sub _check_dup_id { + my ($self, $dupe_of) = @_; + my $dbh = Bugzilla->dbh; + + $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; + + # 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 he can see the bug... + if ($self->reporter->can_see_bug($dupe_of)) { + + # We only add him if he's 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 { - # 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 }); - - # BMO : allow bugs to be always placed into some groups - if (!$product->group_always_settable($group) - && !$product->group_is_settable($group)) { - ThrowUserError('group_restriction_not_allowed', { %args, name => $name }); - } - $add_groups{$group->id} = $group; - } - } + # Note that here we don't check if he user is already the reporter + # of the dupe_of bug, since we already checked if he 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 he's already the reporter of the + # dup_of bug. + my $vars = {}; + my $template = Bugzilla->template; - # Now enforce mandatory groups. - $add_groups{$_->id} = $_ foreach @{ $product->groups_mandatory }; + # 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; + } + } - my @add_groups = values %add_groups; - return \@add_groups; + return $dupe_of; } -sub _check_keywords { - my ($invocant, $keywords_in, undef, $params) = @_; +sub _check_groups { + my ($invocant, $group_names, undef, $params) = @_; - return [] if !defined $keywords_in; + my $bug_id = blessed($invocant) ? $invocant->id : undef; + my $product = blessed($invocant) ? $invocant->product_obj : $params->{product}; + my %add_groups; - my $keyword_array = $keywords_in; - if (!ref $keyword_array) { - $keywords_in = trim($keywords_in); - $keyword_array = [split(/[\s,]+/, $keywords_in)]; + # 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; - my %keywords; - foreach my $keyword (@$keyword_array) { - next unless $keyword; - my $obj = Bugzilla::Keyword->check($keyword); - $keywords{$obj->id} = $obj; - } + # 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}); - my %old_kw_id; - if (blessed $invocant) { - my @old_keywords = @{$invocant->keyword_objects}; - %old_kw_id = map { $_->id => 1 } @old_keywords; + # BMO : allow bugs to be always placed into some groups + if ( !$product->group_always_settable($group) + && !$product->group_is_settable($group)) + { + ThrowUserError('group_restriction_not_allowed', {%args, name => $name}); + } + $add_groups{$group->id} = $group; } + } - foreach my $keyword (values %keywords) { - next if $keyword->is_active || exists $old_kw_id{$keyword->id}; - ThrowUserError('value_inactive', - { value => $keyword->name, class => ref $keyword }); - } + # Now enforce mandatory groups. + $add_groups{$_->id} = $_ foreach @{$product->groups_mandatory}; + + my @add_groups = values %add_groups; + return \@add_groups; +} + +sub _check_keywords { + my ($invocant, $keywords_in, undef, $params) = @_; + + return [] if !defined $keywords_in; - 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; + } + + my %old_kw_id; + if (blessed $invocant) { + my @old_keywords = @{$invocant->keyword_objects}; + %old_kw_id = map { $_->id => 1 } @old_keywords; + } + + foreach my $keyword (values %keywords) { + next if $keyword->is_active || exists $old_kw_id{$keyword->id}; + ThrowUserError('value_inactive', + {value => $keyword->name, class => ref $keyword}); + } + + 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); + + # 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_priority { - my ($invocant, $priority) = @_; - if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { - $priority = Bugzilla->params->{'defaultpriority'}; - } - return $invocant->_check_select_field($priority, 'priority'); + my ($invocant, $priority) = @_; + if (!ref $invocant && !Bugzilla->params->{'letsubmitterchoosepriority'}) { + $priority = Bugzilla->params->{'defaultpriority'}; + } + 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}; - my $id; - 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) - { - $id = $component->default_qa_contact->id; - } + my ($invocant, $qa_contact, undef, $params) = @_; + $qa_contact = trim($qa_contact) if !ref $qa_contact; + my $component + = blessed($invocant) ? $invocant->component_obj : $params->{component}; + my $id; + 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) + { + $id = $component->default_qa_contact->id; } + } - # If a QA Contact was specified or if we're updating, check - # the QA Contact for validity. - if (!defined $id && $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); - } + # If a QA Contact was specified or if we're updating, check + # the QA Contact for validity. + if (!defined $id && $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; + # "0" always means "undef", for QA Contact. + return $id || undef; } 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 he 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 he must be logged in first!). + Bugzilla->login(LOGIN_REQUIRED); + $reporter = Bugzilla->user->id; + } + return $reporter; } sub _check_resolution { - my ($self, $resolution) = @_; - $resolution = trim($resolution); - - # 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 => $self->status->name }) - if !$resolution && !$self->status->is_open; - - # Make sure this is a valid resolution. - $resolution = $self->_check_select_field($resolution, 'resolution'); - - # Don't allow open bugs to have resolutions. - ThrowUserError('resolution_not_allowed') if $self->status->is_open; - - # Check noresolveonopenblockers. - if (Bugzilla->params->{"noresolveonopenblockers"} - && $resolution eq 'FIXED' - && (!$self->resolution || $resolution ne $self->resolution) - && scalar @{$self->dependson}) - { - my $dep_bugs = Bugzilla::Bug->new_from_list($self->dependson); - my $count_open = grep { $_->isopened } @$dep_bugs; - if ($count_open) { - ThrowUserError("still_unresolved_bugs", - { bug_id => $self->id, dep_count => $count_open }); - } - } - - # Check if they're changing the resolution and need to comment. - if (Bugzilla->params->{'commentonchange_resolution'} - && $self->resolution && $resolution ne $self->resolution - && !$self->{added_comments}) - { - ThrowUserError('comment_required'); - } - - return $resolution; + my ($self, $resolution) = @_; + $resolution = trim($resolution); + + # 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 => $self->status->name}) + if !$resolution && !$self->status->is_open; + + # Make sure this is a valid resolution. + $resolution = $self->_check_select_field($resolution, 'resolution'); + + # Don't allow open bugs to have resolutions. + ThrowUserError('resolution_not_allowed') if $self->status->is_open; + + # Check noresolveonopenblockers. + if ( Bugzilla->params->{"noresolveonopenblockers"} + && $resolution eq 'FIXED' + && (!$self->resolution || $resolution ne $self->resolution) + && scalar @{$self->dependson}) + { + my $dep_bugs = Bugzilla::Bug->new_from_list($self->dependson); + my $count_open = grep { $_->isopened } @$dep_bugs; + if ($count_open) { + ThrowUserError("still_unresolved_bugs", + {bug_id => $self->id, dep_count => $count_open}); + } + } + + # Check if they're changing the resolution and need to comment. + if ( Bugzilla->params->{'commentonchange_resolution'} + && $self->resolution + && $resolution ne $self->resolution + && !$self->{added_comments}) + { + ThrowUserError('comment_required'); + } + + 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. 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 ($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; + my @related_users = @$ccs; + push(@related_users, $assignee) if $assignee; - if (Bugzilla->params->{'useqacontact'} && $qa_contact) { - push(@related_users, $qa_contact); - } + if (Bugzilla->params->{'useqacontact'} && $qa_contact) { + push(@related_users, $qa_contact); + } - @related_users = @{Bugzilla::User->new_from_list(\@related_users)} - if !ref $invocant; + @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); - } + # 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 : ''; - $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; + 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_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 +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; } 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) { + 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_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; + 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 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); - } + # 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; + return $checked_id; } sub _check_integer_field { - my ($invocant, $value, $field) = @_; - $value = defined($value) ? trim($value) : ''; + my ($invocant, $value, $field) = @_; + $value = defined($value) ? trim($value) : ''; - # BMO - allow empty values - if ($value eq '') { - return undef; - } + # BMO - allow empty values + if ($value eq '') { + return undef; + } - my $orig_value = $value; - if (!detaint_signed($value)) { - ThrowUserError("number_not_integer", - {field => $field, num => $orig_value}); - } - elsif ($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 ($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); + # 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) = @_; - # 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}); - } + # Don't do anything if this bug doesn't have any dependencies. + return unless defined($dep_id); - # 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 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); + _check_relationship_loop($field, $dep_id, $next_dep_id, $ids); } ##################################################################### @@ -2521,31 +2581,32 @@ 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} ); + 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" : (), - return @fields; + # Custom Fields + map { $_->name } Bugzilla->active_custom_fields + ); + Bugzilla::Hook::process('bug_fields', {'fields' => \@fields}); + + return @fields; } ##################################################################### @@ -2554,30 +2615,29 @@ sub fields { # 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 = user_id_to_login($value); - $current = user_id_to_login($current); - } - ThrowUserError('illegal_change', { field => $field, - oldvalue => $current, - newvalue => $value, - privs => $privs }); - } - $self->_check_field_is_mandatory($value, $field); + 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 = user_id_to_login($value); + $current = user_id_to_login($current); + } + ThrowUserError('illegal_change', + {field => $field, oldvalue => $current, newvalue => $value, privs => $privs}); + } + $self->_check_field_is_mandatory($value, $field); } ################# @@ -2587,523 +2647,568 @@ 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; - - # BMO - allow extensions to morph params - Bugzilla::Hook::process('bug_start_of_set_all', { bug => $self, params => $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'}); - } + 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; + + # BMO - allow extensions to morph params + Bugzilla::Hook::process('bug_start_of_set_all', + {bug => $self, params => $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->{'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 (defined $params->{comment_tags} && Bugzilla->user->can_tag_comments()) { + $self->{added_comment_tags} + = ref $params->{comment_tags} + ? $params->{comment_tags} + : [$params->{comment_tags}]; + } - 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'} }); - } + my %normal_set_all; + foreach my $name (keys %$params) { - if (defined $params->{comment_tags} && Bugzilla->user->can_tag_comments()) { - $self->{added_comment_tags} = ref $params->{comment_tags} - ? $params->{comment_tags} - : [ $params->{comment_tags} ]; + # These are handled separately below. + if ($self->can("set_$name")) { + $normal_set_all{$name} = $params->{$name}; } + } + $self->SUPER::set_all(\%normal_set_all); - 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}; - } - } - $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'}; + $self->_add_remove($params, 'see_also'); - $self->_add_remove($params, 'see_also'); - - # And set custom fields. - my @custom_fields = grep { $_->type != FIELD_TYPE_EXTENSION } - 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 + = grep { $_->type != FIELD_TYPE_EXTENSION } 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, 'cc'); - # 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$//; - 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$//; + my $add_method = "add_$name"; + my $remove_method = "remove_$name"; + $self->$add_method($_) foreach @add; + $self->$remove_method($_) foreach @remove; } sub set_alias { $_[0]->set('alias', $_[1]); } + 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, $comment_id, $isprivate) = @_; + my ($self, $comment_id, $isprivate) = @_; - # We also allow people to pass in a hash of comment ids to update. - if (ref $comment_id) { - while (my ($id, $is) = each %$comment_id) { - $self->set_comment_is_private($id, $is); - } - return; - } - - my ($comment) = grep($comment_id == $_->id, @{ $self->comments }); - ThrowUserError('comment_invalid_isprivate', { id => $comment_id }) - if !$comment; - - $isprivate = $isprivate ? 1 : 0; - if ($isprivate != $comment->is_private) { - ThrowUserError('user_not_insider') if !Bugzilla->user->is_insider; - $self->{comment_isprivate} ||= []; - $comment->set_is_private($isprivate); - push @{$self->{comment_isprivate}}, $comment; - } -} -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); - } + # We also allow people to pass in a hash of comment ids to update. + if (ref $comment_id) { + while (my ($id, $is) = each %$comment_id) { + $self->set_comment_is_private($id, $is); } + return; + } + + my ($comment) = grep($comment_id == $_->id, @{$self->comments}); + ThrowUserError('comment_invalid_isprivate', {id => $comment_id}) if !$comment; + + $isprivate = $isprivate ? 1 : 0; + if ($isprivate != $comment->is_private) { + ThrowUserError('user_not_insider') if !Bugzilla->user->is_insider; + $self->{comment_isprivate} ||= []; + $comment->set_is_private($isprivate); + push @{$self->{comment_isprivate}}, $comment; + } } -sub set_custom_field { - my ($self, $field, $value) = @_; - if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { - $value = $value->[0]; +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); } - ThrowCodeError('field_not_custom', { field => $field }) if !$field->custom; - $self->set($field->name, $value); + } +} + +sub set_custom_field { + my ($self, $field, $value) = @_; + + if (ref $value eq 'ARRAY' && $field->type != FIELD_TYPE_MULTI_SELECT) { + $value = $value->[0]; + } + 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) = @_; - ($dependson, $blocked) = $self->_check_dependencies($dependson, $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) = @_; + ($dependson, $blocked) = $self->_check_dependencies($dependson, $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) = @_; - Bugzilla::Hook::process('bug_set_flags', { bug => $self, flags => $flags, new_flags => $new_flags }); - Bugzilla::Flag->set_flag($self, $_) foreach (@$flags, @$new_flags); + my ($self, $flags, $new_flags) = @_; + Bugzilla::Hook::process('bug_set_flags', + {bug => $self, flags => $flags, new_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 $invalid_groups; - my @idlist = ($self->id); - push(@idlist, map { $_->id } @{ $params->{other_bugs} }) - if $params->{other_bugs}; - @idlist = uniq @idlist; - - my $verified = $params->{product_change_confirmed}; - - # BMO - if everything is ok then we can skip the verfication page when using bug_modal - if (Bugzilla->input_params->{format} // '' eq 'modal' - && !$verified - && $component_ok - && $version_ok - && $milestone_ok - ) { - $invalid_groups = $self->get_invalid_groups({ bug_ids => \@idlist, product => $product }); - my $has_invalid_group = 0; - foreach my $group (@$invalid_groups) { - if (any { $_ eq $group->name } @{ $params->{groups}->{add} }) { - $has_invalid_group = 1; - last; - } - } - $verified = - # always check for invalid groups - !$has_invalid_group - # never skip verification when changing multiple bugs - && scalar(@idlist) == 1 - # ensure the user has seen the group ui for private bugs - && (!@{ $self->groups_in } || Bugzilla->input_params->{group_verified}); - } + my ($self, $name, $params) = @_; + my $old_product = $self->product_obj; + my $product = $self->_check_product($name); - 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 $product_changed = 0; + if ($old_product->id != $product->id) { + $self->{product_id} = $product->id; + $self->{product} = $product->name; + $self->{product_obj} = $product; - if (!$verified) { - $vars{verify_bug_groups} = 1; - $vars{old_groups} = $invalid_groups || $self->get_invalid_groups({ bug_ids => \@idlist, product => $product }); - } + # For update() + $self->{_old_product_name} = $old_product->name; - if (%vars) { - $vars{product} = $product; - $vars{bug} = $self; - require Bugzilla::Error::Template; - die Bugzilla::Error::Template->new( - file => "bug/process/verify-new-product.html.tmpl", - vars => \%vars - ); - } + # 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 { - # 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); - } + # Have to set this directly to bypass the validators. + $self->{target_milestone} = $product->default_milestone; } - 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 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 $invalid_groups; + my @idlist = ($self->id); + push(@idlist, map { $_->id } @{$params->{other_bugs}}) if $params->{other_bugs}; + @idlist = uniq @idlist; - # Make sure the bug is in all the mandatory groups for the new product. - foreach my $group (@{$product->groups_mandatory}) { - $self->add_group($group); + my $verified = $params->{product_change_confirmed}; + +# BMO - if everything is ok then we can skip the verfication page when using bug_modal + if (Bugzilla->input_params->{format} + // '' eq 'modal' && !$verified && $component_ok && $version_ok && $milestone_ok) + { + $invalid_groups + = $self->get_invalid_groups({bug_ids => \@idlist, product => $product}); + my $has_invalid_group = 0; + foreach my $group (@$invalid_groups) { + if (any { $_ eq $group->name } @{$params->{groups}->{add}}) { + $has_invalid_group = 1; + last; } + } + $verified = + + # always check for invalid groups + !$has_invalid_group + + # never skip verification when changing multiple bugs + && scalar(@idlist) == 1 + + # ensure the user has seen the group ui for private bugs + && (!@{$self->groups_in} || Bugzilla->input_params->{group_verified}); + } + + 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})]; + } + + if (!$verified) { + $vars{verify_bug_groups} = 1; + $vars{old_groups} = $invalid_groups + || $self->get_invalid_groups({bug_ids => \@idlist, product => $product}); + } + + if (%vars) { + $vars{product} = $product; + $vars{bug} = $self; + require Bugzilla::Error::Template; + die Bugzilla::Error::Template->new( + file => "bug/process/verify-new-product.html.tmpl", + vars => \%vars + ); + } + } + 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); + } + } + + 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); + } + } + + # 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; + 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); } 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 ($self, $value, $params) = @_; + + my $old_res = $self->resolution; + $self->set('resolution', $value); + delete $self->{choices}; + my $new_res = $self->resolution; - my $old_res = $self->resolution; - $self->set('resolution', $value); - delete $self->{choices}; - my $new_res = $self->resolution; + if ($new_res ne $old_res) { - 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(); - } + # 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; } -sub set_severity { $_[0]->set('bug_severity', $_[1]); } +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]); } @@ -3120,355 +3225,374 @@ 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. 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) = @_; + 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); } # 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; } # $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) = @_; - - $action ||= 'set'; - if (!grep($action eq $_, qw(add remove set))) { - $action = 'set'; - } - - $keywords = $self->_check_keywords($keywords); - - my (@result, $any_changes); - if ($action eq 'set') { - @result = @$keywords; - # Check if anything was added or removed. - my @old_ids = map { $_->id } @{$self->keyword_objects}; - my @new_ids = map { $_->id } @result; - my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids); - $any_changes = scalar @$removed || scalar @$added; + my ($self, $keywords, $action) = @_; + + $action ||= 'set'; + if (!grep($action eq $_, qw(add remove set))) { + $action = 'set'; + } + + $keywords = $self->_check_keywords($keywords); + + my (@result, $any_changes); + if ($action eq 'set') { + @result = @$keywords; + + # Check if anything was added or removed. + my @old_ids = map { $_->id } @{$self->keyword_objects}; + my @new_ids = map { $_->id } @result; + my ($removed, $added) = diff_arrays(\@old_ids, \@new_ids); + $any_changes = scalar @$removed || scalar @$added; + } + else { + # We're adding or deleting specific keywords. + my %keys = map { $_->id => $_ } @{$self->keyword_objects}; + if ($action eq 'add') { + $keys{$_->id} = $_ foreach @$keywords; } else { - # We're adding or deleting specific keywords. - my %keys = map {$_->id => $_} @{$self->keyword_objects}; - if ($action eq 'add') { - $keys{$_->id} = $_ foreach @$keywords; - } - else { - delete $keys{$_->id} foreach @$keywords; - } - @result = values %keys; - $any_changes = scalar @$keywords; + delete $keys{$_->id} foreach @$keywords; } - # Make sure we retain the sort order. - @result = sort {lc($a->name) cmp lc($b->name)} @result; + @result = values %keys; + $any_changes = scalar @$keywords; + } - 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 }); - } + # Make sure we retain the sort order. + @result = sort { lc($a->name) cmp lc($b->name) } @result; - $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); - - # BMO : allow bugs to be always placed into some groups by the bug's - # reporter, or by users with editbugs - my $user = Bugzilla->user; - if (!$self->product_obj->group_always_settable($group) - || ($self->{reporter_id} != $user->id && !$user->in_group('editbugs'))) - { - # 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 (!$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); + + # BMO : allow bugs to be always placed into some groups by the bug's + # reporter, or by users with editbugs + my $user = Bugzilla->user; + if (!$self->product_obj->group_always_settable($group) + || ($self->{reporter_id} != $user->id && !$user->in_group('editbugs'))) + { + # 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 (!$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); } 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) = @_; - - # This is needed by xt/search.t. - $input = $input->name if blessed($input); + my ($self, $input, $skip_recursion) = @_; - $input = trim($input); - return if !$input; + # This is needed by xt/search.t. + $input = $input->name if blessed($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}; } ##################################################################### @@ -3478,31 +3602,31 @@ sub tags { # These are accessors that don't need to access the database. # Keep them in alphabetical order. -sub alias { return $_[0]->{alias} } -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 alias { return $_[0]->{alias} } +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 @@ -3521,668 +3645,691 @@ 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 $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 him 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 ($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 %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 him 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 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}); - return $self->{'attachments'}; + $self->{'attachments'} + = Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1}); + 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'}; - return $self->{'assigned_to_obj'} - = new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 }); + my ($self) = @_; + return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'}; + $self->{'assigned_to'} = 0 if $self->{'error'}; + return $self->{'assigned_to_obj'} + = new Bugzilla::User({id => $self->{'assigned_to'}, cache => 1}); } 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 + ); - $self->{'cc'} = undef if !scalar(@{$self->{'cc'}}); + $self->{'cc'} = undef if !scalar(@{$self->{'cc'}}); - 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 $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 $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 $self->{classification_id} if exists $self->{classification_id}; - return 0 if $self->{error}; - ($self->{classification_id}) = Bugzilla->dbh->selectrow_array( - 'SELECT classification_id FROM products WHERE id = ?', - undef, $self->{product_id}); - return $self->{classification_id}; + my ($self) = @_; + return $self->{classification_id} if exists $self->{classification_id}; + return 0 if $self->{error}; + ($self->{classification_id}) + = Bugzilla->dbh->selectrow_array( + 'SELECT classification_id FROM products WHERE id = ?', + undef, $self->{product_id}); + return $self->{classification_id}; } sub classification { - my ($self) = @_; - return $self->{classification} if exists $self->{classification}; - return '' if $self->{error}; - ($self->{classification}) = Bugzilla->dbh->selectrow_array( - 'SELECT name FROM classifications WHERE id = ?', - undef, $self->classification_id); - return $self->{classification}; + my ($self) = @_; + return $self->{classification} if exists $self->{classification}; + return '' if $self->{error}; + ($self->{classification}) + = Bugzilla->dbh->selectrow_array( + 'SELECT name FROM classifications WHERE id = ?', + undef, $self->classification_id); + return $self->{classification}; } 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}; } 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, - active_or_has_flags => $self->bug_id }; + my $vars = { + target_type => 'bug', + product_id => $self->{product_id}, + component_id => $self->{component_id}, + bug_id => $self->bug_id, + active_or_has_flags => $self->bug_id + }; - $self->{'flag_types'} = Bugzilla::Flag->_flag_types($vars); - return $self->{'flag_types'}; + $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; - return is_open_state($self->{bug_status}) ? 1 : 0; + my $self = shift; + return is_open_state($self->{bug_status}) ? 1 : 0; } 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 has_keyword { - my ($self, $keyword) = @_; - $keyword = lc($keyword); - return any { lc($_->name) eq $keyword } @{ $self->keyword_objects }; + my ($self, $keyword) = @_; + $keyword = lc($keyword); + return any { lc($_->name) eq $keyword } @{$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; - foreach my $comment (@{ $self->{'comments'} }) { - $comment->{count} = $count++; - $comment->{bug} = $self; - weaken($comment->{bug}); - } - # Some bugs may have no comments when upgrading old installations. - Bugzilla::Comment->preload($self->{'comments'}) if @{ $self->{'comments'} }; - # BMO - for comment deletion support - Bugzilla::Hook::process('bug_comments', - { bug => $self, comments => $self->{'comments'} }); - } - return unless defined wantarray; - - 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); - } - } + my ($self, $params) = @_; + return [] if $self->{'error'}; + $params ||= {}; - if ($params->{after}) { - my $from = datetime_from($params->{after}); - @comments = grep { datetime_from($_->creation_ts) > $from } @comments; + if (!defined $self->{'comments'}) { + $self->{'comments'} = Bugzilla::Comment->match({bug_id => $self->id}); + my $count = 0; + foreach my $comment (@{$self->{'comments'}}) { + $comment->{count} = $count++; + $comment->{bug} = $self; + weaken($comment->{bug}); } - if ($params->{to}) { - my $to = datetime_from($params->{to}); - @comments = grep { datetime_from($_->creation_ts) <= $to } @comments; + + # Some bugs may have no comments when upgrading old installations. + Bugzilla::Comment->preload($self->{'comments'}) if @{$self->{'comments'}}; + + # BMO - for comment deletion support + Bugzilla::Hook::process('bug_comments', + {bug => $self, comments => $self->{'comments'}}); + } + return unless defined wantarray; + + 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); } - return \@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 comment_count { - my ($self) = @_; - return $self->{comment_count} if $self->{comment_count}; - my $dbh = Bugzilla->dbh; - return $self->{comment_count} = - $dbh->selectrow_array('SELECT COUNT(longdescs.comment_id) + my ($self) = @_; + return $self->{comment_count} if $self->{comment_count}; + my $dbh = Bugzilla->dbh; + return $self->{comment_count} = $dbh->selectrow_array( + 'SELECT COUNT(longdescs.comment_id) FROM longdescs - WHERE longdescs.bug_id = ?', - undef, $self->id); + WHERE longdescs.bug_id = ?', undef, $self->id + ); } # 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 { - # XXX - This is somewhat inconsistent with the assignee/reporter - # methods, which will return an empty User if they get a 0. - # However, we're keeping it this way now, for backwards-compatibility. - $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 { + # XXX - This is somewhat inconsistent with the assignee/reporter + # methods, which will return an empty User if they get a 0. + # However, we're keeping it this way now, for backwards-compatibility. + $self->{'qa_contact_obj'} = undef; + } + return $self->{'qa_contact_obj'}; } sub reporter { - my ($self) = @_; - return $self->{'reporter'} if exists $self->{'reporter'}; - $self->{'reporter_id'} = 0 if $self->{'error'}; - return $self->{'reporter'} - = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 }); + my ($self) = @_; + return $self->{'reporter'} if exists $self->{'reporter'}; + $self->{'reporter_id'} = 0 if $self->{'error'}; + return $self->{'reporter'} + = new Bugzilla::User({id => $self->{'reporter_id'}, cache => 1}); } 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 $self = shift; - return [] if $self->{'error'}; - return $self->{'statuses_available'} - if defined $self->{'statuses_available'}; + my $self = shift; + return [] if $self->{'error'}; + return $self->{'statuses_available'} if defined $self->{'statuses_available'}; - my @statuses = @{ $self->status->can_change_to }; + my @statuses = @{$self->status->can_change_to}; - # UNCONFIRMED is only a valid status if it is enabled in this product. - if (!$self->product_obj->allows_unconfirmed) { - @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; - } + # UNCONFIRMED is only a valid status if it is enabled in this product. + if (!$self->product_obj->allows_unconfirmed) { + @statuses = grep { $_->name ne 'UNCONFIRMED' } @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 @available; + foreach my $status (@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); - } + # 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); + } - $self->{'statuses_available'} = \@available; - return $self->{'statuses_available'}; + # 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->{'statuses_available'} = \@available; + return $self->{'statuses_available'}; } 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'}; + 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 $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'}); + + my $rows = $sth->fetchall_arrayref(); + foreach my $row (@$rows) { + my ($groupid, $name, $description, $ison, $ingroup, $isactive, $membercontrol, + $othercontrol) + = @$row; + + $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 $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'}); - - my $rows = $sth->fetchall_arrayref(); - foreach my $row (@$rows) { - my ($groupid, $name, $description, $ison, $ingroup, $isactive, - $membercontrol, $othercontrol) = @$row; - - $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)) - )) + 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, + "ison" => $ison, + "ingroup" => $ingroup, + "mandatory" => $ismandatory, + "description" => $description } - } - - # BMO: if required, hack in groups exposed by -visible membership - # (eg mozilla-employee-confidential-visible), so reporters can add the - # bug to a group on show_bug. - # if the bug is already in the group, the user will not be able to remove - # it unless they are a true group member. - my $user = Bugzilla->user; - if ($self->{'reporter_id'} == $user->id) { - foreach my $group (@{ $user->groups }) { - # map from -visible group to the real one - my $group_name = $group->name; - next unless $group_name =~ s/-visible$//; - next if $user->in_group($group_name); - $group = Bugzilla::Group->new({ name => $group_name, cache => 1 }); - - # only show the group if it's visible to normal members - my ($member_control) = $dbh->selectrow_array( - "SELECT membercontrol + ); + } + } + + # BMO: if required, hack in groups exposed by -visible membership + # (eg mozilla-employee-confidential-visible), so reporters can add the + # bug to a group on show_bug. + # if the bug is already in the group, the user will not be able to remove + # it unless they are a true group member. + my $user = Bugzilla->user; + if ($self->{'reporter_id'} == $user->id) { + foreach my $group (@{$user->groups}) { + + # map from -visible group to the real one + my $group_name = $group->name; + next unless $group_name =~ s/-visible$//; + next if $user->in_group($group_name); + $group = Bugzilla::Group->new({name => $group_name, cache => 1}); + + # only show the group if it's visible to normal members + my ($member_control) = $dbh->selectrow_array( + "SELECT membercontrol FROM groups LEFT JOIN group_control_map ON group_control_map.group_id = groups.id AND group_control_map.product_id = ? - WHERE groups.id = ?", - undef, - $self->{product_id}, $group->id - ); - - if ( - $member_control - && $member_control == CONTROLMAPSHOWN - && !grep { $_->{bit} == $group->id } @groups) - { - push(@groups, { - bit => $group->id, - name => $group->name, - ison => 0, - ingroup => 1, - mandatory => 0, - description => $group->description, - }); - } - } + WHERE groups.id = ?", undef, $self->{product_id}, $group->id + ); + + if ( $member_control + && $member_control == CONTROLMAPSHOWN + && !grep { $_->{bit} == $group->id } @groups) + { + push( + @groups, + { + bit => $group->id, + name => $group->name, + ison => 0, + ingroup => 1, + mandatory => 0, + description => $group->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 $self = shift; + return $self->{'user'} if exists $self->{'user'}; + return {} if $self->{'error'}; - my $user = Bugzilla->user; + my $user = Bugzilla->user; - my $prod_id = $self->{'product_id'}; + my $prod_id = $self->{'product_id'}; - my $unknown_privileges = $user->in_group('editbugs', $prod_id); - my $canedit = $unknown_privileges - || $user->id == $self->{'assigned_to'} - || (Bugzilla->params->{'useqacontact'} - && $self->{'qa_contact'} - && $user->id == $self->{'qa_contact'}); - my $canconfirm = $unknown_privileges - || $user->in_group('canconfirm', $prod_id); - my $isreporter = $user->id - && $user->id == $self->{reporter_id}; + my $unknown_privileges = $user->in_group('editbugs', $prod_id); + my $canedit + = $unknown_privileges + || $user->id == $self->{'assigned_to'} + || (Bugzilla->params->{'useqacontact'} + && $self->{'qa_contact'} + && $user->id == $self->{'qa_contact'}); + my $canconfirm = $unknown_privileges || $user->in_group('canconfirm', $prod_id); + my $isreporter = $user->id && $user->id == $self->{reporter_id}; - $self->{'user'} = {canconfirm => $canconfirm, - canedit => $canedit, - isreporter => $isreporter}; - return $self->{'user'}; + $self->{'user'} + = {canconfirm => $canconfirm, canedit => $canedit, isreporter => $isreporter}; + 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 $name = $self->$key; - $choices{$key} = [grep { $_->is_active || $_->name eq $name } @{ $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, + ); + + 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 $name = $self->$key; + $choices{$key} + = [grep { $_->is_active || $_->name eq $name } @{$choices{$key}}]; + } + + $self->{'choices'} = \%choices; + return $self->{'choices'}; } # Convenience Function. If you need speed, use this. If you need @@ -4191,12 +4338,12 @@ 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) = @_; - return undef unless Bugzilla->params->{"usebugaliases"}; - my $dbh = Bugzilla->dbh; - trick_taint($alias); - return $dbh->selectrow_array( - "SELECT bug_id FROM bugs WHERE alias = ?", undef, $alias); + my ($alias) = @_; + return undef unless Bugzilla->params->{"usebugaliases"}; + my $dbh = Bugzilla->dbh; + trick_taint($alias); + return $dbh->selectrow_array("SELECT bug_id FROM bugs WHERE alias = ?", + undef, $alias); } ##################################################################### @@ -4206,26 +4353,31 @@ 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); - } + 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})}); - Bugzilla::Hook::process('bug_editable_bug_fields', { fields => \@fields }); + # 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; - # Sorted because the old @::log_columns variable, which this replaces, - # was sorted. - return sort(@fields); + # Ensure field exists before attempting to remove it. + splice(@fields, $location, 1) if ($location > -1); + } + + Bugzilla::Hook::process('bug_editable_bug_fields', {fields => \@fields}); + + # 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 @@ -4233,84 +4385,86 @@ 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) = @_; - my %bug_map; - # 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; - } + my ($self, $bug_ids) = @_; + my %bug_map; + + # 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; } - my $bugs = $self->new_from_list(\@missing_ids); - foreach my $bug (@$bugs) { - $bug_map{$bug->id} = $bug; + else { + push @missing_ids, $bug_id; } - return [ map { $bug_map{$_} } @$bug_ids ]; + } + my $bugs = $self->new_from_list(\@missing_ids); + foreach my $bug (@$bugs) { + $bug_map{$bug->id} = $bug; + } + 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 GetBugActivity { - my ($bug_id, $attach_id, $starttime, $include_comment_tags) = @_; - my $dbh = Bugzilla->dbh; - - # Arguments passed to the SQL query. - my @args = ($bug_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 (!Bugzilla->user->is_insider) - { - $suppjoins = "LEFT JOIN attachments + my ($bug_id, $attach_id, $starttime, $include_comment_tags) = @_; + my $dbh = Bugzilla->dbh; + + # Arguments passed to the SQL query. + my @args = ($bug_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 (!Bugzilla->user->is_insider) { + $suppjoins = "LEFT JOIN attachments ON attachments.attach_id = bugs_activity.attach_id"; - $suppwhere = "AND COALESCE(attachments.isprivate, 0) = 0"; - } + $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, + 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 FROM bugs_activity $suppjoins @@ -4323,24 +4477,26 @@ sub GetBugActivity { $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 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, @@ -4352,213 +4508,227 @@ sub GetBugActivity { $datepart $suppwhere "; - push @args, $bug_id; - push @args, $starttime if defined $starttime; + push @args, $bug_id; + push @args, $starttime if defined $starttime; + } + + $query .= "ORDER BY bug_when, comment_id"; + + my $list = $dbh->selectall_arrayref($query, undef, @args); + + 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; + + # check if the user should see this field's activity + if ( $fieldname eq 'remaining_time' + || $fieldname eq 'estimated_time' + || $fieldname eq 'work_time' + || $fieldname eq 'deadline') + { + $activity_visible = Bugzilla->user->is_timetracker; + } + elsif ($fieldname eq 'longdescs.isprivate' + && !Bugzilla->user->is_insider + && $added) + { + $activity_visible = 0; + } + else { + $activity_visible = 1; } - $query .= "ORDER BY bug_when, comment_id"; + if ($activity_visible) { - my $list = $dbh->selectall_arrayref($query, undef, @args); + # Check for the results of an old Bugzilla data corruption bug + if ( ($added eq '?' && $removed eq '?') + || ($added =~ /^\? / || $removed =~ /^\? /)) + { + $incomplete_data = 1; + } - my @operations; - my $operation = {}; - my $changes = []; - my $incomplete_data = 0; + # 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); - foreach my $entry (@$list) { - my ($fieldname, $attachid, $when, $removed, $added, $who, $comment_id) = @$entry; - my %change; - my $activity_visible = 1; + # Create new empty anonymous data structures. + $operation = {}; + $changes = []; + } - # check if the user should see this field's activity - if ($fieldname eq 'remaining_time' - || $fieldname eq 'estimated_time' - || $fieldname eq 'work_time' - || $fieldname eq 'deadline') - { - $activity_visible = Bugzilla->user->is_timetracker; - } - elsif ($fieldname eq 'longdescs.isprivate' - && !Bugzilla->user->is_insider - && $added) - { - $activity_visible = 0; - } - else { - $activity_visible = 1; - } + # If this is the same field as the previoius item, then concatenate + # the data into the same change. + if ( $operation->{'who'} + && $who eq $operation->{'who'} + && $when eq $operation->{'when'} + && $fieldname eq $operation->{'fieldname'} + && ($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); + } - 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 previoius item, then concatenate - # the data into the same change. - if ($operation->{'who'} && $who eq $operation->{'who'} - && $when eq $operation->{'when'} - && $fieldname eq $operation->{'fieldname'} - && ($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); - } - } + $operation->{'who'} = $who; + $operation->{'when'} = $when; + $operation->{'fieldname'} = $change{'fieldname'} = $fieldname; + $operation->{'attachid'} = $change{'attachid'} = $attachid; - if ($operation->{'who'}) { - $operation->{'changes'} = $changes; - push (@operations, $operation); + $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); + return (\@operations, $incomplete_data); } sub _join_activity_entries { - my ($field, $current_change, $new_change) = @_; - # We need to insert characters as these were removed by old - # LogActivityEntry code. - - return $new_change if $current_change eq ''; - - # Buglists and see_also need the comma restored - if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { - if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { - return $current_change . $new_change; - } else { - return $current_change . ', ' . $new_change; - } - } + my ($field, $current_change, $new_change) = @_; - # Assume bug_file_loc contain a single url, don't insert a delimiter - if ($field eq 'bug_file_loc') { - return $current_change . $new_change; - } + # We need to insert characters as these were removed by old + # LogActivityEntry code. + + return $new_change if $current_change eq ''; - # All other fields get a space unless the first character of the second - # string is a comma or space + # Buglists and see_also need the comma restored + if ($field eq 'dependson' || $field eq 'blocked' || $field eq 'see_also') { if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { - return $current_change . $new_change; - } else { - return $current_change . ' ' . $new_change; + return $current_change . $new_change; + } + else { + return $current_change . ', ' . $new_change; } + } + + # Assume bug_file_loc contain a single url, don't insert a delimiter + if ($field eq 'bug_file_loc') { + return $current_change . $new_change; + } + + # All other fields get a space unless the first character of the second + # string is a comma or space + if (substr($new_change, 0, 1) eq ',' || substr($new_change, 0, 1) eq ' ') { + return $current_change . $new_change; + } + else { + return $current_change . ' ' . $new_change; + } } # Update the bugs_activity table to reflect changes made in bugs. sub LogActivityEntry { - my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id, - $attach_id) = @_; - 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); - 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($col); - $dbh->do( - "INSERT INTO bugs_activity + my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id, $attach_id) + = @_; + 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); + 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($col); + $dbh->do( + "INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)", - undef, - ($i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id, - $attach_id)); - } + undef, + ( + $i, $whoid, $timestamp, $fieldid, $removestr, $addstr, $comment_id, $attach_id + ) + ); + } } # 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 ($params, $except) = @_; - my %field_values; - foreach my $field (keys %$params) { - my $field_name; - if ($except->{$field}) { - $field_name = $field; - } - else { - $field_name = FIELD_MAP->{$field} || $field; - } - $field_values{$field_name} = $params->{$field}; + my %field_values; + foreach my $field (keys %$params) { + my $field_name; + if ($except->{$field}) { + $field_name = $field; } - return \%field_values; + else { + $field_name = FIELD_MAP->{$field} || $field; + } + $field_values{$field_name} = $params->{$field}; + } + return \%field_values; } # Return the groups which are no longer valid in the specified product sub get_invalid_groups { - my ($invocant, $params) = @_; - my @idlist = @{ $params->{bug_ids} }; - my $product = $params->{product}; - my $gids = Bugzilla->dbh->selectcol_arrayref( - 'SELECT bgm.group_id + my ($invocant, $params) = @_; + my @idlist = @{$params->{bug_ids}}; + my $product = $params->{product}; + my $gids = Bugzilla->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 @@ -4567,10 +4737,11 @@ sub get_invalid_groups { 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)); - return Bugzilla::Group->new_from_list($gids); + . Bugzilla->user->groups_as_string . ')) + OR gcm.othercontrol != ?) )', undef, + (@idlist, $product->id, CONTROLMAPNA, CONTROLMAPNA) + ); + return Bugzilla::Group->new_from_list($gids); } ################################################################################ @@ -4590,163 +4761,186 @@ sub get_invalid_groups { # $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; - - $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; + my $self = shift; + my ($field, $oldvalue, $newvalue, $PrivilegesRequired) = (@_); + my $user = Bugzilla->user; + + $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 him 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. - if ( grep($_ eq $field, TIMETRACKING_FIELDS) ) { - 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 him 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. + if (grep($_ eq $field, TIMETRACKING_FIELDS)) { + 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; } + } - # 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 him; + # 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; + } - # The reporter may not: - # - reassign bugs, unless the bugs are assigned to him; - # 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 he 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 milestone + if ($field eq 'target_milestone') { + $$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; } # @@ -4755,71 +4949,75 @@ sub _changes_everconfirmed { # 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; - 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("SELECT $target + 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; + 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( + "SELECT $target FROM dependencies - WHERE $me = ?", 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; - } - } + WHERE $me = ?", 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; } @@ -4828,60 +5026,61 @@ 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) { - next if $field->type == FIELD_TYPE_EXTENSION; - 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"}; + + my $fields = Bugzilla->fields({custom => 1}); + foreach my $field (@$fields) { + next if $field->type == FIELD_TYPE_EXTENSION; + my $accessor = $class->_accessor_for($field); + my $name = "${class}::" . $field->name; + { + no strict 'refs'; + next if defined *{$name}; + *{$name} = $accessor; } + } - Bugzilla::Hook::process('bug_create_cf_accessors'); + Bugzilla::Hook::process('bug_create_cf_accessors'); - 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); + } + 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) = @_; + $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; } sub has_attachment_with_mimetype { - my ($self, $type) = @_; - return any { $_->contenttype eq $type } @{ $self->attachments }; + my ($self, $type) = @_; + return any { $_->contenttype eq $type } @{$self->attachments}; } 1; diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm index ebfc95d51..d5c1c4c95 100644 --- a/Bugzilla/BugMail.pm +++ b/Bugzilla/BugMail.pm @@ -27,16 +27,17 @@ use List::MoreUtils qw(uniq firstidx); use Sys::Hostname; use Storable qw(dclone); -use constant BIT_DIRECT => 1; -use constant BIT_WATCHING => 2; +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; } # This is a bit of a hack, basically keeping the old system() @@ -49,505 +50,522 @@ 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'})); - } - - 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)); - } - } - my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); - - my @diffs; - my @referenced_bugs; - if (!$start) { - @diffs = _get_new_bugmail_fields($bug); - } - - if ($params->{dep_only}) { - my $fields = Bugzilla->fields({ by_name => 1 }); - push(@diffs, { field_name => 'bug_status', - field_desc => $fields->{bug_status}->description, - old => $params->{changes}->{bug_status}->[0], - new => $params->{changes}->{bug_status}->[1], - login_name => $changer->login, - blocker => $params->{blocker} }, - { field_name => 'resolution', - field_desc => $fields->{resolution}->description, - old => $params->{changes}->{resolution}->[0], - new => $params->{changes}->{resolution}->[1], - login_name => $changer->login, - blocker => $params->{blocker} }); - push(@referenced_bugs, $params->{blocker}->id); + 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'})); + } + + 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)); } - else { - my ($diffs, $referenced) = _get_diffs($bug, $end, \%user_cache); - push(@diffs, @$diffs); - push(@referenced_bugs, @$referenced); + } + my %user_cache = map { $_->id => $_ } (@assignees, @qa_contacts, @ccs); + + my @diffs; + my @referenced_bugs; + if (!$start) { + @diffs = _get_new_bugmail_fields($bug); + } + + if ($params->{dep_only}) { + my $fields = Bugzilla->fields({by_name => 1}); + push( + @diffs, + { + field_name => 'bug_status', + field_desc => $fields->{bug_status}->description, + old => $params->{changes}->{bug_status}->[0], + new => $params->{changes}->{bug_status}->[1], + login_name => $changer->login, + blocker => $params->{blocker} + }, + { + field_name => 'resolution', + field_desc => $fields->{resolution}->description, + old => $params->{changes}->{resolution}->[0], + new => $params->{changes}->{resolution}->[1], + login_name => $changer->login, + blocker => $params->{blocker} + } + ); + push(@referenced_bugs, $params->{blocker}->id); + } + else { + my ($diffs, $referenced) = _get_diffs($bug, $end, \%user_cache); + push(@diffs, @$diffs); + push(@referenced_bugs, @$referenced); + } + + my $comments = $bug->comments({after => $start, to => $end}); + + # Skip empty comments. + @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; + + # Add duplicate bug to referenced bug list + foreach my $comment (@$comments) { + if ($comment->type == CMT_DUPE_OF || $comment->type == CMT_HAS_DUPE) { + push(@referenced_bugs, $comment->extra_data); } + } - my $comments = $bug->comments({ after => $start, to => $end }); - # Skip empty comments. - @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments; + # Add dependencies to referenced bug list on new bugs + if (!$start) { + push @referenced_bugs, @{$bug->dependson}; + push @referenced_bugs, @{$bug->blocked}; + } - # Add duplicate bug to referenced bug list - foreach my $comment (@$comments) { - if ($comment->type == CMT_DUPE_OF || $comment->type == CMT_HAS_DUPE) { - push(@referenced_bugs, $comment->extra_data); - } - } + # If no changes have been made, there is no need to process further. + return {'sent' => []} unless scalar(@diffs) || scalar(@$comments); - # Add dependencies to referenced bug list on new bugs - if (!$start) { - push @referenced_bugs, @{ $bug->dependson }; - push @referenced_bugs, @{ $bug->blocked }; - } + ########################################################################### + # Start of email filtering code + ########################################################################### - # If no changes have been made, there is no need to process further. - return {'sent' => []} unless scalar(@diffs) || scalar(@$comments); + # A user_id => roles hash to keep track of people. + my %recipients; + my %watching; - ########################################################################### - # Start of email filtering code - ########################################################################### + # 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. - # A user_id => roles hash to keep track of people. - my %recipients; - my %watching; + # CCs + $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs); - # 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. + # Reporter (there's only ever one) + $recipients{$bug->reporter->id}->{+REL_REPORTER} = BIT_DIRECT; - # CCs - $recipients{$_->id}->{+REL_CC} = BIT_DIRECT foreach (@ccs); + # QA Contact + if (Bugzilla->params->{'useqacontact'}) { + foreach (@qa_contacts) { - # 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 $_; - } + # QA Contact can be blank; ignore it if so. + $recipients{$_->id}->{+REL_QA} = BIT_DIRECT if $_; } - - # 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; - } + } + + # 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; + } } - - # 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({ id => $user_id, cache => 1 }); + } + + # 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({id => $user_id, cache => 1}); + } + + Bugzilla::Hook::process( + 'bugmail_recipients', + { + bug => $bug, + recipients => \%recipients, + users => \%user_cache, + diffs => \@diffs } + ); - Bugzilla::Hook::process('bugmail_recipients', - { bug => $bug, recipients => \%recipients, - users => \%user_cache, diffs => \@diffs }); - - 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]); - } - } + if (scalar keys %recipients) { - # Global watcher - my @watchers = split(/\s*,\s*/ms, 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'); - - # Remove duplicate references, and convert to bug objects - @referenced_bugs = @{ Bugzilla::Bug->new_from_list([uniq @referenced_bugs]) }; - - foreach my $user_id (keys %recipients) { - my %rels_which_want; - my $user = $user_cache{$user_id} ||= new Bugzilla::User({ id => $user_id, cache => 1 }); - # 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}; - } - } - } + # Find all those user-watching anyone on the current list, who is not + # on it already themselves. + my $involved = join(",", keys %recipients); - 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; - } - - # Make sure the user isn't in the nomail list, and the dep check passed. - # BMO: never send emails to bugs or .tld addresses. this check needs to - # happen after the bugmail_recipients hook. - if ($user->email_enabled && $dep_ok && - ($user->login !~ /\.(?:bugs|tld)$/)) - { - # Don't show summaries for bugs the user can't access, and - # provide a hook for extensions such as SecureMail to filter - # this list. - # - # We build an array with the short_desc as a separate item to - # allow extensions to modify the summary without touching the - # bug object. - my $referenced_bugs = []; - foreach my $ref (@{ $user->visible_bugs(\@referenced_bugs) }) { - push @$referenced_bugs, { - bug => $ref, - id => $ref->id, - short_desc => $ref->short_desc, - }; - } - Bugzilla::Hook::process('bugmail_referenced_bugs', - { updated_bug => $bug, - referenced_bugs => $referenced_bugs }); - - 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, - referenced_bugs => $referenced_bugs, - dep_only => $params->{dep_only} - }); - push(@sent, $user->login) if $sent_mail; - } - } - } + my $userwatchers = $dbh->selectall_arrayref( + "SELECT watcher, watched FROM watch + WHERE watched IN ($involved)" + ); - # 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; + # 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]); } - - return {'sent' => \@sent}; -} - -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 $referenced_bugs = $params->{referenced_bugs}; - my $dep_only = $params->{dep_only}; - my $attach_id; - - # 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; + } + + # Global watcher + my @watchers = split(/\s*,\s*/ms, 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'); + + # Remove duplicate references, and convert to bug objects + @referenced_bugs = @{Bugzilla::Bug->new_from_list([uniq @referenced_bugs])}; + + foreach my $user_id (keys %recipients) { + my %rels_which_want; + my $user = $user_cache{$user_id} + ||= new Bugzilla::User({id => $user_id, cache => 1}); + + # 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}; } - push(@display_diffs, $diff) if $add_diff; - $attach_id = $diff->{attach_id} if $diff->{attach_id}; + } } - if (!$user->is_insider) { - @send_comments = grep { !$_->is_private } @send_comments; - } - - if (!scalar(@display_diffs) && !scalar(@send_comments)) { - # Whoops, no differences! - return 0; + 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; + } + + # Make sure the user isn't in the nomail list, and the dep check passed. + # BMO: never send emails to bugs or .tld addresses. this check needs to + # happen after the bugmail_recipients hook. + if ($user->email_enabled && $dep_ok && ($user->login !~ /\.(?:bugs|tld)$/)) { + + # Don't show summaries for bugs the user can't access, and + # provide a hook for extensions such as SecureMail to filter + # this list. + # + # We build an array with the short_desc as a separate item to + # allow extensions to modify the summary without touching the + # bug object. + my $referenced_bugs = []; + foreach my $ref (@{$user->visible_bugs(\@referenced_bugs)}) { + push @$referenced_bugs, + {bug => $ref, id => $ref->id, short_desc => $ref->short_desc,}; + } + Bugzilla::Hook::process('bugmail_referenced_bugs', + {updated_bug => $bug, referenced_bugs => $referenced_bugs}); + + 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, + referenced_bugs => $referenced_bugs, + dep_only => $params->{dep_only} + }); + push(@sent, $user->login) if $sent_mail; + } } + } - 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); - } + # 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; + } - 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 { user_id_to_login($_) } @$watchingRef; - - # BMO: Use field descriptions instead of field names in header - my @changedfields = uniq map { $_->{field_desc} } @display_diffs; - my @changedfieldnames = uniq map { $_->{field_name} } @display_diffs; - - # BMO: Add a field to indicate when a comment was added - if (grep($_->type != CMT_ATTACHMENT_CREATED, @send_comments)) { - push(@changedfields, 'Comment Created'); - push(@changedfieldnames, 'comment'); - } + 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, 'Attachment Created'); - push(@changedfieldnames, 'attachment.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}; + my @diffs = @{$params->{diffs}}; + my $relRef = $params->{rels_which_want}; + my $referenced_bugs = $params->{referenced_bugs}; + my $dep_only = $params->{dep_only}; + my $attach_id; + + # 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; } - - my $bugmailtype = "changed"; - $bugmailtype = "new" if !$bug->lastdiffed; - $bugmailtype = "dep_changed" if $dep_only; - - my $vars = { - date => $date, - to_user => $user, - bug => $bug, - attach_id => $attach_id, - reasons => \@reasons, - reasons_watch => \@reasons_watch, - reasonsheader => join(" ", @headerrel), - reasonswatchheader => join(" ", @watchingrel), - changer => $changer, - diffs => \@display_diffs, - changedfields => \@changedfields, - changedfieldnames => \@changedfieldnames, - new_comments => \@send_comments, - threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), - referenced_bugs => $referenced_bugs, - bugmailtype => $bugmailtype, - }; - - if (Bugzilla->get_param_with_override('use_mailer_queue')) { - enqueue($vars); - } else { - MessageToMTA(_generate_bugmail($vars)); + elsif (!$diff->{isprivate} || $user->is_insider) { + $add_diff = 1; } - - return 1; + push(@display_diffs, $diff) if $add_diff; + $attach_id = $diff->{attach_id} if $diff->{attach_id}; + } + + 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}) { + push(@reasons, $relationship) if ($bits & BIT_DIRECT); + push(@reasons_watch, $relationship) if ($bits & BIT_WATCHING); + } + + 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 { user_id_to_login($_) } @$watchingRef; + + # BMO: Use field descriptions instead of field names in header + my @changedfields = uniq map { $_->{field_desc} } @display_diffs; + my @changedfieldnames = uniq map { $_->{field_name} } @display_diffs; + + # BMO: Add a field to indicate when a comment was added + if (grep($_->type != CMT_ATTACHMENT_CREATED, @send_comments)) { + push(@changedfields, 'Comment Created'); + push(@changedfieldnames, 'comment'); + } + + # Add attachments.created to changedfields if one or more + # comments contain information about a new attachment + if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) { + push(@changedfields, 'Attachment Created'); + push(@changedfieldnames, 'attachment.created'); + } + + my $bugmailtype = "changed"; + $bugmailtype = "new" if !$bug->lastdiffed; + $bugmailtype = "dep_changed" if $dep_only; + + my $vars = { + date => $date, + to_user => $user, + bug => $bug, + attach_id => $attach_id, + reasons => \@reasons, + reasons_watch => \@reasons_watch, + reasonsheader => join(" ", @headerrel), + reasonswatchheader => join(" ", @watchingrel), + changer => $changer, + diffs => \@display_diffs, + changedfields => \@changedfields, + changedfieldnames => \@changedfieldnames, + new_comments => \@send_comments, + threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed), + referenced_bugs => $referenced_bugs, + bugmailtype => $bugmailtype, + }; + + if (Bugzilla->get_param_with_override('use_mailer_queue')) { + enqueue($vars); + } + else { + MessageToMTA(_generate_bugmail($vars)); + } + + return 1; } sub enqueue { - my ($vars) = @_; - - # BMO: allow modification of the email at the time it was generated - Bugzilla::Hook::process('bugmail_enqueue', { vars => $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}); - } - } - foreach my $reference (@{ $vars->{referenced_bugs} }) { - $reference->{bug} = _flatten_object($reference->{bug}); + my ($vars) = @_; + + # BMO: allow modification of the email at the time it was generated + Bugzilla::Hook::process('bugmail_enqueue', {vars => $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 }); + } + foreach my $reference (@{$vars->{referenced_bugs}}) { + $reference->{bug} = _flatten_object($reference->{bug}); + } + Bugzilla->job_queue->insert('bug_mail', {vars => $vars}); } 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 + $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}); } - # generate bugmail and send - MessageToMTA(_generate_bugmail($vars), 1); -} + } -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; + # generate bugmail and send + MessageToMTA(_generate_bugmail($vars), 1); } -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); - - $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 = ( - Email::MIME->create( - attributes => { - content_type => "text/plain", - }, - body => $msg_text, - ) - ); - if ($user->setting('email_format') eq 'html') { - $template->process("email/bugmail.html.tmpl", $vars, \$msg_html) - || ThrowTemplateError($template->error()); - push @parts, Email::MIME->create( - attributes => { - content_type => "text/html", - }, - body => $msg_html, - ); - } - - # TT trims the trailing newline, and threadingmarker may be ignored. - my $email = new Email::MIME("$msg_header\n"); - - # For tracking/diagnostic purposes, add our hostname - $email->header_set('X-Generated-By' => hostname()); +sub _flatten_object { + my ($object) = @_; - if (scalar(@parts) == 1) { - $email->content_type_set($parts[0]->content_type); - } else { - $email->content_type_set('multipart/alternative'); - } - $email->parts_set(\@parts); + # nothing to do if it's already flattened + return $object unless blessed($object); - # BMO: allow modification of the email given the enqueued variables - Bugzilla::Hook::process('bugmail_generate', { vars => $vars, email => $email }); + # 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; +} - return $email; +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); + + $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 = (Email::MIME->create( + attributes => {content_type => "text/plain",}, + body => $msg_text, + )); + if ($user->setting('email_format') eq 'html') { + $template->process("email/bugmail.html.tmpl", $vars, \$msg_html) + || ThrowTemplateError($template->error()); + push @parts, + Email::MIME->create( + attributes => {content_type => "text/html",}, + body => $msg_html, + ); + } + + # TT trims the trailing newline, and threadingmarker may be ignored. + my $email = new Email::MIME("$msg_header\n"); + + # For tracking/diagnostic purposes, add our hostname + $email->header_set('X-Generated-By' => hostname()); + + if (scalar(@parts) == 1) { + $email->content_type_set($parts[0]->content_type); + } + else { + $email->content_type_set('multipart/alternative'); + } + $email->parts_set(\@parts); + + # BMO: allow modification of the email given the enqueued variables + Bugzilla::Hook::process('bugmail_generate', {vars => $vars, email => $email}); + + 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, fielddefs.description AS field_desc, bugs_activity.bug_when, bugs_activity.removed AS old, bugs_activity.added AS new, bugs_activity.attach_id, @@ -557,90 +575,94 @@ sub _get_diffs { ON fielddefs.id = bugs_activity.fieldid WHERE bugs_activity.bug_id = ? $when_restriction - ORDER BY bugs_activity.bug_when, fielddefs.description", {Slice=>{}}, @args); - my $referenced_bugs = []; - - foreach my $diff (@$diffs) { - $user_cache->{$diff->{who}} ||= new Bugzilla::User({ id => $diff->{who}, cache => 1 }); - $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}; - } - elsif ($diff->{field_name} eq 'dependson' || $diff->{field_name} eq 'blocked') { - push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{old}); - push @$referenced_bugs, grep { /^\d+$/ } split(/[\s,]+/, $diff->{new}); - } + ORDER BY bugs_activity.bug_when, fielddefs.description", {Slice => {}}, + @args + ); + my $referenced_bugs = []; + + foreach my $diff (@$diffs) { + $user_cache->{$diff->{who}} + ||= new Bugzilla::User({id => $diff->{who}, cache => 1}); + $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}; } + elsif ($diff->{field_name} eq 'dependson' || $diff->{field_name} eq 'blocked') { + push @$referenced_bugs, grep {/^\d+$/} split(/[\s,]+/, $diff->{old}); + push @$referenced_bugs, grep {/^\d+$/} split(/[\s,]+/, $diff->{new}); + } + } - return ($diffs, $referenced_bugs); + return ($diffs, $referenced_bugs); } sub _get_new_bugmail_fields { - my $bug = shift; - my @fields = @{ Bugzilla->fields({obsolete => 0, in_new_bugmail => 1}) }; - my @diffs; - - # Show fields in the same order as the DEFAULT_FIELDS list, which mirrors - # 4.0's behavour and provides sane grouping of similar fields. - # Any additional fields are sorted by descrsiption - my @prepend; - foreach my $name (map { $_->{name} } Bugzilla::Field::DEFAULT_FIELDS) { - my $idx = firstidx { $_->name eq $name } @fields; - if ($idx != -1) { - push(@prepend, $fields[$idx]); - splice(@fields, $idx, 1); - } + my $bug = shift; + my @fields = @{Bugzilla->fields({obsolete => 0, in_new_bugmail => 1})}; + my @diffs; + + # Show fields in the same order as the DEFAULT_FIELDS list, which mirrors + # 4.0's behavour and provides sane grouping of similar fields. + # Any additional fields are sorted by descrsiption + my @prepend; + foreach my $name (map { $_->{name} } Bugzilla::Field::DEFAULT_FIELDS) { + my $idx = firstidx { $_->name eq $name } @fields; + if ($idx != -1) { + push(@prepend, $fields[$idx]); + splice(@fields, $idx, 1); } - @fields = sort { $a->description cmp $b->description } @fields; - @fields = (@prepend, @fields); - - foreach my $field (@fields) { - my $name = $field->name; - my $value = $bug->$name; - - if (ref $value eq 'ARRAY') { - my @new_values; - foreach my $item (@$value) { - if (blessed($item) && $item->isa('Bugzilla::User')) { - push(@new_values, $item->login); - } - else { - push(@new_values, $item); - } - } - $value = join(', ', @new_values); - } - elsif (blessed($value) && $value->isa('Bugzilla::User')) { - $value = $value->login; - } - elsif (blessed($value) && $value->isa('Bugzilla::Object')) { - $value = $value->name; + } + @fields = sort { $a->description cmp $b->description } @fields; + @fields = (@prepend, @fields); + + foreach my $field (@fields) { + my $name = $field->name; + my $value = $bug->$name; + + if (ref $value eq 'ARRAY') { + my @new_values; + foreach my $item (@$value) { + if (blessed($item) && $item->isa('Bugzilla::User')) { + push(@new_values, $item->login); } - 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; + else { + push(@new_values, $item); } - elsif ($name eq 'deadline') { - $value = time2str("%Y-%m-%d", str2time($value)) if $value; - } - - # If there isn't anything to show, don't include this header. - next unless $value; + } + $value = join(', ', @new_values); + } + 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, - field_desc => $field->description, - 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; + + push(@diffs, + {field_name => $name, field_desc => $field->description, new => $value}); + } + + return @diffs; } 1; diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm index a824d286d..e6c68416c 100644 --- a/Bugzilla/BugUrl.pm +++ b/Bugzilla/BugUrl.pm @@ -27,55 +27,56 @@ 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::Chromium - Bugzilla::BugUrl::Edge - Bugzilla::BugUrl::Debian - Bugzilla::BugUrl::JIRA - Bugzilla::BugUrl::Trac - Bugzilla::BugUrl::MantisBT - Bugzilla::BugUrl::SourceForge - Bugzilla::BugUrl::GitHub - Bugzilla::BugUrl::MozSupport - Bugzilla::BugUrl::Aha - Bugzilla::BugUrl::WebCompat - Bugzilla::BugUrl::ServiceNow - Bugzilla::BugUrl::Splat + Bugzilla::BugUrl::Bugzilla::Local + Bugzilla::BugUrl::Bugzilla + Bugzilla::BugUrl::Launchpad + Bugzilla::BugUrl::Google + Bugzilla::BugUrl::Chromium + Bugzilla::BugUrl::Edge + Bugzilla::BugUrl::Debian + Bugzilla::BugUrl::JIRA + Bugzilla::BugUrl::Trac + Bugzilla::BugUrl::MantisBT + Bugzilla::BugUrl::SourceForge + Bugzilla::BugUrl::GitHub + Bugzilla::BugUrl::MozSupport + Bugzilla::BugUrl::Aha + Bugzilla::BugUrl::WebCompat + Bugzilla::BugUrl::ServiceNow + Bugzilla::BugUrl::Splat ); ############################### #### Accessors ###### ############################### -sub class { return $_[0]->{class} } +sub class { return $_[0]->{class} } sub bug_id { return $_[0]->{bug_id} } ############################### @@ -83,125 +84,120 @@ 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) { - require_module($object->class); - bless $object, $object->class; - } + foreach my $object (@$objects) { + require_module($object->class); + bless $object, $object->class; + } - return $objects + 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 ($class, $value) = @_; - my $uri = URI->new($value); - foreach my $subclass ($class->SUB_CLASSES) { - require_module($subclass); - return wantarray ? ($subclass, $uri) : $subclass - if $subclass->should_handle($uri); - } + my $uri = URI->new($value); + foreach my $subclass ($class->SUB_CLASSES) { + require_module($subclass); + return wantarray ? ($subclass, $uri) : $subclass + if $subclass->should_handle($uri); + } - ThrowUserError('bug_url_invalid', { url => $value, - reason => 'show_bug' }); + ThrowUserError('bug_url_invalid', {url => $value, reason => 'show_bug'}); } sub _check_class { - my ($class, $subclass) = @_; - require_module($subclass); - return $subclass; + my ($class, $subclass) = @_; + require_module($subclass); + 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; diff --git a/Bugzilla/BugUrl/Aha.pm b/Bugzilla/BugUrl/Aha.pm index b467c54d8..cc733ae13 100644 --- a/Bugzilla/BugUrl/Aha.pm +++ b/Bugzilla/BugUrl/Aha.pm @@ -18,28 +18,28 @@ use base qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - return $uri =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!; + return $uri =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!; } sub get_feature_id { - my ($self) = @_; + my ($self) = @_; - if ($self->{value} =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!) { - return $1; - } + if ($self->{value} =~ m!^https?://[^.]+\.aha\.io/features/(\w+-\d+)!) { + return $1; + } } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - # Aha HTTP URLs redirect to HTTPS, so just use the HTTPS scheme. - $uri->scheme('https'); + # Aha HTTP URLs redirect to HTTPS, so just use the HTTPS scheme. + $uri->scheme('https'); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Bugzilla.pm b/Bugzilla/BugUrl/Bugzilla.pm index 3af6d0a54..1cedb1f56 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 14d03f048..73086524e 100644 --- a/Bugzilla/BugUrl/Bugzilla/Local.pm +++ b/Bugzilla/BugUrl/Bugzilla/Local.pm @@ -20,83 +20,80 @@ 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'); - } - - my $product = $ref_bug->product_obj; - if (!Bugzilla->user->can_edit_product($product->id)) { - ThrowUserError("product_edit_denied", - { product => $product->name }); - } - - 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'); + } + + my $product = $ref_bug->product_obj; + if (!Bugzilla->user->can_edit_product($product->id)) { + ThrowUserError("product_edit_denied", {product => $product->name}); + } + + return $uri; } sub local_uri { - my ($self, $bug_id) = @_; - $bug_id ||= ''; - return Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$bug_id"; + my ($self, $bug_id) = @_; + $bug_id ||= ''; + return Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$bug_id"; } 1; diff --git a/Bugzilla/BugUrl/Chromium.pm b/Bugzilla/BugUrl/Chromium.pm index 5560df24c..2d4fcd178 100644 --- a/Bugzilla/BugUrl/Chromium.pm +++ b/Bugzilla/BugUrl/Chromium.pm @@ -21,29 +21,30 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->authority =~ /^bugs.chromium.org$/i) ? 1 : 0; + my ($class, $uri) = @_; + return ($uri->authority =~ /^bugs.chromium.org$/i) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); - - my $value = $uri->as_string; - my $project_name; - if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) { - $project_name = $1; - } else { - ThrowUserError('bug_url_invalid', { url => $value }); - } - my $bug_id = $uri->query_param('id'); - detaint_natural($bug_id); - if (!$bug_id) { - ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' }); - } - - return URI->new($value); + my ($class, $uri) = @_; + + $uri = $class->SUPER::_check_value($uri); + + my $value = $uri->as_string; + my $project_name; + if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) { + $project_name = $1; + } + else { + ThrowUserError('bug_url_invalid', {url => $value}); + } + my $bug_id = $uri->query_param('id'); + detaint_natural($bug_id); + if (!$bug_id) { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'}); + } + + return URI->new($value); } 1; diff --git a/Bugzilla/BugUrl/Debian.pm b/Bugzilla/BugUrl/Debian.pm index e018c1106..88b808382 100644 --- a/Bugzilla/BugUrl/Debian.pm +++ b/Bugzilla/BugUrl/Debian.pm @@ -20,33 +20,33 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->authority =~ /^bugs.debian.org$/i) ? 1 : 0; + my ($class, $uri) = @_; + return ($uri->authority =~ /^bugs.debian.org$/i) ? 1 : 0; } sub _check_value { - my $class = shift; - - my $uri = $class->SUPER::_check_value(@_); - - # Debian BTS URLs can look like various things: - # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234 - # http://bugs.debian.org/1234 - my $bug_id; - if ($uri->path =~ m|^/(\d+)$|) { - $bug_id = $1; - } - elsif ($uri->path =~ /bugreport\.cgi$/) { - $bug_id = $uri->query_param('bug'); - detaint_natural($bug_id); - } - if (!$bug_id) { - ThrowUserError('bug_url_invalid', - { url => $uri->path, reason => 'id' }); - } - # This is the shortest standard URL form for Debian BTS URLs, - # and so we reduce all URLs to this. - return new URI("http://bugs.debian.org/" . $bug_id); + my $class = shift; + + my $uri = $class->SUPER::_check_value(@_); + + # Debian BTS URLs can look like various things: + # http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1234 + # http://bugs.debian.org/1234 + my $bug_id; + if ($uri->path =~ m|^/(\d+)$|) { + $bug_id = $1; + } + elsif ($uri->path =~ /bugreport\.cgi$/) { + $bug_id = $uri->query_param('bug'); + detaint_natural($bug_id); + } + if (!$bug_id) { + ThrowUserError('bug_url_invalid', {url => $uri->path, reason => 'id'}); + } + + # This is the shortest standard URL form for Debian BTS URLs, + # and so we reduce all URLs to this. + return new URI("http://bugs.debian.org/" . $bug_id); } 1; diff --git a/Bugzilla/BugUrl/Edge.pm b/Bugzilla/BugUrl/Edge.pm index 95d24c93a..53145dda9 100644 --- a/Bugzilla/BugUrl/Edge.pm +++ b/Bugzilla/BugUrl/Edge.pm @@ -26,19 +26,21 @@ use List::MoreUtils qw( any ); # https://wpdev.uservoice.com/forums/257854/suggestions/17420707 # https://wpdev.uservoice.com/forums/257854-microsoft-edge-developer/suggestions/17420707-implement-css-display-flow-root-modern-clearfi sub should_handle { - my ($class, $uri) = @_; - return any { lc($uri->authority) eq $_ } qw( developer.microsoft.com wpdev.uservoice.com ); + my ($class, $uri) = @_; + return any { lc($uri->authority) eq $_ } + qw( developer.microsoft.com wpdev.uservoice.com ); } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - return $uri if $uri->path =~ m{^/en-us/microsoft-edge/platform/issues/\d+/$}; - return $uri if $uri->path =~ m{^/forums/\d+(?:-[^/]+)?/suggestions/\d+(?:-[^/]+)?}; + return $uri if $uri->path =~ m{^/en-us/microsoft-edge/platform/issues/\d+/$}; + return $uri + if $uri->path =~ m{^/forums/\d+(?:-[^/]+)?/suggestions/\d+(?:-[^/]+)?}; - ThrowUserError('bug_url_invalid', { url => "$uri" }); + ThrowUserError('bug_url_invalid', {url => "$uri"}); } 1; diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm index 1a0219617..a81564812 100644 --- a/Bugzilla/BugUrl/GitHub.pm +++ b/Bugzilla/BugUrl/GitHub.pm @@ -18,25 +18,25 @@ use base 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 6c8a2f27d..d36f4eb62 100644 --- a/Bugzilla/BugUrl/Google.pm +++ b/Bugzilla/BugUrl/Google.pm @@ -20,35 +20,41 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->authority =~ /^code.google.com$/i) ? 1 : 0; + my ($class, $uri) = @_; + return ($uri->authority =~ /^code.google.com$/i) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); - - my $value = $uri->as_string; - # Google Code URLs only have one form: - # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234 - my $project_name; - if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) { - $project_name = $1; - } else { - ThrowUserError('bug_url_invalid', { url => $value }); - } - my $bug_id = $uri->query_param('id'); - detaint_natural($bug_id); - if (!$bug_id) { - ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' }); - } - # While Google Code URLs can be either HTTP or HTTPS, - # always go with the HTTP scheme, as that's the default. - $value = "http://code.google.com/p/" . $project_name . - "/issues/detail?id=" . $bug_id; - - return new URI($value); + my ($class, $uri) = @_; + + $uri = $class->SUPER::_check_value($uri); + + my $value = $uri->as_string; + + # Google Code URLs only have one form: + # http(s)://code.google.com/p/PROJECT_NAME/issues/detail?id=1234 + my $project_name; + if ($uri->path =~ m|^/p/([^/]+)/issues/detail$|) { + $project_name = $1; + } + else { + ThrowUserError('bug_url_invalid', {url => $value}); + } + my $bug_id = $uri->query_param('id'); + detaint_natural($bug_id); + if (!$bug_id) { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'}); + } + + # While Google Code URLs can be either HTTP or HTTPS, + # always go with the HTTP scheme, as that's the default. + $value + = "http://code.google.com/p/" + . $project_name + . "/issues/detail?id=" + . $bug_id; + + return new URI($value); } 1; diff --git a/Bugzilla/BugUrl/JIRA.pm b/Bugzilla/BugUrl/JIRA.pm index ba4b0e51b..a6cf75e93 100644 --- a/Bugzilla/BugUrl/JIRA.pm +++ b/Bugzilla/BugUrl/JIRA.pm @@ -20,25 +20,26 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->path =~ m|/browse/[A-Z][A-Z]+-\d+$|) ? 1 : 0; + my ($class, $uri) = @_; + 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(@_); - # 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 + # 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 - # 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/BugUrl/Launchpad.pm b/Bugzilla/BugUrl/Launchpad.pm index a56fed4ad..4b3bb3b0d 100644 --- a/Bugzilla/BugUrl/Launchpad.pm +++ b/Bugzilla/BugUrl/Launchpad.pm @@ -19,30 +19,32 @@ use Bugzilla::Error; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->authority =~ /launchpad.net$/) ? 1 : 0; + my ($class, $uri) = @_; + return ($uri->authority =~ /launchpad.net$/) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; - - $uri = $class->SUPER::_check_value($uri); - - my $value = $uri->as_string; - # 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" - if ($uri->path =~ m|bugs?/(\d+)$|) { - # This is the shortest standard URL form for Launchpad bugs, - # and so we reduce all URLs to this. - $value = "https://launchpad.net/bugs/$1"; - } - else { - ThrowUserError('bug_url_invalid', { url => $value, reason => 'id' }); - } - - return new URI($value); + my ($class, $uri) = @_; + + $uri = $class->SUPER::_check_value($uri); + + my $value = $uri->as_string; + + # 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" + if ($uri->path =~ m|bugs?/(\d+)$|) { + + # This is the shortest standard URL form for Launchpad bugs, + # and so we reduce all URLs to this. + $value = "https://launchpad.net/bugs/$1"; + } + else { + ThrowUserError('bug_url_invalid', {url => $value, reason => 'id'}); + } + + return new URI($value); } 1; diff --git a/Bugzilla/BugUrl/MantisBT.pm b/Bugzilla/BugUrl/MantisBT.pm index 48284c7e0..9cf49cdb8 100644 --- a/Bugzilla/BugUrl/MantisBT.pm +++ b/Bugzilla/BugUrl/MantisBT.pm @@ -20,22 +20,22 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->path_query =~ m|view\.php\?id=\d+$|) ? 1 : 0; + my ($class, $uri) = @_; + 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(@_); - # MantisBT URLs look like the following ('bugs' directory is optional): - # http://www.mantisbt.org/bugs/view.php?id=1234 + # MantisBT URLs look like the following ('bugs' directory is optional): + # http://www.mantisbt.org/bugs/view.php?id=1234 - # 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/MozSupport.pm b/Bugzilla/BugUrl/MozSupport.pm index c2442e4df..b924f3f53 100644 --- a/Bugzilla/BugUrl/MozSupport.pm +++ b/Bugzilla/BugUrl/MozSupport.pm @@ -18,23 +18,23 @@ use base qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # Mozilla support questions normally have the form: - # https://support.mozilla.org//questions/ - return ($uri->authority =~ /^support.mozilla.org$/i - and $uri->path =~ m|^(/[^/]+)?/questions/\d+$|) ? 1 : 0; + # Mozilla support questions normally have the form: + # https://support.mozilla.org//questions/ + return ($uri->authority =~ /^support.mozilla.org$/i + and $uri->path =~ m|^(/[^/]+)?/questions/\d+$|) ? 1 : 0; } sub _check_value { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); + $uri = $class->SUPER::_check_value($uri); - # Support.mozilla.org redirects to https automatically - $uri->scheme('https'); + # Support.mozilla.org redirects to https automatically + $uri->scheme('https'); - return $uri; + return $uri; } 1; diff --git a/Bugzilla/BugUrl/ServiceNow.pm b/Bugzilla/BugUrl/ServiceNow.pm index 8e30aa45e..d65106a90 100644 --- a/Bugzilla/BugUrl/ServiceNow.pm +++ b/Bugzilla/BugUrl/ServiceNow.pm @@ -14,15 +14,15 @@ use warnings; use base qw(Bugzilla::BugUrl); sub should_handle { - my ($class, $uri) = @_; - return $uri =~ m#^https?://[^.]+\.service-now\.com/nav_to\.do\?#; + my ($class, $uri) = @_; + return $uri =~ m#^https?://[^.]+\.service-now\.com/nav_to\.do\?#; } sub _check_value { - my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); - $uri->scheme('https'); - return $uri; + my ($class, $uri) = @_; + $uri = $class->SUPER::_check_value($uri); + $uri->scheme('https'); + return $uri; } 1; diff --git a/Bugzilla/BugUrl/SourceForge.pm b/Bugzilla/BugUrl/SourceForge.pm index 3c8dfd51d..5e106880c 100644 --- a/Bugzilla/BugUrl/SourceForge.pm +++ b/Bugzilla/BugUrl/SourceForge.pm @@ -20,29 +20,32 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->authority =~ /^sourceforge.net$/i - and $uri->path =~ m|/tracker/|) ? 1 : 0; + my ($class, $uri) = @_; + return ($uri->authority =~ /^sourceforge.net$/i and $uri->path =~ m|/tracker/|) + ? 1 + : 0; } sub _check_value { - my $class = shift; - - my $uri = $class->SUPER::_check_value(@_); - - # SourceForge tracker URLs have only one form: - # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111 - if ($uri->query_param('func') eq 'detail' and $uri->query_param('aid') - and $uri->query_param('group_id') and $uri->query_param('atid')) - { - # Remove any # part if there is one. - $uri->fragment(undef); - return $uri; - } - else { - my $value = $uri->as_string; - ThrowUserError('bug_url_invalid', { url => $value }); - } + my $class = shift; + + my $uri = $class->SUPER::_check_value(@_); + + # SourceForge tracker URLs have only one form: + # http://sourceforge.net/tracker/?func=detail&aid=111&group_id=111&atid=111 + if ( $uri->query_param('func') eq 'detail' + and $uri->query_param('aid') + and $uri->query_param('group_id') + and $uri->query_param('atid')) + { + # Remove any # part if there is one. + $uri->fragment(undef); + return $uri; + } + else { + my $value = $uri->as_string; + ThrowUserError('bug_url_invalid', {url => $value}); + } } 1; diff --git a/Bugzilla/BugUrl/Splat.pm b/Bugzilla/BugUrl/Splat.pm index 49b2b762f..da471f96d 100644 --- a/Bugzilla/BugUrl/Splat.pm +++ b/Bugzilla/BugUrl/Splat.pm @@ -14,15 +14,15 @@ use warnings; use base qw(Bugzilla::BugUrl); sub should_handle { - my ($class, $uri) = @_; - return $uri =~ m#^https?://hellosplat\.com/s/beanbag/tickets/\d+#; + my ($class, $uri) = @_; + return $uri =~ m#^https?://hellosplat\.com/s/beanbag/tickets/\d+#; } sub _check_value { - my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); - $uri->scheme('https'); # force https - return $uri; + my ($class, $uri) = @_; + $uri = $class->SUPER::_check_value($uri); + $uri->scheme('https'); # force https + return $uri; } 1; diff --git a/Bugzilla/BugUrl/Trac.pm b/Bugzilla/BugUrl/Trac.pm index 600b31105..61b1e99f2 100644 --- a/Bugzilla/BugUrl/Trac.pm +++ b/Bugzilla/BugUrl/Trac.pm @@ -20,25 +20,26 @@ use Bugzilla::Util; ############################### sub should_handle { - my ($class, $uri) = @_; - return ($uri->path =~ m|/ticket/\d+$|) ? 1 : 0; + my ($class, $uri) = @_; + 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(@_); - # Trac URLs can look like various things: - # http://dev.mutt.org/trac/ticket/1234 - # http://trac.roundcube.net/ticket/1484130 + # Trac URLs can look like various things: + # http://dev.mutt.org/trac/ticket/1234 + # http://trac.roundcube.net/ticket/1484130 - # 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/BugUrl/WebCompat.pm b/Bugzilla/BugUrl/WebCompat.pm index bd66dcae7..0ee8fb638 100644 --- a/Bugzilla/BugUrl/WebCompat.pm +++ b/Bugzilla/BugUrl/WebCompat.pm @@ -18,22 +18,22 @@ use base qw(Bugzilla::BugUrl); ############################### sub should_handle { - my ($class, $uri) = @_; + my ($class, $uri) = @_; - # https://webcompat.com/issues/1111 - my $host = lc($uri->authority); - return - ($host eq 'webcompat.com' || $host eq 'www.webcompat.com') - && $uri->path =~ m#^/issues/\d+$#; + # https://webcompat.com/issues/1111 + my $host = lc($uri->authority); + return ($host eq 'webcompat.com' || $host eq 'www.webcompat.com') + && $uri->path =~ m#^/issues/\d+$#; } sub _check_value { - my ($class, $uri) = @_; - $uri = $class->SUPER::_check_value($uri); - # force https and drop www from host - $uri->scheme('https'); - $uri->authority('webcompat.com'); - return $uri; + my ($class, $uri) = @_; + $uri = $class->SUPER::_check_value($uri); + + # force https and drop www from host + $uri->scheme('https'); + $uri->authority('webcompat.com'); + return $uri; } 1; diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm index f40ea17d3..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 4be384b67..f47ba734c 100644 --- a/Bugzilla/CGI.pm +++ b/Bugzilla/CGI.pm @@ -25,43 +25,49 @@ use File::Basename; use URI; BEGIN { - if (ON_WINDOWS) { - # Help CGI find the correct temp directory as the default list - # isn't Windows friendly (Bug 248988) - $ENV{'TMPDIR'} = $ENV{'TEMP'} || $ENV{'TMP'} || "$ENV{'WINDIR'}\\TEMP"; - } - *AUTOLOAD = \&CGI::AUTOLOAD; + if (ON_WINDOWS) { + + # Help CGI find the correct temp directory as the default list + # isn't Windows friendly (Bug 248988) + $ENV{'TMPDIR'} = $ENV{'TEMP'} || $ENV{'TMP'} || "$ENV{'WINDIR'}\\TEMP"; + } + *AUTOLOAD = \&CGI::AUTOLOAD; } sub DEFAULT_CSP { - my %policy = ( - default_src => [ 'self' ], - script_src => [ 'self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com' ], - frame_src => [ 'none', ], - worker_src => [ 'none', ], - img_src => [ 'self', 'blob:', 'https://secure.gravatar.com' ], - style_src => [ 'self', 'unsafe-inline' ], - object_src => [ 'none' ], - connect_src => [ - 'self', - # This is for extensions/GoogleAnalytics using beacon or XHR - 'https://www.google-analytics.com', - # This is from extensions/OrangeFactor/web/js/orange_factor.js - 'https://treeherder.mozilla.org/api/failurecount/', - ], - form_action => [ - 'self', - # used in template/en/default/search/search-google.html.tmpl - 'https://www.google.com/search' - ], - frame_ancestors => [ 'none' ], - report_only => 1, - ); - if (Bugzilla->params->{github_client_id} && !Bugzilla->user->id) { - push @{$policy{form_action}}, 'https://github.com/login/oauth/authorize', 'https://github.com/login'; - } - - return %policy; + my %policy = ( + default_src => ['self'], + script_src => + ['self', 'nonce', 'unsafe-inline', 'https://www.google-analytics.com'], + frame_src => ['none',], + worker_src => ['none',], + img_src => ['self', 'blob:', 'https://secure.gravatar.com'], + style_src => ['self', 'unsafe-inline'], + object_src => ['none'], + connect_src => [ + 'self', + + # This is for extensions/GoogleAnalytics using beacon or XHR + 'https://www.google-analytics.com', + + # This is from extensions/OrangeFactor/web/js/orange_factor.js + 'https://treeherder.mozilla.org/api/failurecount/', + ], + form_action => [ + 'self', + + # used in template/en/default/search/search-google.html.tmpl + 'https://www.google.com/search' + ], + frame_ancestors => ['none'], + report_only => 1, + ); + if (Bugzilla->params->{github_client_id} && !Bugzilla->user->id) { + push @{$policy{form_action}}, 'https://github.com/login/oauth/authorize', + 'https://github.com/login'; + } + + return %policy; } # Because show_bug code lives in many different .cgi files, @@ -69,364 +75,394 @@ sub DEFAULT_CSP { # normally the policy would just live in one .cgi file. # Additionally, Bugzilla->localconfig->{urlbase} cannot be called at compile time, so this can't be a constant. sub SHOW_BUG_MODAL_CSP { - my ($bug_id) = @_; - my %policy = ( - script_src => ['self', 'nonce', 'unsafe-inline', 'unsafe-eval', 'https://www.google-analytics.com' ], - img_src => [ 'self', 'https://secure.gravatar.com' ], - connect_src => [ - 'self', - # This is for extensions/GoogleAnalytics using beacon or XHR - 'https://www.google-analytics.com', - # This is from extensions/OrangeFactor/web/js/orange_factor.js - 'https://treeherder.mozilla.org/api/failurecount/', - ], - frame_src => [ 'self', ], - worker_src => [ 'none', ], - ); - if (use_attachbase() && $bug_id) { - my $attach_base = Bugzilla->localconfig->{'attachment_base'}; - $attach_base =~ s/\%bugid\%/$bug_id/g; - push @{ $policy{img_src} }, $attach_base; - } + my ($bug_id) = @_; + my %policy = ( + script_src => [ + 'self', 'nonce', + 'unsafe-inline', 'unsafe-eval', + 'https://www.google-analytics.com' + ], + img_src => ['self', 'https://secure.gravatar.com'], + connect_src => [ + 'self', + + # This is for extensions/GoogleAnalytics using beacon or XHR + 'https://www.google-analytics.com', + + # This is from extensions/OrangeFactor/web/js/orange_factor.js + 'https://treeherder.mozilla.org/api/failurecount/', + ], + frame_src => ['self',], + worker_src => ['none',], + ); + if (use_attachbase() && $bug_id) { + my $attach_base = Bugzilla->localconfig->{'attachment_base'}; + $attach_base =~ s/\%bugid\%/$bug_id/g; + push @{$policy{img_src}}, $attach_base; + } - return %policy; + return %policy; } sub _init_bz_cgi_globals { - my $invocant = shift; - # We need to disable output buffering - see bug 179174 - $| = 1; - - # 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; + + # We need to disable output buffering - see bug 179174 + $| = 1; + + # 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) + ); } BEGIN { __PACKAGE__->_init_bz_cgi_globals() if i_am_cgi(); } 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(); - - 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 = $self->path_info) { - my @whitelist = ("rest.cgi"); - Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist }); - if (!grep($_ eq $script, @whitelist)) { - # apache collapses // to / in $ENV{PATH_INFO} but not in $self->path_info. - # url() requires the full path in ENV in order to generate the correct url. - $ENV{PATH_INFO} = $path; - DEBUG("redirecting because we see PATH_INFO and don't like it"); - print $self->redirect($self->url(-path => 0, -query => 1)); - exit; - } + 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(); + + 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 = $self->path_info) { + my @whitelist = ("rest.cgi"); + Bugzilla::Hook::process('path_info_whitelist', {whitelist => \@whitelist}); + if (!grep($_ eq $script, @whitelist)) { + + # apache collapses // to / in $ENV{PATH_INFO} but not in $self->path_info. + # url() requires the full path in ENV in order to generate the correct url. + $ENV{PATH_INFO} = $path; + DEBUG("redirecting because we see PATH_INFO and don't like it"); + print $self->redirect($self->url(-path => 0, -query => 1)); + exit; } + } - # Send appropriate charset - $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : ''); + # Send appropriate charset + $self->charset(Bugzilla->params->{'utf8'} ? 'UTF-8' : ''); - # Redirect to urlbase if we are not viewing an attachment. - if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { - DEBUG("Redirecting to urlbase because the url is in the attachment base and not attachment.cgi"); - $self->redirect_to_urlbase(); - } + # Redirect to urlbase if we are not viewing an attachment. + if ($self->url_is_attachment_base and $script ne 'attachment.cgi') { + DEBUG( + "Redirecting to urlbase because the url is in the attachment base and not 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"; - } + # Check for errors + # All of the Bugzilla code wants to do this, so do it here instead of + # in each script - return $self; + 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; } sub target_uri { - my ($self) = @_; - - my $base = Bugzilla->localconfig->{urlbase}; - if (my $request_uri = $self->request_uri) { - my $base_uri = URI->new($base); - $base_uri->path(''); - $base_uri->query(undef); - return $base_uri . $request_uri; - } - else { - return $base . ($self->url(-relative => 1, -query => 1) || 'index.cgi'); - } + my ($self) = @_; + + my $base = Bugzilla->localconfig->{urlbase}; + if (my $request_uri = $self->request_uri) { + my $base_uri = URI->new($base); + $base_uri->path(''); + $base_uri->query(undef); + return $base_uri . $request_uri; + } + else { + return $base . ($self->url(-relative => 1, -query => 1) || 'index.cgi'); + } } sub content_security_policy { - my ($self, %add_params) = @_; - if (%add_params || !$self->{Bugzilla_csp}) { - my %params = DEFAULT_CSP; - delete $params{report_only} if %add_params && !$add_params{report_only}; - foreach my $key (keys %add_params) { - if (defined $add_params{$key}) { - $params{$key} = $add_params{$key}; - } - else { - delete $params{$key}; - } - } - $self->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params); + my ($self, %add_params) = @_; + if (%add_params || !$self->{Bugzilla_csp}) { + my %params = DEFAULT_CSP; + delete $params{report_only} if %add_params && !$add_params{report_only}; + foreach my $key (keys %add_params) { + if (defined $add_params{$key}) { + $params{$key} = $add_params{$key}; + } + else { + delete $params{$key}; + } } + $self->{Bugzilla_csp} = Bugzilla::CGI::ContentSecurityPolicy->new(%params); + } - return $self->{Bugzilla_csp}; + return $self->{Bugzilla_csp}; } sub csp_nonce { - my ($self) = @_; + my ($self) = @_; - my $csp = $self->content_security_policy; - return $csp->has_nonce ? $csp->nonce : ''; + my $csp = $self->content_security_policy; + return $csp->has_nonce ? $csp->nonce : ''; } # 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); + my $esc_key = url_quote($key); - push(@parameters, "$esc_key=$esc_value"); - } - } - } + foreach my $value ($self->param($key)) { - return join("&", @parameters); -} + # Omit params with an empty value + if (defined($value) && $value ne '') { + my $esc_value = url_quote($value); -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"); - } - - # 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); } - - # 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'); - - # 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'); + } + + # 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")) { + foreach my $field (qw(type assigned_to reporter qa_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'); + + # 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'); + } } 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; - } + 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; } + } - return 0; + return 0; } # Overwrite to ensure nph doesn't get set, and unset HEADERS_ONCE sub multipart_init { - my $self = shift; - - # Keys are case-insensitive, map to lowercase - my %args = @_; - my %param; - foreach my $key (keys %args) { - $param{lc $key} = $args{$key}; - } - - # Set the MIME boundary and content-type - my $boundary = $param{'-boundary'} - || '------- =_' . generate_random_password(16); - delete $param{'-boundary'}; - $self->{'separator'} = "\r\n--$boundary\r\n"; - $self->{'final_separator'} = "\r\n--$boundary--\r\n"; - $param{'-type'} = CGI::SERVER_PUSH($boundary); - - # Note: CGI.pm::multipart_init up to v3.04 explicitly set nph to 0 - # CGI.pm::multipart_init v3.05 explicitly sets nph to 1 - # CGI.pm's header() sets nph according to a param or $CGI::NPH, which - # is the desired behaviour. - - return $self->header( - %param, - ) . "WARNING: YOUR BROWSER DOESN'T SUPPORT THIS SERVER-PUSH TECHNOLOGY." . $self->multipart_end; + my $self = shift; + + # Keys are case-insensitive, map to lowercase + my %args = @_; + my %param; + foreach my $key (keys %args) { + $param{lc $key} = $args{$key}; + } + + # Set the MIME boundary and content-type + my $boundary + = $param{'-boundary'} || '------- =_' . generate_random_password(16); + delete $param{'-boundary'}; + $self->{'separator'} = "\r\n--$boundary\r\n"; + $self->{'final_separator'} = "\r\n--$boundary--\r\n"; + $param{'-type'} = CGI::SERVER_PUSH($boundary); + + # Note: CGI.pm::multipart_init up to v3.04 explicitly set nph to 0 + # CGI.pm::multipart_init v3.05 explicitly sets nph to 1 + # CGI.pm's header() sets nph according to a param or $CGI::NPH, which + # is the desired behaviour. + + return + $self->header(%param,) + . "WARNING: YOUR BROWSER DOESN'T SUPPORT THIS SERVER-PUSH TECHNOLOGY." + . $self->multipart_end; } # Have to add the cookies in. sub multipart_start { - my $self = shift; + my $self = shift; - my %args = @_; + 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(); - } + # CGI.pm::multipart_start doesn't honour its own charset information, so + # we do it ourselves here + if (defined $self->charset() && defined $args{-type}) { - 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; + # 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); - - if ($self->{_multipart_in_progress}) { - print $self->multipart_end(); - print $self->multipart_start(-type => $contenttype); - } - else { - print $self->header($contenttype); - } + 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); + } + else { + print $self->header($contenttype); + } } 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) = @_; - state $safe_content_type_re = qr{ + my ($self, $headers) = @_; + state $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 @@ -440,12 +476,13 @@ sub _prevent_unsafe_response { # used for HTTP push responses | multipart/x-mixed-replace) }sx; - state $safe_referer_re = do { - # Note that urlbase must end with a /. - # It almost certainly does, but let's be extra careful. - my $urlbase = Bugzilla->localconfig->{urlbase}; - $urlbase =~ s{/$}{}; - qr{ + state $safe_referer_re = do { + + # Note that urlbase must end with a /. + # It almost certainly does, but let's be extra careful. + my $urlbase = Bugzilla->localconfig->{urlbase}; + $urlbase =~ s{/$}{}; + qr{ # Begins with literal urlbase ^ (*COMMIT) \Q$urlbase\E @@ -453,396 +490,420 @@ sub _prevent_unsafe_response { (?: / | $ ) }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"; - } - 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; + + # 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"; + } + exit; } + } } sub should_block_referrer { - my ($self) = @_; - return length($self->self_url) > 8000; + my ($self) = @_; + return length($self->self_url) > 8000; } # 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 = @_; + 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 + ); + } + + # We generate a cookie and store it in the request cache + # To initiate github login, a form POSTs to github.cgi with the + # github_secret as a parameter. It must match the github_secret cookie. + # this prevents some types of redirection attacks. + unless ($user->id || $self->{bz_redirecting}) { + $self->send_cookie( + -name => 'github_secret', + -value => Bugzilla->github_secret, + -httponly => 1 + ); + } + + # Add the cookies in if we have any + if (scalar(@{$self->{Bugzilla_cookie_list}})) { + $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; + } + + # 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; + } - $self->_prevent_unsafe_response(\%headers); + # Add X-Frame-Options header to prevent framing and subsequent + # possible clickjacking problems. + unless ($self->url_is_attachment_base) { + $headers{'-x_frame_options'} = 'SAMEORIGIN'; + } - if ($self->{'_content_disp'}) { - $headers{'-content_disposition'} = $self->{'_content_disp'}; - } + 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}; + # 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'; - $self->send_cookie(-name => 'Bugzilla_login_request_cookie', - -value => generate_random_password(), - -httponly => 1, - %args); - } - - # We generate a cookie and store it in the request cache - # To initiate github login, a form POSTs to github.cgi with the - # github_secret as a parameter. It must match the github_secret cookie. - # this prevents some types of redirection attacks. - unless ($user->id || $self->{bz_redirecting}) { - $self->send_cookie(-name => 'github_secret', - -value => Bugzilla->github_secret, - -httponly => 1); - } - # Add the cookies in if we have any - if (scalar(@{$self->{Bugzilla_cookie_list}})) { - $headers{'-cookie'} = $self->{Bugzilla_cookie_list}; - } + # 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 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; - } + Bugzilla::Hook::process('cgi_headers', {cgi => $self, headers => \%headers}); + $self->{_header_done} = 1; - # Add X-Frame-Options header to prevent framing and subsequent - # possible clickjacking problems. - unless ($self->url_is_attachment_base) { - $headers{'-x_frame_options'} = 'SAMEORIGIN'; + if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + if ($self->should_block_referrer) { + $headers{'-referrer_policy'} = 'origin'; } - - if ($self->{'_content_disp'}) { - $headers{'-content_disposition'} = $self->{'_content_disp'}; + my $csp = $self->content_security_policy; + if (defined $csp && !$csp->disable) { + $csp->add_cgi_headers(\%headers); } - # 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'; - - # 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'; - - Bugzilla::Hook::process('cgi_headers', - { cgi => $self, headers => \%headers } + my @fonts = ( + "skins/standard/fonts/FiraMono-Regular.woff2?v=3.202", + "skins/standard/fonts/FiraSans-Bold.woff2?v=4.203", + "skins/standard/fonts/FiraSans-Italic.woff2?v=4.203", + "skins/standard/fonts/FiraSans-Regular.woff2?v=4.203", + "skins/standard/fonts/FiraSans-SemiBold.woff2?v=4.203", + "skins/standard/fonts/MaterialIcons-Regular.woff2", + ); + $headers{'-link'} = join( + ", ", + map { + sprintf('; rel="preload"; as="font"', Bugzilla->VERSION, $_) + } @fonts ); - $self->{_header_done} = 1; - - if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - if ($self->should_block_referrer) { - $headers{'-referrer_policy'} = 'origin'; - } - my $csp = $self->content_security_policy; - if (defined $csp && !$csp->disable) { - $csp->add_cgi_headers(\%headers) - } - - my @fonts = ( - "skins/standard/fonts/FiraMono-Regular.woff2?v=3.202", - "skins/standard/fonts/FiraSans-Bold.woff2?v=4.203", - "skins/standard/fonts/FiraSans-Italic.woff2?v=4.203", - "skins/standard/fonts/FiraSans-Regular.woff2?v=4.203", - "skins/standard/fonts/FiraSans-SemiBold.woff2?v=4.203", - "skins/standard/fonts/MaterialIcons-Regular.woff2", - ); - $headers{'-link'} = join(", ", map { sprintf('; rel="preload"; as="font"', Bugzilla->VERSION, $_) } @fonts); - if (Bugzilla->params->{google_analytics_tracking_id}) { - $headers{'-link'} .= ', ; rel="preconnect"; crossorigin'; - } + if (Bugzilla->params->{google_analytics_tracking_id}) { + $headers{'-link'} + .= ', ; rel="preconnect"; crossorigin'; + } + } + my $headers = $self->SUPER::header(%headers) || ''; + if ($self->server_software eq 'Bugzilla::Quantum::CGI') { + my $c = $Bugzilla::Quantum::CGI::C; + $c->res->headers->parse($headers); + my $status = $c->res->headers->status; + if ($status && $status =~ /^([0-9]+)/) { + $c->res->code($1); } - my $headers = $self->SUPER::header(%headers) || ''; - if ($self->server_software eq 'Bugzilla::Quantum::CGI') { - my $c = $Bugzilla::Quantum::CGI::C; - $c->res->headers->parse($headers); - my $status = $c->res->headers->status; - if ($status && $status =~ /^([0-9]+)/) { - $c->res->code($1); - } - elsif ($c->res->headers->location) { - $c->res->code(302); - } - else { - $c->res->code(200); - } - return ''; + elsif ($c->res->headers->location) { + $c->res->code(302); } else { - LOGDIE("Bugzilla::CGI->header() should only be called from inside Bugzilla::Quantum::CGI!"); + $c->res->code(200); } + return ''; + } + else { + LOGDIE( + "Bugzilla::CGI->header() should only be called from inside Bugzilla::Quantum::CGI!" + ); + } } sub param { - my $self = shift; - - # We don't let CGI.pm warn about list context, but we do it ourselves. - local $CGI::LIST_CONTEXT_WARN = 0; - if (0) { - state $has_warned = {}; - - ## no critic (Freenode::Wantarray) - if ( wantarray && @_ ) { - my ( $package, $filename, $line ) = caller; - if ( $package ne 'CGI' && ! $has_warned->{"$filename:$line"}++) { - WARN("Bugzilla::CGI::param called in list context from $package $filename:$line"); - } - } - ## use critic - } - - # 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') - { - # Some servers fail to set the QUERY_STRING parameter, which - # causes undef issues - $ENV{'QUERY_STRING'} = '' unless exists $ENV{'QUERY_STRING'}; - @result = $self->SUPER::url_param(@_); - } - - # Fix UTF-8-ness of input parameters. - if (Bugzilla->params->{'utf8'}) { - @result = map { _fix_utf8($_) } @result; - } - - return wantarray ? @result : $result[0]; + my $self = shift; + + # We don't let CGI.pm warn about list context, but we do it ourselves. + local $CGI::LIST_CONTEXT_WARN = 0; + if (0) { + state $has_warned = {}; + + ## no critic (Freenode::Wantarray) + if (wantarray && @_) { + my ($package, $filename, $line) = caller; + if ($package ne 'CGI' && !$has_warned->{"$filename:$line"}++) { + WARN( + "Bugzilla::CGI::param called in list context from $package $filename:$line"); + } } - # 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') + ## use critic + } + + # 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; + # Some servers fail to set the QUERY_STRING parameter, which + # causes undef issues + $ENV{'QUERY_STRING'} = '' unless exists $ENV{'QUERY_STRING'}; + @result = $self->SUPER::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 _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, %paramhash) = @_; + my ($self, %paramhash) = @_; - # Complain if -value is not given or empty (bug 268146). - if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { - ThrowCodeError('cookies_need_value'); - } + # Complain if -value is not given or empty (bug 268146). + if (!exists($paramhash{'-value'}) || !$paramhash{'-value'}) { + ThrowCodeError('cookies_need_value'); + } - # Add the default path and the domain in. - state $uri = URI->new( Bugzilla->localconfig->{urlbase} ); - $paramhash{'-path'} = $uri->path; - # we don't set the domain. - $paramhash{'-secure'} = 1 - if lc( $uri->scheme ) eq 'https'; + # Add the default path and the domain in. + state $uri = URI->new(Bugzilla->localconfig->{urlbase}); + $paramhash{'-path'} = $uri->path; - $paramhash{'-samesite'} = 'Lax'; + # we don't set the domain. + $paramhash{'-secure'} = 1 if lc($uri->scheme) eq 'https'; - push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(%paramhash)); + $paramhash{'-samesite'} = 'Lax'; + + push(@{$self->{'Bugzilla_cookie_list'}}, $self->cookie(%paramhash)); } # 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' + ); } # To avoid infinite redirection recursion, track when we're within a redirect # request. sub redirect { - my $self = shift; - $self->{bz_redirecting} = 1; - return $self->SUPER::redirect(@_); + my $self = shift; + $self->{bz_redirecting} = 1; + return $self->SUPER::redirect(@_); } use Bugzilla::Logging; + # 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; - $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; - # 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) { - DEBUG("Redirecting search url"); - print $self->redirect(-url => $self_url); - exit; + # 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; + } + + $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); + } + + # 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) { + DEBUG("Redirecting search url"); + print $self->redirect(-url => $self_url); + exit; + } } sub redirect_to_https { - my $self = shift; - my $urlbase = Bugzilla->localconfig->{'urlbase'}; - - # 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 = $urlbase . $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. - DEBUG("Redirecting to https"); - print $self->redirect(-location => $url, -status => 301); - exit; + my $self = shift; + my $urlbase = Bugzilla->localconfig->{'urlbase'}; + + # 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 + = $urlbase . $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. + DEBUG("Redirecting to https"); + print $self->redirect(-location => $url, -status => 301); + 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' => Bugzilla->localconfig->{urlbase} . $path); - exit; + my $self = shift; + my $path = $self->url('-path_info' => 1, '-query' => 1, '-relative' => 1); + print $self->redirect('-location' => Bugzilla->localconfig->{urlbase} . $path); + exit; } sub url_is_attachment_base { - my ($self, $id) = @_; - return 0 if !use_attachbase() or !i_am_cgi(); - my $attach_base = Bugzilla->localconfig->{'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->localconfig->{'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; } 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; } ########################## @@ -852,30 +913,30 @@ sub set_dated_content_disp { # 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 # 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; diff --git a/Bugzilla/CGI/ContentSecurityPolicy.pm b/Bugzilla/CGI/ContentSecurityPolicy.pm index 50a399cdc..557a896ab 100644 --- a/Bugzilla/CGI/ContentSecurityPolicy.pm +++ b/Bugzilla/CGI/ContentSecurityPolicy.pm @@ -17,123 +17,125 @@ use Type::Utils; use Bugzilla::Util qw(generate_random_password); -my $SRC_KEYWORD = enum['none', 'self', 'unsafe-inline', 'unsafe-eval', 'nonce']; +my $SRC_KEYWORD + = enum ['none', 'self', 'unsafe-inline', 'unsafe-eval', 'nonce']; my $SRC_URI = declare as Str, where { - $_ =~ m{ + $_ =~ m{ ^(?: https?:// )? # optional http:// or https:// [*A-Za-z0-9.-]+ # hostname including wildcards. Possibly too permissive. (?: :[0-9]+ )? # optional port }x; }; -my $SRC = $SRC_KEYWORD | $SRC_URI; -my $SOURCE_LIST = ArrayRef[$SRC]; -my $REFERRER_KEYWORD = enum [qw( +my $SRC = $SRC_KEYWORD | $SRC_URI; +my $SOURCE_LIST = ArrayRef [$SRC]; +my $REFERRER_KEYWORD = enum [ + qw( no-referrer no-referrer-when-downgrade origin origin-when-cross-origin unsafe-url -)]; + ) +]; my @ALL_BOOL = qw( sandbox upgrade_insecure_requests ); -my @ALL_SRC = qw( - default_src worker_src connect_src - font_src img_src media_src - object_src script_src style_src - frame_src frame_ancestors form_action +my @ALL_SRC = qw( + default_src worker_src connect_src + font_src img_src media_src + object_src script_src style_src + frame_src frame_ancestors form_action ); -has \@ALL_SRC => ( is => 'ro', isa => $SOURCE_LIST, predicate => 1 ); -has \@ALL_BOOL => ( is => 'ro', isa => Bool, default => 0 ); -has 'report_uri' => ( is => 'ro', isa => Str, predicate => 1 ); -has 'base_uri' => ( is => 'ro', isa => Str, predicate => 1 ); -has 'report_only' => ( is => 'ro', isa => Bool ); -has 'referrer' => ( is => 'ro', isa => $REFERRER_KEYWORD, predicate => 1 ); -has 'value' => ( is => 'lazy' ); -has 'nonce' => ( is => 'lazy', init_arg => undef, predicate => 1 ); -has 'disable' => ( is => 'ro', isa => Bool, default => 0 ); +has \@ALL_SRC => (is => 'ro', isa => $SOURCE_LIST, predicate => 1); +has \@ALL_BOOL => (is => 'ro', isa => Bool, default => 0); +has 'report_uri' => (is => 'ro', isa => Str, predicate => 1); +has 'base_uri' => (is => 'ro', isa => Str, predicate => 1); +has 'report_only' => (is => 'ro', isa => Bool); +has 'referrer' => (is => 'ro', isa => $REFERRER_KEYWORD, predicate => 1); +has 'value' => (is => 'lazy'); +has 'nonce' => (is => 'lazy', init_arg => undef, predicate => 1); +has 'disable' => (is => 'ro', isa => Bool, default => 0); sub _has_directive { - my ($self, $directive) = @_; - my $method = 'has_' . $directive; - return $self->$method; + my ($self, $directive) = @_; + my $method = 'has_' . $directive; + return $self->$method; } sub header_names { - my ($self) = @_; - my @names = ('Content-Security-Policy'); - if ($self->report_only) { - return map { $_ . '-Report-Only' } @names; - } - else { - return @names; - } + my ($self) = @_; + my @names = ('Content-Security-Policy'); + if ($self->report_only) { + return map { $_ . '-Report-Only' } @names; + } + else { + return @names; + } } sub add_cgi_headers { - my ($self, $headers) = @_; - return if $self->disable; - foreach my $name ($self->header_names) { - $headers->{"-$name"} = $self->value; - } + my ($self, $headers) = @_; + return if $self->disable; + foreach my $name ($self->header_names) { + $headers->{"-$name"} = $self->value; + } } sub _build_value { - my $self = shift; - my @result; - - my @list_directives = (@ALL_SRC); - my @boolean_directives = (@ALL_BOOL); - my @single_directives = qw(report_uri base_uri); - - foreach my $directive (@list_directives) { - next unless $self->_has_directive($directive); - my @values = map { $self->_quote($_) } @{ $self->$directive }; - if (@values) { - push @result, join(' ', _name($directive), @values); - } + my $self = shift; + my @result; + + my @list_directives = (@ALL_SRC); + my @boolean_directives = (@ALL_BOOL); + my @single_directives = qw(report_uri base_uri); + + foreach my $directive (@list_directives) { + next unless $self->_has_directive($directive); + my @values = map { $self->_quote($_) } @{$self->$directive}; + if (@values) { + push @result, join(' ', _name($directive), @values); } + } - foreach my $directive (@single_directives) { - next unless $self->_has_directive($directive); - my $value = $self->$directive; - if (defined $value) { - push @result, _name($directive) . ' ' . $value; - } + foreach my $directive (@single_directives) { + next unless $self->_has_directive($directive); + my $value = $self->$directive; + if (defined $value) { + push @result, _name($directive) . ' ' . $value; } + } - foreach my $directive (@boolean_directives) { - if ($self->$directive) { - push @result, _name($directive); - } + foreach my $directive (@boolean_directives) { + if ($self->$directive) { + push @result, _name($directive); } + } - return join('; ', @result); + return join('; ', @result); } sub _build_nonce { - return generate_random_password(48); + return generate_random_password(48); } sub _name { - my $name = shift; - $name =~ tr/_/-/; - return $name; + my $name = shift; + $name =~ tr/_/-/; + return $name; } sub _quote { - my ($self, $val) = @_; - - if ($val eq 'nonce') { - return q{'nonce-} . $self->nonce . q{'}; - } - elsif ($SRC_KEYWORD->check($val)) { - return qq{'$val'}; - } - else { - return $val; - } + my ($self, $val) = @_; + + if ($val eq 'nonce') { + return q{'nonce-} . $self->nonce . q{'}; + } + elsif ($SRC_KEYWORD->check($val)) { + return qq{'$val'}; + } + else { + return $val; + } } - 1; __END__ diff --git a/Bugzilla/CPAN.pm b/Bugzilla/CPAN.pm index 1b6fb93b9..96a7cee05 100644 --- a/Bugzilla/CPAN.pm +++ b/Bugzilla/CPAN.pm @@ -15,107 +15,110 @@ use Bugzilla::Constants qw(bz_locations); use Bugzilla::Install::Requirements qw(check_cpan_feature); BEGIN { - my $json_xs_ok = eval { - require JSON::XS; - require JSON; - JSON->VERSION("2.5"); - 1; - }; - if ($json_xs_ok) { - $ENV{PERL_JSON_BACKEND} = 'JSON::XS'; - } + my $json_xs_ok = eval { + require JSON::XS; + require JSON; + JSON->VERSION("2.5"); + 1; + }; + if ($json_xs_ok) { + $ENV{PERL_JSON_BACKEND} = 'JSON::XS'; + } } use constant _CAN_HAS_FEATURE => eval { - require CPAN::Meta::Prereqs; - require CPAN::Meta::Requirements; - require Module::Metadata; - require Module::Runtime; - CPAN::Meta::Prereqs->VERSION('2.132830'); - CPAN::Meta::Requirements->VERSION('2.121'); - Module::Metadata->VERSION('1.000019'); - 1; + require CPAN::Meta::Prereqs; + require CPAN::Meta::Requirements; + require Module::Metadata; + require Module::Runtime; + CPAN::Meta::Prereqs->VERSION('2.132830'); + CPAN::Meta::Requirements->VERSION('2.121'); + Module::Metadata->VERSION('1.000019'); + 1; }; my (%FEATURE, %FEATURE_LOADED); sub cpan_meta { - my ($class) = @_; - my $dir = bz_locations()->{libpath}; - my $file = File::Spec->catfile($dir, 'MYMETA.json'); - state $CPAN_META; - - return $CPAN_META if $CPAN_META; - - if (-f $file) { - open my $meta_fh, '<', $file or die "unable to open $file: $!"; - my $str = do { local $/ = undef; scalar <$meta_fh> }; - # detaint - $str =~ /^(.+)$/s; $str = $1; - close $meta_fh; - - return $CPAN_META = CPAN::Meta->load_json_string($str); - } - else { - require Bugzilla::Error; - Bugzilla::Error::ThrowCodeError('cpan_meta_missing'); - } + my ($class) = @_; + my $dir = bz_locations()->{libpath}; + my $file = File::Spec->catfile($dir, 'MYMETA.json'); + state $CPAN_META; + + return $CPAN_META if $CPAN_META; + + if (-f $file) { + open my $meta_fh, '<', $file or die "unable to open $file: $!"; + my $str = do { local $/ = undef; scalar <$meta_fh> }; + + # detaint + $str =~ /^(.+)$/s; + $str = $1; + close $meta_fh; + + return $CPAN_META = CPAN::Meta->load_json_string($str); + } + else { + require Bugzilla::Error; + Bugzilla::Error::ThrowCodeError('cpan_meta_missing'); + } } sub cpan_requirements { - my ($class, $prereqs) = @_; - if ($prereqs->can('merged_requirements')) { - return $prereqs->merged_requirements( [ 'configure', 'runtime' ], ['requires'] ); - } - else { - my $req = CPAN::Meta::Requirements->new; - $req->add_requirements( $prereqs->requirements_for('configure', 'requires') ); - $req->add_requirements( $prereqs->requirements_for('runtime', 'requires') ); - return $req; - } + my ($class, $prereqs) = @_; + if ($prereqs->can('merged_requirements')) { + return $prereqs->merged_requirements(['configure', 'runtime'], ['requires']); + } + else { + my $req = CPAN::Meta::Requirements->new; + $req->add_requirements($prereqs->requirements_for('configure', 'requires')); + $req->add_requirements($prereqs->requirements_for('runtime', 'requires')); + return $req; + } } sub has_feature { - my ($class, $feature_name) = @_; + my ($class, $feature_name) = @_; - return 0 unless _CAN_HAS_FEATURE; - return $FEATURE{$feature_name} if exists $FEATURE{ $feature_name }; + return 0 unless _CAN_HAS_FEATURE; + return $FEATURE{$feature_name} if exists $FEATURE{$feature_name}; - my $meta = $class->cpan_meta; - my $feature = eval { $meta->feature($feature_name) }; - unless ($feature) { - require Bugzilla::Error; - Bugzilla::Error::ThrowCodeError('invalid_feature', { feature => $feature_name }); - } + my $meta = $class->cpan_meta; + my $feature = eval { $meta->feature($feature_name) }; + unless ($feature) { + require Bugzilla::Error; + Bugzilla::Error::ThrowCodeError('invalid_feature', {feature => $feature_name}); + } - return $FEATURE{$feature_name} = check_cpan_feature($feature)->{ok}; + return $FEATURE{$feature_name} = check_cpan_feature($feature)->{ok}; } # Bugzilla expects this will also load all the modules.. so we have to do that. # Later we should put a deprecation warning here, and favor calling has_feature(). sub feature { - my ($class, $feature_name) = @_; - return 0 unless _CAN_HAS_FEATURE; - return 1 if $FEATURE_LOADED{$feature_name}; - return 0 unless $class->has_feature($feature_name); - - my $meta = $class->cpan_meta; - my $feature = $meta->feature($feature_name); - my @modules = $feature->prereqs->merged_requirements(['runtime'], ['requires'])->required_modules; - Module::Runtime::require_module($_) foreach grep { !/^Test::Taint$/ } @modules; - return $FEATURE_LOADED{$feature_name} = 1; + my ($class, $feature_name) = @_; + return 0 unless _CAN_HAS_FEATURE; + return 1 if $FEATURE_LOADED{$feature_name}; + return 0 unless $class->has_feature($feature_name); + + my $meta = $class->cpan_meta; + my $feature = $meta->feature($feature_name); + my @modules = $feature->prereqs->merged_requirements(['runtime'], ['requires']) + ->required_modules; + Module::Runtime::require_module($_) foreach grep { !/^Test::Taint$/ } @modules; + return $FEATURE_LOADED{$feature_name} = 1; } sub preload_features { - my ($class) = @_; - return 0 unless _CAN_HAS_FEATURE; - my $meta = $class->cpan_meta; - - foreach my $feature ($meta->features) { - next if $feature->identifier eq 'mod_perl'; - $class->feature($feature->identifier); - } + my ($class) = @_; + return 0 unless _CAN_HAS_FEATURE; + my $meta = $class->cpan_meta; + + foreach my $feature ($meta->features) { + next if $feature->identifier eq 'mod_perl'; + $class->feature($feature->identifier); + } } 1; diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm index 9dce19eb9..c9aa1f2dd 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; + my $invocant = shift; + my $class = ref($invocant) || $invocant; - # Create a ref to an empty hash and bless it - my $self = {}; - bless($self, $class); + # 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 \($#_\)($_)"); - } + 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); - } + 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 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}}); - } + # Store all the labels + if ($param =~ /^label(\d+)$/) { + $self->{'labels'}[$1] = $cgi->param($param); } - - # 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 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 = @_; + 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]); - } + 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 = @_; + 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 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; + # We can't add less than two things. + return if scalar(@line_ids) < 2; - my @series; - my $label = ""; - my $biggestlength = 0; + 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]}; + # 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); - } + 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]; - } + # 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); + } } + # We are done with the series making up this line, go to the next one + $line_index++; + } - # Add the x-axis labels into the data structure - my $date_progression = generateDateProgression($datefrom, $dateto); - unshift(@data, $date_progression); + # calculate maximum y value + if ($self->{'cumulate'}) { - if ($self->{'gt'}) { - # Add Grand Total to label list - push(@{$self->{'labels'}}, $self->{'labelgt'}); - - $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; + + while ($datefrom < $dateto) { + push(@progression, time2str("%Y-%m-%d", $datefrom)); + $datefrom += $oneday; + } - return \@progression; + return \@progression; } sub dump { - my $self = shift; + my $self = shift; - # Make sure we've read in our data - my $data = $self->data; + # Make sure we've read in our data + my $data = $self->data; - require Data::Dumper; - print "
Bugzilla::Chart object:\n";
-    print html_quote(Data::Dumper::Dumper($self));
-    print "
"; + require Data::Dumper; + print "
Bugzilla::Chart object:\n";
+  print html_quote(Data::Dumper::Dumper($self));
+  print "
"; } 1; diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm index a931767d2..a0bcaa477 100644 --- a/Bugzilla/Classification.pm +++ b/Bugzilla/Classification.pm @@ -25,26 +25,26 @@ use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object); 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, }; ############################### @@ -52,29 +52,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(); } @@ -83,38 +85,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; } ##################################### @@ -123,41 +128,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'}; } ############################### @@ -165,7 +174,7 @@ sub products { ############################### sub description { return $_[0]->{'description'}; } -sub sortkey { return $_[0]->{'sortkey'}; } +sub sortkey { return $_[0]->{'sortkey'}; } 1; diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm index f9a6f7d3a..50bab3fec 100644 --- a/Bugzilla/Comment.pm +++ b/Bugzilla/Comment.pm @@ -35,47 +35,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'], }; with 'Bugzilla::Elastic::Role::ChildObject'; @@ -83,20 +84,20 @@ with 'Bugzilla::Elastic::Role::ChildObject'; use constant ES_TYPE => 'comment'; use constant ES_PARENT_CLASS => 'Bugzilla::Bug'; -sub ES_OBJECTS_AT_ONCE { 50 } +sub ES_OBJECTS_AT_ONCE {50} sub ES_PROPERTIES { - return { - body => { type => "string", analyzer => 'bz_text_analyzer' }, - is_private => { type => "boolean" }, - tags => { type => "string" }, - }; + return { + body => {type => "string", analyzer => 'bz_text_analyzer'}, + is_private => {type => "boolean"}, + tags => {type => "string"}, + }; } sub ES_SELECT_UPDATED_SQL { - my ($class, $mtime) = @_; + my ($class, $mtime) = @_; - my $sql = q{ + my $sql = q{ SELECT DISTINCT comment_id FROM @@ -113,22 +114,19 @@ sub ES_SELECT_UPDATED_SQL { WHERE change_when > FROM_UNIXTIME(?) }; - return ($sql, [$mtime, $mtime]); + return ($sql, [$mtime, $mtime]); } sub es_parent_id { - my ($self) = @_; + my ($self) = @_; - return $self->ES_PARENT_CLASS->ES_TYPE . '_' . $self->bug_id, + return $self->ES_PARENT_CLASS->ES_TYPE . '_' . $self->bug_id,; } sub es_document { - my ($self) = @_; + my ($self) = @_; - return { - body => $self->body, - is_private => $self->is_private, - }; + return {body => $self->body, is_private => $self->is_private,}; } ######################### @@ -136,92 +134,95 @@ sub es_document { ######################### 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 - 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 + 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) . " - GROUP BY comment_id"); - foreach my $row (@$rows) { - $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ]; - } - foreach my $comment (@$comments) { - $comment->{tags} //= []; - } + GROUP BY comment_id" + ); + foreach my $row (@$rows) { + $comment_map{$row->[0]}->{tags} = [split(/,/, $row->[1])]; + } + foreach my $comment (@$comments) { + $comment->{tags} //= []; + } } ############################### @@ -229,136 +230,140 @@ 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) = @_; - return [] unless Bugzilla->params->{'comment_taggers_group'}; - $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref( - "SELECT tag + my ($self) = @_; + return [] unless Bugzilla->params->{'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) = @_; - return $self->{collapsed} if exists $self->{collapsed}; - return 0 unless Bugzilla->params->{'comment_taggers_group'}; - $self->{collapsed} = 0; - Bugzilla->request_cache->{comment_tags_collapsed} - ||= [ split(/\s*,\s*/, lc(Bugzilla->params->{'collapsed_comment_tags'})) ]; - my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} }; - my @reason; - foreach my $my_tag (map { lc } @{ $self->tags }) { - foreach my $collapsed_tag (@collapsed_tags) { - push @reason, $my_tag if $my_tag eq $collapsed_tag; - } - } - if (@reason) { - $self->{collapsed} = 1; - $self->{collapsed_reason} = join(', ', sort @reason); + my ($self) = @_; + return $self->{collapsed} if exists $self->{collapsed}; + return 0 unless Bugzilla->params->{'comment_taggers_group'}; + $self->{collapsed} = 0; + Bugzilla->request_cache->{comment_tags_collapsed} + ||= [split(/\s*,\s*/, lc(Bugzilla->params->{'collapsed_comment_tags'}))]; + my @collapsed_tags = @{Bugzilla->request_cache->{comment_tags_collapsed}}; + my @reason; + foreach my $my_tag (map {lc} @{$self->tags}) { + + foreach my $collapsed_tag (@collapsed_tags) { + push @reason, $my_tag if $my_tag eq $collapsed_tag; } - return $self->{collapsed}; + } + if (@reason) { + $self->{collapsed} = 1; + $self->{collapsed_reason} = join(', ', sort @reason); + } + return $self->{collapsed}; } sub collapsed_reason { - my ($self) = @_; - return 0 unless $self->collapsed; - return $self->{collapsed_reason}; + my ($self) = @_; + return 0 unless $self->collapsed; + return $self->{collapsed_reason}; } sub bug { - my $self = shift; - require Bugzilla::Bug; - my $bug = $self->{bug} ||= new Bugzilla::Bug($self->bug_id); - weaken($self->{bug}) unless isweak($self->{bug}); - return $bug; + my $self = shift; + require Bugzilla::Bug; + 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; - return $self->{'author'} - ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 }); + my $self = shift; + return $self->{'author'} + ||= new Bugzilla::User({id => $self->{'who'}, cache => 1}); } 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 ]; - Bugzilla::Hook::process("comment_after_add_tag", - { comment => $self, tag => $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]; + 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); - Bugzilla::Hook::process("comment_after_remove_tag", - { comment => $self, tag => $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); + Bugzilla::Hook::process("comment_after_remove_tag", + {comment => $self, tag => $tag}); } ############## @@ -366,168 +371,166 @@ 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) = @_; + my ($invocant, $thetext) = @_; - ThrowCodeError('param_required',{function => 'Bugzilla::Comment->create', - param => 'thetext'}) unless defined $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. + # 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. - ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH; - return $thetext; + 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 comment_id < ?", - undef, $self->bug_id, $self->id); + AND comment_id < ?", undef, $self->bug_id, $self->id + ); - return $self->{'count'}; + return $self->{'count'}; } 1; diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm index 4919244ce..e733ed8fb 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 78e144a55..d117c83ad 100644 --- a/Bugzilla/Component.pm +++ b/Bugzilla/Component.pm @@ -28,152 +28,145 @@ 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'; use constant DB_COLUMNS => qw( - id - name - product_id - initialowner - initialqacontact - description - isactive - triage_owner_id + id + name + product_id + initialowner + initialqacontact + description + isactive + triage_owner_id ); use constant UPDATE_COLUMNS => qw( - name - initialowner - initialqacontact - description - isactive - triage_owner_id + name + initialowner + initialqacontact + description + isactive + triage_owner_id ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; 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, - triage_owner_id => \&_check_triage_owner, + 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, + triage_owner_id => \&_check_triage_owner, }; -use constant VALIDATOR_DEPENDENCIES => { - name => ['product'], -}; +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(@_); - 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; + # 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; - - $self->_check_if_controller(); # From ChoiceInterface - - $dbh->bz_start_transaction(); - - 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}); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + $self->_check_if_controller(); # From ChoiceInterface + + $dbh->bz_start_transaction(); + + 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}); } + } - $dbh->do('DELETE FROM flaginclusions WHERE component_id = ?', - undef, $self->id); - $dbh->do('DELETE FROM flagexclusions WHERE component_id = ?', - undef, $self->id); - $dbh->do('DELETE FROM component_cc WHERE component_id = ?', - undef, $self->id); - $dbh->do('DELETE FROM components WHERE id = ?', undef, $self->id); + $dbh->do('DELETE FROM flaginclusions WHERE component_id = ?', undef, $self->id); + $dbh->do('DELETE FROM flagexclusions WHERE component_id = ?', undef, $self->id); + $dbh->do('DELETE FROM component_cc WHERE component_id = ?', undef, $self->id); + $dbh->do('DELETE FROM components WHERE id = ?', undef, $self->id); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } ################################ @@ -181,76 +174,77 @@ 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 $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 ($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; } 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 %cc_ids; - foreach my $cc (@$cc_list) { - my $id = login_to_id($cc, THROW_ERROR); - $cc_ids{$id} = 1; - } - return [keys %cc_ids]; + 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]; } sub _check_triage_owner { - my ($invocant, $triage_owner) = @_; - my $triage_owner_id; - $triage_owner_id = Bugzilla::User->check($triage_owner)->id if $triage_owner; - return $triage_owner_id; + my ($invocant, $triage_owner) = @_; + my $triage_owner_id; + $triage_owner_id = Bugzilla::User->check($triage_owner)->id if $triage_owner; + return $triage_owner_id; } ############################### @@ -258,183 +252,209 @@ sub _check_triage_owner { ############################### sub _update_cc_list { - my ($self, $cc_list) = @_; - my $dbh = Bugzilla->dbh; + 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 $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)]; - } + 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); + $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); + my $sth = $dbh->prepare( + 'INSERT INTO component_cc + (user_id, component_id) VALUES (?, ?)' + ); + $sth->execute($_, $self->id) foreach (@$cc_list); - return $diff; + 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); - $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_qa_contact}; } + 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 set_triage_owner { - my ($self, $triage_owner) = @_; - $self->set('triage_owner_id', $triage_owner); - # Reset the triage owner object - delete $self->{triage_owner}; + my ($self, $triage_owner) = @_; + $self->set('triage_owner_id', $triage_owner); + + # Reset the triage owner object + delete $self->{triage_owner}; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(q{ + 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'}; + WHERE component_id = ?}, undef, $self->id + ) || 0; + } + 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; - return $self->{'default_assignee'} - ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 }); + my $self = shift; + return $self->{'default_assignee'} + ||= new Bugzilla::User({id => $self->{'initialowner'}, cache => 1}); } sub default_qa_contact { - my $self = shift; - - if (!defined $self->{'default_qa_contact'}) { - my $params = $self->{'initialqacontact'} - ? { id => $self->{'initialqacontact'}, cache => 1 } - : $self->{'initialqacontact'}; - $self->{'default_qa_contact'} = new Bugzilla::User($params); - } - return $self->{'default_qa_contact'}; + my $self = shift; + + if (!defined $self->{'default_qa_contact'}) { + my $params + = $self->{'initialqacontact'} + ? {id => $self->{'initialqacontact'}, cache => 1} + : $self->{'initialqacontact'}; + $self->{'default_qa_contact'} = new Bugzilla::User($params); + } + return $self->{'default_qa_contact'}; } sub triage_owner { - my $self = shift; - if (!defined $self->{'triage_owner'}) { - my $params = $self->{'triage_owner_id'} - ? { id => $self->{'triage_owner_id'}, cache => 1 } - : $self->{'triage_owner_id'}; - $self->{'triage_owner'} = Bugzilla::User->new($params); - } - return $self->{'triage_owner'}; + my $self = shift; + if (!defined $self->{'triage_owner'}) { + my $params + = $self->{'triage_owner_id'} + ? {id => $self->{'triage_owner_id'}, cache => 1} + : $self->{'triage_owner_id'}; + $self->{'triage_owner'} = Bugzilla::User->new($params); + } + return $self->{'triage_owner'}; } sub flag_types { - my ($self, $params) = @_; - $params ||= {}; - - if (!defined $self->{'flag_types'}) { - my $flagtypes = Bugzilla::FlagType::match({ product_id => $self->product_id, - component_id => $self->id, - %$params }); - - $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'}; + my ($self, $params) = @_; + $params ||= {}; + + if (!defined $self->{'flag_types'}) { + my $flagtypes + = Bugzilla::FlagType::match({ + product_id => $self->product_id, component_id => $self->id, %$params + }); + + $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 find_first_flag_type { - my ($self, $target_type, $name) = @_; + my ($self, $target_type, $name) = @_; - return first { $_->name eq $name } @{ $self->flag_types->{$target_type} }; + return first { $_->name eq $name } @{$self->flag_types->{$target_type}}; } 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; + my $self = shift; - require Bugzilla::Product; - $self->{'product'} ||= Bugzilla::Product->new({ id => $self->product_id, cache => 1 }); - return $self->{'product'}; + require Bugzilla::Product; + $self->{'product'} + ||= Bugzilla::Product->new({id => $self->product_id, cache => 1}); + return $self->{'product'}; } ############################### @@ -442,8 +462,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'}; } sub triage_owner_id { return $_[0]->{'triage_owner_id'} } @@ -455,11 +475,12 @@ use constant FIELD_NAME => 'component'; use constant is_default => 0; sub is_set_on_bug { - my ($self, $bug) = @_; - # We treat it like a hash always, so that we don't have to check if it's - # a hash or an object. - return 0 if !defined $bug->{component_id}; - $bug->{component_id} == $self->id ? 1 : 0; + my ($self, $bug) = @_; + + # We treat it like a hash always, so that we don't have to check if it's + # a hash or an object. + return 0 if !defined $bug->{component_id}; + $bug->{component_id} == $self->id ? 1 : 0; } ############################### diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm index 1016d51e4..bc907424e 100644 --- a/Bugzilla/Config.pm +++ b/Bugzilla/Config.pm @@ -21,296 +21,296 @@ use Module::Runtime qw(require_module); # 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}; - require_module($module); - 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}; + require_module($module); + 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 $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'; - } - } + my ($params) = @_; + my $answer = Bugzilla->installation_answers; + + my $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'; + } } - - # 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 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 + if (exists $param->{'mail_delivery_method'} + && $param->{'mail_delivery_method'} !~ /[A-Z]/) + { + my $method = $param->{'mail_delivery_method'}; + my %translation = ( + 'sendmail' => 'Sendmail', + 'smtp' => 'SMTP', + 'qmail' => 'Qmail', + 'testfile' => 'Test', + 'none' => 'None' + ); + $param->{'mail_delivery_method'} = $translation{$method}; + } + + # 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}; + 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'}; - } - - # 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 - if (exists $param->{'mail_delivery_method'} - && $param->{'mail_delivery_method'} !~ /[A-Z]/) { - my $method = $param->{'mail_delivery_method'}; - my %translation = ( - 'sendmail' => 'Sendmail', - 'smtp' => 'SMTP', - 'qmail' => 'Qmail', - 'testfile' => 'Test', - 'none' => 'None'); - $param->{'mail_delivery_method'} = $translation{$method}; - } - - # 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}; - 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'}; - } + else { + my $checker = $item->{'checker'}; + my $updater = $item->{'updater'}; + if ($checker) { + my $error = $checker->($param->{$name}, $item); + if ($error && $updater) { + my $new_val = $updater->($param->{$name}); + $param->{$name} = $new_val unless $checker->($new_val, $item); } - else { - my $checker = $item->{'checker'}; - my $updater = $item->{'updater'}; - if ($checker) { - my $error = $checker->($param->{$name}, $item); - if ($error && $updater) { - my $new_val = $updater->( $param->{$name} ); - $param->{$name} = $new_val unless $checker->($new_val, $item); - } - elsif ($error) { - warn "Invalid parameter: $name\n"; - } - } + elsif ($error) { + warn "Invalid parameter: $name\n"; } + } } - - # Generate unique Duo integration secret key - if ($param->{duo_akey} eq '') { - require Bugzilla::Util; - $param->{duo_akey} = Bugzilla::Util::generate_random_password(40); + } + + # Generate unique Duo integration secret key + if ($param->{duo_akey} eq '') { + require Bugzilla::Util; + $param->{duo_akey} = Bugzilla::Util::generate_random_password(40); + } + + $param->{'utf8'} = 1 if $new_install; + + my %oldparams; + + if ( ON_WINDOWS + && !-e SENDMAIL_EXE + && $param->{'mail_delivery_method'} eq 'Sendmail') + { + my $smtp = $answer->{'SMTP_SERVER'}; + if (!$smtp) { + print "\nBugzilla requires an SMTP server to function on", + " Windows.\nPlease enter your SMTP server's hostname: "; + $smtp = ; + chomp $smtp; + if ($smtp) { + $param->{'smtpserver'} = $smtp; + } + else { + print "\nWarning: No SMTP Server provided, defaulting to", " localhost\n"; + } } - $param->{'utf8'} = 1 if $new_install; - - my %oldparams; - - if (ON_WINDOWS && !-e SENDMAIL_EXE - && $param->{'mail_delivery_method'} eq 'Sendmail') - { - my $smtp = $answer->{'SMTP_SERVER'}; - if (!$smtp) { - print "\nBugzilla requires an SMTP server to function on", - " Windows.\nPlease enter your SMTP server's hostname: "; - $smtp = ; - chomp $smtp; - if ($smtp) { - $param->{'smtpserver'} = $smtp; - } - else { - print "\nWarning: No SMTP Server provided, defaulting to", - " localhost\n"; - } - } + $param->{'mail_delivery_method'} = 'SMTP'; + } - $param->{'mail_delivery_method'} = 'SMTP'; - } + write_params($param); - write_params($param); - - # 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_data) = @_; + $param_data ||= Bugzilla->params; - local $Data::Dumper::Sortkeys = 1; + local $Data::Dumper::Sortkeys = 1; - my %params = %$param_data; - $params{urlbase} = Bugzilla->localconfig->{urlbase}; - __PACKAGE__->_write_file( Data::Dumper->Dump([\%params], ['*param']) ); + my %params = %$param_data; + $params{urlbase} = Bugzilla->localconfig->{urlbase}; + __PACKAGE__->_write_file(Data::Dumper->Dump([\%params], ['*param'])); - # 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 $datadir = bz_locations()->{'datadir'}; - if (-e "$datadir/params") { - # Note that checksetup.pl sets file permissions on '$datadir/params' - - # Using Safe mode is _not_ a guarantee of safety if someone does - # manage to write to the file. However, it won't hurt... - # See bug 165144 for not needing to eval this at all - my $s = new Safe; - - $s->rdo("$datadir/params"); - die "Error reading $datadir/params: $!" if $!; - die "Error evaluating $datadir/params: $@" if $@; - - # Now read the param back out from the sandbox - %params = %{$s->varglob('param')}; - } - 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 $datadir/params file does not exist." - . ' You probably need to run checksetup.pl.', - } - return \%params; + my %params; + my $datadir = bz_locations()->{'datadir'}; + if (-e "$datadir/params") { + + # Note that checksetup.pl sets file permissions on '$datadir/params' + + # Using Safe mode is _not_ a guarantee of safety if someone does + # manage to write to the file. However, it won't hurt... + # See bug 165144 for not needing to eval this at all + my $s = new Safe; + + $s->rdo("$datadir/params"); + die "Error reading $datadir/params: $!" if $!; + die "Error evaluating $datadir/params: $@" if $@; + + # Now read the param back out from the sandbox + %params = %{$s->varglob('param')}; + } + 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 $datadir/params file does not exist." + . ' You probably need to run checksetup.pl.',; + } + return \%params; } sub _write_file { - my ($class, $str) = @_; - my $datadir = bz_locations()->{'datadir'}; - my $param_file = "$datadir/params"; - my ($fh, $tmpname) = File::Temp::tempfile('params.XXXXX', - DIR => $datadir ); - print $fh $str || die "Can't write param file: $!"; - close $fh || die "Can't close param file: $!"; - - rename $tmpname, $param_file - or die "Can't rename $tmpname to $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); + my ($class, $str) = @_; + my $datadir = bz_locations()->{'datadir'}; + my $param_file = "$datadir/params"; + my ($fh, $tmpname) = File::Temp::tempfile('params.XXXXX', DIR => $datadir); + print $fh $str || die "Can't write param file: $!"; + close $fh || die "Can't close param file: $!"; + + rename $tmpname, $param_file or die "Can't rename $tmpname to $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); } 1; diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm index ac1c4ca0e..cae4cb25d 100644 --- a/Bugzilla/Config/Admin.pm +++ b/Bugzilla/Config/Admin.pm @@ -19,78 +19,59 @@ use Scalar::Util qw(looks_like_number); our $sortkey = 200; sub get_param_list { - 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 => 'rate_limit_active', - type => 'b', - default => 1, - }, - - { - name => 'rate_limit_rules', - type => 'l', - default => '{"get_bug": [75, 60], "show_bug": [75, 60], "github": [10, 60]}', - checker => \&check_rate_limit_rules, - updater => \&update_rate_limit_rules, - }, - - { - name => 'log_user_requests', - type => 'b', - default => 0, - } - ); - return @param_list; + 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 => 'rate_limit_active', type => 'b', default => 1,}, + + { + name => 'rate_limit_rules', + type => 'l', + default => '{"get_bug": [75, 60], "show_bug": [75, 60], "github": [10, 60]}', + checker => \&check_rate_limit_rules, + updater => \&update_rate_limit_rules, + }, + + {name => 'log_user_requests', type => 'b', default => 0,} + ); + return @param_list; } sub check_rate_limit_rules { - my $rules = shift; + my $rules = shift; - my $val = eval { decode_json($rules) }; - return "failed to parse json" unless defined $val; - return "value is not HASH" unless ref $val && ref($val) eq 'HASH'; - return "rules are invalid" unless all { - ref($_) eq 'ARRAY' && looks_like_number( $_->[0] ) && looks_like_number( $_->[1] ) - } values %$val; + my $val = eval { decode_json($rules) }; + return "failed to parse json" unless defined $val; + return "value is not HASH" unless ref $val && ref($val) eq 'HASH'; + return "rules are invalid" unless all { + ref($_) eq 'ARRAY' && looks_like_number($_->[0]) && looks_like_number($_->[1]) + } + values %$val; - foreach my $required (qw( show_bug get_bug github )) { - return "missing $required" unless exists $val->{$required}; - } + foreach my $required (qw( show_bug get_bug github )) { + return "missing $required" unless exists $val->{$required}; + } - return ""; + return ""; } sub update_rate_limit_rules { - my ($rules) = @_; - my $val = decode_json($rules); - $val->{github} = [10, 60]; - return encode_json($val); + my ($rules) = @_; + my $val = decode_json($rules); + $val->{github} = [10, 60]; + return encode_json($val); } 1; diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm index 398f02701..7b76944e1 100644 --- a/Bugzilla/Config/Advanced.pm +++ b/Bugzilla/Config/Advanced.pm @@ -17,25 +17,17 @@ use Bugzilla::Util qw(validate_ip); our $sortkey = 1700; use constant get_param_list => ( - { - 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 => 'disable_bug_updates', - type => 'b', - default => 0 - }, + {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 => 'disable_bug_updates', type => 'b', default => 0}, ); 1; diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm index c3dbd03ed..821996eba 100644 --- a/Bugzilla/Config/Attachment.pm +++ b/Bugzilla/Config/Attachment.pm @@ -16,77 +16,57 @@ use Bugzilla::Config::Common; our $sortkey = 400; sub get_param_list { - my $class = shift; - my @param_list = ( - { - name => 'allow_attachment_display', - type => 'b', - default => 0 - }, - { - name => 'allow_attachment_deletion', - type => 'b', - default => 0 - }, - { - name => 'maxattachmentsize', - type => 't', - default => '1000', - checker => \&check_maxattachmentsize - }, - { - name => 'attachment_storage', - type => 's', - choices => [ 'database', 'filesystem', 's3' ], - default => 'database', - checker => \&check_storage - }, - { - name => 's3_bucket', - type => 't', - default => '', - }, - { - name => 'aws_access_key_id', - type => 't', - default => '', - }, - { - name => 'aws_secret_access_key', - type => 't', - default => '', - }, - ); - return @param_list; + my $class = shift; + my @param_list = ( + {name => 'allow_attachment_display', type => 'b', default => 0}, + {name => 'allow_attachment_deletion', type => 'b', default => 0}, + { + name => 'maxattachmentsize', + type => 't', + default => '1000', + checker => \&check_maxattachmentsize + }, + { + name => 'attachment_storage', + type => 's', + choices => ['database', 'filesystem', 's3'], + default => 'database', + checker => \&check_storage + }, + {name => 's3_bucket', type => 't', default => '',}, + {name => 'aws_access_key_id', type => 't', default => '',}, + {name => 'aws_secret_access_key', type => 't', default => '',}, + ); + return @param_list; } sub check_params { - my ( $class, $params ) = @_; - return '' unless $params->{attachment_storage} eq 's3'; + my ($class, $params) = @_; + return '' unless $params->{attachment_storage} eq 's3'; - if ( $params->{s3_bucket} eq '' - || $params->{aws_access_key_id} eq '' - || $params->{aws_secret_access_key} eq '' ) - { - return - "You must set s3_bucket, aws_access_key_id, and aws_secret_access_key when attachment_storage is set to S3"; - } - return ''; + if ( $params->{s3_bucket} eq '' + || $params->{aws_access_key_id} eq '' + || $params->{aws_secret_access_key} eq '') + { + return + "You must set s3_bucket, aws_access_key_id, and aws_secret_access_key when attachment_storage is set to S3"; + } + return ''; } sub check_storage { - my ( $value, $param ) = (@_); - my $check_multi = check_multi( $value, $param ); - return $check_multi if $check_multi; + my ($value, $param) = (@_); + my $check_multi = check_multi($value, $param); + return $check_multi if $check_multi; - if ( $value eq 's3' ) { - return Bugzilla->feature('s3') - ? '' - : 'The perl modules required for S3 support are not installed'; - } - else { - return ''; - } + if ($value eq 's3') { + return Bugzilla->feature('s3') + ? '' + : 'The perl modules required for S3 support are not installed'; + } + else { + return ''; + } } 1; diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm index 965d922d7..664c1b263 100644 --- a/Bugzilla/Config/Auth.pm +++ b/Bugzilla/Config/Auth.pm @@ -18,241 +18,195 @@ use Types::Common::Numeric qw(PositiveInt); our $sortkey = 300; sub get_param_list { - 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', 'bmo' ], - default => 'no_constraints', - checker => \&check_multi - }, - - { - name => 'password_check_on_login', - type => 'b', - default => '1' - }, - - { - name => 'passwdqc_min', - type => 't', - default => 'undef, 24, 11, 8, 7', - checker => \&_check_passwdqc_min, - }, - - { - name => 'passwdqc_max', - type => 't', - default => '40', - checker => \&_check_passwdqc_max, - }, - - { - name => 'passwdqc_passphrase_words', - type => 't', - default => '3', - checker => \&check_numeric, - }, - - { - name => 'passwdqc_match_length', - type => 't', - default => '4', - checker => \&check_numeric, - }, - - { - name => 'passwdqc_random_bits', - type => 't', - default => '47', - checker => \&_check_passwdqc_random_bits, - }, - - { - name => 'passwdqc_desc', - type => 'l', - default => 'The password must be complex.', - }, - - { - name => 'auth_delegation', - type => 'b', - default => 0, - }, - - { - name => 'duo_host', - type => 't', - default => '', - }, - { - name => 'duo_akey', - type => 't', - default => '', - }, - { - name => 'duo_ikey', - type => 't', - default => '', - }, - { - name => 'duo_skey', - type => 't', - default => '', - }, - - { - name => 'mfa_group', - type => 's', - choices => \&get_all_group_names, - default => '', - checker => \&check_group, - }, - - { - name => 'mfa_group_grace_period', - type => 't', - default => '7', - checker => \&check_numeric, - } - ); - return @param_list; + 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', 'bmo'], + default => 'no_constraints', + checker => \&check_multi + }, + + {name => 'password_check_on_login', type => 'b', default => '1'}, + + { + name => 'passwdqc_min', + type => 't', + default => 'undef, 24, 11, 8, 7', + checker => \&_check_passwdqc_min, + }, + + { + name => 'passwdqc_max', + type => 't', + default => '40', + checker => \&_check_passwdqc_max, + }, + + { + name => 'passwdqc_passphrase_words', + type => 't', + default => '3', + checker => \&check_numeric, + }, + + { + name => 'passwdqc_match_length', + type => 't', + default => '4', + checker => \&check_numeric, + }, + + { + name => 'passwdqc_random_bits', + type => 't', + default => '47', + checker => \&_check_passwdqc_random_bits, + }, + + { + name => 'passwdqc_desc', + type => 'l', + default => 'The password must be complex.', + }, + + {name => 'auth_delegation', type => 'b', default => 0,}, + + {name => 'duo_host', type => 't', default => '',}, + {name => 'duo_akey', type => 't', default => '',}, + {name => 'duo_ikey', type => 't', default => '',}, + {name => 'duo_skey', type => 't', default => '',}, + + { + name => 'mfa_group', + type => 's', + choices => \&get_all_group_names, + default => '', + checker => \&check_group, + }, + + { + name => 'mfa_group_grace_period', + type => 't', + default => '7', + checker => \&check_numeric, + } + ); + return @param_list; } -my $passwdqc_min = Tuple[ - Maybe[PositiveInt], - Maybe[PositiveInt], - Maybe[PositiveInt], - Maybe[PositiveInt], - Maybe[PositiveInt], +my $passwdqc_min = Tuple [ + Maybe [PositiveInt], + Maybe [PositiveInt], + Maybe [PositiveInt], + Maybe [PositiveInt], + Maybe [PositiveInt], ]; sub _check_passwdqc_min { - my ($value) = @_; - my @values = map { $_ eq 'undef' ? undef : $_ } split( /\s*,\s*/, $value ); - - unless ( $passwdqc_min->check( \@values ) ) { - return "must be list of five values, that are either integers > 0 or undef"; + my ($value) = @_; + my @values = map { $_ eq 'undef' ? undef : $_ } split(/\s*,\s*/, $value); + + unless ($passwdqc_min->check(\@values)) { + return "must be list of five values, that are either integers > 0 or undef"; + } + + my ($max, $max_pos); + my $pos = 0; + foreach my $value (@values) { + if (defined $max && defined $value) { + if ($value > $max) { + return "Int$pos is larger than Int$max_pos ($max)"; + } } - - my ( $max, $max_pos ); - my $pos = 0; - foreach my $value (@values) { - if ( defined $max && defined $value ) { - if ( $value > $max ) { - return "Int$pos is larger than Int$max_pos ($max)"; - } - } - elsif ( defined $value ) { - $max = $value; - $max_pos = $pos; - } - $pos++; + elsif (defined $value) { + $max = $value; + $max_pos = $pos; } - return ""; + $pos++; + } + return ""; } sub _check_passwdqc_max { - my ($value) = @_; - return "must be a positive integer" unless PositiveInt->check($value); - return "must be greater than 8" unless $value > 8; - return ""; + my ($value) = @_; + return "must be a positive integer" unless PositiveInt->check($value); + return "must be greater than 8" unless $value > 8; + return ""; } sub _check_passwdqc_random_bits { - my ($value) = @_; - return "must be a positive integer" unless PositiveInt->check($value); - return "must be between 24 and 85 inclusive" unless $value >= 24 && $value <= 85; - return ""; + my ($value) = @_; + return "must be a positive integer" unless PositiveInt->check($value); + return "must be between 24 and 85 inclusive" + unless $value >= 24 && $value <= 85; + return ""; } -1; \ No newline at end of file +1; diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm index f9c33512a..ad1cafefc 100644 --- a/Bugzilla/Config/BugChange.pm +++ b/Bugzilla/Config/BugChange.pm @@ -17,67 +17,43 @@ use Bugzilla::Status; our $sortkey = 500; sub get_param_list { - my $class = shift; - - # Hardcoded bug statuses which existed before Bugzilla 3.1. - my @closed_bug_statuses = ( 'RESOLVED', 'VERIFIED', 'CLOSED' ); - - # If we are upgrading from 3.0 or older, bug statuses are not customisable - # 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 @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, - } - ); - return @param_list; + my $class = shift; + + # Hardcoded bug statuses which existed before Bugzilla 3.1. + my @closed_bug_statuses = ('RESOLVED', 'VERIFIED', 'CLOSED'); + + # If we are upgrading from 3.0 or older, bug statuses are not customisable + # 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 @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,} + ); + return @param_list; } 1; diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm index 94a16b7c2..87ecc6122 100644 --- a/Bugzilla/Config/BugFields.pm +++ b/Bugzilla/Config/BugFields.pm @@ -17,89 +17,61 @@ use Bugzilla::Field; our $sortkey = 600; sub get_param_list { - my $class = shift; - - my @legal_priorities = @{ get_legal_field_values('priority') }; - my @legal_severities = @{ get_legal_field_values('bug_severity') }; - my @legal_platforms = @{ get_legal_field_values('rep_platform') }; - 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 => 'usebugaliases', - 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; + my $class = shift; + + my @legal_priorities = @{get_legal_field_values('priority')}; + my @legal_severities = @{get_legal_field_values('bug_severity')}; + my @legal_platforms = @{get_legal_field_values('rep_platform')}; + 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 => 'usebugaliases', 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; } 1; diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm index 24b636099..b3d0cb5f8 100644 --- a/Bugzilla/Config/Common.pm +++ b/Bugzilla/Config/Common.pm @@ -22,311 +22,318 @@ use Bugzilla::Status; use base qw(Exporter); @Bugzilla::Config::Common::EXPORT = qw( - check_multi check_numeric check_regexp check_url check_group - check_priority check_severity check_platform - check_opsys check_shadowdb check_urlbase check_webdotbase - check_user_verify_class - check_mail_delivery_method check_notification check_utf8 - check_bug_status check_smtp_auth check_theschwartz_available - check_maxattachmentsize check_email - check_comment_taggers_group - get_all_group_names + check_multi check_numeric check_regexp check_url check_group + check_priority check_severity check_platform + check_opsys check_shadowdb check_urlbase check_webdotbase + check_user_verify_class + check_mail_delivery_method check_notification check_utf8 + check_bug_status check_smtp_auth check_theschwartz_available + check_maxattachmentsize check_email + check_comment_taggers_group + get_all_group_names ); # 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 ""; - } - elsif ( $param->{'type'} eq 'm' || $param->{'type'} eq 'o' ) { - foreach my $chkParam ( split( ',', $value ) ) { - unless ( scalar( grep { $_ eq $chkParam } ( @{ $param->{'choices'} } ) ) ) { - return "Invalid choice '$chkParam' for multi-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'}'"; } - else { - return "Invalid param type '$param->{'type'}' for check_multi(); " . "contact your Bugzilla administrator"; + + return ""; + } + elsif ($param->{'type'} eq 'm' || $param->{'type'} eq 'o') { + foreach my $chkParam (split(',', $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_utf8 { - my ($utf8, $entry) = @_; + my ($utf8, $entry) = @_; - # You cannot turn off the UTF-8 parameter. - if ( !$utf8 ) { - return "You cannot disable UTF-8 support."; - } - elsif ($entry eq 'utf8mb4' && $utf8 ne 'utf8mb4') { - return "You cannot disable UTF8-MB4 support."; - } + # You cannot turn off the UTF-8 parameter. + if (!$utf8) { + return "You cannot disable UTF-8 support."; + } + elsif ($entry eq 'utf8mb4' && $utf8 ne 'utf8mb4') { + return "You cannot disable UTF8-MB4 support."; + } - return ""; + 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 ""; + 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."; } - 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; - } + + # 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 ""; + } + 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"}; - } + # 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"}; } - return ""; + 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"; - } + 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 ""; + } + 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_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_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); } sub get_all_group_names { - return [ - '', - map { $_->name } Bugzilla::Group->get_all, - ]; + return ['', map { $_->name } Bugzilla::Group->get_all,]; } # OK, here are the parameter definitions themselves. @@ -393,7 +400,7 @@ sub get_all_group_names { # for list (single and multiple) parameter types. sub get_param_list { - return; + return; } 1; diff --git a/Bugzilla/Config/DependencyGraph.pm b/Bugzilla/Config/DependencyGraph.pm index 965be803a..2b48d634c 100644 --- a/Bugzilla/Config/DependencyGraph.pm +++ b/Bugzilla/Config/DependencyGraph.pm @@ -40,16 +40,14 @@ use Bugzilla::Config::Common; our $sortkey = 800; sub get_param_list { - my $class = shift; - my @param_list = ( - { - name => 'webdotbase', - type => 't', - default => 'http://www.research.att.com/~north/cgi-bin/webdot.cgi/%urlbase%', - checker => \&check_webdotbase - } - ); - return @param_list; + my $class = shift; + my @param_list = ({ + name => 'webdotbase', + type => 't', + default => 'http://www.research.att.com/~north/cgi-bin/webdot.cgi/%urlbase%', + checker => \&check_webdotbase + }); + return @param_list; } 1; diff --git a/Bugzilla/Config/Elastic.pm b/Bugzilla/Config/Elastic.pm index 690f5fac5..d0ca7a02a 100644 --- a/Bugzilla/Config/Elastic.pm +++ b/Bugzilla/Config/Elastic.pm @@ -16,23 +16,11 @@ use Bugzilla::Config::Common; our $sortkey = 1550; sub get_param_list { - return ( - { - name => 'elasticsearch', - type => 'b', - default => 0, - }, - { - name => 'elasticsearch_nodes', - type => 't', - default => 'localhost:9200', - }, - { - name => 'elasticsearch_index', - type => 't', - default => 'bugzilla', - }, - ); + return ( + {name => 'elasticsearch', type => 'b', default => 0,}, + {name => 'elasticsearch_nodes', type => 't', default => 'localhost:9200',}, + {name => 'elasticsearch_index', type => 't', default => 'bugzilla',}, + ); } 1; diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm index fa7cf2d08..bf4088248 100644 --- a/Bugzilla/Config/General.pm +++ b/Bugzilla/Config/General.pm @@ -16,50 +16,49 @@ use Bugzilla::Config::Common; 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 => 'nobody_user', - type => 't', - no_reset => '1', - default => 'nobody@mozilla.org', - checker => \&check_email - }, + { + name => 'nobody_user', + type => 't', + no_reset => '1', + default => 'nobody@mozilla.org', + checker => \&check_email + }, - { - name => 'docs_urlbase', - type => 't', - default => 'docs/%lang%/html/', - checker => \&check_url - }, + { + name => 'docs_urlbase', + type => 't', + default => 'docs/%lang%/html/', + checker => \&check_url + }, - { - name => 'utf8', - type => 's', - choices => [ '1', 'utf8', 'utf8mb4' ], - default => 'utf8', - checker => \&check_utf8 - }, + { + name => 'utf8', + type => 's', + choices => ['1', 'utf8', 'utf8mb4'], + default => 'utf8', + checker => \&check_utf8 + }, - { - 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 + }, ); 1; diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm index 68c852fe6..f6f824098 100644 --- a/Bugzilla/Config/GroupSecurity.pm +++ b/Bugzilla/Config/GroupSecurity.pm @@ -17,78 +17,65 @@ use Bugzilla::Group; our $sortkey = 900; 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 - } - ); - return @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} + ); + return @param_list; } - 1; diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm index a3ce04fbe..75f58e141 100644 --- a/Bugzilla/Config/LDAP.pm +++ b/Bugzilla/Config/LDAP.pm @@ -16,51 +16,23 @@ use Bugzilla::Config::Common; our $sortkey = 1000; sub get_param_list { - my $class = shift; - my @param_list = ( - { - name => 'LDAPserver', - type => 't', - default => '' - }, + my $class = shift; + my @param_list = ( + {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 => '', - } - ); - return @param_list; + {name => 'LDAPfilter', type => 't', default => '',} + ); + return @param_list; } 1; diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm index c23c324d9..c57989ee4 100644 --- a/Bugzilla/Config/MTA.pm +++ b/Bugzilla/Config/MTA.pm @@ -41,78 +41,51 @@ use Bugzilla::Config::Common; # deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send # to disable these warnings. BEGIN { - $Return::Value::NO_CLUCK = 1; + $Return::Value::NO_CLUCK = 1; } use Email::Send; our $sortkey = 1200; sub get_param_list { - my $class = shift; - my @param_list = ( - { - name => 'mail_delivery_method', - type => 's', + my $class = shift; + my @param_list = ( + { + name => 'mail_delivery_method', + type => 's', - # Bugzilla is not ready yet to send mails to newsgroups, and 'IO' - # is of no use for now as we already have our own 'Test' mode. - choices => [ grep { $_ ne 'NNTP' && $_ ne 'IO' } Email::Send->new()->all_mailers(), 'None' ], - default => 'Sendmail', - checker => \&check_mail_delivery_method - }, + # Bugzilla is not ready yet to send mails to newsgroups, and 'IO' + # is of no use for now as we already have our own 'Test' mode. + choices => [ + grep { $_ ne 'NNTP' && $_ ne 'IO' } Email::Send->new()->all_mailers(), 'None' + ], + default => 'Sendmail', + checker => \&check_mail_delivery_method + }, - { - name => 'mailfrom', - type => 't', - default => 'bugzilla-daemon' - }, + {name => 'mailfrom', type => 't', default => 'bugzilla-daemon'}, - { - name => 'use_mailer_queue', - type => 'b', - default => 0, - checker => \&check_theschwartz_available, - }, + { + name => 'use_mailer_queue', + type => 'b', + default => 0, + checker => \&check_theschwartz_available, + }, - { - name => 'smtpserver', - type => 't', - default => 'localhost' - }, - { - name => 'smtp_username', - type => 't', - default => '', - checker => \&check_smtp_auth - }, - { - name => 'smtp_password', - type => 'p', - default => '' - }, - { - name => 'smtp_debug', - type => 'b', - default => 0 - }, - { - name => 'whinedays', - type => 't', - default => 7, - checker => \&check_numeric - }, - { - name => 'globalwatchers', - type => 't', - default => '', - }, - { - name => 'silent_users', - type => 't', - default => '', - }, - ); - return @param_list; + {name => 'smtpserver', type => 't', default => 'localhost'}, + { + name => 'smtp_username', + type => 't', + default => '', + checker => \&check_smtp_auth + }, + {name => 'smtp_password', type => 'p', default => ''}, + {name => 'smtp_debug', type => 'b', default => 0}, + {name => 'whinedays', type => 't', default => 7, checker => \&check_numeric}, + {name => 'globalwatchers', type => 't', default => '',}, + {name => 'silent_users', type => 't', default => '',}, + ); + return @param_list; } 1; diff --git a/Bugzilla/Config/PatchViewer.pm b/Bugzilla/Config/PatchViewer.pm index 98c8c675f..e735aac63 100644 --- a/Bugzilla/Config/PatchViewer.pm +++ b/Bugzilla/Config/PatchViewer.pm @@ -40,39 +40,19 @@ use Bugzilla::Config::Common; our $sortkey = 1300; sub get_param_list { - my $class = shift; - my @param_list = ( - { - name => 'cvsroot', - type => 't', - default => '', - }, + my $class = shift; + my @param_list = ( + {name => 'cvsroot', type => 't', default => '',}, - { - name => 'cvsroot_get', - type => 't', - default => '', - }, + {name => 'cvsroot_get', type => 't', default => '',}, - { - name => 'bonsai_url', - type => 't', - default => '' - }, + {name => 'bonsai_url', type => 't', default => ''}, - { - name => 'lxr_url', - type => 't', - default => '' - }, + {name => 'lxr_url', type => 't', default => ''}, - { - name => 'lxr_root', - type => 't', - default => '', - } - ); - return @param_list; + {name => 'lxr_root', type => 't', default => '',} + ); + return @param_list; } 1; diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm index b6397a835..83115717a 100644 --- a/Bugzilla/Config/Query.pm +++ b/Bugzilla/Config/Query.pm @@ -16,58 +16,54 @@ use Bugzilla::Config::Common; our $sortkey = 1400; sub get_param_list { - my $class = shift; - my @param_list = ( - { - name => 'quip_list_entry_control', - type => 's', - choices => [ 'open', 'moderated', 'closed' ], - default => 'open', - checker => \&check_multi - }, + my $class = shift; + my @param_list = ( + { + name => 'quip_list_entry_control', + type => 's', + choices => ['open', 'moderated', 'closed'], + default => 'open', + checker => \&check_multi + }, - { - name => 'mostfreqthreshold', - type => 't', - default => '2', - checker => \&check_numeric - }, + { + name => 'mostfreqthreshold', + type => 't', + default => '2', + checker => \&check_numeric + }, - { - name => 'mybugstemplate', - type => 't', - default => - 'buglist.cgi?resolution=---&emailassigned_to1=1&emailreporter1=1&emailtype1=exact&email1=%userid%' - }, + { + 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 => '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 => 'search_allow_no_criteria', type => 'b', default => 1}, - { - name => 'default_search_limit', - type => 't', - default => '500', - checker => \&check_numeric - }, + { + 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; + { + name => 'max_search_results', + type => 't', + default => '10000', + checker => \&check_numeric + }, + ); + return @param_list; } 1; diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm index bc980a1ec..b0a5ddbf5 100644 --- a/Bugzilla/Config/RADIUS.pm +++ b/Bugzilla/Config/RADIUS.pm @@ -16,33 +16,17 @@ use Bugzilla::Config::Common; our $sortkey = 1100; sub get_param_list { - 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 => '' - }, - ); - return @param_list; + 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 => ''}, + ); + return @param_list; } 1; diff --git a/Bugzilla/Config/Reports.pm b/Bugzilla/Config/Reports.pm index 26c5aad57..dfb6db7a3 100644 --- a/Bugzilla/Config/Reports.pm +++ b/Bugzilla/Config/Reports.pm @@ -16,22 +16,14 @@ use Bugzilla::Config::Common; our $sortkey = 1100; sub get_param_list { - my $class = shift; - my @param_list = ( - { - name => 'report_secbugs_active', - type => 'b', - default => 1, - }, - { - name => 'report_secbugs_emails', - type => 't', - default => 'bugzilla-admin@mozilla.org' - }, - { - name => 'report_secbugs_products', - type => 'l', - default => '[]' - }, - ); + my $class = shift; + my @param_list = ( + {name => 'report_secbugs_active', type => 'b', default => 1,}, + { + name => 'report_secbugs_emails', + type => 't', + default => 'bugzilla-admin@mozilla.org' + }, + {name => 'report_secbugs_products', type => 'l', default => '[]'}, + ); } diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm index 772df5675..101e4678f 100644 --- a/Bugzilla/Config/ShadowDB.pm +++ b/Bugzilla/Config/ShadowDB.pm @@ -16,37 +16,24 @@ use Bugzilla::Config::Common; our $sortkey = 1500; sub get_param_list { - 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 - } - ); - return @param_list; + 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} + ); + return @param_list; } 1; diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm index ddb850f3b..a1f8a3eb2 100644 --- a/Bugzilla/Config/UserMatch.pm +++ b/Bugzilla/Config/UserMatch.pm @@ -16,34 +16,22 @@ use Bugzilla::Config::Common; our $sortkey = 1600; sub get_param_list { - 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, - } - ); - return @param_list; + 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,} + ); + return @param_list; } 1; diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm index cd478c33e..26341967d 100644 --- a/Bugzilla/Constants.pm +++ b/Bugzilla/Constants.pm @@ -19,185 +19,185 @@ use Cwd qw(realpath); use Memoize; @Bugzilla::Constants::EXPORT = qw( - BUGZILLA_VERSION + BUGZILLA_VERSION - REMOTE_FILE - LOCAL_FILE + REMOTE_FILE + LOCAL_FILE - bz_locations + bz_locations - IS_NULL - NOT_NULL + IS_NULL + NOT_NULL - CONTROLMAPNA - CONTROLMAPSHOWN - CONTROLMAPDEFAULT - CONTROLMAPMANDATORY + CONTROLMAPNA + CONTROLMAPSHOWN + CONTROLMAPDEFAULT + CONTROLMAPMANDATORY - AUTH_OK - AUTH_NODATA - AUTH_ERROR - AUTH_LOGINFAILED - AUTH_DISABLED - AUTH_NO_SUCH_USER - AUTH_LOCKOUT + AUTH_OK + AUTH_NODATA + AUTH_ERROR + AUTH_LOGINFAILED + AUTH_DISABLED + AUTH_NO_SUCH_USER + AUTH_LOCKOUT - USER_PASSWORD_MIN_LENGTH + USER_PASSWORD_MIN_LENGTH - LOGIN_OPTIONAL - LOGIN_NORMAL - LOGIN_REQUIRED + LOGIN_OPTIONAL + LOGIN_NORMAL + LOGIN_REQUIRED - LOGOUT_ALL - LOGOUT_CURRENT - LOGOUT_KEEP_CURRENT + LOGOUT_ALL + LOGOUT_CURRENT + LOGOUT_KEEP_CURRENT - GRANT_DIRECT - GRANT_REGEXP + GRANT_DIRECT + GRANT_REGEXP - GROUP_MEMBERSHIP - GROUP_BLESS - GROUP_VISIBLE - - MAILTO_USER - MAILTO_GROUP + GROUP_MEMBERSHIP + GROUP_BLESS + GROUP_VISIBLE + + MAILTO_USER + MAILTO_GROUP - DEFAULT_COLUMN_LIST - DEFAULT_QUERY_NAME - DEFAULT_MILESTONE + DEFAULT_COLUMN_LIST + DEFAULT_QUERY_NAME + DEFAULT_MILESTONE - SAVE_NUM_SEARCHES + 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 + 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 + ADMIN_GROUP_NAME + PER_PRODUCT_PRIVILEGES - SENDMAIL_EXE - SENDMAIL_PATH + 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_EXTENSION + 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_EXTENSION - FIELD_TYPE_HIGHEST_PLUS_ONE + FIELD_TYPE_HIGHEST_PLUS_ONE - EMPTY_DATETIME_REGEX + EMPTY_DATETIME_REGEX - ABNORMAL_SELECTS - - TIMETRACKING_FIELDS + 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 - USAGE_MODE_MOJO - - ERROR_MODE_WEBPAGE - ERROR_MODE_DIE - ERROR_MODE_DIE_SOAP_FAULT - ERROR_MODE_JSON_RPC - ERROR_MODE_TEST - ERROR_MODE_REST - ERROR_MODE_MOJO + USAGE_MODE_BROWSER + USAGE_MODE_CMDLINE + USAGE_MODE_XMLRPC + USAGE_MODE_EMAIL + USAGE_MODE_JSON + USAGE_MODE_TEST + USAGE_MODE_REST + USAGE_MODE_MOJO + + ERROR_MODE_WEBPAGE + ERROR_MODE_DIE + ERROR_MODE_DIE_SOAP_FAULT + ERROR_MODE_JSON_RPC + ERROR_MODE_TEST + ERROR_MODE_REST + ERROR_MODE_MOJO - COLOR_ERROR - COLOR_SUCCESS + COLOR_ERROR + COLOR_SUCCESS - INSTALLATION_MODE_INTERACTIVE - INSTALLATION_MODE_NON_INTERACTIVE - - DB_MODULE - ROOT_USER - ON_WINDOWS - ON_ACTIVESTATE + INSTALLATION_MODE_INTERACTIVE + INSTALLATION_MODE_NON_INTERACTIVE + + DB_MODULE + ROOT_USER + ON_WINDOWS + ON_ACTIVESTATE - MAX_TOKEN_AGE - MAX_SHORT_TOKEN_HOURS - MAX_LOGINCOOKIE_AGE - MAX_SUDO_TOKEN_AGE - MAX_LOGIN_ATTEMPTS - LOGIN_LOCKOUT_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_FREETEXT_LENGTH - MAX_BUG_URL_LENGTH - MAX_POSSIBLE_DUPLICATES - 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 - - EMAIL_LIMIT_PER_MINUTE - EMAIL_LIMIT_PER_HOUR - EMAIL_LIMIT_EXCEPTION - - JOB_QUEUE_VIEW_MAX_JOBS - - BZ_PERSISTENT + MAX_TOKEN_AGE + MAX_SHORT_TOKEN_HOURS + MAX_LOGINCOOKIE_AGE + MAX_SUDO_TOKEN_AGE + MAX_LOGIN_ATTEMPTS + LOGIN_LOCKOUT_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_FREETEXT_LENGTH + MAX_BUG_URL_LENGTH + MAX_POSSIBLE_DUPLICATES + 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 + + EMAIL_LIMIT_PER_MINUTE + EMAIL_LIMIT_PER_HOUR + EMAIL_LIMIT_EXCEPTION + + JOB_QUEUE_VIEW_MAX_JOBS + + BZ_PERSISTENT ); @Bugzilla::Constants::EXPORT_OK = qw(contenttypes); @@ -208,14 +208,14 @@ use Memoize; # BMO: we don't map exactly to a specific bugzilla version, so override our # reported version with a parameter. sub BUGZILLA_VERSION { - my $bugzilla_version = '4.2'; - eval { require Bugzilla } || return $bugzilla_version; - eval { Bugzilla->VERSION } || $bugzilla_version; + my $bugzilla_version = '4.2'; + eval { require Bugzilla } || return $bugzilla_version; + eval { Bugzilla->VERSION } || $bugzilla_version; } # Location of the remote and local XML files to track new releases. use constant REMOTE_FILE => 'https://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. # These are unique values that are unlikely to match a string or a number, # to be used in criteria for match() functions and other things. They start @@ -255,47 +255,47 @@ 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; # The minimum length a password must have. # BMO uses 8 characters. 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 @@ -310,6 +310,7 @@ use constant SAVE_NUM_SEARCHES => 10; # 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; @@ -318,9 +319,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; @@ -330,27 +332,26 @@ 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; # 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. 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_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 @@ -358,34 +359,34 @@ 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 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; # 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; @@ -393,10 +394,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'; @@ -408,18 +411,18 @@ use constant SENDMAIL_PATH => '/usr/lib:/usr/sbin:/usr/ucblib'; # display a user picker). Fields of type FIELD_TYPE_EXTENSION should generally # be ignored by the core code and is used primary by extensions. -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_EXTENSION => 99; +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_EXTENSION => 99; # Add new field types above this line, and change the below value in the # obvious fashion @@ -429,29 +432,30 @@ 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, -}; +use constant ABNORMAL_SELECTS => + {classification => 1, component => 1, product => 1,}; # 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 deadline); + qw(estimated_time remaining_time work_time actual_time + percentage_complete deadline); # The maximum number of days a token will remain valid. use constant MAX_TOKEN_AGE => 3; + # The maximum number of hours a short-lived token will remain valid. use constant MAX_SHORT_TOKEN_HOURS => 1; + # How many days a logincookie will remain valid if not used. 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; @@ -461,38 +465,41 @@ use constant LOGIN_LOCKOUT_INTERVAL => 30; use constant MAX_STS_AGE => 31536000; # 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" , - "txt" => "text/plain", - }; +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", + "txt" => "text/plain", +}; # 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_MOJO => 7; +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_MOJO => 7; # Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE # usually). Use with Bugzilla->error_mode. @@ -505,58 +512,69 @@ use constant ERROR_MODE_REST => 5; use constant ERROR_MODE_MOJO => 6; # 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 => { - # Require MySQL 5.6.x for innodb's fulltext support - 'mysql' => {db => 'Bugzilla::DB::Mysql', db_version => '5.6.12', - 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', - version => '1.45', - }, - 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'}, + + # Require MySQL 5.6.x for innodb's fulltext support + 'mysql' => { + db => 'Bugzilla::DB::Mysql', + db_version => '5.6.12', + 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', version => '1.45',}, + 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. @@ -564,7 +582,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; @@ -602,6 +620,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 changes his password. use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256'; + # How long of a salt should we use? Note that if you change this, none # of your users will be able to log in until they reset their passwords. use constant PASSWORD_SALT_LENGTH => 8; @@ -630,82 +649,90 @@ use constant AUDIT_REMOVE => '__remove__'; # Setting a limit to 0 will disable this feature. use constant EMAIL_LIMIT_PER_MINUTE => 1000; use constant EMAIL_LIMIT_PER_HOUR => 2500; + # Don't change this exception message. -use constant EMAIL_LIMIT_EXCEPTION => "email_limit_exceeded\n"; +use constant EMAIL_LIMIT_EXCEPTION => "email_limit_exceeded\n"; # The maximum number of jobs to show when viewing the job queue # (view_job_queue.cgi). use constant JOB_QUEUE_VIEW_MAX_JOBS => 2500; 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. - # - # Always use an absolute path, based on the location of this file. - my $libpath = realpath(dirname(dirname(__FILE__))); - # We have to detaint $libpath, but we can't use Bugzilla::Util here. - $libpath =~ /(.*)/; - $libpath = $1; - - my ($localconfig, $datadir, $confdir); - if ($project && $project =~ /^(\w+)$/) { - $project = $1; - $localconfig = "localconfig.$project"; - $datadir = "data/$project"; - $confdir = "conf/$project"; - } else { - $project = undef; - $localconfig = "localconfig"; - $datadir = "data"; - $confdir = "conf"; - } - - $datadir = "$libpath/$datadir"; - $confdir = "$libpath/$confdir"; - # 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' => "$libpath/template_cache", - '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", - 'logsdir' => "$libpath/logs", - 'assetsdir' => "$datadir/assets", - 'confdir' => $confdir, - }; + 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. + # + # Always use an absolute path, based on the location of this file. + my $libpath = realpath(dirname(dirname(__FILE__))); + + # We have to detaint $libpath, but we can't use Bugzilla::Util here. + $libpath =~ /(.*)/; + $libpath = $1; + + my ($localconfig, $datadir, $confdir); + if ($project && $project =~ /^(\w+)$/) { + $project = $1; + $localconfig = "localconfig.$project"; + $datadir = "data/$project"; + $confdir = "conf/$project"; + } + else { + $project = undef; + $localconfig = "localconfig"; + $datadir = "data"; + $confdir = "conf"; + } + + $datadir = "$libpath/$datadir"; + $confdir = "$libpath/$confdir"; + + # 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' => "$libpath/template_cache", + '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", + 'logsdir' => "$libpath/logs", + 'assetsdir' => "$datadir/assets", + 'confdir' => $confdir, + }; } use constant BZ_PERSISTENT => $main::BUGZILLA_PERSISTENT; # 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; diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm index 87110aaaa..1003c4673 100644 --- a/Bugzilla/DB.pm +++ b/Bugzilla/DB.pm @@ -13,10 +13,7 @@ use Moo; use DBI; use DBIx::Connector; -has 'connector' => ( - is => 'lazy', - handles => [ qw( dbh ) ], -); +has 'connector' => (is => 'lazy', handles => [qw( dbh )],); use Bugzilla::Logging; use Bugzilla::Constants; @@ -36,39 +33,36 @@ use Storable qw(dclone); use English qw(-no_match_vars); use Module::Runtime qw(require_module); -has [qw(dsn user pass attrs)] => ( - is => 'ro', - required => 1, -); +has [qw(dsn user pass attrs)] => (is => 'ro', required => 1,); # Install proxy methods to the DBI object. # We can't use handles() as DBIx::Connector->dbh has to be called each # time we need a DBI handle to ensure the connection is alive. { - my @DBI_METHODS = qw( - begin_work column_info commit do errstr get_info last_insert_id ping prepare - primary_key quote_identifier rollback selectall_arrayref selectall_hashref - selectcol_arrayref selectrow_array selectrow_arrayref selectrow_hashref table_info + my @DBI_METHODS = qw( + begin_work column_info commit do errstr get_info last_insert_id ping prepare + primary_key quote_identifier rollback selectall_arrayref selectall_hashref + selectcol_arrayref selectrow_array selectrow_arrayref selectrow_hashref table_info + ); + my $stash = Package::Stash->new(__PACKAGE__); + + foreach my $method (@DBI_METHODS) { + my $symbol = '&' . $method; + $stash->add_symbol( + $symbol => sub { + my $self = shift; + return $self->dbh->$method(@_); + } ); - my $stash = Package::Stash->new(__PACKAGE__); - - foreach my $method (@DBI_METHODS) { - my $symbol = '&' . $method; - $stash->add_symbol( - $symbol => sub { - my $self = shift; - return $self->dbh->$method(@_); - } - ); - } + } } ##################################################################### # 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 @@ -81,14 +75,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, @@ -122,10 +116,10 @@ use constant INDEX_DROPS_REQUIRE_FK_DROPS => 1; ##################################################################### sub quote { - my $self = shift; - my $retval = $self->dbh->quote(@_); - trick_taint($retval) if defined $retval; - return $retval; + my $self = shift; + my $retval = $self->dbh->quote(@_); + trick_taint($retval) if defined $retval; + return $retval; } ##################################################################### @@ -133,203 +127,210 @@ sub quote { ##################################################################### sub connect_shadow { - state $shadow_dbh; - if ($shadow_dbh && $shadow_dbh->bz_in_transaction) { - FATAL("Somehow in a transaction at connection time"); - $shadow_dbh->bz_rollback_transaction(); - } - return $shadow_dbh if $shadow_dbh; - my $params = Bugzilla->params; - die "Tried to connect to non-existent shadowdb" - unless Bugzilla->get_param_with_override('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 = dclone(Bugzilla->localconfig); - $connect_params->{db_host} = Bugzilla->get_param_with_override('shadowdbhost'); - $connect_params->{db_name} = Bugzilla->get_param_with_override('shadowdb'); - $connect_params->{db_port} = Bugzilla->get_param_with_override('shadowdbport'); - $connect_params->{db_sock} = Bugzilla->get_param_with_override('shadowdbsock'); - - if ( Bugzilla->localconfig->{'shadowdb_user'} && Bugzilla->localconfig->{'shadowdb_pass'} ) { - $connect_params->{db_user} = Bugzilla->localconfig->{'shadowdb_user'}; - $connect_params->{db_pass} = Bugzilla->localconfig->{'shadowdb_pass'}; - } - return $shadow_dbh = _connect($connect_params); + state $shadow_dbh; + if ($shadow_dbh && $shadow_dbh->bz_in_transaction) { + FATAL("Somehow in a transaction at connection time"); + $shadow_dbh->bz_rollback_transaction(); + } + return $shadow_dbh if $shadow_dbh; + my $params = Bugzilla->params; + die "Tried to connect to non-existent shadowdb" + unless Bugzilla->get_param_with_override('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 = dclone(Bugzilla->localconfig); + $connect_params->{db_host} = Bugzilla->get_param_with_override('shadowdbhost'); + $connect_params->{db_name} = Bugzilla->get_param_with_override('shadowdb'); + $connect_params->{db_port} = Bugzilla->get_param_with_override('shadowdbport'); + $connect_params->{db_sock} = Bugzilla->get_param_with_override('shadowdbsock'); + + if ( Bugzilla->localconfig->{'shadowdb_user'} + && Bugzilla->localconfig->{'shadowdb_pass'}) + { + $connect_params->{db_user} = Bugzilla->localconfig->{'shadowdb_user'}; + $connect_params->{db_pass} = Bugzilla->localconfig->{'shadowdb_pass'}; + } + return $shadow_dbh = _connect($connect_params); } sub connect_main { - state $main_dbh = _connect(Bugzilla->localconfig); - if ($main_dbh->bz_in_transaction) { - FATAL("Somehow in a transaction at connection time"); - $main_dbh->bz_rollback_transaction(); - } - return $main_dbh; + state $main_dbh = _connect(Bugzilla->localconfig); + if ($main_dbh->bz_in_transaction) { + FATAL("Somehow in a transaction at connection time"); + $main_dbh->bz_rollback_transaction(); + } + return $main_dbh; } 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_module($pkg_module) } - || die ("'$driver' is not a valid choice for \$db_driver in " - . " localconfig: " . $@); + # do the actual import + eval { require_module($pkg_module) } + || die( + "'$driver' is not a valid choice for \$db_driver in " . " localconfig: " . $@); - # instantiate the correct DB specific module + # instantiate the correct DB specific module - return $pkg_module->new($params); + return $pkg_module->new($params); } 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; - # BMO: stracktrace disabled: - # $_[0] = Carp::longmess($_[0]); - - # BMO: catch long running query timeouts and translate into a sane message - #if ($_[0] =~ /Lost connection to MySQL server during query/) { - # warn(Carp::longmess($_[0])); - # $_[0] = "The database query took too long to complete and has been canceled.\n" - # . "(Lost connection to MySQL server during query)"; - #} - - #if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { - # ThrowCodeError("db_error", { err_message => $_[0] }); - #} - - # keep tests happy - if (0) { - ThrowCodeError("db_error", { err_message => $_[0] }); - } + require Carp; + + # Cut down the error string to a reasonable size + $_[0] = substr($_[0], 0, 2000) . ' ... ' . substr($_[0], -2000) + if length($_[0]) > 4000; + + # BMO: stracktrace disabled: + # $_[0] = Carp::longmess($_[0]); + +# BMO: catch long running query timeouts and translate into a sane message +#if ($_[0] =~ /Lost connection to MySQL server during query/) { +# warn(Carp::longmess($_[0])); +# $_[0] = "The database query took too long to complete and has been canceled.\n" +# . "(Lost connection to MySQL server during query)"; +#} + + #if (Bugzilla->usage_mode == USAGE_MODE_BROWSER) { + # ThrowCodeError("db_error", { err_message => $_[0] }); + #} + + # keep tests happy + if (0) { + ThrowCodeError("db_error", {err_message => $_[0]}); + } - return 0; # Now let DBI handle raising the error + 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'}; + } - # 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_server_version { - my ($self, $db, $output) = @_; + my ($self, $db, $output) = @_; - my $sql_vers = $self->bz_server_version; + my $sql_vers = $self->bz_server_version; - 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(); - print "Creating database $db_name...\n"; - - # 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(); + print "Creating database $db_name...\n"; + + # 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(); } + } } # 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_prefix_match { - my ($self, $column, $str) = @_; - my $must_escape = $str =~ s/([_%!])/!$1/g; - my $escape = $must_escape ? q/ESCAPE '!'/ : ''; - my $quoted_str = $self->quote("$str%"); - return "$column LIKE $quoted_str $escape"; + my ($self, $column, $str) = @_; + my $must_escape = $str =~ s/([_%!])/!$1/g; + my $escape = $must_escape ? q/ESCAPE '!'/ : ''; + my $quoted_str = $self->quote("$str%"); + return "$column LIKE $quoted_str $escape"; } 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_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) = @_; + my ($self, @params) = @_; - return '(' . join(' || ', @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"; } ##################################################################### @@ -487,24 +491,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}); } ##################################################################### @@ -512,99 +519,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) { - print install_string('db_table_setup'), "\n"; - } + 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) { + print install_string('db_table_setup'), "\n"; + } + + 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) { - print get_text('install_fk_setup'), "\n"; - } + 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) { + print get_text('install_fk_setup'), "\n"; + } + + 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); } + } } ##################################################################### @@ -612,119 +620,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; + } } @@ -750,39 +760,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); - print "Updating column $name in table $table ...\n"; - if (defined $current_def) { - my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); - print "Old: $old_ddl\n"; - } - print "New: $new_ddl\n"; - $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); + print "Updating column $name in table $table ...\n"; + if (defined $current_def) { + my $old_ddl = $self->_bz_schema->get_type_ddl($current_def); + print "Old: $old_ddl\n"; + } + print "New: $new_ddl\n"; + $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) @@ -802,36 +813,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; - } + 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)); - $self->_bz_real_schema->add_table($name, $table_def); - $self->_bz_store_real_schema; + 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 @@ -849,158 +860,168 @@ sub bz_add_table { # 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}) - { - print install_string('db_table_new', { table => $name }), "\n"; - } - $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}) { + print install_string('db_table_new', {table => $name}), "\n"; + } + $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) = @_; -sub bz_add_field_tables { - my ($self, $field) = @_; + # We do nothing if the table already exists. + return if $self->bz_table_info($name); - $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); + # 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}; + } - $self->bz_add_fks($ms_table, - { bug_id => {TABLE => 'bugs', COLUMN => 'bug_id', - DELETE => 'CASCADE'}, + # INDEXES is supposed to be an arrayref, so we have to convert back. + my @indexes_array = %fixed_indexes; + $table_schema{INDEXES} = \@indexes_array; - value => {TABLE => $field->name, COLUMN => 'value'} }); - } + # 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'} + } + ); + } } 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) { + $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) @@ -1020,108 +1041,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; } ##################################################################### @@ -1129,12 +1153,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() @@ -1148,53 +1172,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(); } ##################################################################### @@ -1213,9 +1238,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() @@ -1225,9 +1250,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]})}; } ##################################################################### @@ -1235,54 +1260,58 @@ 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; } 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) = @_; + + 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; + } } 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; - } 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; + } + 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; + } } ##################################################################### @@ -1290,43 +1319,45 @@ sub bz_rollback_transaction { ##################################################################### sub _build_connector { - my ($self) = @_; - my ($dsn, $user, $pass, $override_attrs) = - map { $self->$_ } 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 => 1, - AutoCommit => 1, - PrintError => 0, - ShowErrorStatement => 1, - HandleError => \&_handle_error, - TaintIn => 1, - FetchHashKeyName => 'NAME', - # Note: NAME_lc causes crash on ActiveState Perl - # 5.8.4 (see Bug 253696) - # XXX - This will likely cause problems in DB - # back ends that twiddle column case (Oracle?) - }; - - if ($override_attrs) { - foreach my $key (keys %$override_attrs) { - $attributes->{$key} = $override_attrs->{$key}; - } + my ($self) = @_; + my ($dsn, $user, $pass, $override_attrs) + = map { $self->$_ } 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 => 1, + AutoCommit => 1, + PrintError => 0, + ShowErrorStatement => 1, + HandleError => \&_handle_error, + TaintIn => 1, + FetchHashKeyName => 'NAME', + + # Note: NAME_lc causes crash on ActiveState Perl + # 5.8.4 (see Bug 253696) + # XXX - This will likely cause problems in DB + # back ends that twiddle column case (Oracle?) + }; + + if ($override_attrs) { + foreach my $key (keys %$override_attrs) { + $attributes->{$key} = $override_attrs->{$key}; } - my $class = ref $self; - weaken($self); - $attributes->{Callbacks} = { - connected => sub { - my ($dbh, $dsn) = @_; - INFO("$PROGRAM_NAME connected mysql $dsn"); - ThrowCodeError('not_in_transaction') if $self && $self->bz_in_transaction; - $class->on_dbi_connected(@_) if $class->can('on_dbi_connected'); - return - }, - }; - - return DBIx::Connector->new($dsn, $user, $pass, $attributes); + } + my $class = ref $self; + weaken($self); + $attributes->{Callbacks} = { + connected => sub { + my ($dbh, $dsn) = @_; + INFO("$PROGRAM_NAME connected mysql $dsn"); + ThrowCodeError('not_in_transaction') if $self && $self->bz_in_transaction; + $class->on_dbi_connected(@_) if $class->can('on_dbi_connected'); + return; + }, + }; + + return DBIx::Connector->new($dsn, $user, $pass, $attributes); } ##################################################################### @@ -1350,55 +1381,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'); - } + print install_string('db_schema_init'), "\n"; + 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(); - print install_string('db_schema_init'), "\n"; - 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()> @@ -1412,24 +1442,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()> @@ -1449,106 +1478,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; diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm index 4dd2620d3..640cf89ec 100644 --- a/Bugzilla/DB/Mysql.pm +++ b/Bugzilla/DB/Mysql.pm @@ -44,224 +44,231 @@ use constant MAX_COMMENTS => 50; use constant FULLTEXT_OR => '|'; sub BUILDARGS { - my ($class, $params) = @_; - my ($user, $pass, $host, $dbname, $port, $sock) = - @$params{qw(db_user db_pass db_host db_name db_port db_sock)}; + 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; + # 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 => 1 ); + my %attrs = (mysql_enable_utf8 => 1); - return { dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs }; + return {dsn => $dsn, user => $user, pass => $pass, attrs => \%attrs}; } sub on_dbi_connected { - my ($class, $dbh) = @_; - - # This makes sure that if the tables are encoded as UTF-8, we - # return their data correctly. - my $charset = $class->utf8_charset; - my $collate = $class->utf8_collate; - $dbh->do("SET NAMES $charset COLLATE $collate"); - - # Bug 321645 - disable MySQL strict mode, if set - my ($var, $sql_mode) = $dbh->selectrow_array( - "SHOW VARIABLES LIKE 'sql\\_mode'"); - - 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 {$_ !~ /^STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL$/} - split(/,/, $sql_mode)); - - if ($sql_mode ne $new_sql_mode) { - $dbh->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); - } + my ($class, $dbh) = @_; + + # This makes sure that if the tables are encoded as UTF-8, we + # return their data correctly. + my $charset = $class->utf8_charset; + my $collate = $class->utf8_collate; + $dbh->do("SET NAMES $charset COLLATE $collate"); + + # Bug 321645 - disable MySQL strict mode, if set + my ($var, $sql_mode) + = $dbh->selectrow_array("SHOW VARIABLES LIKE 'sql\\_mode'"); + + 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 { $_ !~ /^STRICT_(?:TRANS|ALL)_TABLES|TRADITIONAL$/ } + split(/,/, $sql_mode)); + + if ($sql_mode ne $new_sql_mode) { + $dbh->do("SET SESSION sql_mode = ?", undef, $new_sql_mode); } + } - # Allow large GROUP_CONCATs (largely for inserting comments - # into bugs_fulltext). - $dbh->do('SET SESSION group_concat_max_len = 128000000'); + # Allow large GROUP_CONCATs (largely for inserting comments + # into bugs_fulltext). + $dbh->do('SET SESSION group_concat_max_len = 128000000'); } # 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) = @_; - $separator = $self->quote(', ') if !defined $separator; - $sort = 1 if !defined $sort; - if ($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) = @_; + $separator = $self->quote(', ') if !defined $separator; + $sort = 1 if !defined $sort; + if ($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) = @_; + my ($self, @params) = @_; - return 'CONCAT(' . join(', ', @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 = grep { defined } 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 . '"'; - # match words that contain only boolean operators - } elsif ($word =~ /^[\+\-\<\>\~\*]+$/) { - $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 = grep {defined} 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 . '"'; + + # match words that contain only boolean operators } - $text = join(FULLTEXT_OR, @terms); + elsif ($word =~ /^[\+\-\<\>\~\*]+$/) { + $word = '"' . $word . '"'; + } + } + $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) = @_; + my ($self, $string) = @_; - return $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) = @_; + my ($self, $date, $operator, $interval, $units) = @_; - return "$date $operator INTERVAL $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(); } ##################################################################### @@ -269,435 +276,451 @@ 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'); + } + + if ($self->utf8_charset eq 'utf8mb4') { + my %global = map {@$_} + @{$self->selectall_arrayref(q(SHOW GLOBAL VARIABLES LIKE 'innodb_%'))}; + my $utf8mb4_supported + = $global{innodb_file_format} eq 'Barracuda' + && $global{innodb_file_per_table} eq 'ON' + && $global{innodb_large_prefix} eq 'ON'; + + die install_string('mysql_innodb_settings') unless $utf8mb4_supported; + + my $tables = $self->selectall_arrayref('SHOW TABLE STATUS'); + foreach my $table (@$tables) { + my ($table, undef, undef, $row_format) = @$table; + my $new_row_format = $self->default_row_format($table); + next if $new_row_format =~ /compact/i; + if (lc($new_row_format) ne lc($row_format)) { + print install_string( + 'mysql_row_format_conversion', {table => $table, format => $new_row_format} + ), + "\n"; + $self->do(sprintf 'ALTER TABLE %s ROW_FORMAT=%s', $table, $new_row_format); + } } + } - # 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(); - if ($self->utf8_charset eq 'utf8mb4') { - my %global = map { @$_ } @{ $self->selectall_arrayref(q(SHOW GLOBAL VARIABLES LIKE 'innodb_%')) }; - my $utf8mb4_supported - = $global{innodb_file_format} eq 'Barracuda' - && $global{innodb_file_per_table} eq 'ON' - && $global{innodb_large_prefix} eq 'ON'; - - die install_string('mysql_innodb_settings') unless $utf8mb4_supported; - - my $tables = $self->selectall_arrayref('SHOW TABLE STATUS'); - foreach my $table (@$tables) { - my ($table, undef, undef, $row_format) = @$table; - my $new_row_format = $self->default_row_format($table); - next if $new_row_format =~ /compact/i; - if (lc($new_row_format) ne lc($row_format)) { - print install_string('mysql_row_format_conversion', { table => $table, format => $new_row_format }), "\n"; - $self->do(sprintf 'ALTER TABLE %s ROW_FORMAT=%s', $table, $new_row_format); - } - } + # 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'); } - - 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 ($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 ($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 (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'); } - - # 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'); - - 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 ($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' + ); + + 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"); - # 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; - } + my $bug_count = $self->selectrow_array("SELECT COUNT(*) FROM bugs"); - # 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'); - } + # 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; - # 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. - unless ( $self->bz_db_is_utf8 ) { - $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; } - # 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"); + # 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'); } - # 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 $charset = $self->utf8_charset; - my $collate = $self->utf8_collate; - my $non_utf8_tables = $self->selectrow_array( - "SELECT 1 FROM information_schema.TABLES + # 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. + unless ($self->bz_db_is_utf8) { + $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; + } + + # 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 $charset = $self->utf8_charset; + my $collate = $self->utf8_collate; + 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 != ? - LIMIT 1", undef, $db_name, $collate); - - 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 $charset (collate $collate). 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} ne $collate) - { - my $name = $column->{Field}; - - print "$table.$name needs to be converted to $charset (collate $collate)...\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 $charset COLLATE $collate/; - 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 $charset COLLATE $collate"); - $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 $charset COLLATE $collate"); - } - - } # foreach my $table (@tables) + LIMIT 1", undef, $db_name, $collate + ); + + 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. - unless ($self->bz_db_is_utf8) { - $self->_alter_db_charset_to_utf8(); - } + print + "Converting table storage format to $charset (collate $collate). 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} ne $collate) + { + my $name = $column->{Field}; - $self->_fix_defaults(); + print "$table.$name needs to be converted to $charset (collate $collate)...\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 $charset COLLATE $collate/; + 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 $charset COLLATE $collate"); + $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 $charset COLLATE $collate"); + } + + } # 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. + unless ($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 @@ -707,151 +730,160 @@ 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 utf8_charset { - return 'utf8' unless Bugzilla->params->{'utf8'}; - return Bugzilla->params->{'utf8'} eq 'utf8mb4' ? 'utf8mb4' : 'utf8'; + return 'utf8' unless Bugzilla->params->{'utf8'}; + return Bugzilla->params->{'utf8'} eq 'utf8mb4' ? 'utf8mb4' : 'utf8'; } sub utf8_collate { - my $charset = utf8_charset(); - if ($charset eq 'utf8') { - return 'utf8_general_ci'; - } - elsif ($charset eq 'utf8mb4') { - return 'utf8mb4_unicode_520_ci'; - } - else { - croak "invalid charset: $charset"; - } + my $charset = utf8_charset(); + if ($charset eq 'utf8') { + return 'utf8_general_ci'; + } + elsif ($charset eq 'utf8mb4') { + return 'utf8mb4_unicode_520_ci'; + } + else { + croak "invalid charset: $charset"; + } } sub default_row_format { - my ($class, $table) = @_; - my $charset = utf8_charset(); - if ($charset eq 'utf8') { - return 'Compact'; - } - elsif ($charset eq 'utf8mb4') { - my @no_compress = qw( - bug_user_last_visit - cc - email_rates - logincookies - token_data - tokens - ts_error - ts_exitstatus - ts_funcmap - ts_job - ts_note - user_request_log - votes - ); - return 'Dynamic' if any { $table eq $_ } @no_compress; - return 'Compressed'; - } - else { - croak "invalid charset: $charset"; - } + my ($class, $table) = @_; + my $charset = utf8_charset(); + if ($charset eq 'utf8') { + return 'Compact'; + } + elsif ($charset eq 'utf8mb4') { + my @no_compress = qw( + bug_user_last_visit + cc + email_rates + logincookies + token_data + tokens + ts_error + ts_exitstatus + ts_funcmap + ts_job + ts_note + user_request_log + votes + ); + return 'Dynamic' if any { $table eq $_ } @no_compress; + return 'Compressed'; + } + else { + croak "invalid charset: $charset"; + } } sub _alter_db_charset_to_utf8 { - my $self = shift; - my $db_name = Bugzilla->localconfig->{db_name}; - my $charset = $self->utf8_charset; - my $collate = $self->utf8_collate; - $self->do("ALTER DATABASE $db_name CHARACTER SET $charset COLLATE $collate"); + my $self = shift; + my $db_name = Bugzilla->localconfig->{db_name}; + my $charset = $self->utf8_charset; + my $collate = $self->utf8_collate; + $self->do("ALTER DATABASE $db_name CHARACTER SET $charset COLLATE $collate"); } sub bz_db_is_utf8 { - my $self = shift; - my $db_charset = $self->selectrow_arrayref( - "SHOW VARIABLES LIKE 'character_set_database'"); - # First column holds the variable name, second column holds the value. - my $charset = $self->utf8_charset; - return $db_charset->[1] eq $charset ? 1 : 0; + my $self = shift; + my $db_charset + = $self->selectrow_arrayref("SHOW VARIABLES LIKE 'character_set_database'"); + + # First column holds the variable name, second column holds the value. + my $charset = $self->utf8_charset; + return $db_charset->[1] eq $charset ? 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; } ##################################################################### @@ -882,29 +914,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 @@ -918,42 +950,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 @@ -966,10 +999,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]})}; } ##################################################################### @@ -993,34 +1027,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 a519bb796..81ca1090f 100644 --- a/Bugzilla/DB/Oracle.pm +++ b/Bugzilla/DB/Oracle.pm @@ -37,466 +37,477 @@ 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 '; our $fulltext_label = 0; sub BUILDARGS { - 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)}; - # You can never connect to Oracle without a DB name, - # and there is no default DB. - $dbname ||= Bugzilla->localconfig->{db_name}; + # 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'}; + # 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, - }; - return { dsn => $dsn, user => $user, pass => $pass, attrs => $attrs }; + # 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, + }; + return {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}; } sub on_dbi_connected { - my ($class, $dbh) = @_; + my ($class, $dbh) = @_; + + # Set the session's default date format to match MySQL + $dbh->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); + $dbh->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") + if Bugzilla->params->{'utf8'}; - # Set the session's default date format to match MySQL - $dbh->do("ALTER SESSION SET NLS_DATE_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $dbh->do("ALTER SESSION SET NLS_TIMESTAMP_FORMAT='YYYY-MM-DD HH24:MI:SS'"); - $dbh->do("ALTER SESSION SET NLS_LENGTH_SEMANTICS='CHAR'") - if Bugzilla->params->{'utf8'}; - # To allow case insensitive query. - $dbh->do("ALTER SESSION SET NLS_COMP='ANSI'"); - $dbh->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); + # To allow case insensitive query. + $dbh->do("ALTER SESSION SET NLS_COMP='ANSI'"); + $dbh->do("ALTER SESSION SET NLS_SORT='BINARY_AI'"); } 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); + 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) = @_; - $text = $self->quote($text); - trick_taint($text); - $fulltext_label++; - return "CONTAINS($column,$text,$fulltext_label) > 0", "SCORE($fulltext_label)"; + my ($self, $column, $text) = @_; + $text = $self->quote($text); + trick_taint($text); + $fulltext_label++; + return "CONTAINS($column,$text,$fulltext_label) > 0", "SCORE($fulltext_label)"; } 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; - $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) { + 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. 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; + 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 recognizes CURRENT_DATE, but not CURRENT_DATE() + # and its CURRENT_DATE is a date+time, so wrap in TRUNC() + $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 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; + # 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 recognizes CURRENT_DATE, but not CURRENT_DATE() # and its CURRENT_DATE is a date+time, so wrap in TRUNC() - $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); - - # Oracle recognizes CURRENT_DATE, but not CURRENT_DATE() - # and its CURRENT_DATE is a date+time, so wrap in TRUNC() - $nonstring =~ s/\bCURRENT_DATE\b(?:\(\))?/TRUNC(CURRENT_DATE)/io; - - # Oracle use SUBSTR instead of SUBSTRING - $nonstring =~ s/\bSUBSTRING\b/SUBSTR/io; - - # Oracle need no 'AS' - $nonstring =~ s/\bAS\b//ig; - - # Take the first 4000 chars for comparison - $nonstring =~ s/\(\s*(longdescs_\d+\.thetext|attachdata_\d+\.thedata)/ + # Take the first 4000 chars for comparison + $nonstring =~ s/\(\s*(longdescs_\d+\.thetext|attachdata_\d+\.thedata)/ \(DBMS_LOB.SUBSTR\($1, 4000, 1\)/ig; - # Look for a LIMIT clause - ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); + # Look for a LIMIT clause + ($limit) = ($nonstring =~ m(/\* LIMIT (\d*) \*/)o); - if(!length($string)){ - push @result, EMPTY_STRING; - push @result, $nonstring; - } else { - push @result, $string; - push @result, $nonstring; - } + if (!length($string)) { + push @result, EMPTY_STRING; + push @result, $nonstring; } + else { + push @result, $string; + push @result, $nonstring; + } + } - my $new_sql = join "'", @result; + 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); + # 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 + # 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; + if (defined($limit)) { + if ($new_sql !~ /\bWHERE\b/) { + $new_sql = $new_sql . " WHERE 1=1"; } - return $new_sql; + 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; + } + 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; } ##################################################################### @@ -504,20 +515,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; } ##################################################################### @@ -525,32 +538,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( @@ -568,9 +586,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 @@ -614,110 +634,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; } ############################################################################ @@ -726,65 +753,66 @@ package Bugzilla::DB::Oracle::st; use base 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); + } + elsif (ref($row) eq 'HASH') { + Bugzilla::DB::Oracle::_fix_hashref($row); } - return $ref; + } + 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 0db349412..d1bb0f798 100644 --- a/Bugzilla/DB/Pg.pm +++ b/Bugzilla/DB/Pg.pm @@ -31,151 +31,153 @@ use DBD::Pg; # This module extends the DB interface via inheritance extends qw(Bugzilla::DB); -use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA }; +use constant BLOB_TYPE => {pg_type => DBD::Pg::PG_BYTEA}; sub BUILDARGS { - 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'}}; - return { dsn => $dsn, user => $user, pass => $pass, attrs => $attrs } + return {dsn => $dsn, user => $user, pass => $pass, attrs => $attrs}; } # 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) = @_; - $sort = 1 if !defined $sort; - $separator = $self->quote(', ') if !defined $separator; - my $sql = "array_accum($text)"; - if ($sort) { - $sql = "array_sort($sql)"; - } - return "array_to_string($sql, $separator)"; + my ($self, $text, $separator, $sort) = @_; + $sort = 1 if !defined $sort; + $separator = $self->quote(', ') if !defined $separator; + my $sql = "array_accum($text)"; + if ($sort) { + $sql = "array_sort($sql)"; + } + return "array_to_string($sql, $separator)"; } 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_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) = @_; + my ($self, $date, $format) = @_; - $format = "%Y.%m.%d %H:%i:%s" if !$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 ($self, $date, $operator, $interval, $units) = @_; - return "$date $operator $interval * INTERVAL '1 $units'"; + return "$date $operator $interval * INTERVAL '1 $units'"; } sub sql_string_concat { - my ($self, @params) = @_; + 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))'; + # 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); } ##################################################################### @@ -183,38 +185,42 @@ sub bz_explain { ##################################################################### 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(@_); - - # Custom Functions - 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(@_); + + # Custom Functions + 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 @@ -228,117 +234,132 @@ SELECT ARRAY( $$; END - # 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')" + ); + } } + } } 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); } + } } ##################################################################### @@ -348,12 +369,13 @@ sub _fix_case_differences { # 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(@_); - 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; + # All PostgreSQL system tables start with "pg_" or "sql_" + my @table_list = grep(!/(^pg_)|(^sql_)/, @full_table_list); + return @table_list; } 1; diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm index e1c19fa51..f681445b0 100644 --- a/Bugzilla/DB/Schema.pm +++ b/Bugzilla/DB/Schema.pm @@ -31,6 +31,7 @@ use List::MoreUtils qw(firstidx natatime); use Try::Tiny; use Module::Runtime qw(require_module); use Safe; + # Historical, needed for SCHEMA_VERSION = '1.00' use Storable qw(dclone freeze thaw); @@ -199,1644 +200,1593 @@ 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 => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id'} + }, + resolution => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}, + target_milestone => {TYPE => 'varchar(20)', 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'}, + alias => {TYPE => 'varchar(40)'}, ], - # 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_alias_idx => {FIELDS => ['alias'], TYPE => 'UNIQUE'}, + 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 => 'INT2', NOTNULL => 1, - REFERENCES => {TABLE => 'components', - COLUMN => 'id'}}, - resolution => {TYPE => 'varchar(64)', - NOTNULL => 1, DEFAULT => "''"}, - target_milestone => {TYPE => 'varchar(20)', - 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'}, - alias => {TYPE => 'varchar(40)'}, - ], - INDEXES => [ - bugs_alias_idx => {FIELDS => ['alias'], - TYPE => 'UNIQUE'}, - 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'], - ], - }, - - 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)'}, - is_markdown => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'} - ], - INDEXES => [ - longdescs_bug_id_idx => ['bug_id'], - 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(100)', 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'}, - attach_size => {TYPE => 'INT4', NOTNULL => 1, - DEFAULT => 0}, - ], - 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'], - attachments_ispatch_idx => ['ispatch'], - ], - }, - 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}, - is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - 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 => 'INT2', - 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 => 'INT2', - 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}, - 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(20)', 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 => "''"}, - nickname => {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'}, - password_change_required => { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }, - password_change_reason => { TYPE => 'varchar(64)' }, - mfa => {TYPE => 'varchar(8)', DEFAULT => "''" }, - mfa_required_date => {TYPE => 'DATETIME'}, - ], - INDEXES => [ - profiles_login_name_idx => {FIELDS => ['login_name'], - TYPE => 'UNIQUE'}, - profiles_extern_id_idx => {FIELDS => ['extern_id'], - TYPE => 'UNIQUE'}, - profiles_nickname_idx => ['nickname'], - profiles_realname_ft_idx => {FIELDS => ['realname'], - TYPE => 'FULLTEXT'}, - ], - }, - - 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'], - ], - }, - - profile_mfa => { - FIELDS => [ - id => { TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - user_id => { TYPE => 'INT3', NOTNULL => 1, - REFERENCES => { TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE' } }, - name => { TYPE => 'varchar(16)', NOTNULL => 1 }, - value => { TYPE => 'varchar(255)' }, - ], - INDEXES => [ - profile_mfa_userid_name_idx => { FIELDS => [ 'user_id', 'name' ], TYPE => 'UNIQUE' }, - ], - }, - - 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'}, - ], - }, - - component_cc => { - - FIELDS => [ - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - component_id => {TYPE => 'INT2', 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}, - restrict_ipaddr => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}, - ], - 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 => [ - userid => {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - issuedate => {TYPE => 'DATETIME', NOTNULL => 1} , - token => {TYPE => 'varchar(22)', NOTNULL => 1, - PRIMARYKEY => 1}, - tokentype => {TYPE => 'varchar(16)', NOTNULL => 1} , - eventdata => {TYPE => 'TINYTEXT'}, - ], - INDEXES => [ - tokens_userid_idx => ['userid'], - ], - }, - - token_data => { - FIELDS => [ - id => { TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - token => { TYPE => 'varchar(22)', NOTNULL => 1, - REFERENCES => { TABLE => 'tokens', COLUMN => 'token', DELETE => 'CASCADE' }}, - extra_data => { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, - ], - INDEXES => [ - token_data_idx => { FIELDS => ['token'], TYPE => 'UNIQUE' }, - ], - }, - - # 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'}, - owner_user_id => {TYPE => 'INT3', - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid'}}, - idle_member_removal => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'} - ], - 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(20)', - NOTNULL => 1, DEFAULT => "'---'"}, - allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, - DEFAULT => 'TRUE'}, - ], - INDEXES => [ - products_name_idx => {FIELDS => ['name'], - TYPE => 'UNIQUE'}, - ], - }, - - components => { - FIELDS => [ - id => {TYPE => 'SMALLSERIAL', 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'}, - triage_owner_id => {TYPE => 'INT3', - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL'}}, - ], - 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 => 'MEDIUMTEXT', 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)'}, - category => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'General'"} - ], - }, - - 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'}, - ], - }, - - email_rates => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - recipient => {TYPE => 'varchar(255)', NOTNULL => 1}, - message_ts => {TYPE => 'DATETIME', NOTNULL => 1}, - ], - INDEXES => [ - email_rates_idx => [qw(recipient message_ts)], - email_rates_message_ts_idx => ['message_ts'], - ], - }, - - # 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'}, - last_used_ip => {TYPE => 'varchar(40)'}, - app_id => {TYPE => 'varchar(64)'}, - ], - INDEXES => [ - user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, - user_api_keys_user_id_idx => ['user_id'], - user_api_keys_user_id_app_id_idx => ['user_id', 'app_id'], - ], - }, - - user_request_log => { - FIELDS => [ - id => {TYPE => 'INTSERIAL', NOTNULL => 1, - PRIMARYKEY => 1}, - user_id => {TYPE => 'INT3', NOTNULL => 1 }, - ip_address => {TYPE => 'varchar(40)', NOTNULL => 1}, - user_agent => {TYPE => 'TINYTEXT', NOTNULL => 1}, - timestamp => {TYPE => 'DATETIME', NOTNULL => 1}, - bug_id => {TYPE => 'INT3', NOTNULL => 0}, - attach_id => {TYPE => 'INT4', NOTNULL => 0}, - request_url => {TYPE => 'TINYTEXT', NOTNULL => 1}, - method => {TYPE => 'TINYTEXT', NOTNULL => 1}, - action => {TYPE => 'varchar(20)', NOTNULL => 1}, - server => {TYPE => 'varchar(7)', NOTNULL => 1}, - ], - INDEXES => [ - user_user_request_log_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 => { + 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)'}, + is_markdown => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'} + ], + INDEXES => [ + longdescs_bug_id_idx => ['bug_id'], + 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(100)', 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'}, + attach_size => {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}, + ], + 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'], + attachments_ispatch_idx => ['ispatch'], + ], + }, + 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}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + 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 => 'INT2', + 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 => 'INT2', + 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}, + 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(20)', 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 => "''"}, + nickname => {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'}, + password_change_required => + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, + password_change_reason => {TYPE => 'varchar(64)'}, + mfa => {TYPE => 'varchar(8)', DEFAULT => "''"}, + mfa_required_date => {TYPE => 'DATETIME'}, + ], + INDEXES => [ + profiles_login_name_idx => {FIELDS => ['login_name'], TYPE => 'UNIQUE'}, + profiles_extern_id_idx => {FIELDS => ['extern_id'], TYPE => 'UNIQUE'}, + profiles_nickname_idx => ['nickname'], + profiles_realname_ft_idx => {FIELDS => ['realname'], TYPE => 'FULLTEXT'}, + ], + }, + + 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'], + ], + }, + + profile_mfa => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(16)', NOTNULL => 1}, + value => {TYPE => 'varchar(255)'}, + ], + INDEXES => [ + profile_mfa_userid_name_idx => + {FIELDS => ['user_id', 'name'], TYPE => 'UNIQUE'}, + ], + }, + + 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'},], + }, + + component_cc => { + + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + component_id => { + TYPE => 'INT2', + 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}, + restrict_ipaddr => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}, + ], + 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 => [ + userid => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + issuedate => {TYPE => 'DATETIME', NOTNULL => 1}, + token => {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1}, + tokentype => {TYPE => 'varchar(16)', NOTNULL => 1}, + eventdata => {TYPE => 'TINYTEXT'}, + ], + INDEXES => [tokens_userid_idx => ['userid'],], + }, + + token_data => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + token => { + TYPE => 'varchar(22)', + NOTNULL => 1, + REFERENCES => {TABLE => 'tokens', COLUMN => 'token', DELETE => 'CASCADE'} + }, + extra_data => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + ], + INDEXES => [token_data_idx => {FIELDS => ['token'], TYPE => 'UNIQUE'},], + }, + + # 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'}, + owner_user_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'}}, + idle_member_removal => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'} + ], + 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(20)', NOTNULL => 1, DEFAULT => "'---'"}, + allows_unconfirmed => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}, + ], + INDEXES => [products_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE'},], + }, + + components => { + FIELDS => [ + id => {TYPE => 'SMALLSERIAL', 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'}, + triage_owner_id => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + }, + ], + 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 => 'MEDIUMTEXT', 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)'}, + category => {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'General'"} + ], + }, + + 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'}, + ], + }, + + email_rates => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + recipient => {TYPE => 'varchar(255)', NOTNULL => 1}, + message_ts => {TYPE => 'DATETIME', NOTNULL => 1}, + ], + INDEXES => [ + email_rates_idx => [qw(recipient message_ts)], + email_rates_message_ts_idx => ['message_ts'], + ], + }, + + # 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 => [ - bug_id => {TYPE => 'INT3', NOTNULL => 1}, - value => {TYPE => 'varchar(64)', NOTNULL => 1}, + 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 => [ - bug_id_idx => {FIELDS => [qw( bug_id value)], TYPE => 'UNIQUE'}, + 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'}, + last_used_ip => {TYPE => 'varchar(40)'}, + app_id => {TYPE => 'varchar(64)'}, + ], + INDEXES => [ + user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'}, + user_api_keys_user_id_idx => ['user_id'], + user_api_keys_user_id_app_id_idx => ['user_id', 'app_id'], + ], + }, + + user_request_log => { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + user_id => {TYPE => 'INT3', NOTNULL => 1}, + ip_address => {TYPE => 'varchar(40)', NOTNULL => 1}, + user_agent => {TYPE => 'TINYTEXT', NOTNULL => 1}, + timestamp => {TYPE => 'DATETIME', NOTNULL => 1}, + bug_id => {TYPE => 'INT3', NOTNULL => 0}, + attach_id => {TYPE => 'INT4', NOTNULL => 0}, + request_url => {TYPE => 'TINYTEXT', NOTNULL => 1}, + method => {TYPE => 'TINYTEXT', NOTNULL => 1}, + action => {TYPE => 'varchar(20)', NOTNULL => 1}, + server => {TYPE => 'varchar(7)', NOTNULL => 1}, + ], + INDEXES => [user_user_request_log_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'},], }; #-------------------------------------------------------------------------- @@ -1871,30 +1821,31 @@ 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; - try { - require_module($class); - } - catch { - die "The $class class could not be found ($subclass not supported?): $_"; - }; + if ($driver) { + (my $subclass = $driver) =~ s/^(\S)/\U$1/; + $class .= '::' . $subclass; + try { + require_module($class); } - die "$class is an abstract base class. Instantiate a subclass instead." - if ($class eq __PACKAGE__); + catch { + die "The $class class could not be found ($subclass not supported?): $_"; + }; + } + 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 { @@ -1917,33 +1868,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 { @@ -1959,36 +1911,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 { @@ -2022,30 +1979,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); + + # DEFAULT attribute must appear before any column constraints + # (e.g., NOT NULL), for Oracle + $type_ddl .= " DEFAULT $default" if (defined($default)); - 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}); + # PRIMARY KEY must appear before NOT NULL for SQLite. + $type_ddl .= " PRIMARY KEY" if ($finfo->{PRIMARYKEY}); + $type_ddl .= " NOT NULL" if ($finfo->{NOTNULL}); - return($type_ddl); + return ($type_ddl); -} #eosub--get_type_ddl +} #eosub--get_type_ddl sub get_fk_ddl { @@ -2079,78 +2040,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); + 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"); } - 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); + 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 { @@ -2161,8 +2124,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 { @@ -2179,16 +2142,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 { @@ -2203,8 +2166,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 { @@ -2218,34 +2181,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 { @@ -2262,30 +2224,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 { @@ -2298,28 +2259,27 @@ 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; } @@ -2337,16 +2297,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 { @@ -2365,22 +2326,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 { @@ -2401,20 +2365,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 = ''; - } + my ($index_fields, $index_type); - return $self->_get_create_index_ddl($table, $name, $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 { @@ -2437,85 +2402,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 { @@ -2529,11 +2497,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 { @@ -2547,8 +2515,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 @@ -2560,8 +2528,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 { @@ -2579,8 +2547,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."; } @@ -2610,8 +2578,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 @@ -2624,13 +2592,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 { @@ -2647,15 +2615,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 @@ -2673,29 +2641,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 { @@ -2711,16 +2681,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 @@ -2734,8 +2704,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 @@ -2751,22 +2721,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 @@ -2776,10 +2744,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 { @@ -2794,17 +2762,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 { @@ -2820,11 +2789,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 { @@ -2845,10 +2814,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 @@ -2858,19 +2827,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 { @@ -2891,36 +2861,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 @@ -2938,16 +2911,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 { @@ -2965,24 +2939,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; } @@ -3006,18 +2980,18 @@ sub columns_equal { =cut sub serialize_abstract { - my ($self) = @_; + my ($self) = @_; - # Make it ok to eval - local $Data::Dumper::Purity = 1; + # Make it ok to eval + local $Data::Dumper::Purity = 1; - # Avoid cross-refs - local $Data::Dumper::Deepcopy = 1; + # Avoid cross-refs + local $Data::Dumper::Deepcopy = 1; - # Always sort keys to allow textual compare - local $Data::Dumper::Sortkeys = 1; + # Always sort keys to allow textual compare + local $Data::Dumper::Sortkeys = 1; - return Dumper($self->{abstract_schema}); + return Dumper($self->{abstract_schema}); } =item C @@ -3036,36 +3010,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); } ##################################################################### @@ -3093,8 +3065,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 79814140a..0b8ee59c3 100644 --- a/Bugzilla/DB/Schema/Mysql.pm +++ b/Bugzilla/DB/Schema/Mysql.pm @@ -34,198 +34,218 @@ use base qw(Bugzilla::DB::Schema); # 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. }; #------------------------------------------------------------------------------ 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', + TEXT => 'text', - TINYTEXT => 'tinytext', - MEDIUMTEXT => 'mediumtext', - LONGTEXT => 'mediumtext', - TEXT => 'text', + LONGBLOB => 'longblob', - LONGBLOB => 'longblob', + NATIVE_DATETIME => 'datetime', + DATETIME => 'timestamp', + DATE => 'date', + }; - NATIVE_DATETIME => 'datetime', - DATETIME => 'timestamp', - DATE => 'date', - }; + $self->_adjust_schema; - $self->_adjust_schema; + return $self; - return $self; +} #eosub--_initialize -} #eosub--_initialize #------------------------------------------------------------------------------ sub _get_create_table_ddl { - # Returns a "create table" SQL statement. - my($self, $table) = @_; - my $charset = Bugzilla::DB::Mysql->utf8_charset; - my $collate = Bugzilla::DB::Mysql->utf8_collate; - my $row_format = Bugzilla::DB::Mysql->default_row_format($table); - my @parts = ( - $self->SUPER::_get_create_table_ddl($table), - 'ENGINE = InnoDB', - "CHARACTER SET $charset COLLATE $collate", - "ROW_FORMAT=$row_format", - ); - return join(' ', @parts); -} #eosub--_get_create_table_ddl + + # Returns a "create table" SQL statement. + my ($self, $table) = @_; + my $charset = Bugzilla::DB::Mysql->utf8_charset; + my $collate = Bugzilla::DB::Mysql->utf8_collate; + my $row_format = Bugzilla::DB::Mysql->default_row_format($table); + my @parts = ( + $self->SUPER::_get_create_table_ddl($table), 'ENGINE = InnoDB', + "CHARACTER SET $charset COLLATE $collate", "ROW_FORMAT=$row_format", + ); + return join(' ', @parts); +} #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 $charset = Bugzilla::DB::Mysql->utf8_charset; - my $collate = Bugzilla::DB::Mysql->utf8_collate; - return ("CREATE DATABASE $name CHARACTER SET $charset COLLATE $collate"); + 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 $charset = Bugzilla::DB::Mysql->utf8_charset; + my $collate = Bugzilla::DB::Mysql->utf8_collate; + return ("CREATE DATABASE $name CHARACTER SET $charset COLLATE $collate"); } # 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"); + push( + @statements, "ALTER TABLE $table ALTER COLUMN $column + SET DEFAULT " . $new_def->{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"); - } + 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; + 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. @@ -235,29 +255,31 @@ sub get_drop_index_ddl { # 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. @@ -265,145 +287,158 @@ 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 =~ /^(-)?(\d+)(.\d+)?$/); - $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 =~ /^(-)?(\d+)(.\d+)?$/); + $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"); } sub get_type_ddl { - my $self = shift; - my $type_ddl = $self->SUPER::get_type_ddl(@_); - - # TIMESTAMPS as of 5.6.6 still default to - # 'NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' - # unless explicitly setup in the table definition. This will change in future releases - # and can be disabled by using 'explicit_defaults_for_timestamp = 1' in my.cnf. - # So instead, we explicitly setup TIMESTAMP types to not be automatic. - if ($type_ddl =~ /^timestamp/i) { - if ($type_ddl !~ /NOT NULL/) { - $type_ddl .= ' NULL DEFAULT NULL'; - } - if ($type_ddl =~ /NOT NULL/ && $type_ddl !~ /DEFAULT/) { - $type_ddl .= ' DEFAULT CURRENT_TIMESTAMP'; - } + my $self = shift; + my $type_ddl = $self->SUPER::get_type_ddl(@_); + +# TIMESTAMPS as of 5.6.6 still default to +# 'NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP' +# unless explicitly setup in the table definition. This will change in future releases +# and can be disabled by using 'explicit_defaults_for_timestamp = 1' in my.cnf. +# So instead, we explicitly setup TIMESTAMP types to not be automatic. + if ($type_ddl =~ /^timestamp/i) { + if ($type_ddl !~ /NOT NULL/) { + $type_ddl .= ' NULL DEFAULT NULL'; + } + if ($type_ddl =~ /NOT NULL/ && $type_ddl !~ /DEFAULT/) { + $type_ddl .= ' DEFAULT CURRENT_TIMESTAMP'; } + } - return $type_ddl; + return $type_ddl; } 1; diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm index b67ddfd59..36f957820 100644 --- a/Bugzilla/DB/Schema/Oracle.pm +++ b/Bugzilla/DB/Schema/Oracle.pm @@ -21,8 +21,9 @@ use base 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,91 +32,95 @@ 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; + +} #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 @@ -124,30 +129,31 @@ sub get_drop_index_ddl { # - 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); + 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; + 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)"); - } - # 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"); + 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"); } - - 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}; - } - - 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)); + 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 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; + 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 7606faa3d..8af1af8c0 100644 --- a/Bugzilla/DB/Schema/Pg.pm +++ b/Bugzilla/DB/Schema/Pg.pm @@ -23,169 +23,191 @@ 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"); - } - - return @statements; + 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; } 1; diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm index 6d524db59..1bd53515a 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 ($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_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,137 @@ 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; + } + 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/; + } + 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"; } + 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(@_); + 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 +286,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 ($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; + 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); + return $self->_sqlite_alter_schema($table, \@clauses); } diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm index 81ee7d888..7a97ad06a 100644 --- a/Bugzilla/DB/Sqlite.pm +++ b/Bugzilla/DB/Sqlite.pm @@ -45,23 +45,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)); } ############### @@ -69,73 +69,80 @@ sub _sqlite_position_ci { ############### sub BUILDARGS { - my ($class, $params) = @_; - my $db_name = $params->{db_name}; - - # Let people specify paths intead of data/ for the DB. - if ($db_name && $db_name ne ':memory:' && $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 && $db_name ne ':memory:' && $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"; + } + + # construct the DSN from the parameters we got + my $dsn = "dbi:SQLite:dbname=$db_name"; - # construct the DSN from the parameters we got - my $dsn = "dbi:SQLite:dbname=$db_name"; + my $attrs = { - my $attrs = { - # XXX Should we just enforce this to be always on? - sqlite_unicode => Bugzilla->params->{'utf8'}, - }; + # XXX Should we just enforce this to be always on? + sqlite_unicode => Bugzilla->params->{'utf8'}, + }; - return { dsn => $dsn, user => '', pass => '', attrs => $attrs }; + return {dsn => $dsn, user => '', pass => '', attrs => $attrs}; } sub on_dbi_connected { - my ($class, $dbh) = @_; - - 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) { - $dbh->do("PRAGMA $name = $value"); - } - - $dbh->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); - $dbh->sqlite_create_function('position', 2, \&_sqlite_position); - $dbh->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". - $dbh->sqlite_create_function('substring', 3, \&CORE::substr); - $dbh->sqlite_create_function('mod', 2, \&_sqlite_mod); - $dbh->sqlite_create_function('now', 0, \&_sqlite_now); - $dbh->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); - $dbh->sqlite_create_function('floor', 1, \&POSIX::floor); + my ($class, $dbh) = @_; + + 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) { + $dbh->do("PRAGMA $name = $value"); + } + + $dbh->sqlite_create_collation('bugzilla', \&_sqlite_collate_ci); + $dbh->sqlite_create_function('position', 2, \&_sqlite_position); + $dbh->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". + $dbh->sqlite_create_function('substring', 3, \&CORE::substr); + $dbh->sqlite_create_function('mod', 2, \&_sqlite_mod); + $dbh->sqlite_create_function('now', 0, \&_sqlite_now); + $dbh->sqlite_create_function('localtimestamp', 1, \&_sqlite_now); + $dbh->sqlite_create_function('floor', 1, \&POSIX::floor); } ############### @@ -143,85 +150,88 @@ sub on_dbi_connected { ############### 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; - return "STRFTIME(" . $self->quote($format) . ", $date)"; + my ($self, $date, $format) = @_; + $format = "%Y.%m.%d %H:%M:%S" if !$format; + $format =~ s/\%i/\%M/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')"; } ############### @@ -229,56 +239,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/DaemonControl.pm b/Bugzilla/DaemonControl.pm index 5cb32973f..4f3c624af 100644 --- a/Bugzilla/DaemonControl.pm +++ b/Bugzilla/DaemonControl.pm @@ -29,256 +29,250 @@ use POSIX qw(WEXITSTATUS); use base qw(Exporter); our @EXPORT_OK = qw( - run_httpd run_cereal run_jobqueue - run_cereal_and_httpd run_cereal_and_jobqueue - catch_signal on_finish on_exception - assert_httpd assert_database assert_selenium + run_httpd run_cereal run_jobqueue + run_cereal_and_httpd run_cereal_and_jobqueue + catch_signal on_finish on_exception + assert_httpd assert_database assert_selenium ); our %EXPORT_TAGS = ( - all => \@EXPORT_OK, - run => [grep { /^run_/ } @EXPORT_OK], - utils => [qw(catch_signal on_exception on_finish)], + all => \@EXPORT_OK, + run => [grep {/^run_/} @EXPORT_OK], + utils => [qw(catch_signal on_exception on_finish)], ); my $BUGZILLA_DIR = bz_locations->{cgi_path}; -my $JOBQUEUE_BIN = catfile( $BUGZILLA_DIR, 'jobqueue.pl' ); -my $CEREAL_BIN = catfile( $BUGZILLA_DIR, 'scripts', 'cereal.pl' ); -my $BUGZILLA_BIN = catfile( $BUGZILLA_DIR, 'bugzilla.pl' ); -my $HYPNOTOAD_BIN = catfile( $BUGZILLA_DIR, 'local', 'bin', 'hypnotoad' ); -my @PERL5LIB = ( $BUGZILLA_DIR, catdir($BUGZILLA_DIR, 'lib'), catdir($BUGZILLA_DIR, 'local', 'lib', 'perl5') ); +my $JOBQUEUE_BIN = catfile($BUGZILLA_DIR, 'jobqueue.pl'); +my $CEREAL_BIN = catfile($BUGZILLA_DIR, 'scripts', 'cereal.pl'); +my $BUGZILLA_BIN = catfile($BUGZILLA_DIR, 'bugzilla.pl'); +my $HYPNOTOAD_BIN = catfile($BUGZILLA_DIR, 'local', 'bin', 'hypnotoad'); +my @PERL5LIB = ( + $BUGZILLA_DIR, + catdir($BUGZILLA_DIR, 'lib'), + catdir($BUGZILLA_DIR, 'local', 'lib', 'perl5') +); my %HTTP_BACKENDS = ( - hypnotoad => [ $HYPNOTOAD_BIN, $BUGZILLA_BIN, '-f' ], - simple => [ $BUGZILLA_BIN, 'daemon' ], + hypnotoad => [$HYPNOTOAD_BIN, $BUGZILLA_BIN, '-f'], + simple => [$BUGZILLA_BIN, 'daemon'], ); sub catch_signal { - my ($name, @done) = @_; - my $loop = IO::Async::Loop->new; - my $signal_f = $loop->new_future; - my $signal = IO::Async::Signal->new( - name => $name, - on_receipt => sub { - my ($self) = @_; - my $l = IO::Async::Loop->new; - $signal_f->done(@done); - $l->remove($self); - } - ); - $signal_f->on_cancel( - sub { - my $l = IO::Async::Loop->new; - $l->remove($signal); - }, - ); + my ($name, @done) = @_; + my $loop = IO::Async::Loop->new; + my $signal_f = $loop->new_future; + my $signal = IO::Async::Signal->new( + name => $name, + on_receipt => sub { + my ($self) = @_; + my $l = IO::Async::Loop->new; + $signal_f->done(@done); + $l->remove($self); + } + ); + $signal_f->on_cancel( + sub { + my $l = IO::Async::Loop->new; + $l->remove($signal); + }, + ); - $loop->add($signal); + $loop->add($signal); - return $signal_f; + return $signal_f; } sub run_cereal { - my $loop = IO::Async::Loop->new; - my $exit_f = $loop->new_future; - my $cereal = IO::Async::Process->new( - command => [$CEREAL_BIN], - on_finish => on_finish($exit_f), - on_exception => on_exception( 'cereal', $exit_f ), - ); - $exit_f->on_cancel( sub { $cereal->kill('TERM') } ); - $exit_f->on_ready( - sub { - delete $ENV{LOG4PERL_STDERR_DISABLE}; - } - ); - $loop->add($cereal); - $ENV{LOG4PERL_STDERR_DISABLE} = 1; - - return $exit_f; + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my $cereal = IO::Async::Process->new( + command => [$CEREAL_BIN], + on_finish => on_finish($exit_f), + on_exception => on_exception('cereal', $exit_f), + ); + $exit_f->on_cancel(sub { $cereal->kill('TERM') }); + $exit_f->on_ready(sub { + delete $ENV{LOG4PERL_STDERR_DISABLE}; + }); + $loop->add($cereal); + $ENV{LOG4PERL_STDERR_DISABLE} = 1; + + return $exit_f; } sub run_httpd { - my (@args) = @_; - - my $loop = IO::Async::Loop->new; - my $exit_f = $loop->new_future; - my $httpd = IO::Async::Process->new( - code => sub { - $ENV{BUGZILLA_HTTPD_ARGS} = encode_json(\@args); - $ENV{PERL5LIB} = join(':', @PERL5LIB); - my $backend = $ENV{HTTP_BACKEND} // 'hypnotoad'; - my $command = $HTTP_BACKENDS{ $backend }; - exec @$command - or die "failed to exec $command->[0] $!"; - }, - on_finish => on_finish($exit_f), - on_exception => on_exception( 'httpd', $exit_f ), - ); - $exit_f->on_cancel( sub { $httpd->kill('TERM') } ); - $loop->add($httpd); - - return $exit_f; + my (@args) = @_; + + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my $httpd = IO::Async::Process->new( + code => sub { + $ENV{BUGZILLA_HTTPD_ARGS} = encode_json(\@args); + $ENV{PERL5LIB} = join(':', @PERL5LIB); + my $backend = $ENV{HTTP_BACKEND} // 'hypnotoad'; + my $command = $HTTP_BACKENDS{$backend}; + exec @$command or die "failed to exec $command->[0] $!"; + }, + on_finish => on_finish($exit_f), + on_exception => on_exception('httpd', $exit_f), + ); + $exit_f->on_cancel(sub { $httpd->kill('TERM') }); + $loop->add($httpd); + + return $exit_f; } sub run_jobqueue { - my (@args) = @_; - - my $loop = IO::Async::Loop->new; - my $exit_f = $loop->new_future; - my $jobqueue = IO::Async::Process->new( - command => [ $JOBQUEUE_BIN, 'start', '-f', '-d', @args ], - on_finish => on_finish($exit_f), - on_exception => on_exception( 'httpd', $exit_f ), - ); - $exit_f->on_cancel( sub { $jobqueue->kill('TERM') } ); - $loop->add($jobqueue); - - return $exit_f; + my (@args) = @_; + + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my $jobqueue = IO::Async::Process->new( + command => [$JOBQUEUE_BIN, 'start', '-f', '-d', @args], + on_finish => on_finish($exit_f), + on_exception => on_exception('httpd', $exit_f), + ); + $exit_f->on_cancel(sub { $jobqueue->kill('TERM') }); + $loop->add($jobqueue); + + return $exit_f; } sub run_cereal_and_jobqueue { - my (@jobqueue_args) = @_; + my (@jobqueue_args) = @_; - my $signal_f = catch_signal('TERM', 0); - my $cereal_exit_f = run_cereal(); + my $signal_f = catch_signal('TERM', 0); + my $cereal_exit_f = run_cereal(); - return assert_cereal()->then( - sub { - my $jobqueue_exit_f = run_jobqueue(@jobqueue_args); - return Future->wait_any($cereal_exit_f, $jobqueue_exit_f, $signal_f); - } - ); + return assert_cereal()->then(sub { + my $jobqueue_exit_f = run_jobqueue(@jobqueue_args); + return Future->wait_any($cereal_exit_f, $jobqueue_exit_f, $signal_f); + }); } sub run_cereal_and_httpd { - my @httpd_args = @_; - - my $signal_f = catch_signal('TERM', 0); - my $cereal_exit_f = run_cereal(); - - return assert_cereal()->then( - sub { - push @httpd_args, '-DNETCAT_LOGS'; - - my $lc = Bugzilla::Install::Localconfig::read_localconfig(); - if ( ($lc->{inbound_proxies} // '') eq '*' && $lc->{urlbase} =~ /^https/) { - push @httpd_args, '-DHTTPS'; - } - elsif ($lc->{urlbase} =~ /^https/) { - WARN('HTTPS urlbase but inbound_proxies is not "*"'); - } - my $httpd_exit_f = run_httpd(@httpd_args); - - return Future->wait_any($cereal_exit_f, $httpd_exit_f, $signal_f); - } - ); + my @httpd_args = @_; + + my $signal_f = catch_signal('TERM', 0); + my $cereal_exit_f = run_cereal(); + + return assert_cereal()->then(sub { + push @httpd_args, '-DNETCAT_LOGS'; + + my $lc = Bugzilla::Install::Localconfig::read_localconfig(); + if (($lc->{inbound_proxies} // '') eq '*' && $lc->{urlbase} =~ /^https/) { + push @httpd_args, '-DHTTPS'; + } + elsif ($lc->{urlbase} =~ /^https/) { + WARN('HTTPS urlbase but inbound_proxies is not "*"'); + } + my $httpd_exit_f = run_httpd(@httpd_args); + + return Future->wait_any($cereal_exit_f, $httpd_exit_f, $signal_f); + }); } sub assert_httpd { - my $loop = IO::Async::Loop->new; - my $port = $ENV{PORT} // 8000; - my $repeat = repeat { - $loop->delay_future(after => 0.25)->then( - sub { - Future->wrap(get("http://localhost:$port/__lbheartbeat__") // ''); - }, - ); - } until => sub { - my $f = shift; - ( $f->get =~ /^httpd OK/ ); - }; - my $timeout = $loop->timeout_future(after => 20)->else_fail('assert_httpd timeout'); - return Future->wait_any($repeat, $timeout); + my $loop = IO::Async::Loop->new; + my $port = $ENV{PORT} // 8000; + my $repeat = repeat { + $loop->delay_future(after => 0.25)->then( + sub { + Future->wrap(get("http://localhost:$port/__lbheartbeat__") // ''); + }, + ); + } + until => sub { + my $f = shift; + ($f->get =~ /^httpd OK/); + }; + my $timeout + = $loop->timeout_future(after => 20)->else_fail('assert_httpd timeout'); + return Future->wait_any($repeat, $timeout); } sub assert_selenium { - my ($host, $port) = @_; - $host //= 'localhost'; - $port //= 4444; + my ($host, $port) = @_; + $host //= 'localhost'; + $port //= 4444; - return assert_connect($host, $port, 'assert_selenium'); + return assert_connect($host, $port, 'assert_selenium'); } sub assert_cereal { - return assert_connect( - 'localhost', - $ENV{LOGGING_PORT} // 5880, - 'assert_cereal' - ); + return assert_connect('localhost', $ENV{LOGGING_PORT} // 5880, 'assert_cereal'); } sub assert_connect { - my ($host, $port, $name) = @_; - my $loop = IO::Async::Loop->new; - my $repeat = repeat { - $loop->delay_future(after => 1)->then( - sub { - my $sock = IO::Socket::INET->new( PeerAddr => $host, PeerPort => $port ); - Future->wrap($sock ? 1 : 0); - }, - ); - } until => sub { shift->get }; - my $timeout = $loop->timeout_future(after => 60)->else_fail("$name timeout"); - return Future->wait_any($repeat, $timeout); + my ($host, $port, $name) = @_; + my $loop = IO::Async::Loop->new; + my $repeat = repeat { + $loop->delay_future(after => 1)->then( + sub { + my $sock = IO::Socket::INET->new(PeerAddr => $host, PeerPort => $port); + Future->wrap($sock ? 1 : 0); + }, + ); + } + until => sub { shift->get }; + my $timeout = $loop->timeout_future(after => 60)->else_fail("$name timeout"); + return Future->wait_any($repeat, $timeout); } sub assert_database { - my $loop = IO::Async::Loop->new; - my $lc = Bugzilla::Install::Localconfig::read_localconfig(); - - for my $var (qw(db_name db_host db_user db_pass)) { - return $loop->new_future->die("$var is not set!") unless $lc->{$var}; - } - - my $dsn = "dbi:mysql:database=$lc->{db_name};host=$lc->{db_host}"; - my $repeat = repeat { - $loop->delay_future( after => 0.25 )->then( - sub { - my $dbh = DBI->connect( - $dsn, - $lc->{db_user}, - $lc->{db_pass}, - { RaiseError => 0, PrintError => 0 }, - ); - Future->wrap($dbh); - } + my $loop = IO::Async::Loop->new; + my $lc = Bugzilla::Install::Localconfig::read_localconfig(); + + for my $var (qw(db_name db_host db_user db_pass)) { + return $loop->new_future->die("$var is not set!") unless $lc->{$var}; + } + + my $dsn = "dbi:mysql:database=$lc->{db_name};host=$lc->{db_host}"; + my $repeat = repeat { + $loop->delay_future(after => 0.25)->then(sub { + my $dbh + = DBI->connect($dsn, $lc->{db_user}, $lc->{db_pass}, + {RaiseError => 0, PrintError => 0}, ); - } until => sub { defined shift->get }; - - my $timeout = $loop->timeout_future( after => 20 )->else_fail('assert_database timeout'); - my $any_f = Future->wait_any( $repeat, $timeout ); - return $any_f->transform( - done => sub { return }, - fail => sub { "unable to connect to $dsn as $lc->{db_user}" }, - ); + Future->wrap($dbh); + }); + } + until => sub { defined shift->get }; + + my $timeout + = $loop->timeout_future(after => 20)->else_fail('assert_database timeout'); + my $any_f = Future->wait_any($repeat, $timeout); + return $any_f->transform( + done => sub {return}, + fail => sub {"unable to connect to $dsn as $lc->{db_user}"}, + ); } sub on_finish { - my ($f) = @_; + my ($f) = @_; - return sub { - my ($self, $exitcode) = @_; - $f->done(WEXITSTATUS($exitcode)); - }; + return sub { + my ($self, $exitcode) = @_; + $f->done(WEXITSTATUS($exitcode)); + }; } sub on_exception { - my ( $name, $f ) = @_; - - return sub { - my ( $self, $exception, $errno, $exitcode ) = @_; - - if ( length $exception ) { - $f->fail( "$name died with the exception $exception (errno was $errno)\n" ); - } - elsif ( ( my $status = WEXITSTATUS($exitcode) ) == 255 ) { - $f->fail("$name failed to exec() - $errno\n"); - } - else { - $f->fail("$name exited with exit status $status\n"); - } - }; + my ($name, $f) = @_; + + return sub { + my ($self, $exception, $errno, $exitcode) = @_; + + if (length $exception) { + $f->fail("$name died with the exception $exception (errno was $errno)\n"); + } + elsif ((my $status = WEXITSTATUS($exitcode)) == 255) { + $f->fail("$name failed to exec() - $errno\n"); + } + else { + $f->fail("$name exited with exit status $status\n"); + } + }; } 1; diff --git a/Bugzilla/DuoAPI.pm b/Bugzilla/DuoAPI.pm index ab50a61e2..fb2b4ba38 100644 --- a/Bugzilla/DuoAPI.pm +++ b/Bugzilla/DuoAPI.pm @@ -68,94 +68,82 @@ use MIME::Base64 qw(encode_base64); use POSIX qw(strftime); sub new { - my($proto, $ikey, $skey, $host) = @_; - my $class = ref($proto) || $proto; - my $self = { - 'ikey' => $ikey, - 'skey' => $skey, - 'host' => $host, - }; - bless($self, $class); - return $self; + my ($proto, $ikey, $skey, $host) = @_; + my $class = ref($proto) || $proto; + my $self = {'ikey' => $ikey, 'skey' => $skey, 'host' => $host,}; + bless($self, $class); + return $self; } sub canonicalize_params { - my ($self, $params) = @_; + my ($self, $params) = @_; - my @ret; - while (my ($k, $v) = each(%{$params})) { - push(@ret, join('=', CGI::escape($k), CGI::escape($v))); - } - return join('&', sort(@ret)); + my @ret; + while (my ($k, $v) = each(%{$params})) { + push(@ret, join('=', CGI::escape($k), CGI::escape($v))); + } + return join('&', sort(@ret)); } sub sign { - my ($self, $method, $path, $canon_params, $date) = @_; - my $canon = join("\n", - $date, - uc($method), - lc($self->{'host'}), - $path, - $canon_params); - my $sig = hmac_sha1_hex($canon, $self->{'skey'}); - my $auth = join(':', - $self->{'ikey'}, - $sig); - $auth = 'Basic ' . encode_base64($auth, ''); - return $auth; + my ($self, $method, $path, $canon_params, $date) = @_; + my $canon + = join("\n", $date, uc($method), lc($self->{'host'}), $path, $canon_params); + my $sig = hmac_sha1_hex($canon, $self->{'skey'}); + my $auth = join(':', $self->{'ikey'}, $sig); + $auth = 'Basic ' . encode_base64($auth, ''); + return $auth; } sub api_call { - my ($self, $method, $path, $params) = @_; - $params ||= {}; - - my $canon_params = $self->canonicalize_params($params); - my $date = strftime('%a, %d %b %Y %H:%M:%S -0000', - gmtime(time())); - my $auth = $self->sign($method, $path, $canon_params, $date); - - my $ua = LWP::UserAgent->new(); - my $req = HTTP::Request->new(); - $req->method($method); - $req->protocol('HTTP/1.1'); - $req->header('If-SSL-Cert-Subject' => qr{CN=[^=]+\.duosecurity.com$}); - $req->header('Authorization' => $auth); - $req->header('Date' => $date); - $req->header('Host' => $self->{'host'}); - - if (grep(/^$method$/, qw(POST PUT))) { - $req->header('Content-type' => 'application/x-www-form-urlencoded'); - $req->content($canon_params); - } - else { - $path .= '?' . $canon_params; - } - - $req->uri('https://' . $self->{'host'} . $path); - if ($ENV{'DEBUG'}) { - print STDERR $req->as_string(); - } - my $res = $ua->request($req); - return $res; + my ($self, $method, $path, $params) = @_; + $params ||= {}; + + my $canon_params = $self->canonicalize_params($params); + my $date = strftime('%a, %d %b %Y %H:%M:%S -0000', gmtime(time())); + my $auth = $self->sign($method, $path, $canon_params, $date); + + my $ua = LWP::UserAgent->new(); + my $req = HTTP::Request->new(); + $req->method($method); + $req->protocol('HTTP/1.1'); + $req->header('If-SSL-Cert-Subject' => qr{CN=[^=]+\.duosecurity.com$}); + $req->header('Authorization' => $auth); + $req->header('Date' => $date); + $req->header('Host' => $self->{'host'}); + + if (grep(/^$method$/, qw(POST PUT))) { + $req->header('Content-type' => 'application/x-www-form-urlencoded'); + $req->content($canon_params); + } + else { + $path .= '?' . $canon_params; + } + + $req->uri('https://' . $self->{'host'} . $path); + if ($ENV{'DEBUG'}) { + print STDERR $req->as_string(); + } + my $res = $ua->request($req); + return $res; } sub json_api_call { - my $self = shift; - my $res = $self->api_call(@_); - my $json = $res->content(); - if ($json !~ /^{/) { - croak($json); - } - my $ret = decode_json($json); - if (($ret->{'stat'} || '') ne 'OK') { - my $msg = join('', - 'Error ', $ret->{'code'}, ': ', $ret->{'message'}); - if (defined($ret->{'message_detail'})) { - $msg .= ' (' . $ret->{'message_detail'} . ')'; - } - croak($msg); + my $self = shift; + my $res = $self->api_call(@_); + my $json = $res->content(); + if ($json !~ /^{/) { + croak($json); + } + my $ret = decode_json($json); + if (($ret->{'stat'} || '') ne 'OK') { + my $msg = join('', 'Error ', $ret->{'code'}, ': ', $ret->{'message'}); + if (defined($ret->{'message_detail'})) { + $msg .= ' (' . $ret->{'message_detail'} . ')'; } - return $ret->{'response'}; + croak($msg); + } + return $ret->{'response'}; } 1; diff --git a/Bugzilla/DuoWeb.pm b/Bugzilla/DuoWeb.pm index 722c032e3..9dbfc46ad 100644 --- a/Bugzilla/DuoWeb.pm +++ b/Bugzilla/DuoWeb.pm @@ -47,67 +47,69 @@ my $SKEY_LEN = 40; my $AKEY_LEN = 40; our $ERR_USER = 'ERR|The username passed to sign_request() is invalid.'; -our $ERR_IKEY = 'ERR|The Duo integration key passed to sign_request() is invalid.'; +our $ERR_IKEY + = 'ERR|The Duo integration key passed to sign_request() is invalid.'; our $ERR_SKEY = 'ERR|The Duo secret key passed to sign_request() is invalid.'; -our $ERR_AKEY = "ERR|The application secret key passed to sign_request() must be at least $AKEY_LEN characters."; +our $ERR_AKEY + = "ERR|The application secret key passed to sign_request() must be at least $AKEY_LEN characters."; our $ERR_UNKNOWN = 'ERR|An unknown error has occurred.'; sub _sign_vals { - my ($key, $vals, $prefix, $expire) = @_; + my ($key, $vals, $prefix, $expire) = @_; - my $exp = time + $expire; + my $exp = time + $expire; - my $val = join '|', @{$vals}, $exp; - my $b64 = encode_base64($val, ''); - my $cookie = "$prefix|$b64"; + my $val = join '|', @{$vals}, $exp; + my $b64 = encode_base64($val, ''); + my $cookie = "$prefix|$b64"; - my $sig = hmac_sha1_hex($cookie, $key); + my $sig = hmac_sha1_hex($cookie, $key); - return "$cookie|$sig"; + return "$cookie|$sig"; } sub _parse_vals { - my ($key, $val, $prefix, $ikey) = @_; + my ($key, $val, $prefix, $ikey) = @_; - my $ts = time; + my $ts = time; - if (not defined $val) { - return ''; - } + if (not defined $val) { + return ''; + } - my @parts = split /\|/, $val; - if (scalar(@parts) != 3) { - return ''; - } - my ($u_prefix, $u_b64, $u_sig) = @parts; + my @parts = split /\|/, $val; + if (scalar(@parts) != 3) { + return ''; + } + my ($u_prefix, $u_b64, $u_sig) = @parts; - my $sig = hmac_sha1_hex("$u_prefix|$u_b64", $key); + my $sig = hmac_sha1_hex("$u_prefix|$u_b64", $key); - if (hmac_sha1_hex($sig, $key) ne hmac_sha1_hex($u_sig, $key)) { - return ''; - } + if (hmac_sha1_hex($sig, $key) ne hmac_sha1_hex($u_sig, $key)) { + return ''; + } - if ($u_prefix ne $prefix) { - return ''; - } + if ($u_prefix ne $prefix) { + return ''; + } - my @cookie_parts = split /\|/, decode_base64($u_b64); - if (scalar(@cookie_parts) != 3) { - return ''; - } - my ($user, $u_ikey, $exp) = @cookie_parts; + my @cookie_parts = split /\|/, decode_base64($u_b64); + if (scalar(@cookie_parts) != 3) { + return ''; + } + my ($user, $u_ikey, $exp) = @cookie_parts; - if ($u_ikey ne $ikey) { - return ''; - } + if ($u_ikey ne $ikey) { + return ''; + } - if ($ts >= $exp) { - return ''; - } + if ($ts >= $exp) { + return ''; + } - return $user; + return $user; } =pod @@ -124,38 +126,38 @@ sub _parse_vals { =cut sub sign_request { - my ($ikey, $skey, $akey, $username) = @_; + my ($ikey, $skey, $akey, $username) = @_; - if (not $username) { - return $ERR_USER; - } + if (not $username) { + return $ERR_USER; + } - if (index($username, '|') != -1) { - return $ERR_USER; - } + if (index($username, '|') != -1) { + return $ERR_USER; + } - if (not $ikey or length $ikey != $IKEY_LEN) { - return $ERR_IKEY; - } + if (not $ikey or length $ikey != $IKEY_LEN) { + return $ERR_IKEY; + } - if (not $skey or length $skey != $SKEY_LEN) { - return $ERR_SKEY; - } + if (not $skey or length $skey != $SKEY_LEN) { + return $ERR_SKEY; + } - if (not $akey or length $akey < $AKEY_LEN) { - return $ERR_AKEY; - } + if (not $akey or length $akey < $AKEY_LEN) { + return $ERR_AKEY; + } - my $vals = [ $username, $ikey ]; + my $vals = [$username, $ikey]; - my $duo_sig = _sign_vals($skey, $vals, $DUO_PREFIX, $DUO_EXPIRE); - my $app_sig = _sign_vals($akey, $vals, $APP_PREFIX, $APP_EXPIRE); + my $duo_sig = _sign_vals($skey, $vals, $DUO_PREFIX, $DUO_EXPIRE); + my $app_sig = _sign_vals($akey, $vals, $APP_PREFIX, $APP_EXPIRE); - if (not $duo_sig or not $app_sig) { - return $ERR_UNKNOWN; - } + if (not $duo_sig or not $app_sig) { + return $ERR_UNKNOWN; + } - return "$duo_sig:$app_sig"; + return "$duo_sig:$app_sig"; } =pod @@ -175,20 +177,20 @@ sub sign_request { =cut sub verify_response { - my ($ikey, $skey, $akey, $sig_response) = @_; + my ($ikey, $skey, $akey, $sig_response) = @_; - if (not defined $sig_response) { - return ''; - } + if (not defined $sig_response) { + return ''; + } - my ($auth_sig, $app_sig) = split /:/, $sig_response; - my $auth_user = _parse_vals($skey, $auth_sig, $AUTH_PREFIX, $ikey); - my $app_user = _parse_vals($akey, $app_sig, $APP_PREFIX, $ikey); + my ($auth_sig, $app_sig) = split /:/, $sig_response; + my $auth_user = _parse_vals($skey, $auth_sig, $AUTH_PREFIX, $ikey); + my $app_user = _parse_vals($akey, $app_sig, $APP_PREFIX, $ikey); - if ($auth_user ne $app_user) { - return ''; - } + if ($auth_user ne $app_user) { + return ''; + } - return $auth_user; + return $auth_user; } 1; diff --git a/Bugzilla/Elastic.pm b/Bugzilla/Elastic.pm index a01d1be42..805094f03 100644 --- a/Bugzilla/Elastic.pm +++ b/Bugzilla/Elastic.pm @@ -14,44 +14,42 @@ use Bugzilla::Util qw(trick_taint); with 'Bugzilla::Elastic::Role::HasClient'; sub suggest_users { - my ($self, $text) = @_; - - unless (Bugzilla->params->{elasticsearch}) { - # optimization: faster than a regular method call. - goto &_suggest_users_fallback; - } - - my $field = 'suggest_user'; - if ($text =~ /^:(.+)$/) { - $text = $1; - $field = 'suggest_nick'; - } - - my $result = eval { - $self->client->suggest( - index => Bugzilla::User->ES_INDEX, - body => { - $field => { - text => $text, - completion => { field => $field, size => 25 }, - } - } - ); - }; - if (defined $result) { - return [ map { $_->{payload} } @{$result->{$field}[0]{options}} ]; - } - else { - warn "suggest_users error: $@"; - # optimization: faster than a regular method call. - goto &_suggest_users_fallback; - } + my ($self, $text) = @_; + + unless (Bugzilla->params->{elasticsearch}) { + + # optimization: faster than a regular method call. + goto &_suggest_users_fallback; + } + + my $field = 'suggest_user'; + if ($text =~ /^:(.+)$/) { + $text = $1; + $field = 'suggest_nick'; + } + + my $result = eval { + $self->client->suggest( + index => Bugzilla::User->ES_INDEX, + body => + {$field => {text => $text, completion => {field => $field, size => 25},}} + ); + }; + if (defined $result) { + return [map { $_->{payload} } @{$result->{$field}[0]{options}}]; + } + else { + warn "suggest_users error: $@"; + + # optimization: faster than a regular method call. + goto &_suggest_users_fallback; + } } sub _suggest_users_fallback { - my ($self, $text) = @_; - my $users = Bugzilla::User::match($text, 25, 1); - return [ map { { real_name => $_->name, name => $_->login } } @$users]; + my ($self, $text) = @_; + my $users = Bugzilla::User::match($text, 25, 1); + return [map { {real_name => $_->name, name => $_->login} } @$users]; } 1; diff --git a/Bugzilla/Elastic/Indexer.pm b/Bugzilla/Elastic/Indexer.pm index a9d796ae7..579829014 100644 --- a/Bugzilla/Elastic/Indexer.pm +++ b/Bugzilla/Elastic/Indexer.pm @@ -16,206 +16,197 @@ use namespace::clean; with 'Bugzilla::Elastic::Role::HasClient'; -has 'shadow_dbh' => ( is => 'lazy' ); +has 'shadow_dbh' => (is => 'lazy'); -has 'debug_sql' => ( - is => 'ro', - default => 0, -); +has 'debug_sql' => (is => 'ro', default => 0,); -has 'progress_bar' => ( - is => 'ro', - predicate => 'has_progress_bar', -); +has 'progress_bar' => (is => 'ro', predicate => 'has_progress_bar',); sub _create_index { - my ($self, $class) = @_; - my $indices = $self->client->indices; - my $index_name = $class->ES_INDEX; - - unless ($indices->exists(index => $index_name)) { - $indices->create( - index => $index_name, - body => { settings => $class->ES_SETTINGS }, - ); - } + my ($self, $class) = @_; + my $indices = $self->client->indices; + my $index_name = $class->ES_INDEX; + + unless ($indices->exists(index => $index_name)) { + $indices->create( + index => $index_name, + body => {settings => $class->ES_SETTINGS}, + ); + } } sub _bulk_helper { - my ($self, $class) = @_; + my ($self, $class) = @_; - return $self->client->bulk_helper( - index => $class->ES_INDEX, - type => $class->ES_TYPE, - ); + return $self->client->bulk_helper( + index => $class->ES_INDEX, + type => $class->ES_TYPE, + ); } sub _find_largest { - my ($self, $class, $field) = @_; - - my $result = $self->client->search( - index => $class->ES_INDEX, - type => $class->ES_TYPE, - body => { - aggs => { $field => { extended_stats => { field => $field } } }, - size => 0 - } - ); - - my $max = $result->{aggregations}{$field}{max}; - if (not defined $max) { - return 0; - } - elsif (looks_like_number($max)) { - return $max; - } - else { - die "largest value for '$field' is not a number: $max"; - } + my ($self, $class, $field) = @_; + + my $result = $self->client->search( + index => $class->ES_INDEX, + type => $class->ES_TYPE, + body => {aggs => {$field => {extended_stats => {field => $field}}}, size => 0} + ); + + my $max = $result->{aggregations}{$field}{max}; + if (not defined $max) { + return 0; + } + elsif (looks_like_number($max)) { + return $max; + } + else { + die "largest value for '$field' is not a number: $max"; + } } sub _find_largest_mtime { - my ($self, $class) = @_; + my ($self, $class) = @_; - return $self->_find_largest($class, 'es_mtime'); + return $self->_find_largest($class, 'es_mtime'); } sub _find_largest_id { - my ($self, $class) = @_; + my ($self, $class) = @_; - return $self->_find_largest($class, $class->ID_FIELD); + return $self->_find_largest($class, $class->ID_FIELD); } sub _put_mapping { - my ($self, $class) = @_; - - my %body = ( properties => scalar $class->ES_PROPERTIES ); - if ($class->does('Bugzilla::Elastic::Role::ChildObject')) { - $body{_parent} = { type => $class->ES_PARENT_TYPE }; - } - - $self->client->indices->put_mapping( - index => $class->ES_INDEX, - type => $class->ES_TYPE, - body => \%body, - ); + my ($self, $class) = @_; + + my %body = (properties => scalar $class->ES_PROPERTIES); + if ($class->does('Bugzilla::Elastic::Role::ChildObject')) { + $body{_parent} = {type => $class->ES_PARENT_TYPE}; + } + + $self->client->indices->put_mapping( + index => $class->ES_INDEX, + type => $class->ES_TYPE, + body => \%body, + ); } sub _debug_sql { - my ($self, $sql, $params) = @_; - if ($self->debug_sql) { - my ($out, @args) = ($sql, $params ? (@$params) : ()); - $out =~ s/^\n//gs; - $out =~ s/^\s{8}//gm; - $out =~ s/\?/Bugzilla->dbh->quote(shift @args)/ge; - warn $out, "\n"; - } - - return ($sql, $params) + my ($self, $sql, $params) = @_; + if ($self->debug_sql) { + my ($out, @args) = ($sql, $params ? (@$params) : ()); + $out =~ s/^\n//gs; + $out =~ s/^\s{8}//gm; + $out =~ s/\?/Bugzilla->dbh->quote(shift @args)/ge; + warn $out, "\n"; + } + + return ($sql, $params); } sub bulk_load { - my ( $self, $class ) = @_; + my ($self, $class) = @_; - $self->_create_index($class); + $self->_create_index($class); - my $bulk = $self->_bulk_helper($class); - my $last_mtime = $self->_find_largest_mtime($class); - my $last_id = $self->_find_largest_id($class); - my $new_ids = $self->_select_all_ids($class, $last_id); - my $updated_ids = $self->_select_updated_ids($class, $last_mtime); + my $bulk = $self->_bulk_helper($class); + my $last_mtime = $self->_find_largest_mtime($class); + my $last_id = $self->_find_largest_id($class); + my $new_ids = $self->_select_all_ids($class, $last_id); + my $updated_ids = $self->_select_updated_ids($class, $last_mtime); - $self->_put_mapping($class); - $self->_bulk_load_ids($bulk, $class, $new_ids) if @$new_ids; - $self->_bulk_load_ids($bulk, $class, $updated_ids) if @$updated_ids; + $self->_put_mapping($class); + $self->_bulk_load_ids($bulk, $class, $new_ids) if @$new_ids; + $self->_bulk_load_ids($bulk, $class, $updated_ids) if @$updated_ids; - return { - new => scalar @$new_ids, - updated => scalar @$updated_ids, - }; + return {new => scalar @$new_ids, updated => scalar @$updated_ids,}; } sub _select_all_ids { - my ($self, $class, $last_id) = @_; + my ($self, $class, $last_id) = @_; - my $dbh = Bugzilla->dbh; - my ($sql, $params) = $self->_debug_sql($class->ES_SELECT_ALL_SQL($last_id)); - return $dbh->selectcol_arrayref($sql, undef, @$params); + my $dbh = Bugzilla->dbh; + my ($sql, $params) = $self->_debug_sql($class->ES_SELECT_ALL_SQL($last_id)); + return $dbh->selectcol_arrayref($sql, undef, @$params); } sub _select_updated_ids { - my ($self, $class, $last_mtime) = @_; + my ($self, $class, $last_mtime) = @_; - my $dbh = Bugzilla->dbh; - my ($updated_sql, $updated_params) = $self->_debug_sql($class->ES_SELECT_UPDATED_SQL($last_mtime)); - return $dbh->selectcol_arrayref($updated_sql, undef, @$updated_params); + my $dbh = Bugzilla->dbh; + my ($updated_sql, $updated_params) + = $self->_debug_sql($class->ES_SELECT_UPDATED_SQL($last_mtime)); + return $dbh->selectcol_arrayref($updated_sql, undef, @$updated_params); } sub bulk_load_ids { - my ($self, $class, $ids) = @_; + my ($self, $class, $ids) = @_; - $self->_create_index($class); - $self->_put_mapping($class); - $self->_bulk_load_ids($self->_bulk_helper($class), $class, $ids); + $self->_create_index($class); + $self->_put_mapping($class); + $self->_bulk_load_ids($self->_bulk_helper($class), $class, $ids); } sub _bulk_load_ids { - my ($self, $bulk, $class, $all_ids) = @_; - - my $iter = natatime $class->ES_OBJECTS_AT_ONCE, @$all_ids; - my $mtime = $self->_current_mtime; - my $progress_bar; - my $next_update; - - if ($self->has_progress_bar) { - my $name = (split(/::/, $class))[-1]; - $progress_bar = $self->progress_bar->new({ - name => $name, - count => scalar @$all_ids, - ETA => 'linear' - }); - $progress_bar->message(sprintf "loading %d $class objects, %d at a time", scalar @$all_ids, $class->ES_OBJECTS_AT_ONCE); - $next_update = $progress_bar->update(0); - $progress_bar->max_update_rate(1); + my ($self, $bulk, $class, $all_ids) = @_; + + my $iter = natatime $class->ES_OBJECTS_AT_ONCE, @$all_ids; + my $mtime = $self->_current_mtime; + my $progress_bar; + my $next_update; + + if ($self->has_progress_bar) { + my $name = (split(/::/, $class))[-1]; + $progress_bar + = $self->progress_bar->new({ + name => $name, count => scalar @$all_ids, ETA => 'linear' + }); + $progress_bar->message( + sprintf "loading %d $class objects, %d at a time", + scalar @$all_ids, + $class->ES_OBJECTS_AT_ONCE + ); + $next_update = $progress_bar->update(0); + $progress_bar->max_update_rate(1); + } + + my $total = 0; + my $start = time; + while (my @ids = $iter->()) { + if ($progress_bar) { + $total += @ids; + if ($total >= $next_update) { + $next_update = $progress_bar->update($total); + my $duration = time - $start || 1; + } } - my $total = 0; - my $start = time; - while (my @ids = $iter->()) { - if ($progress_bar) { - $total += @ids; - if ($total >= $next_update) { - $next_update = $progress_bar->update($total); - my $duration = time - $start || 1; - } - } - - my $objects = $class->new_from_list(\@ids); - foreach my $object (@$objects) { - my %doc = ( - id => $object->es_id, - source => scalar $object->es_document($mtime), - ); - - if ($class->does('Bugzilla::Elastic::Role::ChildObject')) { - $doc{parent} = $object->es_parent_id; - } - - $bulk->index(\%doc); - } - Bugzilla->_cleanup(); + my $objects = $class->new_from_list(\@ids); + foreach my $object (@$objects) { + my %doc + = (id => $object->es_id, source => scalar $object->es_document($mtime),); + + if ($class->does('Bugzilla::Elastic::Role::ChildObject')) { + $doc{parent} = $object->es_parent_id; + } + + $bulk->index(\%doc); } + Bugzilla->_cleanup(); + } - $bulk->flush; + $bulk->flush; } sub _build_shadow_dbh { Bugzilla->switch_to_shadow_db } sub _current_mtime { - my ($self) = @_; - my ($mtime) = $self->shadow_dbh->selectrow_array("SELECT UNIX_TIMESTAMP(NOW())"); - return $mtime; + my ($self) = @_; + my ($mtime) + = $self->shadow_dbh->selectrow_array("SELECT UNIX_TIMESTAMP(NOW())"); + return $mtime; } 1; diff --git a/Bugzilla/Elastic/Role/HasClient.pm b/Bugzilla/Elastic/Role/HasClient.pm index a971392e0..41d8e7647 100644 --- a/Bugzilla/Elastic/Role/HasClient.pm +++ b/Bugzilla/Elastic/Role/HasClient.pm @@ -13,13 +13,13 @@ use Moo::Role; has 'client' => (is => 'lazy'); sub _build_client { - my ($self) = @_; + my ($self) = @_; - require Search::Elasticsearch; - return Search::Elasticsearch->new( - nodes => [ split(/\s+/, Bugzilla->params->{elasticsearch_nodes}) ], - cxn_pool => 'Sniff', - ); + require Search::Elasticsearch; + return Search::Elasticsearch->new( + nodes => [split(/\s+/, Bugzilla->params->{elasticsearch_nodes})], + cxn_pool => 'Sniff', + ); } 1; diff --git a/Bugzilla/Elastic/Role/Object.pm b/Bugzilla/Elastic/Role/Object.pm index 674545d04..6974d9087 100644 --- a/Bugzilla/Elastic/Role/Object.pm +++ b/Bugzilla/Elastic/Role/Object.pm @@ -12,45 +12,44 @@ use Role::Tiny; requires qw(ES_TYPE ES_INDEX ES_SETTINGS ES_PROPERTIES es_document); requires qw(ID_FIELD DB_TABLE); -sub ES_OBJECTS_AT_ONCE { 100 } +sub ES_OBJECTS_AT_ONCE {100} sub ES_SELECT_ALL_SQL { - my ($class, $last_id) = @_; + my ($class, $last_id) = @_; - my $id = $class->ID_FIELD; - my $table = $class->DB_TABLE; + my $id = $class->ID_FIELD; + my $table = $class->DB_TABLE; - return ("SELECT $id FROM $table WHERE $id > ? ORDER BY $id", [$last_id // 0]); + return ("SELECT $id FROM $table WHERE $id > ? ORDER BY $id", [$last_id // 0]); } requires qw(ES_SELECT_UPDATED_SQL); sub es_id { - my ($self) = @_; - return join('_', $self->ES_TYPE, $self->id); + my ($self) = @_; + return join('_', $self->ES_TYPE, $self->id); } around 'ES_PROPERTIES' => sub { - my $orig = shift; - my $self = shift; - my $properties = $orig->($self, @_); - $properties->{es_mtime} = { type => 'long' }; - $properties->{$self->ID_FIELD} = { type => 'long', analyzer => 'keyword' }; + my $orig = shift; + my $self = shift; + my $properties = $orig->($self, @_); + $properties->{es_mtime} = {type => 'long'}; + $properties->{$self->ID_FIELD} = {type => 'long', analyzer => 'keyword'}; - return $properties; + return $properties; }; around 'es_document' => sub { - my ($orig, $self, $mtime) = @_; - my $doc = $orig->($self); + my ($orig, $self, $mtime) = @_; + my $doc = $orig->($self); - $doc->{es_mtime} = $mtime; - $doc->{$self->ID_FIELD} = $self->id; - $doc->{_id} = $self->es_id; + $doc->{es_mtime} = $mtime; + $doc->{$self->ID_FIELD} = $self->id; + $doc->{_id} = $self->es_id; - return $doc; + return $doc; }; - 1; diff --git a/Bugzilla/Elastic/Search.pm b/Bugzilla/Elastic/Search.pm index 26ab71bec..032f9b03a 100644 --- a/Bugzilla/Elastic/Search.pm +++ b/Bugzilla/Elastic/Search.pm @@ -16,63 +16,62 @@ use namespace::clean; use Bugzilla::Elastic::Search::FakeCGI; -has 'quicksearch' => ( is => 'ro' ); -has 'limit' => ( is => 'ro', predicate => 'has_limit' ); -has 'offset' => ( is => 'ro', predicate => 'has_offset' ); -has 'fields' => ( is => 'ro', isa => \&_arrayref_of_fields, default => sub { [] } ); -has 'params' => ( is => 'lazy' ); -has 'clause' => ( is => 'lazy' ); -has 'es_query' => ( is => 'lazy' ); +has 'quicksearch' => (is => 'ro'); +has 'limit' => (is => 'ro', predicate => 'has_limit'); +has 'offset' => (is => 'ro', predicate => 'has_offset'); +has 'fields' => + (is => 'ro', isa => \&_arrayref_of_fields, default => sub { [] }); +has 'params' => (is => 'lazy'); +has 'clause' => (is => 'lazy'); +has 'es_query' => (is => 'lazy'); has 'search_description' => (is => 'lazy'); -has 'query_time' => ( is => 'rwp' ); +has 'query_time' => (is => 'rwp'); -has '_input_order' => ( is => 'ro', init_arg => 'order', required => 1); -has '_order' => ( is => 'lazy', init_arg => undef ); -has 'invalid_order_columns' => ( is => 'lazy' ); +has '_input_order' => (is => 'ro', init_arg => 'order', required => 1); +has '_order' => (is => 'lazy', init_arg => undef); +has 'invalid_order_columns' => (is => 'lazy'); with 'Bugzilla::Elastic::Role::HasClient'; with 'Bugzilla::Elastic::Role::Search'; my @SUPPORTED_FIELDS = qw( - bug_id product component short_desc - priority status_whiteboard bug_status resolution - keywords alias assigned_to reporter delta_ts - longdesc cf_crash_signature classification bug_severity - commenter + bug_id product component short_desc + priority status_whiteboard bug_status resolution + keywords alias assigned_to reporter delta_ts + longdesc cf_crash_signature classification bug_severity + commenter ); my %IS_SUPPORTED_FIELD = map { $_ => 1 } @SUPPORTED_FIELDS; $IS_SUPPORTED_FIELD{relevance} = 1; my @NORMAL_FIELDS = qw( - priority - bug_severity - bug_status - resolution - product - component - classification - short_desc - assigned_to - reporter + priority + bug_severity + bug_status + resolution + product + component + classification + short_desc + assigned_to + reporter ); my %SORT_MAP = ( - bug_id => '_id', - relevance => '_score', - map { $_ => "$_.eq" } @NORMAL_FIELDS, + bug_id => '_id', + relevance => '_score', + map { $_ => "$_.eq" } @NORMAL_FIELDS, ); -my %EQUALS_MAP = ( - map { $_ => "$_.eq" } @NORMAL_FIELDS, -); +my %EQUALS_MAP = (map { $_ => "$_.eq" } @NORMAL_FIELDS,); sub _arrayref_of_fields { - my $f = $_; - foreach my $field (@$f) { - Bugzilla::Elastic::Search::UnsupportedField->throw(field => $field) - unless $IS_SUPPORTED_FIELD{$field}; - } + my $f = $_; + foreach my $field (@$f) { + Bugzilla::Elastic::Search::UnsupportedField->throw(field => $field) + unless $IS_SUPPORTED_FIELD{$field}; + } } @@ -82,360 +81,342 @@ sub _arrayref_of_fields { # But the DB column stayed the same... and elasticsearch uses the db name # However search likes to use the "new" name. # for now we hack a fix in here. -my %REMAP_NAME = ( - changeddate => 'delta_ts', -); +my %REMAP_NAME = (changeddate => 'delta_ts',); sub data { - my ($self) = @_; - my $body = $self->es_query; - my $result = eval { - $self->client->search( - index => Bugzilla::Bug->ES_INDEX, - type => Bugzilla::Bug->ES_TYPE, - body => $body, - ); - }; - die $@ unless $result; - $self->_set_query_time($result->{took} / 1000); - - my @fields = map { $REMAP_NAME{$_} // $_ } @{ $self->fields }; - my (@ids, %hits); - foreach my $hit (@{ $result->{hits}{hits} }) { - push @ids, $hit->{_id}; - my $source = $hit->{_source}; - $source->{relevance} = $hit->{_score}; - foreach my $val (values %$source) { - next unless defined $val; - trick_taint($val); - } - trick_taint($hit->{_id}); - if ($source) { - $hits{$hit->{_id}} = [ @$source{@fields} ]; - } - else { - $hits{$hit->{_id}} = $hit->{_id}; - } + my ($self) = @_; + my $body = $self->es_query; + my $result = eval { + $self->client->search( + index => Bugzilla::Bug->ES_INDEX, + type => Bugzilla::Bug->ES_TYPE, + body => $body, + ); + }; + die $@ unless $result; + $self->_set_query_time($result->{took} / 1000); + + my @fields = map { $REMAP_NAME{$_} // $_ } @{$self->fields}; + my (@ids, %hits); + foreach my $hit (@{$result->{hits}{hits}}) { + push @ids, $hit->{_id}; + my $source = $hit->{_source}; + $source->{relevance} = $hit->{_score}; + foreach my $val (values %$source) { + next unless defined $val; + trick_taint($val); + } + trick_taint($hit->{_id}); + if ($source) { + $hits{$hit->{_id}} = [@$source{@fields}]; + } + else { + $hits{$hit->{_id}} = $hit->{_id}; } - my $visible_ids = Bugzilla->user->visible_bugs(\@ids); + } + my $visible_ids = Bugzilla->user->visible_bugs(\@ids); - return [ map { $hits{$_} } @$visible_ids ]; + return [map { $hits{$_} } @$visible_ids]; } sub _valid_order { - my ($self) = @_; + my ($self) = @_; - return grep { $IS_SUPPORTED_FIELD{$_->[0]} } @{$self->_order}; + return grep { $IS_SUPPORTED_FIELD{$_->[0]} } @{$self->_order}; } sub order { - my ($self) = @_; + my ($self) = @_; - return map { $_->[0] } $self->_valid_order; + return map { $_->[0] } $self->_valid_order; } sub _quicksearch_to_params { - my ($quicksearch) = @_; - no warnings 'redefine'; - my $cgi = Bugzilla::Elastic::Search::FakeCGI->new; - local *Bugzilla::cgi = sub { $cgi }; - local $Bugzilla::Search::Quicksearch::ELASTIC = 1; - quicksearch($quicksearch); - - return $cgi->params; + my ($quicksearch) = @_; + no warnings 'redefine'; + my $cgi = Bugzilla::Elastic::Search::FakeCGI->new; + local *Bugzilla::cgi = sub {$cgi}; + local $Bugzilla::Search::Quicksearch::ELASTIC = 1; + quicksearch($quicksearch); + + return $cgi->params; } sub _build_fields { return \@SUPPORTED_FIELDS } sub _build__order { - my ($self) = @_; - - my @order; - foreach my $order (@{$self->_input_order}) { - if ($order =~ /^(.+)\s+(asc|desc)$/i) { - push @order, [ $1, lc $2 ]; - } - else { - push @order, [ $order ]; - } + my ($self) = @_; + + my @order; + foreach my $order (@{$self->_input_order}) { + if ($order =~ /^(.+)\s+(asc|desc)$/i) { + push @order, [$1, lc $2]; + } + else { + push @order, [$order]; } - return \@order; + } + return \@order; } sub _build_invalid_order_columns { - my ($self) = @_; + my ($self) = @_; - return [ map { $_->[0] } grep { !$IS_SUPPORTED_FIELD{$_->[0]} } @{ $self->_order } ]; + return [map { $_->[0] } + grep { !$IS_SUPPORTED_FIELD{$_->[0]} } @{$self->_order}]; } sub _build_params { - my ($self) = @_; + my ($self) = @_; - return _quicksearch_to_params($self->quicksearch); + return _quicksearch_to_params($self->quicksearch); } sub _build_clause { - my ($self) = @_; - my $search = Bugzilla::Search->new(params => $self->params); + my ($self) = @_; + my $search = Bugzilla::Search->new(params => $self->params); - return $search->_params_to_data_structure; + return $search->_params_to_data_structure; } sub _build_search_description { - my ($self) = @_; + my ($self) = @_; - return [_describe($self->clause)]; + return [_describe($self->clause)]; } sub _describe { - my ($thing) = @_; + my ($thing) = @_; - state $class_to_func = { - 'Bugzilla::Search::Condition' => \&_describe_condition, - 'Bugzilla::Search::Clause' => \&_describe_clause - }; + state $class_to_func = { + 'Bugzilla::Search::Condition' => \&_describe_condition, + 'Bugzilla::Search::Clause' => \&_describe_clause + }; - my $func = $class_to_func->{ref $thing} or die "nothing for $thing\n"; + my $func = $class_to_func->{ref $thing} or die "nothing for $thing\n"; - return $func->($thing); + return $func->($thing); } sub _describe_clause { - my ($clause) = @_; + my ($clause) = @_; - return map { _describe($_) } @{$clause->children}; + return map { _describe($_) } @{$clause->children}; } sub _describe_condition { - my ($cond) = @_; + my ($cond) = @_; - return { field => $cond->field, type => $cond->operator, value => _describe_value($cond->value) }; + return { + field => $cond->field, + type => $cond->operator, + value => _describe_value($cond->value) + }; } sub _describe_value { - my ($val) = @_; + my ($val) = @_; - return ref($val) ? join(", ", @$val) : $val; + return ref($val) ? join(", ", @$val) : $val; } sub _build_es_query { - my ($self) = @_; - my @extra; - - if ($self->_valid_order) { - my @sort = map { - my $f = $SORT_MAP{$_->[0]} // $_->[0]; - @$_ > 1 ? { $f => lc $_[1] } : $f; - } $self->_valid_order; - push @extra, sort => \@sort; - } - if ($self->has_offset) { - push @extra, from => $self->offset; + my ($self) = @_; + my @extra; + + if ($self->_valid_order) { + my @sort = map { + my $f = $SORT_MAP{$_->[0]} // $_->[0]; + @$_ > 1 ? {$f => lc $_[1]} : $f; + } $self->_valid_order; + push @extra, sort => \@sort; + } + if ($self->has_offset) { + push @extra, from => $self->offset; + } + my $max_limit = Bugzilla->params->{max_search_results}; + my $limit = Bugzilla->params->{default_search_limit}; + if ($self->has_limit) { + if ($self->limit) { + my $l = $self->limit; + $limit = $l < $max_limit ? $l : $max_limit; } - my $max_limit = Bugzilla->params->{max_search_results}; - my $limit = Bugzilla->params->{default_search_limit}; - if ($self->has_limit) { - if ($self->limit) { - my $l = $self->limit; - $limit = $l < $max_limit ? $l : $max_limit; - } - else { - $limit = $max_limit; - } + else { + $limit = $max_limit; } - push @extra, size => $limit; - return { - _source => @{$self->fields} ? \1 : \0, - query => _query($self->clause), - @extra, - }; + } + push @extra, size => $limit; + return { + _source => @{$self->fields} ? \1 : \0, + query => _query($self->clause), + @extra, + }; } sub _query { - my ($thing) = @_; - state $class_to_func = { - 'Bugzilla::Search::Condition' => \&_query_condition, - 'Bugzilla::Search::Clause' => \&_query_clause, - }; + my ($thing) = @_; + state $class_to_func = { + 'Bugzilla::Search::Condition' => \&_query_condition, + 'Bugzilla::Search::Clause' => \&_query_clause, + }; - my $func = $class_to_func->{ref $thing} or die "nothing for $thing\n"; + my $func = $class_to_func->{ref $thing} or die "nothing for $thing\n"; - return $func->($thing); + return $func->($thing); } sub _query_condition { - my ($cond) = @_; - state $operator_to_es = { - equals => \&_operator_equals, - substring => \&_operator_substring, - anyexact => \&_operator_anyexact, - anywords => \&_operator_anywords, - allwords => \&_operator_allwords, - }; - - my $field = $cond->field; - my $operator = $cond->operator; - my $value = $cond->value; - - if ($field eq 'resolution') { - $value = [ map { $_ eq '---' ? '' : $_ } ref $value ? @$value : $value ]; - } - - unless ($IS_SUPPORTED_FIELD{$field}) { - Bugzilla::Elastic::Search::UnsupportedField->throw(field => $field); - } - - my $op = $operator_to_es->{$operator} - or Bugzilla::Elastic::Search::UnsupportedOperator->throw(operator => $operator); - - my $result; - if (ref $op) { - $result = $op->($field, $value); - } else { - $result = { $op => { $field => $value } }; - } - - return $result; + my ($cond) = @_; + state $operator_to_es = { + equals => \&_operator_equals, + substring => \&_operator_substring, + anyexact => \&_operator_anyexact, + anywords => \&_operator_anywords, + allwords => \&_operator_allwords, + }; + + my $field = $cond->field; + my $operator = $cond->operator; + my $value = $cond->value; + + if ($field eq 'resolution') { + $value = [map { $_ eq '---' ? '' : $_ } ref $value ? @$value : $value]; + } + + unless ($IS_SUPPORTED_FIELD{$field}) { + Bugzilla::Elastic::Search::UnsupportedField->throw(field => $field); + } + + my $op = $operator_to_es->{$operator} + or + Bugzilla::Elastic::Search::UnsupportedOperator->throw(operator => $operator); + + my $result; + if (ref $op) { + $result = $op->($field, $value); + } + else { + $result = {$op => {$field => $value}}; + } + + return $result; } # is equal to any of the strings sub _operator_anyexact { - my ($field, $value) = @_; - my @values = ref $value ? @$value : split(/\s*,\s*/, $value); - if (@values == 1) { - return _operator_equals($field, $values[0]); - } - else { - return { - terms => { - $EQUALS_MAP{$field} // $field => [map { lc } @values], - minimum_should_match => 1, - }, - }; - } + my ($field, $value) = @_; + my @values = ref $value ? @$value : split(/\s*,\s*/, $value); + if (@values == 1) { + return _operator_equals($field, $values[0]); + } + else { + return { + terms => { + $EQUALS_MAP{$field} // $field => [map {lc} @values], + minimum_should_match => 1, + }, + }; + } } # contains any of the words sub _operator_anywords { - my ($field, $value) = @_; - return { - match => { - $field => { query => $value, operator => "or" } - }, - }; + my ($field, $value) = @_; + return {match => {$field => {query => $value, operator => "or"}},}; } # contains all of the words sub _operator_allwords { - my ($field, $value) = @_; - return { - match => { - $field => { query => $value, operator => "and" } - }, - }; + my ($field, $value) = @_; + return {match => {$field => {query => $value, operator => "and"}},}; } sub _operator_equals { - my ($field, $value) = @_; - return { - match => { - $EQUALS_MAP{$field} // $field => $value, - }, - }; + my ($field, $value) = @_; + return {match => {$EQUALS_MAP{$field} // $field => $value,},}; } sub _operator_substring { - my ($field, $value) = @_; - my $is_insider = Bugzilla->user->is_insider; - - if ($field eq 'longdesc') { - return { - has_child => { - type => 'comment', - query => { - bool => { - must => [ - { match => { body => { query => $value, operator => "and" } } }, - $is_insider ? () : { term => { is_private => \0 } }, - ], - }, - }, - }, - } - } - elsif ($field eq 'reporter' or $field eq 'assigned_to') { - return { - prefix => { - $EQUALS_MAP{$field} // $field => lc $value, - } - } - } - elsif ($field eq 'status_whiteboard' && $value =~ /[\[\]]/) { - return { - match => { - $EQUALS_MAP{$field} // $field => $value, - } - }; - } - else { - return { - wildcard => { - $EQUALS_MAP{$field} // $field => lc "*$value*", - } - }; - } + my ($field, $value) = @_; + my $is_insider = Bugzilla->user->is_insider; + + if ($field eq 'longdesc') { + return { + has_child => { + type => 'comment', + query => { + bool => { + must => [ + {match => {body => {query => $value, operator => "and"}}}, + $is_insider ? () : {term => {is_private => \0}}, + ], + }, + }, + }, + }; + } + elsif ($field eq 'reporter' or $field eq 'assigned_to') { + return {prefix => {$EQUALS_MAP{$field} // $field => lc $value,}}; + } + elsif ($field eq 'status_whiteboard' && $value =~ /[\[\]]/) { + return {match => {$EQUALS_MAP{$field} // $field => $value,}}; + } + else { + return {wildcard => {$EQUALS_MAP{$field} // $field => lc "*$value*",}}; + } } sub _query_clause { - my ($clause) = @_; + my ($clause) = @_; - state $joiner_to_func = { - AND => \&_join_and, - OR => \&_join_or, - }; + state $joiner_to_func = {AND => \&_join_and, OR => \&_join_or,}; - my @children = grep { !$_->isa('Bugzilla::Search::Clause') || @{$_->children} } @{$clause->children}; - if (@children == 1) { - return _query($children[0]); - } + my @children = grep { !$_->isa('Bugzilla::Search::Clause') || @{$_->children} } + @{$clause->children}; + if (@children == 1) { + return _query($children[0]); + } - return $joiner_to_func->{$clause->joiner}->([ map { _query($_) } @children ]); + return $joiner_to_func->{$clause->joiner}->([map { _query($_) } @children]); } sub _join_and { - my ($children) = @_; - return { bool => { must => $children } }, + my ($children) = @_; + return {bool => {must => $children}},; } sub _join_or { - my ($children) = @_; - return { bool => { should => $children } }; + my ($children) = @_; + return {bool => {should => $children}}; } # Exceptions BEGIN { - package Bugzilla::Elastic::Search::Redirect; - use Moo; - with 'Throwable'; + package Bugzilla::Elastic::Search::Redirect; + use Moo; + + with 'Throwable'; - has 'redirect_args' => (is => 'ro', required => 1); + has 'redirect_args' => (is => 'ro', required => 1); - package Bugzilla::Elastic::Search::UnsupportedField; - use Moo; - use overload q{""} => sub { "Unsupported field: ", $_[0]->field }, fallback => 1; + package Bugzilla::Elastic::Search::UnsupportedField; + use Moo; + use overload + q{""} => sub { "Unsupported field: ", $_[0]->field }, + fallback => 1; - with 'Throwable'; + with 'Throwable'; - has 'field' => (is => 'ro', required => 1); + has 'field' => (is => 'ro', required => 1); - package Bugzilla::Elastic::Search::UnsupportedOperator; - use Moo; + package Bugzilla::Elastic::Search::UnsupportedOperator; + use Moo; - with 'Throwable'; + with 'Throwable'; - has 'operator' => (is => 'ro', required => 1); + has 'operator' => (is => 'ro', required => 1); } 1; diff --git a/Bugzilla/Elastic/Search/FakeCGI.pm b/Bugzilla/Elastic/Search/FakeCGI.pm index 827c96c52..9772d798c 100644 --- a/Bugzilla/Elastic/Search/FakeCGI.pm +++ b/Bugzilla/Elastic/Search/FakeCGI.pm @@ -13,31 +13,34 @@ has 'params' => (is => 'ro', default => sub { {} }); # we pretend to be Bugzilla::CGI at times. sub canonicalise_query { - return Bugzilla::CGI::canonicalise_query(@_); + return Bugzilla::CGI::canonicalise_query(@_); } sub delete { - my ($self, $key) = @_; - delete $self->params->{$key}; + my ($self, $key) = @_; + delete $self->params->{$key}; } sub redirect { - my ($self, @args) = @_; + my ($self, @args) = @_; - Bugzilla::Elastic::Search::Redirect->throw(redirect_args => \@args); + Bugzilla::Elastic::Search::Redirect->throw(redirect_args => \@args); } sub param { - my ($self, $key, $val, @rest) = @_; - if (@_ > 3) { - $self->params->{$key} = [$val, @rest]; - } elsif (@_ == 3) { - $self->params->{$key} = $val; - } elsif (@_ == 2) { - return $self->params->{$key}; - } else { - return $self->params - } + my ($self, $key, $val, @rest) = @_; + if (@_ > 3) { + $self->params->{$key} = [$val, @rest]; + } + elsif (@_ == 3) { + $self->params->{$key} = $val; + } + elsif (@_ == 2) { + return $self->params->{$key}; + } + else { + return $self->params; + } } 1; diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm index 70430d40d..4a6ab6865 100644 --- a/Bugzilla/Error.pm +++ b/Bugzilla/Error.pm @@ -14,7 +14,8 @@ use warnings; use base qw(Exporter); ## no critic (Modules::ProhibitAutomaticExportation) -our @EXPORT = qw( ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage); +our @EXPORT + = qw( ThrowCodeError ThrowTemplateError ThrowUserError ThrowErrorPage); ## use critic use Bugzilla::Constants; @@ -31,200 +32,207 @@ 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 =~ /^Bugzilla::Quantum::CGI::try/; - $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 =~ /^Bugzilla::Quantum::CGI::try/; + $in_eval = 1 if $sub =~ /^\(eval\)/; + } + return $in_eval; } sub _throw_error { - my ($name, $error, $vars, $logfunc) = @_; - $vars ||= {}; - - # 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. - my $dbh = eval { Bugzilla->dbh }; - $dbh->bz_rollback_transaction() if ($dbh && $dbh->bz_in_transaction() && !_in_eval()); - - if (Bugzilla->error_mode == ERROR_MODE_MOJO) { - my ($type) = $name =~ /^global\/(user|code)-error/; - my $class = $type ? 'Bugzilla::Error::' . ucfirst($type) : 'Mojo::Exception'; - my $e = $class->new($error)->trace(2); - $e->vars($vars) if $e->can('vars'); - CORE::die $e->inspect; - } - - $vars->{error} = $error; - my $template = Bugzilla->template; - my $message; - - # There are some tests that throw and catch a lot of errors, - # and calling $template->process over and over for those errors - # is too slow. So instead, we just "die" with a dump of the arguments. - 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. - require Bugzilla::Hook; - Bugzilla::Hook::process('error_catch', { error => $error, vars => $vars, - message => \$message }); - - if ($Bugzilla::Template::is_processing) { - my ($type) = $name =~ /^global\/(user|code)-error/; - $type //= 'unknown'; - die Template::Exception->new("bugzilla.$type.$error", $vars); + my ($name, $error, $vars, $logfunc) = @_; + $vars ||= {}; + + # 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. + my $dbh = eval { Bugzilla->dbh }; + $dbh->bz_rollback_transaction() + if ($dbh && $dbh->bz_in_transaction() && !_in_eval()); + + if (Bugzilla->error_mode == ERROR_MODE_MOJO) { + my ($type) = $name =~ /^global\/(user|code)-error/; + my $class = $type ? 'Bugzilla::Error::' . ucfirst($type) : 'Mojo::Exception'; + my $e = $class->new($error)->trace(2); + $e->vars($vars) if $e->can('vars'); + CORE::die $e->inspect; + } + + $vars->{error} = $error; + my $template = Bugzilla->template; + my $message; + + # There are some tests that throw and catch a lot of errors, + # and calling $template->process over and over for those errors + # is too slow. So instead, we just "die" with a dump of the arguments. + 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. + require Bugzilla::Hook; + Bugzilla::Hook::process('error_catch', + {error => $error, vars => $vars, message => \$message}); + + if ($Bugzilla::Template::is_processing) { + my ($type) = $name =~ /^global\/(user|code)-error/; + $type //= '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'); + $template->process($name, $vars) || ThrowTemplateError($template->error()); + print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; + $logfunc->("webpage error: $error"); + } + 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; } - if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) { - my $cgi = Bugzilla->cgi; - $cgi->close_standby_message('text/html', 'inline', 'error', 'html'); - $template->process($name, $vars) - || ThrowTemplateError($template->error()); - print $cgi->multipart_final() if $cgi->{_multipart_in_progress}; - $logfunc->("webpage error: $error"); - } - elsif (Bugzilla->error_mode == ERROR_MODE_TEST) { - die Dumper($vars); - } - elsif (Bugzilla->error_mode == ERROR_MODE_DIE) { - die "$message\n"; + if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { + $logfunc->("XMLRPC error: $error ($code)"); + 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) { - $logfunc->("XMLRPC error: $error ($code)"); - 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'}; - $logfunc->("REST error: $error (HTTP $status_code, internal code $code)"); - } - else { - my $fake_code = 100000 + $code; - $logfunc->("JSONRPC error: $error ($fake_code)"); - } - # 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'}; + $logfunc->("REST error: $error (HTTP $status_code, internal code $code)"); + } + else { + my $fake_code = 100000 + $code; + $logfunc->("JSONRPC error: $error ($fake_code)"); + } + + # 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 _add_vars_to_logging_fields { - my ($vars) = @_; + my ($vars) = @_; - foreach my $key (keys %$vars) { - Bugzilla::Logging->fields->{"var_$key"} = $vars->{$key}; - } + foreach my $key (keys %$vars) { + Bugzilla::Logging->fields->{"var_$key"} = $vars->{$key}; + } } sub _make_logfunc { - my ($type) = @_; - my $logger = Log::Log4perl->get_logger("Bugzilla.Error.$type"); - return sub { - local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 3; - if ($type eq 'User') { - $logger->warn(@_); - } - else { - $logger->error(@_); - } - }; + my ($type) = @_; + my $logger = Log::Log4perl->get_logger("Bugzilla.Error.$type"); + return sub { + local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 3; + if ($type eq 'User') { + $logger->warn(@_); + } + else { + $logger->error(@_); + } + }; } sub ThrowUserError { - my ($error, $vars) = @_; - my $logfunc = _make_logfunc('User'); - _add_vars_to_logging_fields($vars); + my ($error, $vars) = @_; + my $logfunc = _make_logfunc('User'); + _add_vars_to_logging_fields($vars); - _throw_error( 'global/user-error.html.tmpl', $error, $vars, $logfunc); + _throw_error('global/user-error.html.tmpl', $error, $vars, $logfunc); } sub ThrowCodeError { - my ($error, $vars) = @_; - my $logfunc = _make_logfunc('Code'); - _add_vars_to_logging_fields($vars); + my ($error, $vars) = @_; + my $logfunc = _make_logfunc('Code'); + _add_vars_to_logging_fields($vars); - _throw_error( 'global/code-error.html.tmpl', $error, $vars, $logfunc ); + _throw_error('global/code-error.html.tmpl', $error, $vars, $logfunc); } sub ThrowTemplateError { - my ($template_err) = @_; - my $dbh = eval { Bugzilla->dbh }; - # Make sure the transaction is rolled back (if supported). - $dbh->bz_rollback_transaction() if $dbh && $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 ($template_err) = @_; + my $dbh = eval { Bugzilla->dbh }; + + # Make sure the transaction is rolled back (if supported). + $dbh->bz_rollback_transaction() if $dbh && $dbh->bz_in_transaction(); - my $vars = {}; - if (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("error: template error: $template_err"); + 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; } + } - # mod_perl overrides exit to call die with this string - # we never want to display this to the user - die $template_err if ref($template_err) eq 'ARRAY' && $template_err->[0] eq "EXIT\n"; + my $vars = {}; + if (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("error: template error: $template_err"); + } - state $logger = Log::Log4perl->get_logger('Bugzilla.Error.Template'); - $logger->error($template_err); + # mod_perl overrides exit to call die with this string + # we never want to display this to the user + die $template_err + if ref($template_err) eq 'ARRAY' && $template_err->[0] eq "EXIT\n"; - $vars->{'template_error_msg'} = $template_err; - $vars->{'error'} = "template_error"; + state $logger = Log::Log4perl->get_logger('Bugzilla.Error.Template'); + $logger->error($template_err); - $vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//; + $vars->{'template_error_msg'} = $template_err; + $vars->{'error'} = "template_error"; - my $template = Bugzilla->template; + $vars->{'template_error_msg'} =~ s/ at \S+ line \d+\.\s*$//; + + my $template = Bugzilla->template; - # Try a template first; but if this one fails too, fall back - # on plain old print statements. - if (!$template->process("global/code-error.html.tmpl", $vars)) { - my $maintainer = html_quote(Bugzilla->params->{'maintainer'}); - my $error = html_quote($vars->{'template_error_msg'}); - my $error2 = html_quote($template->error()); - print <process("global/code-error.html.tmpl", $vars)) { + my $maintainer = html_quote(Bugzilla->params->{'maintainer'}); + my $error = html_quote($vars->{'template_error_msg'}); + my $error2 = html_quote($template->error()); + print <

Bugzilla has suffered an internal error: @@ -241,46 +249,51 @@ sub ThrowTemplateError {

END - } - exit; + } + exit; } sub ThrowErrorPage { - # BMO customisation for bug 659231 - my ($template_name, $message) = @_; - my $dbh = Bugzilla->dbh; - $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); + # BMO customisation for bug 659231 + my ($template_name, $message) = @_; - if (Bugzilla->error_mode == ERROR_MODE_DIE) { - die("error: $message"); - } + my $dbh = Bugzilla->dbh; + $dbh->bz_rollback_transaction() if $dbh->bz_in_transaction(); + + if (Bugzilla->error_mode == ERROR_MODE_DIE) { + die("error: $message"); + } - if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT - || Bugzilla->error_mode == ERROR_MODE_JSON_RPC) - { - my $code = ERROR_UNKNOWN_TRANSIENT; - if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { - die SOAP::Fault->faultcode($code)->faultstring($message); - } else { - my $server = Bugzilla->_json_server; - $server->raise_error(code => 100000 + $code, - message => $message, - id => $server->{_bz_request_id}, - version => $server->version); - die if _in_eval(); - $server->response($server->error_response_header); - } - } else { - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - my $vars = {}; - $vars->{message} = $message; - print $cgi->header(); - $template->process($template_name, $vars) - || ThrowTemplateError($template->error()); - exit; + if ( Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT + || Bugzilla->error_mode == ERROR_MODE_JSON_RPC) + { + my $code = ERROR_UNKNOWN_TRANSIENT; + if (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT) { + die SOAP::Fault->faultcode($code)->faultstring($message); } + else { + my $server = Bugzilla->_json_server; + $server->raise_error( + code => 100000 + $code, + message => $message, + id => $server->{_bz_request_id}, + version => $server->version + ); + die if _in_eval(); + $server->response($server->error_response_header); + } + } + else { + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + my $vars = {}; + $vars->{message} = $message; + print $cgi->header(); + $template->process($template_name, $vars) + || ThrowTemplateError($template->error()); + exit; + } } 1; diff --git a/Bugzilla/Error/Base.pm b/Bugzilla/Error/Base.pm index ea44c272a..ed3bb2cd5 100644 --- a/Bugzilla/Error/Base.pm +++ b/Bugzilla/Error/Base.pm @@ -13,9 +13,9 @@ use Mojo::Base 'Mojo::Exception'; has 'vars' => sub { {} }; has 'template' => sub { - my $self = shift; - my $type = lc( (split(/::/, ref $self))[-1] ); - return "global/$type-error"; + my $self = shift; + my $type = lc((split(/::/, ref $self))[-1]); + return "global/$type-error"; }; 1; diff --git a/Bugzilla/Error/Template.pm b/Bugzilla/Error/Template.pm index a3afa7e4d..5cae8f5bc 100644 --- a/Bugzilla/Error/Template.pm +++ b/Bugzilla/Error/Template.pm @@ -12,15 +12,9 @@ use strict; use warnings; use Moo; -has 'file' => ( - is => 'ro', - required => 1, -); +has 'file' => (is => 'ro', required => 1,); -has 'vars' => ( - is => 'ro', - default => sub { {} }, -); +has 'vars' => (is => 'ro', default => sub { {} },); -1; \ No newline at end of file +1; diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm index 8e173c711..901999978 100644 --- a/Bugzilla/Extension.pm +++ b/Bugzilla/Extension.pm @@ -22,52 +22,54 @@ use Taint::Util qw(untaint); BEGIN { push @INC, \&INC_HOOK } sub INC_HOOK { - my (undef, $fake_file) = @_; - state $bz_locations = bz_locations(); - my ($vol, $dir, $file) = File::Spec->splitpath($fake_file); - my @dirs = grep { length $_ } File::Spec->splitdir($dir); - - if (@dirs > 2 && $dirs[0] eq 'Bugzilla' && $dirs[1] eq 'Extension') { - my $extension = $dirs[2]; - splice @dirs, 0, 3, File::Spec->splitdir($bz_locations->{extensionsdir}), $extension, "lib"; - my $real_file = Cwd::realpath(File::Spec->catpath($vol, File::Spec->catdir(@dirs), $file)); - - my $first = 1; - untaint($real_file); - $INC{$fake_file} = $real_file; - my $found = open my $fh, '<', $real_file; - unless ($found) { - require Carp; - Carp::croak "Can't locate $fake_file while looking for $real_file in \@INC (\@INC contains: @INC)"; - } - return sub { - no warnings; - if ( !$first ) { - return 0 if eof $fh; - $_ = readline $fh - or return 0; - untaint($_); - return 1; - } - else { - $_ = qq{# line 1 "$real_file"\n}; - $first = 0; - return 1; - } - }; + my (undef, $fake_file) = @_; + state $bz_locations = bz_locations(); + my ($vol, $dir, $file) = File::Spec->splitpath($fake_file); + my @dirs = grep { length $_ } File::Spec->splitdir($dir); + + if (@dirs > 2 && $dirs[0] eq 'Bugzilla' && $dirs[1] eq 'Extension') { + my $extension = $dirs[2]; + splice @dirs, 0, 3, File::Spec->splitdir($bz_locations->{extensionsdir}), + $extension, "lib"; + my $real_file + = Cwd::realpath(File::Spec->catpath($vol, File::Spec->catdir(@dirs), $file)); + + my $first = 1; + untaint($real_file); + $INC{$fake_file} = $real_file; + my $found = open my $fh, '<', $real_file; + unless ($found) { + require Carp; + Carp::croak + "Can't locate $fake_file while looking for $real_file in \@INC (\@INC contains: @INC)"; } - return; -}; + return sub { + no warnings; + if (!$first) { + return 0 if eof $fh; + $_ = readline $fh or return 0; + untaint($_); + return 1; + } + else { + $_ = qq{# line 1 "$real_file"\n}; + $first = 0; + return 1; + } + }; + } + return; +} #################### # Subclass Methods # #################### sub new { - my ($class, $params) = @_; - $params ||= {}; - bless $params, $class; - return $params; + my ($class, $params) = @_; + $params ||= {}; + bless $params, $class; + return $params; } ####################################### @@ -75,83 +77,80 @@ 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"; - } - } - - if ($map and defined $map->{$extension_file}) { - $package = $map->{$extension_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"; + my $name = require $config_file; + if ($name =~ /^\d+$/) { + ThrowCodeError('extension_must_return_name', + {extension => $config_file, returned => $name}); + } + $package = "${class}::$name"; } + } + + if ($map and defined $map->{$extension_file}) { + $package = $map->{$extension_file}; + } + else { + my $name = require $extension_file; + if ($name =~ /^\d+$/) { + ThrowCodeError('extension_must_return_name', + {extension => $extension_file, returned => $name}); + } + $package = "${class}::$name"; + } - $class->_validate_package($package, $extension_file); - return $package; + $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'; - } - - 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 }); - } + 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; - state $EXTENSIONS = []; - return $EXTENSIONS if @$EXTENSIONS; - - my ($file_sets) = extension_code_files(); - foreach my $file_set (@$file_sets) { - my $package = $class->load(@$file_set); - push(@$EXTENSIONS, $package); - } + my $class = shift; + state $EXTENSIONS = []; + return $EXTENSIONS if @$EXTENSIONS; + + my ($file_sets) = extension_code_files(); + foreach my $file_set (@$file_sets) { + my $package = $class->load(@$file_set); + push(@$EXTENSIONS, $package); + } - return $EXTENSIONS; + return $EXTENSIONS; } #################### @@ -160,21 +159,21 @@ sub load_all { use constant enabled => 1; -sub package_dir { - my ($class) = @_; - state $bz_locations = bz_locations(); - my (undef, undef, $name) = split(/::/, $class); - return File::Spec->catdir($bz_locations->{extensionsdir}, $name); +sub package_dir { + my ($class) = @_; + state $bz_locations = bz_locations(); + my (undef, undef, $name) = split(/::/, $class); + return File::Spec->catdir($bz_locations->{extensionsdir}, $name); } sub template_dir { - my ($class) = @_; - return File::Spec->catdir($class->package_dir, "template"); + my ($class) = @_; + return File::Spec->catdir($class->package_dir, "template"); } sub web_dir { - my ($class) = @_; - return File::Spec->catdir($class->package_dir, "web"); + my ($class) = @_; + return File::Spec->catdir($class->package_dir, "web"); } 1; diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm index 837e1c0de..b0b7224dd 100644 --- a/Bugzilla/Field.pm +++ b/Bugzilla/Field.pm @@ -83,80 +83,79 @@ use constant DB_TABLE => 'fielddefs'; use constant LIST_ORDER => 'sortkey, name'; use constant DB_COLUMNS => qw( - id - name - description - type - custom - mailhead - sortkey - obsolete - enter_bug - buglist - visibility_field_id - value_field_id - reverse_desc - is_mandatory - is_numeric + id + name + description + 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, - 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, + 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 - mailhead - sortkey - obsolete - enter_bug - buglist - visibility_field_id - value_field_id - reverse_desc - is_mandatory - is_numeric - type + description + 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' }, - # BMO : allow integer fields to be NULL - FIELD_TYPE_INTEGER, { TYPE => 'INT4' }, + + # 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'}, + + # BMO : allow integer fields to be NULL + FIELD_TYPE_INTEGER, {TYPE => 'INT4'}, }; # Field definitions for the fields that ship with Bugzilla. @@ -164,108 +163,217 @@ 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 => 'component', desc => 'Component', 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 => '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 => '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 => 'assignee_last_login', desc => 'Assignee Last Login Date', 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', - 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 => 'Tags'}, - {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1, - type => FIELD_TYPE_DATETIME}, - {name => 'bug_interest_ts', desc => 'Bug Interest', buglist => 1, - type => FIELD_TYPE_DATETIME}, - {name => 'comment_tag', desc => 'Comment Tag'}, - {name => 'triage_owner', desc => 'Triage Owner', buglist => 1}, + { + 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 => 'component', + desc => 'Component', + 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 => '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 => '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 => 'assignee_last_login', + desc => 'Assignee Last Login Date', + 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', 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 => 'Tags'}, + { + name => 'last_visit_ts', + desc => 'Last Visit', + buglist => 1, + type => FIELD_TYPE_DATETIME + }, + { + name => 'bug_interest_ts', + desc => 'Bug Interest', + buglist => 1, + type => FIELD_TYPE_DATETIME + }, + {name => 'comment_tag', desc => 'Comment Tag'}, + {name => 'triage_owner', desc => 'Triage Owner', buglist => 1}, ); ################ @@ -274,15 +382,15 @@ 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]; - } - if (delete $params->{skip_extensions}) { - $params->{WHERE}{'type != ?'} = FIELD_TYPE_EXTENSION; - } - return $self->SUPER::match(@_); + my $self = shift; + my ($params) = @_; + if (delete $params->{is_select}) { + $params->{type} = [FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT]; + } + if (delete $params->{skip_extensions}) { + $params->{WHERE}{'type != ?'} = FIELD_TYPE_EXTENSION; + } + return $self->SUPER::match(@_); } ############## @@ -292,142 +400,144 @@ 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_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; + + 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); + 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 - } + 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; + $reverse_desc = clean_text($reverse_desc); + return $reverse_desc; } sub _check_is_mandatory { return $_[1] ? 1 : 0; } @@ -563,11 +673,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) + ? 1 + : 0; } =over @@ -588,19 +700,19 @@ 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'}) { + require Bugzilla::Field::Choice; + my @values = Bugzilla::Field::Choice->type($self)->get_all(); + $self->{'legal_values'} = \@values; + } + return $self->{'legal_values'}; } =pod @@ -617,8 +729,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 @@ -637,12 +749,12 @@ 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({ id => $self->{visibility_field_id}, cache => 1 }); - } - return $self->{visibility_field}; + my $self = shift; + if ($self->{visibility_field_id}) { + $self->{visibility_field} + ||= $self->new({id => $self->{visibility_field_id}, cache => 1}); + } + return $self->{visibility_field}; } =pod @@ -660,22 +772,23 @@ or undef if there is no C set. =cut sub visibility_values { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - return [] if !$self->{visibility_field_id}; + return [] if !$self->{visibility_field_id}; - if (!defined $self->{visibility_values}) { - my $visibility_value_ids = - $dbh->selectcol_arrayref("SELECT value_id FROM field_visibility - WHERE field_id = ?", undef, $self->id); + if (!defined $self->{visibility_values}) { + my $visibility_value_ids = $dbh->selectcol_arrayref( + "SELECT value_id FROM field_visibility + WHERE field_id = ?", undef, $self->id + ); - $self->{visibility_values} = - Bugzilla::Field::Choice->type($self->visibility_field) - ->new_from_list($visibility_value_ids); - } + $self->{visibility_values} + = Bugzilla::Field::Choice->type($self->visibility_field) + ->new_from_list($visibility_value_ids); + } - return $self->{visibility_values}; + return $self->{visibility_values}; } =pod @@ -692,10 +805,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 @@ -713,11 +826,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 @@ -734,10 +847,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 @@ -751,15 +864,15 @@ 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 @@ -775,13 +888,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 @@ -866,28 +979,31 @@ They will throw an error if you try to set the values to something invalid. =cut -sub set_description { $_[0]->set('description', $_[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_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]); } @@ -911,83 +1027,87 @@ 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; + + if (!$self->custom) { + ThrowCodeError('field_not_custom', {'name' => $name}); + } + + if (!$self->obsolete) { + ThrowUserError('customfield_not_obsolete', {'name' => $self->name}); + } + + # BMO: disable bug updates during field creation + # using an eval as try/finally + eval { + SetParam('disable_bug_updates', 1); + write_params(); + + $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}); + } - my $name = $self->name; + # Check to see if bugs table has records (slow) + my $bugs_query = ""; - if (!$self->custom) { - ThrowCodeError('field_not_custom', {'name' => $name }); + if ($self->type == FIELD_TYPE_MULTI_SELECT) { + $bugs_query = "SELECT COUNT(*) FROM bug_$name"; + } + else { + $bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL"; + if ( $self->type != FIELD_TYPE_BUG_ID + && $self->type != FIELD_TYPE_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->obsolete) { - ThrowUserError('customfield_not_obsolete', {'name' => $self->name }); + my $has_bugs = $dbh->selectrow_array($bugs_query); + if ($has_bugs) { + ThrowUserError('customfield_has_contents', {'name' => $name}); } - # BMO: disable bug updates during field creation - # using an eval as try/finally - eval { - SetParam('disable_bug_updates', 1); - write_params(); - - $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 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 != '---'"; - } - } - - my $has_bugs = $dbh->selectrow_array($bugs_query); - if ($has_bugs) { - ThrowUserError('customfield_has_contents', {'name' => $name }); - } - - # Once we reach here, we should be OK to delete. - $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id); - - 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); - } - - if ($self->is_select) { - # Delete the table that holds the legal values for this field. - $dbh->bz_drop_field_tables($self); - } - - Bugzilla->memcached->clear({ table => 'fielddefs', id => $self->id }); - Bugzilla->memcached->clear_config(); - - $dbh->bz_commit_transaction(); - }; - my $error = "$@"; - SetParam('disable_bug_updates', 0); - write_params(); - die $error if $error; + # Once we reach here, we should be OK to delete. + $dbh->do('DELETE FROM fielddefs WHERE id = ?', undef, $self->id); + + 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); + } + + if ($self->is_select) { + + # Delete the table that holds the legal values for this field. + $dbh->bz_drop_field_tables($self); + } + + Bugzilla->memcached->clear({table => 'fielddefs', id => $self->id}); + Bugzilla->memcached->clear_config(); + + $dbh->bz_commit_transaction(); + }; + my $error = "$@"; + SetParam('disable_bug_updates', 0); + write_params(); + die $error if $error; } =pod @@ -1031,109 +1151,114 @@ C - boolean - Whether this field is mandatory. Defaults to 0. =cut sub create { - my $class = shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; - - # BMO: disable bug updates during field creation - # using an eval as try/finally - my $field; - eval { - if ($params->{'custom'}) { - SetParam('disable_bug_updates', 1); - write_params(); - } - - # 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}; - $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}); - } - - 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(); - } - }; - - my $error = "$@"; + my $class = shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + # BMO: disable bug updates during field creation + # using an eval as try/finally + my $field; + eval { if ($params->{'custom'}) { - SetParam('disable_bug_updates', 0); - write_params(); + SetParam('disable_bug_updates', 1); + write_params(); } - die $error if $error; - Bugzilla::Hook::process("field_end_of_create", { field => $field }); + # 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; - return $field; + # 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}; + $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}); + } + + 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(); + } + }; + + my $error = "$@"; + if ($params->{'custom'}) { + SetParam('disable_bug_updates', 0); + write_params(); + } + die $error if $error; + + Bugzilla::Hook::process("field_end_of_create", {field => $field}); + + return $field; } 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 $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; } 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); - } + 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 @@ -1156,13 +1281,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 @@ -1181,107 +1307,115 @@ Returns: nothing =cut sub populate_field_definitions { - 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); - } + 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); } + } - # 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)) { - print "SQL fragment found in the 'fielddefs' table...\n"; - print "Old field name: " . $old_field_name . "\n"; - # 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. - print "Fixing the 'fielddefs' table...\n"; - print "New field name: " . $new_field_name . "\n"; - $dbh->do('UPDATE fielddefs SET name = ? WHERE id = ?', - undef, ($new_field_name, $old_field_id)); + # 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)) { + print "SQL fragment found in the 'fielddefs' table...\n"; + print "Old field name: " . $old_field_name . "\n"; + + # 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); } - # 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 }); + # 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. + print "Fixing the 'fielddefs' table...\n"; + print "New field name: " . $new_field_name . "\n"; + $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 @@ -1313,32 +1447,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 @@ -1360,15 +1494,17 @@ Returns: the corresponding field ID or an error if the field name =cut sub get_field_id { - my ($name) = @_; - my $dbh = Bugzilla->dbh; + my ($name) = @_; + my $dbh = Bugzilla->dbh; - trick_taint($name); - my $id = $dbh->selectrow_array('SELECT id FROM fielddefs - WHERE name = ?', undef, $name); + trick_taint($name); + my $id = $dbh->selectrow_array( + 'SELECT id FROM fielddefs + WHERE name = ?', undef, $name + ); - ThrowCodeError('invalid_field_name', {field => $name}) unless $id; - return $id + ThrowCodeError('invalid_field_name', {field => $name}) unless $id; + return $id; } 1; diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm index 10f8f38e6..eab2c20f3 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,32 +76,32 @@ 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 ($class->CLASS_MAP->{$field_name}) { - return $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 <check($field); + my $field_name = $field_obj->name; + + if ($class->CLASS_MAP->{$field_name}) { + return $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 < '$field_name'; EOC - } + } - return $package; + return $package; } ################ @@ -112,11 +112,11 @@ EOC # 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(@_); } ######################### @@ -128,64 +128,66 @@ 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) { + $dbh->do("UPDATE bug_$fname SET value = ? WHERE value = ?", undef, $new, $old); + } + else { + $dbh->do("UPDATE bugs SET $fname = ? WHERE $fname = ?", undef, $new, $old); } - $dbh->bz_commit_transaction(); - return wantarray ? ($changes, $old_self) : $changes; + 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; } 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(); } ############ @@ -193,12 +195,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}; } ############## @@ -206,73 +209,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) - || 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) + || 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 bcfd75578..207ac9cb5 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,149 @@ 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) { + $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 625794974..3ed055b3d 100644 --- a/Bugzilla/Flag.pm +++ b/Bugzilla/Flag.pm @@ -58,8 +58,9 @@ use base 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; @@ -70,35 +71,32 @@ use constant SKIP_REQUESTEE_ON_ERROR => 1; our $disable_flagmail = 0; 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 ###### @@ -140,15 +138,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'}; } ############################### @@ -182,44 +180,43 @@ is an attachment flag, else undefined. =cut sub type { - my $self = shift; + my $self = shift; - return $self->{'type'} - ||= new Bugzilla::FlagType($self->{'type_id'}, cache => 1 ); + return $self->{'type'} + ||= new Bugzilla::FlagType($self->{'type_id'}, cache => 1); } 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}); } ################################ @@ -241,26 +238,27 @@ 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; } + } + + # 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; + } - return $class->SUPER::match(@_); + return $class->SUPER::match(@_); } =pod @@ -278,8 +276,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(@_)}; } ###################################################################### @@ -287,145 +285,157 @@ 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); + 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); } - # 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); - } - 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->reporter->id != $obj_flag->requestee->id) - { - $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) + { + $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 + ); + + 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 @@ -441,153 +451,163 @@ 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 }); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + my $timestamp = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + + my $changes = $self->SUPER::update(@_); - # BMO - provide a hook which passes the flag object - Bugzilla::Hook::process('flag_updated', {flag => $self, changes => $changes, timestamp => $timestamp}); + 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; + # BMO - provide a hook which passes the flag object + Bugzilla::Hook::process('flag_updated', + {flag => $self, changes => $changes, timestamp => $timestamp}); + + 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 @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; - $new_flag->{creation_date} = format_time($timestamp, '%Y.%m.%d %H:%i:%s'); - $new_flag->{modification_date} = format_time($timestamp, '%Y.%m.%d %H:%i:%s'); - $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}; - } + my ($class, $self, $old_self, $timestamp) = @_; + + 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; + $new_flag->{creation_date} = format_time($timestamp, '%Y.%m.%d %H:%i:%s'); + $new_flag->{modification_date} = format_time($timestamp, '%Y.%m.%d %H:%i:%s'); + $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); - - # BMO - provide a hook which passes the timestamp, - # because that isn't passed to remove_from_db(). - Bugzilla::Hook::process('flag_deleted', {flag => $old_flag, timestamp => $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); + + # BMO - provide a hook which passes the timestamp, + # because that isn't passed to remove_from_db(). + Bugzilla::Hook::process('flag_deleted', + {flag => $old_flag, timestamp => $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 @@ -595,53 +615,56 @@ 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); - - # BMO - provide a hook which passes the timestamp, - # because that isn't passed to remove_from_db(). - my ($timestamp) = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - Bugzilla::Hook::process('flag_deleted', {flag => $flag, timestamp => $timestamp}); - $flag->remove_from_db(); - } + 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); + + # BMO - provide a hook which passes the timestamp, + # because that isn't passed to remove_from_db(). + my ($timestamp) = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + Bugzilla::Hook::process('flag_deleted', + {flag => $flag, timestamp => $timestamp}); + $flag->remove_from_db(); } - return @removed; + } + return @removed; } ############################### @@ -649,153 +672,168 @@ 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 him or leave him alone. + ThrowCodeError('flag_type_requestee_disabled', {type => $self->type}) + if !$self->type->is_requesteeble; + + # BMO customisation: + # 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 him or leave him alone. - ThrowCodeError('flag_type_requestee_disabled', { type => $self->type }) - if !$self->type->is_requesteeble; - - # BMO customisation: - # 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) - || ThrowCodeError('invalid_user'); - - # set_status() has already been called. So this refers - # to the new flag status. - my $status = $self->status; - - ThrowUserError('flag_update_denied', - { name => $self->type->name, - status => $status, - old_status => $self->{_old_status} }) - unless $setter->can_change_flag($self->type, $self->{_old_status} || 'X', $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; + my ($self, $setter) = @_; + + # By default, the currently logged in user is the setter. + $setter ||= Bugzilla->user; + (blessed($setter) && $setter->isa('Bugzilla::User') && $setter->id) + || ThrowCodeError('invalid_user'); + + # set_status() has already been called. So this refers + # to the new flag status. + my $status = $self->status; + + ThrowUserError( + 'flag_update_denied', + { + name => $self->type->name, + status => $status, + old_status => $self->{_old_status} } - return $setter; + ) + unless $setter->can_change_flag($self->type, $self->{_old_status} || 'X', + $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; } ###################################################################### @@ -816,128 +854,146 @@ array of hashes. This array is then passed to Flag::create(). =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, $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'; + } + + # 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' => $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, 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); + + # 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}); } + } - # 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 @@ -954,118 +1010,126 @@ or deleted. =cut sub notify { - my ($class, $flag, $old_flag, $obj, $timestamp) = @_; - - if ($disable_flagmail) { - return; - } - - my ($bug, $attachment); - if (blessed($obj) && $obj->isa('Bugzilla::Attachment')) { - $attachment = $obj; - $bug = $attachment->bug; + my ($class, $flag, $old_flag, $obj, $timestamp) = @_; + + if ($disable_flagmail) { + return; + } + + 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; } - 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; - } - } - 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; - $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, cache => 1 }); - 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("request/email.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; + $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, cache => 1}); + 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("request/email.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); + } } # This is an internal function used by $bug->flag_types @@ -1073,39 +1137,42 @@ 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}; + $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; } 1; diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm index c973ea662..06159be5d 100644 --- a/Bugzilla/FlagType.pm +++ b/Bugzilla/FlagType.pm @@ -47,115 +47,116 @@ use base qw(Bugzilla::Object); #### Initialization #### ############################### -use constant DB_TABLE => 'flagtypes'; +use constant DB_TABLE => 'flagtypes'; use constant LIST_ORDER => 'sortkey, name'; 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 ); 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 ); 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, }; -use constant UPDATE_VALIDATORS => { - grant_group_id => \&_check_group, - request_group_id => \&_check_group, -}; +use constant UPDATE_VALIDATORS => + {grant_group_id => \&_check_group, request_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(@_); - $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); + # In the DB, only the first character of the target type is stored. + $params->{target_type} = substr($params->{target_type}, 0, 1); - # 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}; + # 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}; - my $flagtype = $class->insert_create_data($params); + my $flagtype = $class->insert_create_data($params); - $flagtype->set_clusions({ inclusions => $inclusions, - exclusions => $exclusions }); - $flagtype->update(); + $flagtype->set_clusions({inclusions => $inclusions, exclusions => $exclusions}); + $flagtype->update(); - Bugzilla::Hook::process('flagtype_end_of_create', { type => $flagtype }); + Bugzilla::Hook::process('flagtype_end_of_create', {type => $flagtype}); - $dbh->bz_commit_transaction(); - return $flagtype; + $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); + $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 (?, ?, ?)"); + 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]; + 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 +167,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); - $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,29 +184,32 @@ 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); + + # 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}); + } } + } - Bugzilla::Hook::process('flagtype_end_of_update', - { type => $self, changed => $changes }); + Bugzilla::Hook::process('flagtype_end_of_update', + {type => $self, changed => $changes}); - $dbh->bz_commit_transaction(); - return $changes; + $dbh->bz_commit_transaction(); + return $changes; } ############################### @@ -262,162 +268,164 @@ Returns the sortkey of the flagtype. =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'}; } ################################ # 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); - # We do not call Util::validate_email_syntax because these - # addresses do not require to match 'emailregexp' and do not - # depend on 'emailsuffix'. So we limit ourselves to a simple - # sanity check: - # - match the syntax of a fully qualified email address; - # - do not contain any illegal character. - foreach my $address (@addresses) { - ($address =~ /^[\w\.\+\-=]+@[\w\.\-]+\.[\w\-]+$/ - && $address !~ /[\\\(\)<>&,;:"\[\] \t\r\n\P{ASCII}]/) - || 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); + + # We do not call Util::validate_email_syntax because these + # addresses do not require to match 'emailregexp' and do not + # depend on 'emailsuffix'. So we limit ourselves to a simple + # sanity check: + # - match the syntax of a fully qualified email address; + # - do not contain any illegal character. + foreach my $address (@addresses) { + ( $address =~ /^[\w\.\+\-=]+@[\w\.\-]+\.[\w\-]+$/ + && $address !~ /[\\\(\)<>&,;:"\[\] \t\r\n\P{ASCII}]/) + || 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; } ############################### #### 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_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); - $user->in_group('editcomponents', $prod_id) - || ThrowUserError('product_access_denied', $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); + $user->in_group('editcomponents', $prod_id) + || ThrowUserError('product_access_denied', $params); } - $self->{$category} = \%clusions; - $self->{"${category}_as_hash"} = \%clusions_as_hash; - $self->{"_update_$category"} = 1; + $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; } + $self->{$category} = \%clusions; + $self->{"${category}_as_hash"} = \%clusions_as_hash; + $self->{"_update_$category"} = 1; + } } =pod @@ -458,77 +466,79 @@ 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}, cache => 1 }); - 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}, cache => 1}); + push(@custusers, $user) if $user_obj->can_set_flag($self); + } + return \@custusers; } 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}; } ###################################################################### @@ -552,11 +562,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 @@ -565,19 +575,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 @@ -594,18 +604,19 @@ 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); - $tables = join(' ', @$tables); - $criteria = join(' AND ', @criteria); + # 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 $flagtype_ids = $dbh->selectcol_arrayref("SELECT flagtypes.id FROM $tables WHERE $criteria"); + my $flagtype_ids = $dbh->selectcol_arrayref( + "SELECT flagtypes.id FROM $tables WHERE $criteria"); - return Bugzilla::FlagType->new_from_list($flagtype_ids); + return Bugzilla::FlagType->new_from_list($flagtype_ids); } =pod @@ -621,18 +632,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; } ###################################################################### @@ -659,83 +672,91 @@ by the query. =cut 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}) { - 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}) { + my $name = $dbh->quote($criteria->{name}); + trick_taint($name); # Detaint data as we have quoted it. + push(@criteria, "flagtypes.name = $name"); + } + 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 (exists($criteria->{active_or_has_flags}) + && $criteria->{active_or_has_flags} =~ /^\d+$/) + { + push(@$tables, + "LEFT JOIN flags AS f ON flagtypes.id = f.type_id " + . "AND f.bug_id = " + . $criteria->{active_or_has_flags}); + push(@criteria, "(flagtypes.is_active = 1 OR f.id IS NOT NULL)"); + } + if ($criteria->{product_id}) { + my $product_id = $criteria->{product_id}; + detaint_natural($product_id) + || 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->{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 { + $addl_join_clause + = "AND e.component_id IS NULL OR (i.component_id = e.component_id) "; } - if (exists($criteria->{is_active})) { - my $is_active = $criteria->{is_active} ? "1" : "0"; - push(@criteria, "flagtypes.is_active = $is_active"); - } - if (exists($criteria->{active_or_has_flags}) && $criteria->{active_or_has_flags} =~ /^\d+$/) { - push(@$tables, "LEFT JOIN flags AS f ON flagtypes.id = f.type_id " . - "AND f.bug_id = " . $criteria->{active_or_has_flags}); - push(@criteria, "(flagtypes.is_active = 1 OR f.id IS NOT NULL)"); - } - if ($criteria->{product_id}) { - my $product_id = $criteria->{product_id}; - detaint_natural($product_id) - || 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)"); - } - - 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.request_group_id = $gid)"); + } + + return @criteria; } 1; diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm index 7f684ea15..df8bc5899 100644 --- a/Bugzilla/Group.pm +++ b/Bugzilla/Group.pm @@ -27,22 +27,22 @@ use Scalar::Util qw(blessed); use constant IS_CONFIG => 1; sub DB_COLUMNS { - my $class = shift; - my @columns = qw( - id - name - description - isbuggroup - userregexp - isactive - icon_url - owner_user_id - idle_member_removal - ); - my $dbh = Bugzilla->dbh; - my $table = $class->DB_TABLE; + my $class = shift; + my @columns = qw( + id + name + description + isbuggroup + userregexp + isactive + icon_url + owner_user_id + idle_member_removal + ); + my $dbh = Bugzilla->dbh; + my $table = $class->DB_TABLE; - return map { "$table.$_" } grep { $dbh->bz_column_info($table, $_) } @columns; + return map {"$table.$_"} grep { $dbh->bz_column_info($table, $_) } @columns; } use constant DB_TABLE => 'groups'; @@ -50,174 +50,178 @@ 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, - owner_user_id => \&_check_owner, - idle_member_removal => \&_check_idle_member_removal + name => \&_check_name, + description => \&_check_description, + userregexp => \&_check_user_regexp, + isactive => \&_check_is_active, + isbuggroup => \&_check_is_bug_group, + icon_url => \&_check_icon_url, + owner_user_id => \&_check_owner, + idle_member_removal => \&_check_idle_member_removal }; use constant UPDATE_COLUMNS => qw( - name - description - userregexp - isactive - icon_url - owner_user_id - idle_member_removal + name + description + userregexp + isactive + icon_url + owner_user_id + idle_member_removal ); # Parameters that are lists of groups. use constant GROUP_PARAMS => qw(chartgroup insidergroup timetrackinggroup - querysharegroup); + querysharegroup); sub DYNAMIC_COLUMNS { - return Bugzilla->usage_mode == USAGE_MODE_CMDLINE; + return Bugzilla->usage_mode == USAGE_MODE_CMDLINE; } ############################### #### 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'}; } sub idle_member_removal { return $_[0]->{'idle_member_removal'}; } 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}; + 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}; } 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}; } # returns all possible members of groups, keyed by the group name or _direct # a user present in multiple groups will be returned multiple times sub members_complete { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - require Bugzilla::User; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + require Bugzilla::User; - my $sth = $dbh->prepare( - "SELECT DISTINCT user_id FROM user_group_map WHERE isbless = 0 AND group_id = ?" + my $sth + = $dbh->prepare( + "SELECT DISTINCT user_id FROM user_group_map WHERE isbless = 0 AND group_id = ?" ); - my $result = { _direct => $self->members_direct() }; - foreach my $group_id (@{ $self->flatten_group_membership($self->id) }) { - next if $group_id == $self->id; - my $group_name = Bugzilla::Group->new({ id => $group_id, cache => 1 })->name; - my $user_ids = $dbh->selectcol_arrayref($sth, undef, $group_id); - $result->{$group_name} = Bugzilla::User->new_from_list($user_ids); - } - return $result; + my $result = {_direct => $self->members_direct()}; + foreach my $group_id (@{$self->flatten_group_membership($self->id)}) { + next if $group_id == $self->id; + my $group_name = Bugzilla::Group->new({id => $group_id, cache => 1})->name; + my $user_ids = $dbh->selectcol_arrayref($sth, undef, $group_id); + $result->{$group_name} = Bugzilla::User->new_from_list($user_ids); + } + return $result; } # A helper for members_direct and members_non_inherited sub _get_members { - my ($self, $grant_type) = @_; - my $dbh = Bugzilla->dbh; - my $grant_clause = $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 = $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, $params) = @_; - $params ||= {}; - require Bugzilla::FlagType; - $self->{flag_types} ||= Bugzilla::FlagType::match({ group => $self->id, %$params }); - return $self->{flag_types}; + my ($self, $params) = @_; + $params ||= {}; + require Bugzilla::FlagType; + $self->{flag_types} + ||= Bugzilla::FlagType::match({group => $self->id, %$params}); + 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}; } sub owner { - my $self = shift; - return $self->{owner} if exists $self->{owner}; - if ($self->{owner_user_id}) { - $self->{owner} = Bugzilla::User->check({ id => $self->{owner_user_id}, cache => 1 }); - } - return $self->{owner} || undef; + my $self = shift; + return $self->{owner} if exists $self->{owner}; + if ($self->{owner_user_id}) { + $self->{owner} + = Bugzilla::User->check({id => $self->{owner_user_id}, cache => 1}); + } + return $self->{owner} || undef; } ############################### @@ -225,134 +229,136 @@ sub owner { ############################### 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 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 set_idle_member_removal { $_[0]->set('idle_member_removal', $_[1]); } sub set_owner { - my ($self, $owner_id) = @_; - $self->set('owner_user_id', $owner_id); - # Reset the default owner object. - delete $self->{owner}; + my ($self, $owner_id) = @_; + $self->set('owner_user_id', $owner_id); + + # Reset the default owner object. + delete $self->{owner}; } 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; + 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; + } - # If we've changed this group to be active, fix any Mandatory groups. - $self->_enforce_mandatory if (exists $changes->{isactive} - && $changes->{isactive}->[1]); + # 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}; + $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; + Bugzilla::Hook::process('group_end_of_update', + {group => $self, changes => $changes}); + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + return $changes; } sub check_remove { - my ($self, $params) = @_; - - # System groups cannot be deleted! - if (!$self->is_bug_group) { - ThrowUserError("system_group_not_deletable", { name => $self->name }); + 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}); + } - # 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 }); - } + return if $params->{'test_only'}; - return if $params->{'test_only'}; + my $cantdelete = 0; - my $cantdelete = 0; + my $users = $self->members_non_inherited; + if (scalar(@$users) && !$params->{'remove_from_users'}) { + $cantdelete = 1; + } - my $users = $self->members_non_inherited; - if (scalar(@$users) && !$params->{'remove_from_users'}) { - $cantdelete = 1; - } + my $bugs = $self->bugs; + if (scalar(@$bugs) && !$params->{'remove_from_bugs'}) { + $cantdelete = 1; + } - 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; + } - my $products = $self->products; - if (scalar(@$products) && !$params->{'remove_from_products'}) { - $cantdelete = 1; - } + my $flag_types = $self->flag_types; + if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) { + $cantdelete = 1; + } - my $flag_types = $self->flag_types; - if (scalar(@$flag_types) && !$params->{'remove_from_flags'}) { - $cantdelete = 1; - } - - ThrowUserError('group_cannot_delete', { group => $self }) if $cantdelete; + ThrowUserError('group_cannot_delete', {group => $self}) 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 + ); + + # 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 @@ -361,148 +367,161 @@ 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}; - 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}; + 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 + my $group = $class->SUPER::create(@_); + + # 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); - } + 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); + } - $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(); - Bugzilla->memcached->clear_config(); - return $group; + Bugzilla::Hook::process('group_end_of_create', {group => $group}); + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + 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; } ############################### @@ -510,53 +529,57 @@ 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; } sub _check_owner { - my ($invocant, $owner, undef, $params) = @_; - return Bugzilla::User->check({ name => $owner, cache => 1 })->id if $owner; - # We require an owner if the group is a not a system group - if (blessed($invocant) && !$invocant->is_bug_group) { - return undef; - } - ThrowUserError('group_needs_owner'); + my ($invocant, $owner, undef, $params) = @_; + return Bugzilla::User->check({name => $owner, cache => 1})->id if $owner; + + # We require an owner if the group is a not a system group + if (blessed($invocant) && !$invocant->is_bug_group) { + return undef; + } + ThrowUserError('group_needs_owner'); } sub _check_idle_member_removal { - my ($invocant, $value) = @_; - detaint_natural($value) - || ThrowUserError('invalid_parameter', { name => 'idle member removal', err => 'must be numeric' }); - return $value <= 0 ? 0 : $value ; + my ($invocant, $value) = @_; + detaint_natural($value) + || ThrowUserError('invalid_parameter', + {name => 'idle member removal', err => 'must be numeric'}); + return $value <= 0 ? 0 : $value; } 1; diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm index bed6a53b0..18335b907 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; diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm index 8bce9b5e7..705a8396c 100644 --- a/Bugzilla/Install.pm +++ b/Bugzilla/Install.pm @@ -31,509 +31,512 @@ use Bugzilla::Util qw(get_text); use Bugzilla::Version; use constant STATUS_WORKFLOW => ( - [undef, 'UNCONFIRMED'], - [undef, 'CONFIRMED'], - [undef, 'IN_PROGRESS'], - ['UNCONFIRMED', 'CONFIRMED'], - ['UNCONFIRMED', 'IN_PROGRESS'], - ['UNCONFIRMED', 'RESOLVED'], - ['CONFIRMED', 'IN_PROGRESS'], - ['CONFIRMED', 'RESOLVED'], - ['IN_PROGRESS', 'CONFIRMED'], - ['IN_PROGRESS', 'RESOLVED'], - ['RESOLVED', 'UNCONFIRMED'], - ['RESOLVED', 'CONFIRMED'], - ['RESOLVED', 'VERIFIED'], - ['VERIFIED', 'UNCONFIRMED'], - ['VERIFIED', 'CONFIRMED'], + [undef, 'UNCONFIRMED'], + [undef, 'CONFIRMED'], + [undef, 'IN_PROGRESS'], + ['UNCONFIRMED', 'CONFIRMED'], + ['UNCONFIRMED', 'IN_PROGRESS'], + ['UNCONFIRMED', 'RESOLVED'], + ['CONFIRMED', 'IN_PROGRESS'], + ['CONFIRMED', 'RESOLVED'], + ['IN_PROGRESS', 'CONFIRMED'], + ['IN_PROGRESS', 'RESOLVED'], + ['RESOLVED', 'UNCONFIRMED'], + ['RESOLVED', 'CONFIRMED'], + ['RESOLVED', 'VERIFIED'], + ['VERIFIED', 'UNCONFIRMED'], + ['VERIFIED', 'CONFIRMED'], ); sub SETTINGS { - return [ + return [ # 2005-03-03 travis@sedsystems.ca -- Bug 41972 { - name => 'display_quips', - options => ["on", "off"], - default => "on", - category => 'Searching' + name => 'display_quips', + options => ["on", "off"], + default => "on", + category => 'Searching' }, + # 2005-03-10 travis@sedsystems.ca -- Bug 199048 { - name => 'comment_sort_order', - options => ["oldest_to_newest", "newest_to_oldest", "newest_to_oldest_desc_first"], - default => "oldest_to_newest", - category => 'Bug Editing' + name => 'comment_sort_order', + options => + ["oldest_to_newest", "newest_to_oldest", "newest_to_oldest_desc_first"], + default => "oldest_to_newest", + category => 'Bug Editing' }, + # 2005-05-12 bugzilla@glob.com.au -- Bug 63536 { - name => 'post_bug_submit_action', - options => ["next_bug", "same_bug", "nothing"], - default => "next_bug", - category => 'Bug Editing' + name => 'post_bug_submit_action', + options => ["next_bug", "same_bug", "nothing"], + default => "next_bug", + category => 'Bug Editing' }, + # 2005-06-29 wurblzap@gmail.com -- Bug 257767 { - name => 'csv_colsepchar', - options => [',',';'], - default => ',', - category => 'Searching' + name => 'csv_colsepchar', + options => [',', ';'], + default => ',', + category => 'Searching' }, + # 2005-10-26 wurblzap@gmail.com -- Bug 291459 { - name => 'zoom_textareas', - options => ["on", "off"], - default => "on", - category => 'Bug Editing' + name => 'zoom_textareas', + options => ["on", "off"], + default => "on", + category => 'Bug Editing' }, + # 2005-10-21 LpSolit@gmail.com -- Bug 313020 { - name => 'per_bug_queries', - options => ['on', 'off'], - default => 'off', - category => 'Searching' + name => 'per_bug_queries', + options => ['on', 'off'], + default => 'off', + category => 'Searching' }, + # 2006-05-01 olav@bkor.dhs.org -- Bug 7710 { - name => 'state_addselfcc', - options => ['always', 'never', 'cc_unless_role'], - default => 'cc_unless_role', - category => 'Bug Editing' + name => 'state_addselfcc', + options => ['always', 'never', 'cc_unless_role'], + default => 'cc_unless_role', + category => 'Bug Editing' }, + # 2006-08-04 wurblzap@gmail.com -- Bug 322693 { - name => 'skin', - subclass => 'Skin', - default => 'Dusk', - category => 'User Interface' + name => 'skin', + subclass => 'Skin', + default => 'Dusk', + category => 'User Interface' }, + # 2006-12-10 LpSolit@gmail.com -- Bug 297186 { - name => 'lang', - subclass => 'Lang', - default => ${Bugzilla->languages}[0], - category => 'User Interface' + name => 'lang', + subclass => 'Lang', + default => ${Bugzilla->languages}[0], + category => 'User Interface' }, + # 2007-07-02 altlist@gmail.com -- Bug 225731 { - name => 'quote_replies', - options => ['quoted_reply', 'simple_reply', 'off'], - default => "quoted_reply", - category => 'Bug Editing' + name => 'quote_replies', + options => ['quoted_reply', 'simple_reply', 'off'], + default => "quoted_reply", + category => 'Bug Editing' }, + # 2009-02-01 mozilla@matt.mchenryfamily.org -- Bug 398473 { - name => 'comment_box_position', - options => ['before_comments', 'after_comments'], - default => 'before_comments', - category => 'Bug Editing' + name => 'comment_box_position', + options => ['before_comments', 'after_comments'], + default => 'before_comments', + category => 'Bug Editing' }, + # 2008-08-27 LpSolit@gmail.com -- Bug 182238 { - name => 'timezone', - subclass => 'Timezone', - default => 'local', - category => 'User Interface' + name => 'timezone', + subclass => 'Timezone', + default => 'local', + category => 'User Interface' }, + # 2011-02-07 dkl@mozilla.com -- Bug 580490 { - name => 'quicksearch_fulltext', - options => ['on', 'off'], - default => 'on', - category => 'Searching' + name => 'quicksearch_fulltext', + options => ['on', 'off'], + default => 'on', + category => 'Searching' }, + # 2011-06-21 glob@mozilla.com -- Bug 589128 { - name => 'email_format', - options => ['html', 'text_only'], - default => 'html', - category => 'Email Notifications' + name => 'email_format', + options => ['html', 'text_only'], + default => 'html', + category => 'Email Notifications' }, + # 2011-06-16 glob@mozilla.com -- Bug 663747 { - name => 'bugmail_new_prefix', - options => ['on', 'off'], - default => 'on', - category => 'Email Notifications' + name => 'bugmail_new_prefix', + options => ['on', 'off'], + default => 'on', + category => 'Email Notifications' }, + # 2013-07-26 joshi_sunil@in.com -- Bug 669535 { - name => 'possible_duplicates', - options => ['on', 'off'], - default => 'on', - category => 'User Interface' + name => 'possible_duplicates', + options => ['on', 'off'], + default => 'on', + category => 'User Interface' }, + # 2011-10-11 glob@mozilla.com -- Bug 301656 { - name => 'requestee_cc', - options => ['on', 'off'], - default => 'on', - category => 'Reviews and Needinfo' + name => 'requestee_cc', + options => ['on', 'off'], + default => 'on', + category => 'Reviews and Needinfo' }, { - name => 'api_key_only', - options => ['on', 'off'], - default => 'off', - category => 'API' + name => 'api_key_only', + options => ['on', 'off'], + default => 'off', + category => 'API' }, { - name => 'use_elasticsearch', - options => ['on', 'off'], - default => 'off', - category => 'Searching' + name => 'use_elasticsearch', + options => ['on', 'off'], + default => 'off', + category => 'Searching' }, { - name => 'autosize_comments', - options => ['on', 'off'], - default => 'on', - category => 'User Interface' + name => 'autosize_comments', + options => ['on', 'off'], + default => 'on', + category => 'User Interface' }, - ]; -}; + ]; +} use constant SYSTEM_GROUPS => ( - { - name => 'admin', - description => 'Administrators' - }, - { - name => 'tweakparams', - description => 'Can change Parameters' - }, - { - name => 'editusers', - description => 'Can edit or disable users' - }, - { - name => 'disableusers', - description => 'Can disable users' - }, - { - name => 'creategroups', - description => 'Can create and destroy groups' - }, - { - name => 'editclassifications', - description => 'Can create, destroy, and edit classifications' - }, - { - name => 'editcomponents', - description => 'Can create, destroy, and edit components' - }, - { - name => 'editkeywords', - description => 'Can create, destroy, and edit keywords' - }, - { - name => 'editbugs', - description => 'Can edit all bug fields', - userregexp => '.*' - }, - { - name => 'canconfirm', - description => 'Can confirm a bug or mark it a duplicate' - }, - { - name => 'bz_canusewhineatothers', - description => 'Can configure whine reports for other users', - }, - { - name => 'bz_canusewhines', - description => 'User can configure whine reports for self', - # inherited_by means that users in the groups listed below are - # automatically members of bz_canusewhines. - inherited_by => ['editbugs', 'bz_canusewhineatothers'], - }, - { - name => 'bz_sudoers', - description => 'Can perform actions as other users', - }, - { - name => 'bz_sudo_protect', - description => 'Can not be impersonated by other users', - inherited_by => ['bz_sudoers'], - }, - { - name => 'bz_quip_moderators', - description => 'Can moderate quips', - }, - { - name => 'bz_can_disable_mfa', - description => 'Can disable MFA when editing users', - }, + {name => 'admin', description => 'Administrators'}, + {name => 'tweakparams', description => 'Can change Parameters'}, + {name => 'editusers', description => 'Can edit or disable users'}, + {name => 'disableusers', description => 'Can disable users'}, + {name => 'creategroups', description => 'Can create and destroy groups'}, + { + name => 'editclassifications', + description => 'Can create, destroy, and edit classifications' + }, + { + name => 'editcomponents', + description => 'Can create, destroy, and edit components' + }, + { + name => 'editkeywords', + description => 'Can create, destroy, and edit keywords' + }, + { + name => 'editbugs', + description => 'Can edit all bug fields', + userregexp => '.*' + }, + { + name => 'canconfirm', + description => 'Can confirm a bug or mark it a duplicate' + }, + { + name => 'bz_canusewhineatothers', + description => 'Can configure whine reports for other users', + }, + { + name => 'bz_canusewhines', + description => 'User can configure whine reports for self', + + # inherited_by means that users in the groups listed below are + # automatically members of bz_canusewhines. + inherited_by => ['editbugs', 'bz_canusewhineatothers'], + }, + {name => 'bz_sudoers', description => 'Can perform actions as other users',}, + { + name => 'bz_sudo_protect', + description => 'Can not be impersonated by other users', + inherited_by => ['bz_sudoers'], + }, + {name => 'bz_quip_moderators', description => 'Can moderate quips',}, + { + name => 'bz_can_disable_mfa', + description => 'Can disable MFA when editing users', + }, ); -use constant DEFAULT_CLASSIFICATION => { - name => 'Unclassified', - description => 'Not assigned to any classification' -}; +use constant DEFAULT_CLASSIFICATION => + {name => 'Unclassified', description => 'Not assigned to any classification'}; use constant DEFAULT_PRODUCT => { - name => 'TestProduct', - description => 'This is a test product.' - . ' This ought to be blown away and replaced with real stuff in a' - . ' finished installation of bugzilla.', - version => Bugzilla::Version::DEFAULT_VERSION, - classification => 'Unclassified', - defaultmilestone => DEFAULT_MILESTONE, + name => 'TestProduct', + description => 'This is a test product.' + . ' This ought to be blown away and replaced with real stuff in a' + . ' finished installation of bugzilla.', + version => Bugzilla::Version::DEFAULT_VERSION, + classification => 'Unclassified', + defaultmilestone => DEFAULT_MILESTONE, }; use constant DEFAULT_COMPONENT => { - name => 'TestComponent', - description => 'This is a test component in the test product database.' - . ' This ought to be blown away and replaced with real stuff in' - . ' a finished installation of Bugzilla.' + name => 'TestComponent', + description => 'This is a test component in the test product database.' + . ' This ought to be blown away and replaced with real stuff in' + . ' a finished installation of Bugzilla.' }; sub update_settings { - my $dbh = Bugzilla->dbh; - # If we're setting up settings for the first time, we want to be quieter. - my $any_settings = $dbh->selectrow_array( - 'SELECT 1 FROM setting ' . $dbh->sql_limit(1)); - if (!$any_settings) { - print get_text('install_setting_setup'), "\n"; - } - - my @settings = @{SETTINGS()}; - foreach my $params (@settings) { - add_setting($params); - } + my $dbh = Bugzilla->dbh; + + # If we're setting up settings for the first time, we want to be quieter. + my $any_settings + = $dbh->selectrow_array('SELECT 1 FROM setting ' . $dbh->sql_limit(1)); + if (!$any_settings) { + print get_text('install_setting_setup'), "\n"; + } + + my @settings = @{SETTINGS()}; + foreach my $params (@settings) { + add_setting($params); + } } sub update_system_groups { - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - # If there is no editbugs group, this is the first time we're - # adding groups. - my $editbugs_exists = new Bugzilla::Group({ name => 'editbugs' }); - if (!$editbugs_exists) { - print get_text('install_groups_setup'), "\n"; - } - - # Create most of the system groups - foreach my $definition (SYSTEM_GROUPS) { - my $exists = new Bugzilla::Group({ name => $definition->{name} }); - if (!$exists) { - $definition->{isbuggroup} = 0; - $definition->{silently} = !$editbugs_exists; - my $inherited_by = delete $definition->{inherited_by}; - my $created = Bugzilla::Group->create($definition); - # Each group in inherited_by is automatically a member of this - # group. - if ($inherited_by) { - foreach my $name (@$inherited_by) { - my $member = Bugzilla::Group->check($name); - $dbh->do('INSERT INTO group_group_map (grantor_id, - member_id) VALUES (?,?)', - undef, $created->id, $member->id); - } - } + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + # If there is no editbugs group, this is the first time we're + # adding groups. + my $editbugs_exists = new Bugzilla::Group({name => 'editbugs'}); + if (!$editbugs_exists) { + print get_text('install_groups_setup'), "\n"; + } + + # Create most of the system groups + foreach my $definition (SYSTEM_GROUPS) { + my $exists = new Bugzilla::Group({name => $definition->{name}}); + if (!$exists) { + $definition->{isbuggroup} = 0; + $definition->{silently} = !$editbugs_exists; + my $inherited_by = delete $definition->{inherited_by}; + my $created = Bugzilla::Group->create($definition); + + # Each group in inherited_by is automatically a member of this + # group. + if ($inherited_by) { + foreach my $name (@$inherited_by) { + my $member = Bugzilla::Group->check($name); + $dbh->do( + 'INSERT INTO group_group_map (grantor_id, + member_id) VALUES (?,?)', undef, $created->id, + $member->id + ); } + } } + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub create_default_classification { - my $dbh = Bugzilla->dbh; - - # Make the default Classification if it doesn't already exist. - if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) { - print get_text('install_default_classification', - { name => DEFAULT_CLASSIFICATION->{name} }) . "\n"; - Bugzilla::Classification->create(DEFAULT_CLASSIFICATION); - } + my $dbh = Bugzilla->dbh; + + # Make the default Classification if it doesn't already exist. + if (!$dbh->selectrow_array('SELECT 1 FROM classifications')) { + print get_text('install_default_classification', + {name => DEFAULT_CLASSIFICATION->{name}}) + . "\n"; + Bugzilla::Classification->create(DEFAULT_CLASSIFICATION); + } } # This function should be called only after creating the admin user. sub create_default_product { - my $dbh = Bugzilla->dbh; - - # And same for the default product/component. - if (!$dbh->selectrow_array('SELECT 1 FROM products')) { - print get_text('install_default_product', - { name => DEFAULT_PRODUCT->{name} }) . "\n"; - - my $product = Bugzilla::Product->create(DEFAULT_PRODUCT); - - # Get the user who will be the owner of the Component. - # We pick the admin with the lowest id, which is probably the - # admin checksetup.pl just created. - my $admin_group = new Bugzilla::Group({name => 'admin'}); - my ($admin_id) = $dbh->selectrow_array( - 'SELECT user_id FROM user_group_map WHERE group_id = ? - ORDER BY user_id ' . $dbh->sql_limit(1), - undef, $admin_group->id); - my $admin = Bugzilla::User->new($admin_id); - - Bugzilla::Component->create({ - %{ DEFAULT_COMPONENT() }, product => $product, - initialowner => $admin->login }); - } + my $dbh = Bugzilla->dbh; + + # And same for the default product/component. + if (!$dbh->selectrow_array('SELECT 1 FROM products')) { + print get_text('install_default_product', {name => DEFAULT_PRODUCT->{name}}) + . "\n"; + + my $product = Bugzilla::Product->create(DEFAULT_PRODUCT); + + # Get the user who will be the owner of the Component. + # We pick the admin with the lowest id, which is probably the + # admin checksetup.pl just created. + my $admin_group = new Bugzilla::Group({name => 'admin'}); + my ($admin_id) = $dbh->selectrow_array( + 'SELECT user_id FROM user_group_map WHERE group_id = ? + ORDER BY user_id ' . $dbh->sql_limit(1), undef, $admin_group->id + ); + my $admin = Bugzilla::User->new($admin_id); + + Bugzilla::Component->create({ + %{DEFAULT_COMPONENT()}, product => $product, initialowner => $admin->login + }); + } } sub init_workflow { - my $dbh = Bugzilla->dbh; - my $has_workflow = $dbh->selectrow_array('SELECT 1 FROM status_workflow'); - return if $has_workflow; - - print get_text('install_workflow_init'), "\n"; - - my %status_ids = @{ $dbh->selectcol_arrayref( - 'SELECT value, id FROM bug_status', {Columns=>[1,2]}) }; - - foreach my $pair (STATUS_WORKFLOW) { - my $old_id = $pair->[0] ? $status_ids{$pair->[0]} : undef; - my $new_id = $status_ids{$pair->[1]}; - $dbh->do('INSERT INTO status_workflow (old_status, new_status) - VALUES (?,?)', undef, $old_id, $new_id); - } + my $dbh = Bugzilla->dbh; + my $has_workflow = $dbh->selectrow_array('SELECT 1 FROM status_workflow'); + return if $has_workflow; + + print get_text('install_workflow_init'), "\n"; + + my %status_ids = @{ + $dbh->selectcol_arrayref('SELECT value, id FROM bug_status', + {Columns => [1, 2]}) + }; + + foreach my $pair (STATUS_WORKFLOW) { + my $old_id = $pair->[0] ? $status_ids{$pair->[0]} : undef; + my $new_id = $status_ids{$pair->[1]}; + $dbh->do( + 'INSERT INTO status_workflow (old_status, new_status) + VALUES (?,?)', undef, $old_id, $new_id + ); + } } sub create_admin { - my ($params) = @_; - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - - my $admin_group = new Bugzilla::Group({ name => 'admin' }); - my $admin_inheritors = - Bugzilla::Group->flatten_group_membership($admin_group->id); - my $admin_group_ids = join(',', @$admin_inheritors); - - my ($admin_count) = $dbh->selectrow_array( - "SELECT COUNT(*) FROM user_group_map - WHERE group_id IN ($admin_group_ids)"); - - return if $admin_count; - - my %answer = %{Bugzilla->installation_answers}; - my $login = $answer{'ADMIN_EMAIL'}; - my $password = $answer{'ADMIN_PASSWORD'}; - my $full_name = $answer{'ADMIN_REALNAME'}; - - if (!$login || !$password || !$full_name) { - print "\n" . get_text('install_admin_setup') . "\n\n"; - } - - while (!$login) { - print get_text('install_admin_get_email') . ' '; - $login = ; - chomp $login; - eval { Bugzilla::User->check_login_name_for_creation($login); }; - if ($@) { - print $@ . "\n"; - undef $login; - } - } - - while (!defined $full_name) { - print get_text('install_admin_get_name') . ' '; - $full_name = ; - chomp($full_name); + my ($params) = @_; + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + + my $admin_group = new Bugzilla::Group({name => 'admin'}); + my $admin_inheritors + = Bugzilla::Group->flatten_group_membership($admin_group->id); + my $admin_group_ids = join(',', @$admin_inheritors); + + my ($admin_count) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM user_group_map + WHERE group_id IN ($admin_group_ids)" + ); + + return if $admin_count; + + my %answer = %{Bugzilla->installation_answers}; + my $login = $answer{'ADMIN_EMAIL'}; + my $password = $answer{'ADMIN_PASSWORD'}; + my $full_name = $answer{'ADMIN_REALNAME'}; + + if (!$login || !$password || !$full_name) { + print "\n" . get_text('install_admin_setup') . "\n\n"; + } + + while (!$login) { + print get_text('install_admin_get_email') . ' '; + $login = ; + chomp $login; + eval { Bugzilla::User->check_login_name_for_creation($login); }; + if ($@) { + print $@ . "\n"; + undef $login; } - - if (!$password) { - $password = _prompt_for_password( - get_text('install_admin_get_password')); - } - - my $admin = Bugzilla::User->create({ login_name => $login, - realname => $full_name, - cryptpassword => $password }); - make_admin($admin); + } + + while (!defined $full_name) { + print get_text('install_admin_get_name') . ' '; + $full_name = ; + chomp($full_name); + } + + if (!$password) { + $password = _prompt_for_password(get_text('install_admin_get_password')); + } + + my $admin + = Bugzilla::User->create({ + login_name => $login, realname => $full_name, cryptpassword => $password + }); + make_admin($admin); } sub make_admin { - my ($user) = @_; - my $dbh = Bugzilla->dbh; - - $user = ref($user) ? $user - : new Bugzilla::User(login_to_id($user, THROW_ERROR)); - - my $group_insert = $dbh->prepare( - 'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, ?, ?)'); - - # Admins get explicit membership and bless capability for the admin group - my $admin_group = new Bugzilla::Group({ name => 'admin' }); - # These are run in an eval so that we can ignore the error of somebody - # already being granted these things. - eval { - $group_insert->execute($user->id, $admin_group->id, 0, GRANT_DIRECT); - }; - eval { - $group_insert->execute($user->id, $admin_group->id, 1, GRANT_DIRECT); - }; - - # Admins should also have editusers directly, even though they'll usually - # inherit it. People could have changed their inheritance structure. - my $editusers = new Bugzilla::Group({ name => 'editusers' }); - eval { - $group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT); - }; - - # If there is no maintainer set, make this user the maintainer. - if (!Bugzilla->params->{'maintainer'}) { - SetParam('maintainer', $user->email); - write_params(); - } - - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\n", get_text('install_admin_created', { user => $user }), "\n"; - } + my ($user) = @_; + my $dbh = Bugzilla->dbh; + + $user + = ref($user) ? $user : new Bugzilla::User(login_to_id($user, THROW_ERROR)); + + my $group_insert = $dbh->prepare( + 'INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)' + ); + + # Admins get explicit membership and bless capability for the admin group + my $admin_group = new Bugzilla::Group({name => 'admin'}); + + # These are run in an eval so that we can ignore the error of somebody + # already being granted these things. + eval { $group_insert->execute($user->id, $admin_group->id, 0, GRANT_DIRECT); }; + eval { $group_insert->execute($user->id, $admin_group->id, 1, GRANT_DIRECT); }; + + # Admins should also have editusers directly, even though they'll usually + # inherit it. People could have changed their inheritance structure. + my $editusers = new Bugzilla::Group({name => 'editusers'}); + eval { $group_insert->execute($user->id, $editusers->id, 0, GRANT_DIRECT); }; + + # If there is no maintainer set, make this user the maintainer. + if (!Bugzilla->params->{'maintainer'}) { + SetParam('maintainer', $user->email); + write_params(); + } + + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\n", get_text('install_admin_created', {user => $user}), "\n"; + } } sub _prompt_for_password { - my $prompt = shift; - - my $password; - while (!$password) { - # trap a few interrupts so we can fix the echo if we get aborted. - local $SIG{HUP} = \&_password_prompt_exit; - local $SIG{INT} = \&_password_prompt_exit; - local $SIG{QUIT} = \&_password_prompt_exit; - local $SIG{TERM} = \&_password_prompt_exit; - - system("stty","-echo") unless ON_WINDOWS; # disable input echoing - - print $prompt, ' '; - $password = ; - chomp $password; - print "\n", get_text('install_confirm_password'), ' '; - my $pass2 = ; - chomp $pass2; - my $pwqc = Bugzilla->passwdqc; - my $ok = $pwqc->validate_password($password); - if (!$ok) { - print "\n", $pwqc->reason, "\n"; - undef $password; - } - elsif ($password ne $pass2) { - print "\n", "passwords do not match\n"; - undef $password; - } - system("stty","echo") unless ON_WINDOWS; + my $prompt = shift; + + my $password; + while (!$password) { + + # trap a few interrupts so we can fix the echo if we get aborted. + local $SIG{HUP} = \&_password_prompt_exit; + local $SIG{INT} = \&_password_prompt_exit; + local $SIG{QUIT} = \&_password_prompt_exit; + local $SIG{TERM} = \&_password_prompt_exit; + + system("stty", "-echo") unless ON_WINDOWS; # disable input echoing + + print $prompt, ' '; + $password = ; + chomp $password; + print "\n", get_text('install_confirm_password'), ' '; + my $pass2 = ; + chomp $pass2; + my $pwqc = Bugzilla->passwdqc; + my $ok = $pwqc->validate_password($password); + if (!$ok) { + print "\n", $pwqc->reason, "\n"; + undef $password; } - return $password; + elsif ($password ne $pass2) { + print "\n", "passwords do not match\n"; + undef $password; + } + system("stty", "echo") unless ON_WINDOWS; + } + return $password; } # This is just in case we get interrupted while getting a password. sub _password_prompt_exit { - # re-enable input echoing - system("stty","echo") unless ON_WINDOWS; - exit 1; + + # re-enable input echoing + system("stty", "echo") unless ON_WINDOWS; + exit 1; } sub reset_password { - my $login = shift; - my $user = Bugzilla::User->check($login); - my $prompt = "\n" . get_text('install_reset_password', { user => $user }); - my $password = _prompt_for_password($prompt); - $user->set_password($password); - $user->update(); - print "\n", get_text('install_reset_password_done'), "\n"; + my $login = shift; + my $user = Bugzilla::User->check($login); + my $prompt = "\n" . get_text('install_reset_password', {user => $user}); + my $password = _prompt_for_password($prompt); + $user->set_password($password); + $user->update(); + print "\n", get_text('install_reset_password_done'), "\n"; } 1; diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm index 8b3d4b8cc..a8db6bb75 100644 --- a/Bugzilla/Install/DB.pm +++ b/Bugzilla/Install/DB.pm @@ -33,95 +33,100 @@ use URI::QueryParam; # NOTE: This is NOT the function for general table updates. See # update_table_definitions for that. This is only for the fielddefs table. sub update_fielddefs_definition { - my $dbh = Bugzilla->dbh; - - # 2005-02-21 - LpSolit@gmail.com - Bug 279910 - # qacontact_accessible and assignee_accessible field names no longer exist - # in the 'bugs' table. Their corresponding entries in the 'bugs_activity' - # table should therefore be marked as obsolete, meaning that they cannot - # be used anymore when querying the database - they are not deleted in - # order to keep track of these fields in the activity table. - if (!$dbh->bz_column_info('fielddefs', 'obsolete')) { - $dbh->bz_add_column('fielddefs', 'obsolete', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - print "Marking qacontact_accessible and assignee_accessible as", - " obsolete fields...\n"; - $dbh->do("UPDATE fielddefs SET obsolete = 1 + my $dbh = Bugzilla->dbh; + + # 2005-02-21 - LpSolit@gmail.com - Bug 279910 + # qacontact_accessible and assignee_accessible field names no longer exist + # in the 'bugs' table. Their corresponding entries in the 'bugs_activity' + # table should therefore be marked as obsolete, meaning that they cannot + # be used anymore when querying the database - they are not deleted in + # order to keep track of these fields in the activity table. + if (!$dbh->bz_column_info('fielddefs', 'obsolete')) { + $dbh->bz_add_column('fielddefs', 'obsolete', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + print "Marking qacontact_accessible and assignee_accessible as", + " obsolete fields...\n"; + $dbh->do( + "UPDATE fielddefs SET obsolete = 1 WHERE name = 'qacontact_accessible' - OR name = 'assignee_accessible'"); - } - - # 2005-08-10 Myk Melez bug 287325 - # Record each field's type and whether or not it's a custom field, - # in fielddefs. - $dbh->bz_add_column('fielddefs', 'type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_column('fielddefs', 'custom', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - $dbh->bz_add_column('fielddefs', 'enter_bug', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - # Change the name of the fieldid column to id, so that fielddefs - # can use Bugzilla::Object easily. We have to do this up here, because - # otherwise adding these field definitions will fail. - $dbh->bz_rename_column('fielddefs', 'fieldid', 'id'); - - # If the largest fielddefs sortkey is less than 100, then - # we're using the old sorting system, and we should convert - # it to the new one before adding any new definitions. - if (!$dbh->selectrow_arrayref( - 'SELECT COUNT(id) FROM fielddefs WHERE sortkey >= 100')) - { - print "Updating the sortkeys for the fielddefs table...\n"; - my $field_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM fielddefs ORDER BY sortkey'); - my $sortkey = 100; - foreach my $field_id (@$field_ids) { - $dbh->do('UPDATE fielddefs SET sortkey = ? WHERE id = ?', - undef, $sortkey, $field_id); - $sortkey += 100; - } + OR name = 'assignee_accessible'" + ); + } + + # 2005-08-10 Myk Melez bug 287325 + # Record each field's type and whether or not it's a custom field, + # in fielddefs. + $dbh->bz_add_column('fielddefs', 'type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('fielddefs', 'custom', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + $dbh->bz_add_column('fielddefs', 'enter_bug', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + # Change the name of the fieldid column to id, so that fielddefs + # can use Bugzilla::Object easily. We have to do this up here, because + # otherwise adding these field definitions will fail. + $dbh->bz_rename_column('fielddefs', 'fieldid', 'id'); + + # If the largest fielddefs sortkey is less than 100, then + # we're using the old sorting system, and we should convert + # it to the new one before adding any new definitions. + if (!$dbh->selectrow_arrayref( + 'SELECT COUNT(id) FROM fielddefs WHERE sortkey >= 100')) + { + print "Updating the sortkeys for the fielddefs table...\n"; + my $field_ids + = $dbh->selectcol_arrayref('SELECT id FROM fielddefs ORDER BY sortkey'); + my $sortkey = 100; + foreach my $field_id (@$field_ids) { + $dbh->do('UPDATE fielddefs SET sortkey = ? WHERE id = ?', + undef, $sortkey, $field_id); + $sortkey += 100; } + } - $dbh->bz_add_column('fielddefs', 'visibility_field_id', {TYPE => 'INT3'}); - $dbh->bz_add_column('fielddefs', 'value_field_id', {TYPE => 'INT3'}); - $dbh->bz_add_index('fielddefs', 'fielddefs_value_field_id_idx', - ['value_field_id']); + $dbh->bz_add_column('fielddefs', 'visibility_field_id', {TYPE => 'INT3'}); + $dbh->bz_add_column('fielddefs', 'value_field_id', {TYPE => 'INT3'}); + $dbh->bz_add_index('fielddefs', 'fielddefs_value_field_id_idx', + ['value_field_id']); - # Bug 344878 - if (!$dbh->bz_column_info('fielddefs', 'buglist')) { - $dbh->bz_add_column('fielddefs', 'buglist', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # Set non-multiselect custom fields as valid buglist fields - # Note that default fields will be handled in Field.pm - $dbh->do('UPDATE fielddefs SET buglist = 1 WHERE custom = 1 AND type != ' . FIELD_TYPE_MULTI_SELECT); - } + # Bug 344878 + if (!$dbh->bz_column_info('fielddefs', 'buglist')) { + $dbh->bz_add_column('fielddefs', 'buglist', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + # Set non-multiselect custom fields as valid buglist fields + # Note that default fields will be handled in Field.pm + $dbh->do('UPDATE fielddefs SET buglist = 1 WHERE custom = 1 AND type != ' + . FIELD_TYPE_MULTI_SELECT); + } - #2008-08-26 elliotte_martin@yahoo.com - Bug 251556 - $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'}); + #2008-08-26 elliotte_martin@yahoo.com - Bug 251556 + $dbh->bz_add_column('fielddefs', 'reverse_desc', {TYPE => 'TINYTEXT'}); - $dbh->do('UPDATE fielddefs SET buglist = 1 - WHERE custom = 1 AND type = ' . FIELD_TYPE_MULTI_SELECT); + $dbh->do( + 'UPDATE fielddefs SET buglist = 1 + WHERE custom = 1 AND type = ' . FIELD_TYPE_MULTI_SELECT + ); - $dbh->bz_add_column('fielddefs', 'is_mandatory', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_index('fielddefs', 'fielddefs_is_mandatory_idx', - ['is_mandatory']); + $dbh->bz_add_column('fielddefs', 'is_mandatory', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_index('fielddefs', 'fielddefs_is_mandatory_idx', ['is_mandatory']); - # 2010-04-05 dkl@redhat.com - Bug 479400 - _migrate_field_visibility_value(); + # 2010-04-05 dkl@redhat.com - Bug 479400 + _migrate_field_visibility_value(); - $dbh->bz_add_column('fielddefs', 'is_numeric', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->do('UPDATE fielddefs SET is_numeric = 1 WHERE type = ' - . FIELD_TYPE_BUG_ID); + $dbh->bz_add_column('fielddefs', 'is_numeric', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->do( + 'UPDATE fielddefs SET is_numeric = 1 WHERE type = ' . FIELD_TYPE_BUG_ID); - Bugzilla::Hook::process('install_update_db_fielddefs'); + Bugzilla::Hook::process('install_update_db_fielddefs'); - # Remember, this is not the function for adding general table changes. - # That is below. Add new changes to the fielddefs table above this - # comment. + # Remember, this is not the function for adding general table changes. + # That is below. Add new changes to the fielddefs table above this + # comment. } # Small changes can be put directly into this function. @@ -147,1110 +152,1136 @@ sub update_fielddefs_definition { # the purpose of a column. # sub update_table_definitions { - my $old_params = shift; - my $dbh = Bugzilla->dbh; - _update_pre_checksetup_bugzillas(); - - $dbh->bz_add_column('attachments', 'submitter_id', - {TYPE => 'INT3', NOTNULL => 1}, 0); - - $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when'); - - _add_bug_vote_cache(); - _update_product_name_definition(); - - $dbh->bz_add_column('profiles', 'disabledtext', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - - _populate_longdescs(); - _update_bugs_activity_field_to_fieldid(); - - if (!$dbh->bz_column_info('bugs', 'lastdiffed')) { - $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE =>'DATETIME'}); - $dbh->do('UPDATE bugs SET lastdiffed = NOW()'); - } - - _add_unique_login_name_index_to_profiles(); - - $dbh->bz_add_column('profiles', 'mybugslink', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - _update_component_user_fields_to_ids(); - - $dbh->bz_add_column('bugs', 'everconfirmed', - {TYPE => 'BOOLEAN', NOTNULL => 1}, 1); - - _populate_milestones_table(); - - # 2000-03-22 Changed the default value for target_milestone to be "---" - # (which is still not quite correct, but much better than what it was - # doing), and made the size of the value field in the milestones table match - # the size of the target_milestone field in the bugs table. - $dbh->bz_alter_column('bugs', 'target_milestone', - {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); - $dbh->bz_alter_column('milestones', 'value', - {TYPE => 'varchar(20)', NOTNULL => 1}); - - _add_products_defaultmilestone(); - - # 2000-03-24 Added unique indexes into the cc and keyword tables. This - # prevents certain database inconsistencies, and, moreover, is required for - # new generalized list code to work. - if (!$dbh->bz_index_info('cc', 'cc_bug_id_idx') - || !$dbh->bz_index_info('cc', 'cc_bug_id_idx')->{TYPE}) - { - $dbh->bz_drop_index('cc', 'cc_bug_id_idx'); - $dbh->bz_add_index('cc', 'cc_bug_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(bug_id who)]}); - } - if (!$dbh->bz_index_info('keywords', 'keywords_bug_id_idx') - || !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx')->{TYPE}) - { - $dbh->bz_drop_index('keywords', 'keywords_bug_id_idx'); - $dbh->bz_add_index('keywords', 'keywords_bug_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(bug_id keywordid)]}); - } - - _copy_from_comments_to_longdescs(); - _populate_duplicates_table(); - - if (!$dbh->bz_column_info('email_setting', 'user_id')) { - $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'}); - } - - if (!$dbh->bz_index_info('email_rates', 'email_rates_message_ts_idx')) { - $dbh->bz_add_index( 'email_rates', 'email_rates_message_ts_idx', ['message_ts'] ); - } - - $dbh->bz_add_column('groups', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - $dbh->bz_add_column('attachments', 'isobsolete', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - $dbh->bz_drop_column("profiles", "emailnotification"); - $dbh->bz_drop_column("profiles", "newemailtech"); - - # 2003-11-19; chicks@chicks.net; bug 225973: fix field size to accommodate - # wider algorithms such as Blowfish. Note that this needs to be run - # before recrypting passwords in the following block. - $dbh->bz_alter_column('profiles', 'cryptpassword', - {TYPE => 'varchar(128)'}); - - _recrypt_plaintext_passwords(); - - # 2001-06-15 kiko@async.com.br - Change bug:version size to avoid - # truncates re http://bugzilla.mozilla.org/show_bug.cgi?id=9352 - $dbh->bz_alter_column('bugs', 'version', - {TYPE => 'varchar(64)', NOTNULL => 1}); - - _update_bugs_activity_to_only_record_changes(); - - # bug 90933: Make disabledtext NOT NULL - if (!$dbh->bz_column_info('profiles', 'disabledtext')->{NOTNULL}) { - $dbh->bz_alter_column("profiles", "disabledtext", - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - } - - $dbh->bz_add_column("bugs", "reporter_accessible", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->bz_add_column("bugs", "cclist_accessible", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT5'}); - - _delete_logincookies_cryptpassword_and_handle_invalid_cookies(); - - # qacontact/assignee should always be able to see bugs: bug 97471 - $dbh->bz_drop_column("bugs", "qacontact_accessible"); - $dbh->bz_drop_column("bugs", "assignee_accessible"); - - # 2002-02-20 jeff.hedlund@matrixsi.com - bug 24789 time tracking - $dbh->bz_add_column("longdescs", "work_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "estimated_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "remaining_time", - {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column("bugs", "deadline", {TYPE => 'DATETIME'}); - - _use_ip_instead_of_hostname_in_logincookies(); - - $dbh->bz_add_column('longdescs', 'isprivate', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('attachments', 'isprivate', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - $dbh->bz_add_column("bugs", "alias", {TYPE => "varchar(20)"}); - $dbh->bz_add_index('bugs', 'bugs_alias_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(alias)]}); - - _move_quips_into_db(); - - $dbh->bz_drop_column("namedqueries", "watchfordiffs"); - - _use_ids_for_products_and_components(); - _convert_groups_system_from_groupset(); - _convert_attachment_statuses_to_flags(); - _remove_spaces_and_commas_from_flagtypes(); - _setup_usebuggroups_backward_compatibility(); - _remove_user_series_map(); - - # 2006-08-03 remi_zara@mac.com bug 346241, make series.creator nullable - # This must happen before calling _copy_old_charts_into_database(). - if ($dbh->bz_column_info('series', 'creator')->{NOTNULL}) { - $dbh->bz_alter_column('series', 'creator', {TYPE => 'INT3'}); - $dbh->do("UPDATE series SET creator = NULL WHERE creator = 0"); - } - - _copy_old_charts_into_database(); - - _add_user_group_map_grant_type(); - _add_group_group_map_grant_type(); - - $dbh->bz_add_column("profiles", "extern_id", {TYPE => 'varchar(64)'}); - - $dbh->bz_add_column('flagtypes', 'grant_group_id', {TYPE => 'INT3'}); - $dbh->bz_add_column('flagtypes', 'request_group_id', {TYPE => 'INT3'}); - - # mailto is no longer just userids - $dbh->bz_rename_column('whine_schedules', 'mailto_userid', 'mailto'); - $dbh->bz_add_column('whine_schedules', 'mailto_type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); - - _add_longdescs_already_wrapped(); - - # Moved enum types to separate tables so we need change the old enum - # types to standard varchars in the bugs table. - $dbh->bz_alter_column('bugs', 'bug_status', - {TYPE => 'varchar(64)', NOTNULL => 1}); - # 2005-03-23 Tomas.Kopal@altap.cz - add default value to resolution, - # bug 286695 - $dbh->bz_alter_column('bugs', 'resolution', - {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}); - $dbh->bz_alter_column('bugs', 'priority', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('bugs', 'bug_severity', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('bugs', 'rep_platform', - {TYPE => 'varchar(64)', NOTNULL => 1}, ''); - $dbh->bz_alter_column('bugs', 'op_sys', - {TYPE => 'varchar(64)', NOTNULL => 1}); - - # When migrating quips from the '$datadir/comments' file to the DB, - # the user ID should be NULL instead of 0 (which is an invalid user ID). - if ($dbh->bz_column_info('quips', 'userid')->{NOTNULL}) { - $dbh->bz_alter_column('quips', 'userid', {TYPE => 'INT3'}); - print "Changing owner to NULL for quips where the owner is", - " unknown...\n"; - $dbh->do('UPDATE quips SET userid = NULL WHERE userid = 0'); - } - - _convert_attachments_filename_from_mediumtext(); - - $dbh->bz_add_column('quips', 'approved', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - # 2002-12-20 Bug 180870 - remove manual shadowdb replication code - $dbh->bz_drop_table("shadowlog"); - - _rename_votes_count_and_force_group_refresh(); - - # 2004/02/15 - Summaries shouldn't be null - see bug 220232 - if (!exists $dbh->bz_column_info('bugs', 'short_desc')->{NOTNULL}) { - $dbh->bz_alter_column('bugs', 'short_desc', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - } + my $old_params = shift; + my $dbh = Bugzilla->dbh; + _update_pre_checksetup_bugzillas(); - $dbh->bz_add_column('products', 'classification_id', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_add_column('attachments', 'submitter_id', + {TYPE => 'INT3', NOTNULL => 1}, 0); - _fix_group_with_empty_name(); + $dbh->bz_rename_column('bugs_activity', 'when', 'bug_when'); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_who_idx', [qw(who)]); + _add_bug_vote_cache(); + _update_product_name_definition(); - # Add defaults for some fields that should have them but didn't. - $dbh->bz_alter_column('bugs', 'status_whiteboard', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - if ($dbh->bz_column_info('bugs', 'votes')) { - $dbh->bz_alter_column('bugs', 'votes', - {TYPE => 'INT3', NOTNULL => 1, DEFAULT => '0'}); - } + $dbh->bz_add_column('profiles', 'disabledtext', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + _populate_longdescs(); + _update_bugs_activity_field_to_fieldid(); - # 2005-03-09 qa_contact should be NULL instead of 0, bug 285534 - if ($dbh->bz_column_info('bugs', 'qa_contact')->{NOTNULL}) { - $dbh->bz_alter_column('bugs', 'qa_contact', {TYPE => 'INT3'}); - $dbh->do("UPDATE bugs SET qa_contact = NULL WHERE qa_contact = 0"); - } + if (!$dbh->bz_column_info('bugs', 'lastdiffed')) { + $dbh->bz_add_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + $dbh->do('UPDATE bugs SET lastdiffed = NOW()'); + } - # 2005-03-27 initialqacontact should be NULL instead of 0, bug 287483 - if ($dbh->bz_column_info('components', 'initialqacontact')->{NOTNULL}) { - $dbh->bz_alter_column('components', 'initialqacontact', - {TYPE => 'INT3'}); - } - $dbh->do("UPDATE components SET initialqacontact = NULL " . - "WHERE initialqacontact = 0"); + _add_unique_login_name_index_to_profiles(); - _migrate_email_prefs_to_new_table(); - _initialize_new_email_prefs(); - _change_all_mysql_booleans_to_tinyint(); + $dbh->bz_add_column('profiles', 'mybugslink', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - # make classification_id field type be consistent with DB:Schema - $dbh->bz_alter_column('products', 'classification_id', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + _update_component_user_fields_to_ids(); - # initialowner was accidentally NULL when we checked-in Schema, - # when it really should be NOT NULL. - $dbh->bz_alter_column('components', 'initialowner', - {TYPE => 'INT3', NOTNULL => 1}, 0); + $dbh->bz_add_column('bugs', 'everconfirmed', {TYPE => 'BOOLEAN', NOTNULL => 1}, + 1); - # 2005-03-28 - bug 238800 - index flags.type_id for editflagtypes.cgi - $dbh->bz_add_index('flags', 'flags_type_id_idx', [qw(type_id)]); + _populate_milestones_table(); - # For a short time, the flags_type_id_idx was misnamed in upgraded installs. - $dbh->bz_drop_index('flags', 'type_id'); + # 2000-03-22 Changed the default value for target_milestone to be "---" + # (which is still not quite correct, but much better than what it was + # doing), and made the size of the value field in the milestones table match + # the size of the target_milestone field in the bugs table. + $dbh->bz_alter_column('bugs', 'target_milestone', + {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); + $dbh->bz_alter_column('milestones', 'value', + {TYPE => 'varchar(20)', NOTNULL => 1}); - # 2005-04-28 - LpSolit@gmail.com - Bug 7233: add an index to versions - $dbh->bz_alter_column('versions', 'value', - {TYPE => 'varchar(64)', NOTNULL => 1}); - _add_versions_product_id_index(); - - if (!exists $dbh->bz_column_info('milestones', 'sortkey')->{DEFAULT}) { - $dbh->bz_alter_column('milestones', 'sortkey', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - } + _add_products_defaultmilestone(); - # 2005-06-14 - LpSolit@gmail.com - Bug 292544 - $dbh->bz_alter_column('bugs', 'creation_ts', {TYPE => 'DATETIME'}); + # 2000-03-24 Added unique indexes into the cc and keyword tables. This + # prevents certain database inconsistencies, and, moreover, is required for + # new generalized list code to work. + if ( !$dbh->bz_index_info('cc', 'cc_bug_id_idx') + || !$dbh->bz_index_info('cc', 'cc_bug_id_idx')->{TYPE}) + { + $dbh->bz_drop_index('cc', 'cc_bug_id_idx'); + $dbh->bz_add_index('cc', 'cc_bug_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(bug_id who)]}); + } + if ( !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx') + || !$dbh->bz_index_info('keywords', 'keywords_bug_id_idx')->{TYPE}) + { + $dbh->bz_drop_index('keywords', 'keywords_bug_id_idx'); + $dbh->bz_add_index('keywords', 'keywords_bug_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(bug_id keywordid)]}); + } - _fix_whine_queries_title_and_op_sys_value(); - _fix_attachments_submitter_id_idx(); - _copy_attachments_thedata_to_attach_data(); - _fix_broken_all_closed_series(); - # 2005-08-14 bugreport@peshkin.net -- Bug 304583 - # Get rid of leftover DERIVED group permissions - use constant GRANT_DERIVED => 1; - $dbh->do("DELETE FROM user_group_map WHERE grant_type = " . GRANT_DERIVED); + _copy_from_comments_to_longdescs(); + _populate_duplicates_table(); - _rederive_regex_groups(); + if (!$dbh->bz_column_info('email_setting', 'user_id')) { + $dbh->bz_add_column('profiles', 'emailflags', {TYPE => 'MEDIUMTEXT'}); + } - # PUBLIC is a reserved word in Oracle. - $dbh->bz_rename_column('series', 'public', 'is_public'); + if (!$dbh->bz_index_info('email_rates', 'email_rates_message_ts_idx')) { + $dbh->bz_add_index('email_rates', 'email_rates_message_ts_idx', ['message_ts']); + } - # 2005-11-04 LpSolit@gmail.com - Bug 305927 - $dbh->bz_alter_column('groups', 'userregexp', - {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + $dbh->bz_add_column('groups', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - _clean_control_characters_from_short_desc(); + $dbh->bz_add_column('attachments', 'isobsolete', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2005-12-07 altlst@sonic.net -- Bug 225221 - $dbh->bz_add_column('longdescs', 'comment_id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_drop_column("profiles", "emailnotification"); + $dbh->bz_drop_column("profiles", "newemailtech"); + + # 2003-11-19; chicks@chicks.net; bug 225973: fix field size to accommodate + # wider algorithms such as Blowfish. Note that this needs to be run + # before recrypting passwords in the following block. + $dbh->bz_alter_column('profiles', 'cryptpassword', {TYPE => 'varchar(128)'}); + + _recrypt_plaintext_passwords(); + + # 2001-06-15 kiko@async.com.br - Change bug:version size to avoid + # truncates re http://bugzilla.mozilla.org/show_bug.cgi?id=9352 + $dbh->bz_alter_column('bugs', 'version', {TYPE => 'varchar(64)', NOTNULL => 1}); + + _update_bugs_activity_to_only_record_changes(); + + # bug 90933: Make disabledtext NOT NULL + if (!$dbh->bz_column_info('profiles', 'disabledtext')->{NOTNULL}) { + $dbh->bz_alter_column("profiles", "disabledtext", + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + } + + $dbh->bz_add_column("bugs", "reporter_accessible", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + $dbh->bz_add_column("bugs", "cclist_accessible", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + $dbh->bz_add_column("bugs_activity", "attach_id", {TYPE => 'INT5'}); + + _delete_logincookies_cryptpassword_and_handle_invalid_cookies(); + + # qacontact/assignee should always be able to see bugs: bug 97471 + $dbh->bz_drop_column("bugs", "qacontact_accessible"); + $dbh->bz_drop_column("bugs", "assignee_accessible"); + + # 2002-02-20 jeff.hedlund@matrixsi.com - bug 24789 time tracking + $dbh->bz_add_column("longdescs", "work_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "estimated_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "remaining_time", + {TYPE => 'decimal(5,2)', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column("bugs", "deadline", {TYPE => 'DATETIME'}); + + _use_ip_instead_of_hostname_in_logincookies(); + + $dbh->bz_add_column('longdescs', 'isprivate', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('attachments', 'isprivate', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + $dbh->bz_add_column("bugs", "alias", {TYPE => "varchar(20)"}); + $dbh->bz_add_index('bugs', 'bugs_alias_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(alias)]}); + + _move_quips_into_db(); + + $dbh->bz_drop_column("namedqueries", "watchfordiffs"); + + _use_ids_for_products_and_components(); + _convert_groups_system_from_groupset(); + _convert_attachment_statuses_to_flags(); + _remove_spaces_and_commas_from_flagtypes(); + _setup_usebuggroups_backward_compatibility(); + _remove_user_series_map(); + + # 2006-08-03 remi_zara@mac.com bug 346241, make series.creator nullable + # This must happen before calling _copy_old_charts_into_database(). + if ($dbh->bz_column_info('series', 'creator')->{NOTNULL}) { + $dbh->bz_alter_column('series', 'creator', {TYPE => 'INT3'}); + $dbh->do("UPDATE series SET creator = NULL WHERE creator = 0"); + } + + _copy_old_charts_into_database(); + + _add_user_group_map_grant_type(); + _add_group_group_map_grant_type(); + + $dbh->bz_add_column("profiles", "extern_id", {TYPE => 'varchar(64)'}); - _stop_storing_inactive_flags(); - _change_short_desc_from_mediumtext_to_varchar(); + $dbh->bz_add_column('flagtypes', 'grant_group_id', {TYPE => 'INT3'}); + $dbh->bz_add_column('flagtypes', 'request_group_id', {TYPE => 'INT3'}); - # 2006-07-01 wurblzap@gmail.com -- Bug 69000 - $dbh->bz_add_column('namedqueries', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - _move_namedqueries_linkinfooter_to_its_own_table(); + # mailto is no longer just userids + $dbh->bz_rename_column('whine_schedules', 'mailto_userid', 'mailto'); + $dbh->bz_add_column('whine_schedules', 'mailto_type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + + _add_longdescs_already_wrapped(); - _add_classifications_sortkey(); - _move_data_nomail_into_db(); + # Moved enum types to separate tables so we need change the old enum + # types to standard varchars in the bugs table. + $dbh->bz_alter_column('bugs', 'bug_status', + {TYPE => 'varchar(64)', NOTNULL => 1}); + + # 2005-03-23 Tomas.Kopal@altap.cz - add default value to resolution, + # bug 286695 + $dbh->bz_alter_column('bugs', 'resolution', + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "''"}); + $dbh->bz_alter_column('bugs', 'priority', + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('bugs', 'bug_severity', + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('bugs', 'rep_platform', + {TYPE => 'varchar(64)', NOTNULL => 1}, ''); + $dbh->bz_alter_column('bugs', 'op_sys', {TYPE => 'varchar(64)', NOTNULL => 1}); + + # When migrating quips from the '$datadir/comments' file to the DB, + # the user ID should be NULL instead of 0 (which is an invalid user ID). + if ($dbh->bz_column_info('quips', 'userid')->{NOTNULL}) { + $dbh->bz_alter_column('quips', 'userid', {TYPE => 'INT3'}); + print "Changing owner to NULL for quips where the owner is", " unknown...\n"; + $dbh->do('UPDATE quips SET userid = NULL WHERE userid = 0'); + } + + _convert_attachments_filename_from_mediumtext(); + + $dbh->bz_add_column('quips', 'approved', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + # 2002-12-20 Bug 180870 - remove manual shadowdb replication code + $dbh->bz_drop_table("shadowlog"); + + _rename_votes_count_and_force_group_refresh(); + + # 2004/02/15 - Summaries shouldn't be null - see bug 220232 + if (!exists $dbh->bz_column_info('bugs', 'short_desc')->{NOTNULL}) { + $dbh->bz_alter_column('bugs', 'short_desc', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + } + + $dbh->bz_add_column('products', 'classification_id', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + + _fix_group_with_empty_name(); + + $dbh->bz_add_index('bugs_activity', 'bugs_activity_who_idx', [qw(who)]); + + # Add defaults for some fields that should have them but didn't. + $dbh->bz_alter_column('bugs', 'status_whiteboard', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + if ($dbh->bz_column_info('bugs', 'votes')) { + $dbh->bz_alter_column('bugs', 'votes', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => '0'}); + } + + $dbh->bz_alter_column('bugs', 'lastdiffed', {TYPE => 'DATETIME'}); + + # 2005-03-09 qa_contact should be NULL instead of 0, bug 285534 + if ($dbh->bz_column_info('bugs', 'qa_contact')->{NOTNULL}) { + $dbh->bz_alter_column('bugs', 'qa_contact', {TYPE => 'INT3'}); + $dbh->do("UPDATE bugs SET qa_contact = NULL WHERE qa_contact = 0"); + } + + # 2005-03-27 initialqacontact should be NULL instead of 0, bug 287483 + if ($dbh->bz_column_info('components', 'initialqacontact')->{NOTNULL}) { + $dbh->bz_alter_column('components', 'initialqacontact', {TYPE => 'INT3'}); + } + $dbh->do("UPDATE components SET initialqacontact = NULL " + . "WHERE initialqacontact = 0"); + + _migrate_email_prefs_to_new_table(); + _initialize_new_email_prefs(); + _change_all_mysql_booleans_to_tinyint(); + + # make classification_id field type be consistent with DB:Schema + $dbh->bz_alter_column('products', 'classification_id', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '1'}); + + # initialowner was accidentally NULL when we checked-in Schema, + # when it really should be NOT NULL. + $dbh->bz_alter_column('components', 'initialowner', + {TYPE => 'INT3', NOTNULL => 1}, 0); + + # 2005-03-28 - bug 238800 - index flags.type_id for editflagtypes.cgi + $dbh->bz_add_index('flags', 'flags_type_id_idx', [qw(type_id)]); + + # For a short time, the flags_type_id_idx was misnamed in upgraded installs. + $dbh->bz_drop_index('flags', 'type_id'); + + # 2005-04-28 - LpSolit@gmail.com - Bug 7233: add an index to versions + $dbh->bz_alter_column('versions', 'value', + {TYPE => 'varchar(64)', NOTNULL => 1}); + _add_versions_product_id_index(); + + if (!exists $dbh->bz_column_info('milestones', 'sortkey')->{DEFAULT}) { + $dbh->bz_alter_column('milestones', 'sortkey', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + } + + # 2005-06-14 - LpSolit@gmail.com - Bug 292544 + $dbh->bz_alter_column('bugs', 'creation_ts', {TYPE => 'DATETIME'}); + + _fix_whine_queries_title_and_op_sys_value(); + _fix_attachments_submitter_id_idx(); + _copy_attachments_thedata_to_attach_data(); + _fix_broken_all_closed_series(); + + # 2005-08-14 bugreport@peshkin.net -- Bug 304583 + # Get rid of leftover DERIVED group permissions + use constant GRANT_DERIVED => 1; + $dbh->do("DELETE FROM user_group_map WHERE grant_type = " . GRANT_DERIVED); + + _rederive_regex_groups(); + + # PUBLIC is a reserved word in Oracle. + $dbh->bz_rename_column('series', 'public', 'is_public'); + + # 2005-11-04 LpSolit@gmail.com - Bug 305927 + $dbh->bz_alter_column('groups', 'userregexp', + {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + + _clean_control_characters_from_short_desc(); - # The products table lacked sensible defaults. - if ($dbh->bz_column_info('products', 'milestoneurl')) { - $dbh->bz_alter_column('products', 'milestoneurl', - {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + # 2005-12-07 altlst@sonic.net -- Bug 225221 + $dbh->bz_add_column('longdescs', 'comment_id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + _stop_storing_inactive_flags(); + _change_short_desc_from_mediumtext_to_varchar(); + + # 2006-07-01 wurblzap@gmail.com -- Bug 69000 + $dbh->bz_add_column('namedqueries', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + _move_namedqueries_linkinfooter_to_its_own_table(); + + _add_classifications_sortkey(); + _move_data_nomail_into_db(); + + # The products table lacked sensible defaults. + if ($dbh->bz_column_info('products', 'milestoneurl')) { + $dbh->bz_alter_column('products', 'milestoneurl', + {TYPE => 'TINYTEXT', NOTNULL => 1, DEFAULT => "''"}); + } + if ($dbh->bz_column_info('products', 'disallownew')) { + $dbh->bz_alter_column('products', 'disallownew', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); + + if ($dbh->bz_column_info('products', 'votesperuser')) { + $dbh->bz_alter_column('products', 'votesperuser', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_alter_column('products', 'votestoconfirm', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); } - if ($dbh->bz_column_info('products', 'disallownew')){ - $dbh->bz_alter_column('products', 'disallownew', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); + } - if ($dbh->bz_column_info('products', 'votesperuser')) { - $dbh->bz_alter_column('products', 'votesperuser', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_alter_column('products', 'votestoconfirm', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - } - } + # 2006-08-04 LpSolit@gmail.com - Bug 305941 + $dbh->bz_drop_column('profiles', 'refreshed_when'); + $dbh->bz_drop_column('groups', 'last_changed'); - # 2006-08-04 LpSolit@gmail.com - Bug 305941 - $dbh->bz_drop_column('profiles', 'refreshed_when'); - $dbh->bz_drop_column('groups', 'last_changed'); + # 2006-08-06 LpSolit@gmail.com - Bug 347521 + $dbh->bz_alter_column('flagtypes', 'id', + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2006-08-06 LpSolit@gmail.com - Bug 347521 - $dbh->bz_alter_column('flagtypes', 'id', - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_alter_column('keyworddefs', 'id', + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_alter_column('keyworddefs', 'id', - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2006-08-19 LpSolit@gmail.com - Bug 87795 + $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'}); - # 2006-08-19 LpSolit@gmail.com - Bug 87795 - $dbh->bz_alter_column('tokens', 'userid', {TYPE => 'INT3'}); + $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx'); - $dbh->bz_drop_index('bugs', 'bugs_short_desc_idx'); + # The profiles table was missing some defaults. + $dbh->bz_alter_column('profiles', 'disabledtext', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + $dbh->bz_alter_column('profiles', 'realname', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); - # The profiles table was missing some defaults. - $dbh->bz_alter_column('profiles', 'disabledtext', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - $dbh->bz_alter_column('profiles', 'realname', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); + _update_longdescs_who_index(); - _update_longdescs_who_index(); + $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'}); - $dbh->bz_add_column('setting', 'subclass', {TYPE => 'varchar(32)'}); + $dbh->bz_alter_column('longdescs', 'thetext', + {TYPE => 'LONGTEXT', NOTNULL => 1}, ''); - $dbh->bz_alter_column('longdescs', 'thetext', - {TYPE => 'LONGTEXT', NOTNULL => 1}, ''); + # 2006-10-20 LpSolit@gmail.com - Bug 189627 + $dbh->bz_add_column('group_control_map', 'editcomponents', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('group_control_map', 'editbugs', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('group_control_map', 'canconfirm', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2006-10-20 LpSolit@gmail.com - Bug 189627 - $dbh->bz_add_column('group_control_map', 'editcomponents', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('group_control_map', 'editbugs', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_add_column('group_control_map', 'canconfirm', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # 2006-11-07 LpSolit@gmail.com - Bug 353656 + $dbh->bz_add_column('longdescs', 'type', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column('longdescs', 'extra_data', {TYPE => 'varchar(255)'}); - # 2006-11-07 LpSolit@gmail.com - Bug 353656 - $dbh->bz_add_column('longdescs', 'type', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); - $dbh->bz_add_column('longdescs', 'extra_data', {TYPE => 'varchar(255)'}); + $dbh->bz_add_column('versions', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column('milestones', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column('versions', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column('milestones', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + _fix_uppercase_custom_field_names(); + _fix_uppercase_index_names(); - _fix_uppercase_custom_field_names(); - _fix_uppercase_index_names(); + # 2007-05-17 LpSolit@gmail.com - Bug 344965 + _initialize_workflow_for_upgrade($old_params); - # 2007-05-17 LpSolit@gmail.com - Bug 344965 - _initialize_workflow_for_upgrade($old_params); + # 2007-08-08 LpSolit@gmail.com - Bug 332149 + $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'}); - # 2007-08-08 LpSolit@gmail.com - Bug 332149 - $dbh->bz_add_column('groups', 'icon_url', {TYPE => 'TINYTEXT'}); + # 2007-08-21 wurblzap@gmail.com - Bug 365378 + _make_lang_setting_dynamic(); - # 2007-08-21 wurblzap@gmail.com - Bug 365378 - _make_lang_setting_dynamic(); + # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129 + _change_text_types(); - # 2007-11-29 xiaoou.wu@oracle.com - Bug 153129 - _change_text_types(); + # 2007-09-09 LpSolit@gmail.com - Bug 99215 + _fix_attachment_modification_date(); - # 2007-09-09 LpSolit@gmail.com - Bug 99215 - _fix_attachment_modification_date(); + $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx'); + _populate_bugs_fulltext(); - $dbh->bz_drop_index('longdescs', 'longdescs_thetext_idx'); - _populate_bugs_fulltext(); + # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292 + $dbh->bz_alter_column('series', 'query', {TYPE => 'MEDIUMTEXT', NOTNULL => 1}); - # 2008-01-18 xiaoou.wu@oracle.com - Bug 414292 - $dbh->bz_alter_column('series', 'query', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }); + # Add FK to multi select field tables + _add_foreign_keys_to_multiselects(); - # Add FK to multi select field tables - _add_foreign_keys_to_multiselects(); + # 2008-07-28 tfu@redhat.com - Bug 431669 + $dbh->bz_alter_column('group_control_map', 'product_id', + {TYPE => 'INT2', NOTNULL => 1}); - # 2008-07-28 tfu@redhat.com - Bug 431669 - $dbh->bz_alter_column('group_control_map', 'product_id', - { TYPE => 'INT2', NOTNULL => 1 }); + # 2008-09-07 LpSolit@gmail.com - Bug 452893 + _fix_illegal_flag_modification_dates(); - # 2008-09-07 LpSolit@gmail.com - Bug 452893 - _fix_illegal_flag_modification_dates(); + _add_visiblity_value_to_value_tables(); - _add_visiblity_value_to_value_tables(); + # 2009-03-02 arbingersys@gmail.com - Bug 423613 + _add_extern_id_index(); - # 2009-03-02 arbingersys@gmail.com - Bug 423613 - _add_extern_id_index(); + # 2009-03-31 LpSolit@gmail.com - Bug 478972 + $dbh->bz_alter_column('group_control_map', 'entry', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_alter_column('group_control_map', 'canedit', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2009-03-31 LpSolit@gmail.com - Bug 478972 - $dbh->bz_alter_column('group_control_map', 'entry', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_alter_column('group_control_map', 'canedit', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # 2009-01-16 oreomike@gmail.com - Bug 302420 + $dbh->bz_add_column('whine_events', 'mailifnobugs', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2009-01-16 oreomike@gmail.com - Bug 302420 - $dbh->bz_add_column('whine_events', 'mailifnobugs', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + _convert_disallownew_to_isactive(); - _convert_disallownew_to_isactive(); + $dbh->bz_alter_column('bugs_activity', 'added', {TYPE => 'varchar(255)'}); + $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']); - $dbh->bz_alter_column('bugs_activity', 'added', - { TYPE => 'varchar(255)' }); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_added_idx', ['added']); + # 2009-09-28 LpSolit@gmail.com - Bug 519032 + $dbh->bz_drop_column('series', 'last_viewed'); - # 2009-09-28 LpSolit@gmail.com - Bug 519032 - $dbh->bz_drop_column('series', 'last_viewed'); + # 2009-09-28 LpSolit@gmail.com - Bug 399073 + _fix_logincookies_ipaddr(); - # 2009-09-28 LpSolit@gmail.com - Bug 399073 - _fix_logincookies_ipaddr(); + # 2009-11-01 LpSolit@gmail.com - Bug 525025 + _fix_invalid_custom_field_names(); - # 2009-11-01 LpSolit@gmail.com - Bug 525025 - _fix_invalid_custom_field_names(); + _set_attachment_comment_types(); - _set_attachment_comment_types(); + $dbh->bz_drop_column('products', 'milestoneurl'); - $dbh->bz_drop_column('products', 'milestoneurl'); + _add_allows_unconfirmed_to_product_table(); + _convert_flagtypes_fks_to_set_null(); + _fix_decimal_types(); + _fix_series_creator_fk(); - _add_allows_unconfirmed_to_product_table(); - _convert_flagtypes_fks_to_set_null(); - _fix_decimal_types(); - _fix_series_creator_fk(); + # 2009-11-14 dkl@redhat.com - Bug 310450 + $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); - # 2009-11-14 dkl@redhat.com - Bug 310450 - $dbh->bz_add_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); + # 2010-04-07 LpSolit@gmail.com - Bug 69621 + $dbh->bz_drop_column('bugs', 'keywords'); - # 2010-04-07 LpSolit@gmail.com - Bug 69621 - $dbh->bz_drop_column('bugs', 'keywords'); + # 2010-05-07 ewong@pw-wspx.org - Bug 463945 + $dbh->bz_alter_column('group_control_map', 'membercontrol', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); + $dbh->bz_alter_column('group_control_map', 'othercontrol', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); - # 2010-05-07 ewong@pw-wspx.org - Bug 463945 - $dbh->bz_alter_column('group_control_map', 'membercontrol', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); - $dbh->bz_alter_column('group_control_map', 'othercontrol', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => CONTROLMAPNA}); + # Add NOT NULL to some columns that need it, and DEFAULT to + # attachments.ispatch. + $dbh->bz_alter_column('attachments', 'ispatch', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_alter_column('keyworddefs', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + $dbh->bz_alter_column('products', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); - # Add NOT NULL to some columns that need it, and DEFAULT to - # attachments.ispatch. - $dbh->bz_alter_column('attachments', 'ispatch', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - $dbh->bz_alter_column('keyworddefs', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); - $dbh->bz_alter_column('products', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); + # Change the default of allows_unconfirmed to TRUE as part + # of the new workflow. + $dbh->bz_alter_column('products', 'allows_unconfirmed', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - # Change the default of allows_unconfirmed to TRUE as part - # of the new workflow. - $dbh->bz_alter_column('products', 'allows_unconfirmed', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE' }); + # 2010-07-18 LpSolit@gmail.com - Bug 119703 + _remove_attachment_isurl(); - # 2010-07-18 LpSolit@gmail.com - Bug 119703 - _remove_attachment_isurl(); + # 2009-05-07 ghendricks@novell.com - Bug 77193 + _add_isactive_to_product_fields(); - # 2009-05-07 ghendricks@novell.com - Bug 77193 - _add_isactive_to_product_fields(); + # 2010-10-09 LpSolit@gmail.com - Bug 505165 + $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1}); - # 2010-10-09 LpSolit@gmail.com - Bug 505165 - $dbh->bz_alter_column('flags', 'setter_id', {TYPE => 'INT3', NOTNULL => 1}); + # 2010-10-09 LpSolit@gmail.com - Bug 451735 + _fix_series_indexes(); - # 2010-10-09 LpSolit@gmail.com - Bug 451735 - _fix_series_indexes(); + $dbh->bz_add_column('bug_see_also', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column('bug_see_also', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + _rename_tags_to_tag(); - _rename_tags_to_tag(); + # 2011-01-29 LpSolit@gmail.com - Bug 616185 + _migrate_user_tags(); - # 2011-01-29 LpSolit@gmail.com - Bug 616185 - _migrate_user_tags(); + _populate_bug_see_also_class(); - _populate_bug_see_also_class(); + # 2011-06-15 dkl@mozilla.com - Bug 658929 + _migrate_disabledtext_boolean(); - # 2011-06-15 dkl@mozilla.com - Bug 658929 - _migrate_disabledtext_boolean(); + # 2011-10-11 miketosh - Bug 690173 + _on_delete_set_null_for_audit_log_userid(); - # 2011-10-11 miketosh - Bug 690173 - _on_delete_set_null_for_audit_log_userid(); + # 2011-11-01 glob@mozilla.com - Bug 240437 + $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'}); - # 2011-11-01 glob@mozilla.com - Bug 240437 - $dbh->bz_add_column('profiles', 'last_seen_date', {TYPE => 'DATETIME'}); + # 2011-11-28 dkl@mozilla.com - Bug 685611 + _fix_notnull_defaults(); - # 2011-11-28 dkl@mozilla.com - Bug 685611 - _fix_notnull_defaults(); - - # 2012-02-15 LpSolit@gmail.com - Bug 722113 - if ($dbh->bz_index_info('profile_search', 'profile_search_user_id')) { - $dbh->bz_drop_index('profile_search', 'profile_search_user_id'); - $dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', [qw(user_id)]); - } + # 2012-02-15 LpSolit@gmail.com - Bug 722113 + if ($dbh->bz_index_info('profile_search', 'profile_search_user_id')) { + $dbh->bz_drop_index('profile_search', 'profile_search_user_id'); + $dbh->bz_add_index('profile_search', 'profile_search_user_id_idx', + [qw(user_id)]); + } - # 2012-06-06 dkl@mozilla.com - Bug 762288 - $dbh->bz_alter_column('bugs_activity', 'removed', - { TYPE => 'varchar(255)' }); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']); + # 2012-06-06 dkl@mozilla.com - Bug 762288 + $dbh->bz_alter_column('bugs_activity', 'removed', {TYPE => 'varchar(255)'}); + $dbh->bz_add_index('bugs_activity', 'bugs_activity_removed_idx', ['removed']); - # 2012-06-13 dkl@mozilla.com - Bug 764457 - $dbh->bz_add_column('bugs_activity', 'id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2012-06-13 dkl@mozilla.com - Bug 764457 + $dbh->bz_add_column('bugs_activity', 'id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2012-06-13 dkl@mozilla.com - Bug 764466 - $dbh->bz_add_column('profiles_activity', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2012-06-13 dkl@mozilla.com - Bug 764466 + $dbh->bz_add_column('profiles_activity', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2012-07-24 dkl@mozilla.com - Bug 776972 - # BMO - we change this to BIGSERIAL further down - #$dbh->bz_alter_column('bugs_activity', 'id', - # {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2012-07-24 dkl@mozilla.com - Bug 776972 + # BMO - we change this to BIGSERIAL further down + #$dbh->bz_alter_column('bugs_activity', 'id', + # {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - # 2012-07-24 dkl@mozilla.com - Bug 776982 - _fix_longdescs_primary_key(); + # 2012-07-24 dkl@mozilla.com - Bug 776982 + _fix_longdescs_primary_key(); - # 2012-08-02 dkl@mozilla.com - Bug 756953 - _fix_dependencies_dupes(); + # 2012-08-02 dkl@mozilla.com - Bug 756953 + _fix_dependencies_dupes(); - # 2013-02-04 dkl@mozilla.com - Bug 824346 - _fix_flagclusions_indexes(); + # 2013-02-04 dkl@mozilla.com - Bug 824346 + _fix_flagclusions_indexes(); - # 2012-04-15 Frank@Frank-Becker.de - Bug 740536 - $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']); + # 2012-04-15 Frank@Frank-Becker.de - Bug 740536 + $dbh->bz_add_index('audit_log', 'audit_log_class_idx', ['class', 'at_time']); - # 2013-08-16 glob@mozilla.com - Bug 905925 - $dbh->bz_add_index('attachments', 'attachments_ispatch_idx', ['ispatch']); + # 2013-08-16 glob@mozilla.com - Bug 905925 + $dbh->bz_add_index('attachments', 'attachments_ispatch_idx', ['ispatch']); - # 2014-06-09 dylan@mozilla.com - Bug 1022923 - $dbh->bz_add_index('bug_user_last_visit', - 'bug_user_last_visit_last_visit_ts_idx', - ['last_visit_ts']); + # 2014-06-09 dylan@mozilla.com - Bug 1022923 + $dbh->bz_add_index('bug_user_last_visit', + 'bug_user_last_visit_last_visit_ts_idx', + ['last_visit_ts']); - # 2014-07-14 sgreen@redhat.com - Bug 726696 - $dbh->bz_alter_column('tokens', 'tokentype', - {TYPE => 'varchar(16)', NOTNULL => 1}); + # 2014-07-14 sgreen@redhat.com - Bug 726696 + $dbh->bz_alter_column('tokens', 'tokentype', + {TYPE => 'varchar(16)', NOTNULL => 1}); - # 2014-07-27 LpSolit@gmail.com - Bug 1044561 - _fix_user_api_keys_indexes(); + # 2014-07-27 LpSolit@gmail.com - Bug 1044561 + _fix_user_api_keys_indexes(); - # 2018-06-14 dylan@mozilla.com - Bug 1468818 - $dbh->bz_add_column('longdescs', 'is_markdown', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + # 2018-06-14 dylan@mozilla.com - Bug 1468818 + $dbh->bz_add_column('longdescs', 'is_markdown', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - # 2014-10-?? dkl@mozilla.com - Bug 1062940 - $dbh->bz_alter_column('bugs', 'alias', { TYPE => 'varchar(40)' }); + # 2014-10-?? dkl@mozilla.com - Bug 1062940 + $dbh->bz_alter_column('bugs', 'alias', {TYPE => 'varchar(40)'}); - # 2015-05-13 dylan@mozilla.com - Bug 1160430 - $dbh->bz_add_column('keyworddefs', 'is_active', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + # 2015-05-13 dylan@mozilla.com - Bug 1160430 + $dbh->bz_add_column('keyworddefs', 'is_active', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->bz_add_column('user_api_keys', 'app_id', - {TYPE => 'varchar(64)'}); - $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_app_id_idx', - [qw(user_id app_id)]); + $dbh->bz_add_column('user_api_keys', 'app_id', {TYPE => 'varchar(64)'}); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_app_id_idx', + [qw(user_id app_id)]); - _add_attach_size(); + _add_attach_size(); - _fix_disable_mail(); + _fix_disable_mail(); - # 2015-07-25 dylan@mozilla.com - Bug 1179856 - $dbh->bz_alter_column('tokens', 'token', - {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1}); + # 2015-07-25 dylan@mozilla.com - Bug 1179856 + $dbh->bz_alter_column('tokens', 'token', + {TYPE => 'varchar(22)', NOTNULL => 1, PRIMARYKEY => 1}); - # 2015-08-20 dylan@mozilla.com - Bug 1196092 - $dbh->bz_alter_column('logincookies', 'cookie', - {TYPE => 'varchar(22)', NOTNULL => 1}); - $dbh->bz_add_index('logincookies', 'logincookies_cookie_idx', - {TYPE => 'UNIQUE', FIELDS => ['cookie']}); - $dbh->bz_add_column('logincookies', 'id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2015-08-20 dylan@mozilla.com - Bug 1196092 + $dbh->bz_alter_column('logincookies', 'cookie', + {TYPE => 'varchar(22)', NOTNULL => 1}); + $dbh->bz_add_index('logincookies', 'logincookies_cookie_idx', + {TYPE => 'UNIQUE', FIELDS => ['cookie']}); + $dbh->bz_add_column('logincookies', 'id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column('user_api_keys', 'last_used_ip', - {TYPE => 'varchar(40)'}); + $dbh->bz_add_column('user_api_keys', 'last_used_ip', {TYPE => 'varchar(40)'}); - _add_restrict_ipaddr(); + _add_restrict_ipaddr(); - $dbh->bz_add_column('profiles', 'password_change_required', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }); - $dbh->bz_add_column('profiles', 'password_change_reason', - { TYPE => 'varchar(64)' }); + $dbh->bz_add_column('profiles', 'password_change_required', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('profiles', 'password_change_reason', + {TYPE => 'varchar(64)'}); - $dbh->bz_add_column('profiles', 'mfa', { TYPE => 'varchar(8)', , DEFAULT => "''" }); + $dbh->bz_add_column('profiles', 'mfa', + {TYPE => 'varchar(8)',, DEFAULT => "''"}); - $dbh->bz_add_column('profiles', 'mfa_required_date', { TYPE => 'DATETIME' }); - _migrate_group_owners(); + $dbh->bz_add_column('profiles', 'mfa_required_date', {TYPE => 'DATETIME'}); + _migrate_group_owners(); - $dbh->bz_add_column('groups', 'idle_member_removal', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); + $dbh->bz_add_column('groups', 'idle_member_removal', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0'}); - _migrate_preference_categories(); + _migrate_preference_categories(); - # 2016-09-01 dkl@mozilla.com - Bug 1268317 - $dbh->bz_add_column('components', 'triage_owner_id', - {TYPE => 'INT3'}); + # 2016-09-01 dkl@mozilla.com - Bug 1268317 + $dbh->bz_add_column('components', 'triage_owner_id', {TYPE => 'INT3'}); - $dbh->bz_add_column('profiles', 'nickname', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); - $dbh->bz_add_index('profiles', 'profiles_nickname_idx', [qw(nickname)]); + $dbh->bz_add_column('profiles', 'nickname', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); + $dbh->bz_add_index('profiles', 'profiles_nickname_idx', [qw(nickname)]); - $dbh->bz_add_index('profiles', 'profiles_realname_ft_idx', - {TYPE => 'FULLTEXT', FIELDS => ['realname']}); - _migrate_nicknames(); + $dbh->bz_add_index('profiles', 'profiles_realname_ft_idx', + {TYPE => 'FULLTEXT', FIELDS => ['realname']}); + _migrate_nicknames(); - ################################################################ - # New --TABLE-- changes should go *** A B O V E *** this point # - ################################################################ + ################################################################ + # New --TABLE-- changes should go *** A B O V E *** this point # + ################################################################ - Bugzilla::Hook::process('install_update_db'); + Bugzilla::Hook::process('install_update_db'); - # We do this here because otherwise the foreign key from - # products.classification_id to classifications.id will fail - # (because products.classification_id defaults to "1", so on upgraded - # installations it's already been set before the first Classification - # exists). - Bugzilla::Install::create_default_classification(); + # We do this here because otherwise the foreign key from + # products.classification_id to classifications.id will fail + # (because products.classification_id defaults to "1", so on upgraded + # installations it's already been set before the first Classification + # exists). + Bugzilla::Install::create_default_classification(); - $dbh->bz_setup_foreign_keys(); + $dbh->bz_setup_foreign_keys(); } # Subroutines should be ordered in the order that they are called. # Thus, newer subroutines should be at the bottom. sub _update_pre_checksetup_bugzillas { - my $dbh = Bugzilla->dbh; - # really old fields that were added before checksetup.pl existed - # but aren't in very old bugzilla's (like 2.1) - # Steve Stock (sstock@iconnect-inc.com) - - $dbh->bz_add_column('bugs', 'target_milestone', - {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); - $dbh->bz_add_column('bugs', 'qa_contact', {TYPE => 'INT3'}); - $dbh->bz_add_column('bugs', 'status_whiteboard', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); - if (!$dbh->bz_column_info('products', 'isactive')){ - $dbh->bz_add_column('products', 'disallownew', - {TYPE => 'BOOLEAN', NOTNULL => 1}, 0); - } - - $dbh->bz_add_column('components', 'initialqacontact', - {TYPE => 'TINYTEXT'}); - $dbh->bz_add_column('components', 'description', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + my $dbh = Bugzilla->dbh; + + # really old fields that were added before checksetup.pl existed + # but aren't in very old bugzilla's (like 2.1) + # Steve Stock (sstock@iconnect-inc.com) + + $dbh->bz_add_column('bugs', 'target_milestone', + {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); + $dbh->bz_add_column('bugs', 'qa_contact', {TYPE => 'INT3'}); + $dbh->bz_add_column('bugs', 'status_whiteboard', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}); + if (!$dbh->bz_column_info('products', 'isactive')) { + $dbh->bz_add_column('products', 'disallownew', + {TYPE => 'BOOLEAN', NOTNULL => 1}, 0); + } + + $dbh->bz_add_column('components', 'initialqacontact', {TYPE => 'TINYTEXT'}); + $dbh->bz_add_column('components', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); } sub _add_bug_vote_cache { - my $dbh = Bugzilla->dbh; - # 1999-10-11 Restructured voting database to add a cached value in each - # bug recording how many total votes that bug has. While I'm at it, - # I removed the unused "area" field from the bugs database. It is - # distressing to realize that the bugs table has reached the maximum - # number of indices allowed by MySQL (16), which may make future - # enhancements awkward. - # (P.S. All is not lost; it appears that the latest betas of MySQL - # support a new table format which will allow 32 indices.) - - if ($dbh->bz_column_info('bugs', 'area')) { - $dbh->bz_drop_column('bugs', 'area'); - $dbh->bz_add_column('bugs', 'votes', {TYPE => 'INT3', NOTNULL => 1, - DEFAULT => 0}); - $dbh->bz_add_index('bugs', 'bugs_votes_idx', [qw(votes)]); - $dbh->bz_add_column('products', 'votesperuser', - {TYPE => 'INT2', NOTNULL => 1}, 0); - } + my $dbh = Bugzilla->dbh; + + # 1999-10-11 Restructured voting database to add a cached value in each + # bug recording how many total votes that bug has. While I'm at it, + # I removed the unused "area" field from the bugs database. It is + # distressing to realize that the bugs table has reached the maximum + # number of indices allowed by MySQL (16), which may make future + # enhancements awkward. + # (P.S. All is not lost; it appears that the latest betas of MySQL + # support a new table format which will allow 32 indices.) + + if ($dbh->bz_column_info('bugs', 'area')) { + $dbh->bz_drop_column('bugs', 'area'); + $dbh->bz_add_column('bugs', 'votes', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_index('bugs', 'bugs_votes_idx', [qw(votes)]); + $dbh->bz_add_column('products', 'votesperuser', {TYPE => 'INT2', NOTNULL => 1}, + 0); + } } sub _update_product_name_definition { - my $dbh = Bugzilla->dbh; - # The product name used to be very different in various tables. - # - # It was varchar(16) in bugs - # tinytext in components - # tinytext in products - # tinytext in versions - # - # tinytext is equivalent to varchar(255), which is quite huge, so I change - # them all to varchar(64). - - # Only do this if these fields still exist - they're removed in - # a later change - if ($dbh->bz_column_info('products', 'product')) { - $dbh->bz_alter_column('bugs', 'product', - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_alter_column('components', 'program', {TYPE => 'varchar(64)'}); - $dbh->bz_alter_column('products', 'product', {TYPE => 'varchar(64)'}); - $dbh->bz_alter_column('versions', 'program', - {TYPE => 'varchar(64)', NOTNULL => 1}); - } + my $dbh = Bugzilla->dbh; + + # The product name used to be very different in various tables. + # + # It was varchar(16) in bugs + # tinytext in components + # tinytext in products + # tinytext in versions + # + # tinytext is equivalent to varchar(255), which is quite huge, so I change + # them all to varchar(64). + + # Only do this if these fields still exist - they're removed in + # a later change + if ($dbh->bz_column_info('products', 'product')) { + $dbh->bz_alter_column('bugs', 'product', {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_alter_column('components', 'program', {TYPE => 'varchar(64)'}); + $dbh->bz_alter_column('products', 'product', {TYPE => 'varchar(64)'}); + $dbh->bz_alter_column('versions', 'program', + {TYPE => 'varchar(64)', NOTNULL => 1}); + } } # A helper for the function below. sub _write_one_longdesc { - my ($id, $who, $when, $buffer) = (@_); - my $dbh = Bugzilla->dbh; - $buffer = trim($buffer); - return if !$buffer; - $dbh->do("INSERT INTO longdescs (bug_id, who, bug_when, thetext) + my ($id, $who, $when, $buffer) = (@_); + my $dbh = Bugzilla->dbh; + $buffer = trim($buffer); + return if !$buffer; + $dbh->do( + "INSERT INTO longdescs (bug_id, who, bug_when, thetext) VALUES (?,?,?,?)", undef, $id, $who, - time2str("%Y/%m/%d %H:%M:%S", $when), $buffer); + time2str("%Y/%m/%d %H:%M:%S", $when), $buffer + ); } sub _populate_longdescs { - my $dbh = Bugzilla->dbh; - # 2000-01-20 Added a new "longdescs" table, which is supposed to have - # all the long descriptions in it, replacing the old long_desc field - # in the bugs table. The below hideous code populates this new table - # with things from the old field, with ugly parsing and heuristics. - - if ($dbh->bz_column_info('bugs', 'long_desc')) { - my ($total) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs"); - - print "Populating new long_desc table. This is slow. There are", - " $total\nbugs to process; a line of dots will be printed", - " for each 50.\n\n"; - local $| = 1; - - # On MySQL, longdescs doesn't benefit from transactions, but this - # doesn't hurt. - $dbh->bz_start_transaction(); - - $dbh->do('DELETE FROM longdescs'); - - my $sth = $dbh->prepare("SELECT bug_id, creation_ts, reporter, - long_desc FROM bugs ORDER BY bug_id"); - $sth->execute(); - my $count = 0; - while (my ($id, $createtime, $reporterid, $desc) = - $sth->fetchrow_array()) + my $dbh = Bugzilla->dbh; + + # 2000-01-20 Added a new "longdescs" table, which is supposed to have + # all the long descriptions in it, replacing the old long_desc field + # in the bugs table. The below hideous code populates this new table + # with things from the old field, with ugly parsing and heuristics. + + if ($dbh->bz_column_info('bugs', 'long_desc')) { + my ($total) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs"); + + print "Populating new long_desc table. This is slow. There are", + " $total\nbugs to process; a line of dots will be printed", + " for each 50.\n\n"; + local $| = 1; + + # On MySQL, longdescs doesn't benefit from transactions, but this + # doesn't hurt. + $dbh->bz_start_transaction(); + + $dbh->do('DELETE FROM longdescs'); + + my $sth = $dbh->prepare( + "SELECT bug_id, creation_ts, reporter, + long_desc FROM bugs ORDER BY bug_id" + ); + $sth->execute(); + my $count = 0; + while (my ($id, $createtime, $reporterid, $desc) = $sth->fetchrow_array()) { + $count++; + indicate_progress({total => $total, current => $count}); + $desc =~ s/\r//g; + my $who = $reporterid; + my $when = str2time($createtime); + my $buffer = ""; + foreach my $line (split(/\n/, $desc)) { + $line =~ s/\s+$//g; # Trim trailing whitespace. + if ($line =~ /^------- Additional Comments From ([^\s]+)\s+(\d.+\d)\s+-------$/) { - $count++; - indicate_progress({ total => $total, current => $count }); - $desc =~ s/\r//g; - my $who = $reporterid; - my $when = str2time($createtime); - my $buffer = ""; - foreach my $line (split(/\n/, $desc)) { - $line =~ s/\s+$//g; # Trim trailing whitespace. - if ($line =~ /^------- Additional Comments From ([^\s]+)\s+(\d.+\d)\s+-------$/) - { - my $name = $1; - my $date = str2time($2); - # Oy, what a hack. The creation time is accurate to the - # second. But the long text only contains things accurate - # to the And so, if someone makes a comment within a - # minute of the original bug creation, then the comment can - # come *before* the bug creation. So, we add 59 seconds to - # the time of all comments, so that they are always - # considered to have happened at the *end* of the given - # minute, not the beginning. - $date += 59; - if ($date >= $when) { - _write_one_longdesc($id, $who, $when, $buffer); - $buffer = ""; - $when = $date; - my $s2 = $dbh->prepare("SELECT userid FROM profiles " . - "WHERE login_name = ?"); - $s2->execute($name); - ($who) = ($s2->fetchrow_array()); - - if (!$who) { - # This username doesn't exist. Maybe someone - # renamed him or something. Invent a new profile - # entry disabled, just to represent him. - $dbh->do("INSERT INTO profiles (login_name, + my $name = $1; + my $date = str2time($2); + + # Oy, what a hack. The creation time is accurate to the + # second. But the long text only contains things accurate + # to the And so, if someone makes a comment within a + # minute of the original bug creation, then the comment can + # come *before* the bug creation. So, we add 59 seconds to + # the time of all comments, so that they are always + # considered to have happened at the *end* of the given + # minute, not the beginning. + $date += 59; + if ($date >= $when) { + _write_one_longdesc($id, $who, $when, $buffer); + $buffer = ""; + $when = $date; + my $s2 = $dbh->prepare("SELECT userid FROM profiles " . "WHERE login_name = ?"); + $s2->execute($name); + ($who) = ($s2->fetchrow_array()); + + if (!$who) { + + # This username doesn't exist. Maybe someone + # renamed him or something. Invent a new profile + # entry disabled, just to represent him. + $dbh->do( + "INSERT INTO profiles (login_name, cryptpassword, disabledtext) VALUES (?,?,?)", undef, $name, '*', - "Account created only to maintain" - . " database integrity"); - $who = $dbh->bz_last_key('profiles', 'userid'); - } - next; - } - } - $buffer .= $line . "\n"; + "Account created only to maintain" . " database integrity" + ); + $who = $dbh->bz_last_key('profiles', 'userid'); } - _write_one_longdesc($id, $who, $when, $buffer); - } # while loop + next; + } + } + $buffer .= $line . "\n"; + } + _write_one_longdesc($id, $who, $when, $buffer); + } # while loop - print "\n\n"; - $dbh->bz_drop_column('bugs', 'long_desc'); - $dbh->bz_commit_transaction(); - } # main if + print "\n\n"; + $dbh->bz_drop_column('bugs', 'long_desc'); + $dbh->bz_commit_transaction(); + } # main if } sub _update_bugs_activity_field_to_fieldid { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - # 2000-01-18 Added a new table fielddefs that records information about the - # different fields we keep an activity log on. The bugs_activity table - # now has a pointer into that table instead of recording the name directly. - if ($dbh->bz_column_info('bugs_activity', 'field')) { - $dbh->bz_add_column('bugs_activity', 'fieldid', - {TYPE => 'INT3', NOTNULL => 1}, 0); + # 2000-01-18 Added a new table fielddefs that records information about the + # different fields we keep an activity log on. The bugs_activity table + # now has a pointer into that table instead of recording the name directly. + if ($dbh->bz_column_info('bugs_activity', 'field')) { + $dbh->bz_add_column('bugs_activity', 'fieldid', {TYPE => 'INT3', NOTNULL => 1}, + 0); - $dbh->bz_add_index('bugs_activity', 'bugs_activity_fieldid_idx', - [qw(fieldid)]); - print "Populating new bugs_activity.fieldid field...\n"; + $dbh->bz_add_index('bugs_activity', 'bugs_activity_fieldid_idx', [qw(fieldid)]); + print "Populating new bugs_activity.fieldid field...\n"; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - my $ids = $dbh->selectall_arrayref( - 'SELECT DISTINCT fielddefs.id, bugs_activity.field + my $ids = $dbh->selectall_arrayref( + 'SELECT DISTINCT fielddefs.id, bugs_activity.field FROM bugs_activity LEFT JOIN fielddefs - ON bugs_activity.field = fielddefs.name', {Slice=>{}}); - - foreach my $item (@$ids) { - my $id = $item->{id}; - my $field = $item->{field}; - # If the id is NULL - if (!$id) { - $dbh->do("INSERT INTO fielddefs (name, description) VALUES " . - "(?, ?)", undef, $field, $field); - $id = $dbh->bz_last_key('fielddefs', 'id'); - } - $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE field = ?", - undef, $id, $field); - } - $dbh->bz_commit_transaction(); + ON bugs_activity.field = fielddefs.name', {Slice => {}} + ); - $dbh->bz_drop_column('bugs_activity', 'field'); + foreach my $item (@$ids) { + my $id = $item->{id}; + my $field = $item->{field}; + + # If the id is NULL + if (!$id) { + $dbh->do("INSERT INTO fielddefs (name, description) VALUES " . "(?, ?)", + undef, $field, $field); + $id = $dbh->bz_last_key('fielddefs', 'id'); + } + $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE field = ?", + undef, $id, $field); } + $dbh->bz_commit_transaction(); + + $dbh->bz_drop_column('bugs_activity', 'field'); + } } sub _add_unique_login_name_index_to_profiles { - my $dbh = Bugzilla->dbh; - - # 2000-01-22 The "login_name" field in the "profiles" table was not - # declared to be unique. Sure enough, somehow, I got 22 duplicated entries - # in my database. This code detects that, cleans up the duplicates, and - # then tweaks the table to declare the field to be unique. What a pain. - if (!$dbh->bz_index_info('profiles', 'profiles_login_name_idx') - || !$dbh->bz_index_info('profiles', 'profiles_login_name_idx')->{TYPE}) - { - print "Searching for duplicate entries in the profiles table...\n"; - while (1) { - # This code is weird in that it loops around and keeps doing this - # select again. That's because I'm paranoid about deleting entries - # out from under us in the profiles table. Things get weird if - # there are *three* or more entries for the same user... - my $sth = $dbh->prepare("SELECT p1.userid, p2.userid, p1.login_name + my $dbh = Bugzilla->dbh; + + # 2000-01-22 The "login_name" field in the "profiles" table was not + # declared to be unique. Sure enough, somehow, I got 22 duplicated entries + # in my database. This code detects that, cleans up the duplicates, and + # then tweaks the table to declare the field to be unique. What a pain. + if ( !$dbh->bz_index_info('profiles', 'profiles_login_name_idx') + || !$dbh->bz_index_info('profiles', 'profiles_login_name_idx')->{TYPE}) + { + print "Searching for duplicate entries in the profiles table...\n"; + while (1) { + + # This code is weird in that it loops around and keeps doing this + # select again. That's because I'm paranoid about deleting entries + # out from under us in the profiles table. Things get weird if + # there are *three* or more entries for the same user... + my $sth = $dbh->prepare( + "SELECT p1.userid, p2.userid, p1.login_name FROM profiles AS p1, profiles AS p2 WHERE p1.userid < p2.userid AND p1.login_name = p2.login_name - ORDER BY p1.login_name"); - $sth->execute(); - my ($u1, $u2, $n) = ($sth->fetchrow_array); - last if !$u1; - - print "Both $u1 & $u2 are ids for $n! Merging $u2 into $u1...\n"; - foreach my $i (["bugs", "reporter"], - ["bugs", "assigned_to"], - ["bugs", "qa_contact"], - ["attachments", "submitter_id"], - ["bugs_activity", "who"], - ["cc", "who"], - ["votes", "who"], - ["longdescs", "who"]) { - my ($table, $field) = (@$i); - if ($dbh->bz_table_info($table)) { - print " Updating $table.$field...\n"; - $dbh->do("UPDATE $table SET $field = $u1 " . - "WHERE $field = $u2"); - } - } - $dbh->do("DELETE FROM profiles WHERE userid = $u2"); + ORDER BY p1.login_name" + ); + $sth->execute(); + my ($u1, $u2, $n) = ($sth->fetchrow_array); + last if !$u1; + + print "Both $u1 & $u2 are ids for $n! Merging $u2 into $u1...\n"; + foreach my $i ( + ["bugs", "reporter"], + ["bugs", "assigned_to"], + ["bugs", "qa_contact"], + ["attachments", "submitter_id"], + ["bugs_activity", "who"], + ["cc", "who"], + ["votes", "who"], + ["longdescs", "who"] + ) + { + my ($table, $field) = (@$i); + if ($dbh->bz_table_info($table)) { + print " Updating $table.$field...\n"; + $dbh->do("UPDATE $table SET $field = $u1 " . "WHERE $field = $u2"); } - print "OK, changing index type to prevent duplicates in the", - " future...\n"; - - $dbh->bz_drop_index('profiles', 'profiles_login_name_idx'); - $dbh->bz_add_index('profiles', 'profiles_login_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(login_name)]}); + } + $dbh->do("DELETE FROM profiles WHERE userid = $u2"); } + print "OK, changing index type to prevent duplicates in the", " future...\n"; + + $dbh->bz_drop_index('profiles', 'profiles_login_name_idx'); + $dbh->bz_add_index('profiles', 'profiles_login_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(login_name)]}); + } } sub _update_component_user_fields_to_ids { - my $dbh = Bugzilla->dbh; - - # components.initialowner - my $comp_init_owner = $dbh->bz_column_info('components', 'initialowner'); - if ($comp_init_owner && $comp_init_owner->{TYPE} eq 'TINYTEXT') { - my $sth = $dbh->prepare("SELECT program, value, initialowner - FROM components"); - $sth->execute(); - while (my ($program, $value, $initialowner) = $sth->fetchrow_array()) { - my ($id) = $dbh->selectrow_array( - "SELECT userid FROM profiles WHERE login_name = ?", - undef, $initialowner); - - unless (defined $id) { - print "Warning: You have an invalid default assignee", - " '$initialowner'\n in component '$value' of program", - " '$program'!\n"; - $id = 0; - } + my $dbh = Bugzilla->dbh; - $dbh->do("UPDATE components SET initialowner = ? - WHERE program = ? AND value = ?", undef, - $id, $program, $value); - } - $dbh->bz_alter_column('components','initialowner',{TYPE => 'INT3'}); + # components.initialowner + my $comp_init_owner = $dbh->bz_column_info('components', 'initialowner'); + if ($comp_init_owner && $comp_init_owner->{TYPE} eq 'TINYTEXT') { + my $sth = $dbh->prepare( + "SELECT program, value, initialowner + FROM components" + ); + $sth->execute(); + while (my ($program, $value, $initialowner) = $sth->fetchrow_array()) { + my ($id) + = $dbh->selectrow_array("SELECT userid FROM profiles WHERE login_name = ?", + undef, $initialowner); + + unless (defined $id) { + print "Warning: You have an invalid default assignee", + " '$initialowner'\n in component '$value' of program", " '$program'!\n"; + $id = 0; + } + + $dbh->do( + "UPDATE components SET initialowner = ? + WHERE program = ? AND value = ?", undef, $id, $program, $value + ); } + $dbh->bz_alter_column('components', 'initialowner', {TYPE => 'INT3'}); + } - # components.initialqacontact - my $comp_init_qa = $dbh->bz_column_info('components', 'initialqacontact'); - if ($comp_init_qa && $comp_init_qa->{TYPE} eq 'TINYTEXT') { - my $sth = $dbh->prepare("SELECT program, value, initialqacontact - FROM components"); - $sth->execute(); - while (my ($program, $value, $initialqacontact) = - $sth->fetchrow_array()) - { - my ($id) = $dbh->selectrow_array( - "SELECT userid FROM profiles WHERE login_name = ?", - undef, $initialqacontact); - - unless (defined $id) { - if ($initialqacontact) { - print "Warning: You have an invalid default QA contact", - " $initialqacontact' in program '$program',", - " component '$value'!\n"; - } - $id = 0; - } - - $dbh->do("UPDATE components SET initialqacontact = ? - WHERE program = ? AND value = ?", undef, - $id, $program, $value); + # components.initialqacontact + my $comp_init_qa = $dbh->bz_column_info('components', 'initialqacontact'); + if ($comp_init_qa && $comp_init_qa->{TYPE} eq 'TINYTEXT') { + my $sth = $dbh->prepare( + "SELECT program, value, initialqacontact + FROM components" + ); + $sth->execute(); + while (my ($program, $value, $initialqacontact) = $sth->fetchrow_array()) { + my ($id) + = $dbh->selectrow_array("SELECT userid FROM profiles WHERE login_name = ?", + undef, $initialqacontact); + + unless (defined $id) { + if ($initialqacontact) { + print "Warning: You have an invalid default QA contact", + " $initialqacontact' in program '$program',", " component '$value'!\n"; } + $id = 0; + } - $dbh->bz_alter_column('components','initialqacontact',{TYPE => 'INT3'}); + $dbh->do( + "UPDATE components SET initialqacontact = ? + WHERE program = ? AND value = ?", undef, $id, $program, $value + ); } + + $dbh->bz_alter_column('components', 'initialqacontact', {TYPE => 'INT3'}); + } } sub _populate_milestones_table { - my $dbh = Bugzilla->dbh; - # 2000-03-21 Adding a table for target milestones to - # database - matthew@zeroknowledge.com - # If the milestones table is empty, and we're still back in a Bugzilla - # that has a bugs.product field, that means that we just created - # the milestones table and it needs to be populated. - my $milestones_exist = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM milestones"); - if (!$milestones_exist && $dbh->bz_column_info('bugs', 'product')) { - print "Replacing blank milestones...\n"; - - $dbh->do("UPDATE bugs + my $dbh = Bugzilla->dbh; + + # 2000-03-21 Adding a table for target milestones to + # database - matthew@zeroknowledge.com + # If the milestones table is empty, and we're still back in a Bugzilla + # that has a bugs.product field, that means that we just created + # the milestones table and it needs to be populated. + my $milestones_exist + = $dbh->selectrow_array("SELECT DISTINCT 1 FROM milestones"); + if (!$milestones_exist && $dbh->bz_column_info('bugs', 'product')) { + print "Replacing blank milestones...\n"; + + $dbh->do( + "UPDATE bugs SET target_milestone = '---' - WHERE target_milestone = ' '"); - - # If we are upgrading from 2.8 or earlier, we will have *created* - # the milestones table with a product_id field, but Bugzilla expects - # it to have a "product" field. So we change the field backward so - # other code can run. The change will be reversed later in checksetup. - if ($dbh->bz_column_info('milestones', 'product_id')) { - # Dropping the column leaves us with a milestones_product_id_idx - # index that is only on the "value" column. We need to drop the - # whole index so that it can be correctly re-created later. - $dbh->bz_drop_index('milestones', 'milestones_product_id_idx'); - $dbh->bz_drop_column('milestones', 'product_id'); - $dbh->bz_add_column('milestones', 'product', - {TYPE => 'varchar(64)', NOTNULL => 1}, ''); - } + WHERE target_milestone = ' '" + ); + + # If we are upgrading from 2.8 or earlier, we will have *created* + # the milestones table with a product_id field, but Bugzilla expects + # it to have a "product" field. So we change the field backward so + # other code can run. The change will be reversed later in checksetup. + if ($dbh->bz_column_info('milestones', 'product_id')) { + + # Dropping the column leaves us with a milestones_product_id_idx + # index that is only on the "value" column. We need to drop the + # whole index so that it can be correctly re-created later. + $dbh->bz_drop_index('milestones', 'milestones_product_id_idx'); + $dbh->bz_drop_column('milestones', 'product_id'); + $dbh->bz_add_column('milestones', 'product', + {TYPE => 'varchar(64)', NOTNULL => 1}, ''); + } - # Populate the milestone table with all existing values in the database - my $sth = $dbh->prepare("SELECT DISTINCT target_milestone, product - FROM bugs"); - $sth->execute(); + # Populate the milestone table with all existing values in the database + my $sth = $dbh->prepare( + "SELECT DISTINCT target_milestone, product + FROM bugs" + ); + $sth->execute(); - print "Populating milestones table...\n"; + print "Populating milestones table...\n"; - while (my ($value, $product) = $sth->fetchrow_array()) { - # check if the value already exists - my $sortkey = substr($value, 1); - if ($sortkey !~ /^\d+$/) { - $sortkey = 0; - } else { - $sortkey *= 10; - } - my $ms_exists = $dbh->selectrow_array( - "SELECT value FROM milestones - WHERE value = ? AND product = ?", undef, $value, $product); + while (my ($value, $product) = $sth->fetchrow_array()) { - if (!$ms_exists) { - $dbh->do("INSERT INTO milestones(value, product, sortkey) - VALUES (?,?,?)", undef, $value, $product, $sortkey); - } - } + # check if the value already exists + my $sortkey = substr($value, 1); + if ($sortkey !~ /^\d+$/) { + $sortkey = 0; + } + else { + $sortkey *= 10; + } + my $ms_exists = $dbh->selectrow_array( + "SELECT value FROM milestones + WHERE value = ? AND product = ?", undef, $value, $product + ); + + if (!$ms_exists) { + $dbh->do( + "INSERT INTO milestones(value, product, sortkey) + VALUES (?,?,?)", undef, $value, $product, $sortkey + ); + } } + } } sub _add_products_defaultmilestone { - my $dbh = Bugzilla->dbh; - - # 2000-03-23 Added a defaultmilestone field to the products table, so that - # we know which milestone to initially assign bugs to. - if (!$dbh->bz_column_info('products', 'defaultmilestone')) { - $dbh->bz_add_column('products', 'defaultmilestone', - {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); - my $sth = $dbh->prepare( - "SELECT product, defaultmilestone FROM products"); - $sth->execute(); - while (my ($product, $default_ms) = $sth->fetchrow_array()) { - my $exists = $dbh->selectrow_array( - "SELECT value FROM milestones - WHERE value = ? AND product = ?", - undef, $default_ms, $product); - if (!$exists) { - $dbh->do("INSERT INTO milestones(value, product) " . - "VALUES (?, ?)", undef, $default_ms, $product); - } - } + my $dbh = Bugzilla->dbh; + + # 2000-03-23 Added a defaultmilestone field to the products table, so that + # we know which milestone to initially assign bugs to. + if (!$dbh->bz_column_info('products', 'defaultmilestone')) { + $dbh->bz_add_column('products', 'defaultmilestone', + {TYPE => 'varchar(20)', NOTNULL => 1, DEFAULT => "'---'"}); + my $sth = $dbh->prepare("SELECT product, defaultmilestone FROM products"); + $sth->execute(); + while (my ($product, $default_ms) = $sth->fetchrow_array()) { + my $exists = $dbh->selectrow_array( + "SELECT value FROM milestones + WHERE value = ? AND product = ?", undef, $default_ms, $product + ); + if (!$exists) { + $dbh->do("INSERT INTO milestones(value, product) " . "VALUES (?, ?)", + undef, $default_ms, $product); + } } + } } sub _copy_from_comments_to_longdescs { - my $dbh = Bugzilla->dbh; - # 2000-11-27 For Bugzilla 2.5 and later. Copy data from 'comments' to - # 'longdescs' - the new name of the comments table. - if ($dbh->bz_table_info('comments')) { - print "Copying data from 'comments' to 'longdescs'...\n"; - my $quoted_when = $dbh->quote_identifier('when'); - $dbh->do("INSERT INTO longdescs (bug_when, bug_id, who, thetext) + my $dbh = Bugzilla->dbh; + + # 2000-11-27 For Bugzilla 2.5 and later. Copy data from 'comments' to + # 'longdescs' - the new name of the comments table. + if ($dbh->bz_table_info('comments')) { + print "Copying data from 'comments' to 'longdescs'...\n"; + my $quoted_when = $dbh->quote_identifier('when'); + $dbh->do( + "INSERT INTO longdescs (bug_when, bug_id, who, thetext) SELECT $quoted_when, bug_id, who, comment - FROM comments"); - $dbh->bz_drop_table("comments"); - } + FROM comments" + ); + $dbh->bz_drop_table("comments"); + } } sub _populate_duplicates_table { - my $dbh = Bugzilla->dbh; - # 2000-07-15 Added duplicates table so Bugzilla tracks duplicates in a - # better way than it used to. This code searches the comments to populate - # the table initially. It's executed if the table is empty; if it's - # empty because there are no dupes (as opposed to having just created - # the table) it won't have any effect anyway, so it doesn't matter. - my ($dups_exist) = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM duplicates"); - # We also check against a schema change that happened later. - if (!$dups_exist && !$dbh->bz_column_info('groups', 'isactive')) { - # populate table - print "Populating duplicates table from comments...\n"; - - my $sth = $dbh->prepare( - "SELECT longdescs.bug_id, thetext + my $dbh = Bugzilla->dbh; + + # 2000-07-15 Added duplicates table so Bugzilla tracks duplicates in a + # better way than it used to. This code searches the comments to populate + # the table initially. It's executed if the table is empty; if it's + # empty because there are no dupes (as opposed to having just created + # the table) it won't have any effect anyway, so it doesn't matter. + my ($dups_exist) = $dbh->selectrow_array("SELECT DISTINCT 1 FROM duplicates"); + + # We also check against a schema change that happened later. + if (!$dups_exist && !$dbh->bz_column_info('groups', 'isactive')) { + + # populate table + print "Populating duplicates table from comments...\n"; + + my $sth = $dbh->prepare( + "SELECT longdescs.bug_id, thetext FROM longdescs LEFT JOIN bugs ON longdescs.bug_id = bugs.bug_id - WHERE (" . $dbh->sql_regexp("thetext", - "'[.*.]{3} This bug has been marked as a duplicate" - . " of [[:digit:]]+ [.*.]{3}'") - . ") + WHERE (" + . $dbh->sql_regexp("thetext", + "'[.*.]{3} This bug has been marked as a duplicate" + . " of [[:digit:]]+ [.*.]{3}'") + . ") AND resolution = 'DUPLICATE' - ORDER BY longdescs.bug_when"); - $sth->execute(); - - my (%dupes, $key); - # Because of the way hashes work, this loop removes all but the - # last dupe resolution found for a given bug. - while (my ($dupe, $dupe_of) = $sth->fetchrow_array()) { - $dupes{$dupe} = $dupe_of; - } + ORDER BY longdescs.bug_when" + ); + $sth->execute(); - foreach $key (keys(%dupes)){ - $dupes{$key} =~ /^.*\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*$/ms; - $dupes{$key} = $1; - $dbh->do("INSERT INTO duplicates VALUES(?, ?)", undef, - $dupes{$key}, $key); - # BugItsADupeOf Dupe - } + my (%dupes, $key); + + # Because of the way hashes work, this loop removes all but the + # last dupe resolution found for a given bug. + while (my ($dupe, $dupe_of) = $sth->fetchrow_array()) { + $dupes{$dupe} = $dupe_of; } + + foreach $key (keys(%dupes)) { + $dupes{$key} + =~ /^.*\*\*\* This bug has been marked as a duplicate of (\d+) \*\*\*$/ms; + $dupes{$key} = $1; + $dbh->do("INSERT INTO duplicates VALUES(?, ?)", undef, $dupes{$key}, $key); + + # BugItsADupeOf Dupe + } + } } sub _recrypt_plaintext_passwords { - my $dbh = Bugzilla->dbh; - # 2001-06-12; myk@mozilla.org; bugs 74032, 77473: - # Recrypt passwords using Perl &crypt instead of the mysql equivalent - # and delete plaintext passwords from the database. - if ($dbh->bz_column_info('profiles', 'password')) { + my $dbh = Bugzilla->dbh; - print <bz_column_info('profiles', 'password')) { + + print <selectrow_array('SELECT COUNT(*) FROM profiles'); - my $sth = $dbh->prepare("SELECT userid, password FROM profiles"); - $sth->execute(); - - my $i = 1; + # Re-crypt everyone's password. + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); + my $sth = $dbh->prepare("SELECT userid, password FROM profiles"); + $sth->execute(); - print "Fixing passwords...\n"; - while (my ($userid, $password) = $sth->fetchrow_array()) { - my $cryptpassword = $dbh->quote(bz_crypt($password)); - $dbh->do("UPDATE profiles " . - "SET cryptpassword = $cryptpassword " . - "WHERE userid = $userid"); - indicate_progress({ total => $total, current => $i, every => 10 }); - } - print "\n"; + my $i = 1; - # Drop the plaintext password field. - $dbh->bz_drop_column('profiles', 'password'); + print "Fixing passwords...\n"; + while (my ($userid, $password) = $sth->fetchrow_array()) { + my $cryptpassword = $dbh->quote(bz_crypt($password)); + $dbh->do("UPDATE profiles " + . "SET cryptpassword = $cryptpassword " + . "WHERE userid = $userid"); + indicate_progress({total => $total, current => $i, every => 10}); } + print "\n"; + + # Drop the plaintext password field. + $dbh->bz_drop_column('profiles', 'password'); + } } sub _update_bugs_activity_to_only_record_changes { - my $dbh = Bugzilla->dbh; - # 2001-07-20 jake@bugzilla.org - Change bugs_activity to only record changes - # http://bugzilla.mozilla.org/show_bug.cgi?id=55161 - if ($dbh->bz_column_info('bugs_activity', 'oldvalue')) { - $dbh->bz_add_column("bugs_activity", "removed", {TYPE => "TINYTEXT"}); - $dbh->bz_add_column("bugs_activity", "added", {TYPE => "TINYTEXT"}); - - # Need to get field id's for the fields that have multiple values - my @multi; - foreach my $f ("cc", "dependson", "blocked", "keywords") { - my $sth = $dbh->prepare("SELECT id " . - "FROM fielddefs " . - "WHERE name = '$f'"); - $sth->execute(); - my ($fid) = $sth->fetchrow_array(); - push (@multi, $fid); + my $dbh = Bugzilla->dbh; + + # 2001-07-20 jake@bugzilla.org - Change bugs_activity to only record changes + # http://bugzilla.mozilla.org/show_bug.cgi?id=55161 + if ($dbh->bz_column_info('bugs_activity', 'oldvalue')) { + $dbh->bz_add_column("bugs_activity", "removed", {TYPE => "TINYTEXT"}); + $dbh->bz_add_column("bugs_activity", "added", {TYPE => "TINYTEXT"}); + + # Need to get field id's for the fields that have multiple values + my @multi; + foreach my $f ("cc", "dependson", "blocked", "keywords") { + my $sth = $dbh->prepare("SELECT id " . "FROM fielddefs " . "WHERE name = '$f'"); + $sth->execute(); + my ($fid) = $sth->fetchrow_array(); + push(@multi, $fid); + } + + # Now we need to process the bugs_activity table and reformat the data + print "Fixing activity log...\n"; + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs_activity'); + my $sth = $dbh->prepare( + "SELECT bug_id, who, bug_when, fieldid, + oldvalue, newvalue FROM bugs_activity" + ); + $sth->execute; + my $i = 0; + while (my ($bug_id, $who, $bug_when, $fieldid, $oldvalue, $newvalue) + = $sth->fetchrow_array()) + { + $i++; + indicate_progress({total => $total, current => $i, every => 10}); + + # Make sure (old|new)value isn't null (to suppress warnings) + $oldvalue ||= ""; + $newvalue ||= ""; + my ($added, $removed) = ""; + if (grep ($_ eq $fieldid, @multi)) { + $oldvalue =~ s/[\s,]+/ /g; + $newvalue =~ s/[\s,]+/ /g; + my @old = split(" ", $oldvalue); + my @new = split(" ", $newvalue); + my (@add, @remove) = (); + + # Find values that were "added" + foreach my $value (@new) { + if (!grep ($_ eq $value, @old)) { + push(@add, $value); + } } - # Now we need to process the bugs_activity table and reformat the data - print "Fixing activity log...\n"; - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs_activity'); - my $sth = $dbh->prepare("SELECT bug_id, who, bug_when, fieldid, - oldvalue, newvalue FROM bugs_activity"); - $sth->execute; - my $i = 0; - while (my ($bug_id, $who, $bug_when, $fieldid, $oldvalue, $newvalue) - = $sth->fetchrow_array()) - { - $i++; - indicate_progress({ total => $total, current => $i, every => 10 }); - # Make sure (old|new)value isn't null (to suppress warnings) - $oldvalue ||= ""; - $newvalue ||= ""; - my ($added, $removed) = ""; - if (grep ($_ eq $fieldid, @multi)) { - $oldvalue =~ s/[\s,]+/ /g; - $newvalue =~ s/[\s,]+/ /g; - my @old = split(" ", $oldvalue); - my @new = split(" ", $newvalue); - my (@add, @remove) = (); - # Find values that were "added" - foreach my $value(@new) { - if (! grep ($_ eq $value, @old)) { - push (@add, $value); - } - } - # Find values that were removed - foreach my $value(@old) { - if (! grep ($_ eq $value, @new)) { - push (@remove, $value); - } - } - $added = join (", ", @add); - $removed = join (", ", @remove); - # If we can't determine what changed, put a ? in both fields - unless ($added || $removed) { - $added = "?"; - $removed = "?"; - } - # If the original field (old|new)value was full, then this - # could be incomplete data. - if (length($oldvalue) == 255 || length($newvalue) == 255) { - $added = "? $added"; - $removed = "? $removed"; - } - } else { - $removed = $oldvalue; - $added = $newvalue; - } - $added = $dbh->quote($added); - $removed = $dbh->quote($removed); - $dbh->do("UPDATE bugs_activity + # Find values that were removed + foreach my $value (@old) { + if (!grep ($_ eq $value, @new)) { + push(@remove, $value); + } + } + $added = join(", ", @add); + $removed = join(", ", @remove); + + # If we can't determine what changed, put a ? in both fields + unless ($added || $removed) { + $added = "?"; + $removed = "?"; + } + + # If the original field (old|new)value was full, then this + # could be incomplete data. + if (length($oldvalue) == 255 || length($newvalue) == 255) { + $added = "? $added"; + $removed = "? $removed"; + } + } + else { + $removed = $oldvalue; + $added = $newvalue; + } + $added = $dbh->quote($added); + $removed = $dbh->quote($removed); + $dbh->do( + "UPDATE bugs_activity SET removed = $removed, added = $added WHERE bug_id = $bug_id AND who = $who AND bug_when = '$bug_when' - AND fieldid = $fieldid"); - } - print "\n"; - $dbh->bz_drop_column("bugs_activity", "oldvalue"); - $dbh->bz_drop_column("bugs_activity", "newvalue"); + AND fieldid = $fieldid" + ); } + print "\n"; + $dbh->bz_drop_column("bugs_activity", "oldvalue"); + $dbh->bz_drop_column("bugs_activity", "newvalue"); + } } sub _delete_logincookies_cryptpassword_and_handle_invalid_cookies { - my $dbh = Bugzilla->dbh; - # 2002-02-04 bbaetz@student.usyd.edu.au bug 95732 - # Remove logincookies.cryptpassword, and delete entries which become - # invalid - if ($dbh->bz_column_info("logincookies", "cryptpassword")) { - # We need to delete any cookies which are invalid before dropping the - # column - print "Removing invalid login cookies...\n"; - - # mysql doesn't support DELETE with multi-table queries, so we have - # to iterate - my $sth = $dbh->prepare("SELECT cookie FROM logincookies, profiles " . - "WHERE logincookies.cryptpassword != " . - "profiles.cryptpassword AND " . - "logincookies.userid = profiles.userid"); - $sth->execute(); - while (my ($cookie) = $sth->fetchrow_array()) { - $dbh->do("DELETE FROM logincookies WHERE cookie = $cookie"); - } - - $dbh->bz_drop_column("logincookies", "cryptpassword"); + my $dbh = Bugzilla->dbh; + + # 2002-02-04 bbaetz@student.usyd.edu.au bug 95732 + # Remove logincookies.cryptpassword, and delete entries which become + # invalid + if ($dbh->bz_column_info("logincookies", "cryptpassword")) { + + # We need to delete any cookies which are invalid before dropping the + # column + print "Removing invalid login cookies...\n"; + + # mysql doesn't support DELETE with multi-table queries, so we have + # to iterate + my $sth + = $dbh->prepare("SELECT cookie FROM logincookies, profiles " + . "WHERE logincookies.cryptpassword != " + . "profiles.cryptpassword AND " + . "logincookies.userid = profiles.userid"); + $sth->execute(); + while (my ($cookie) = $sth->fetchrow_array()) { + $dbh->do("DELETE FROM logincookies WHERE cookie = $cookie"); } + + $dbh->bz_drop_column("logincookies", "cryptpassword"); + } } sub _use_ip_instead_of_hostname_in_logincookies { - my $dbh = Bugzilla->dbh; - - # 2002-03-15 bbaetz@student.usyd.edu.au - bug 129466 - # 2002-05-13 preed@sigkill.com - bug 129446 patch backported to the - # BUGZILLA-2_14_1-BRANCH as a security blocker for the 2.14.2 release - # - # Use the ip, not the hostname, in the logincookies table - if ($dbh->bz_column_info("logincookies", "hostname")) { - print "Clearing the logincookies table...\n"; - # We've changed what we match against, so all entries are now invalid - $dbh->do("DELETE FROM logincookies"); - - # Now update the logincookies schema - $dbh->bz_drop_column("logincookies", "hostname"); - $dbh->bz_add_column("logincookies", "ipaddr", - {TYPE => 'varchar(40)'}); - } + my $dbh = Bugzilla->dbh; + + # 2002-03-15 bbaetz@student.usyd.edu.au - bug 129466 + # 2002-05-13 preed@sigkill.com - bug 129446 patch backported to the + # BUGZILLA-2_14_1-BRANCH as a security blocker for the 2.14.2 release + # + # Use the ip, not the hostname, in the logincookies table + if ($dbh->bz_column_info("logincookies", "hostname")) { + print "Clearing the logincookies table...\n"; + + # We've changed what we match against, so all entries are now invalid + $dbh->do("DELETE FROM logincookies"); + + # Now update the logincookies schema + $dbh->bz_drop_column("logincookies", "hostname"); + $dbh->bz_add_column("logincookies", "ipaddr", {TYPE => 'varchar(40)'}); + } } sub _move_quips_into_db { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations->{'datadir'}; - # 2002-07-15 davef@tetsubo.com - bug 67950 - # Move quips to the db. - if (-e "$datadir/comments") { - print "Populating quips table from $datadir/comments...\n"; - my $comments = new IO::File("$datadir/comments", 'r') - || die "$datadir/comments: $!"; - $comments->untaint; - while (<$comments>) { - chomp; - $dbh->do("INSERT INTO quips (quip) VALUES (?)", undef, $_); - } - - print "\n", install_string('update_quips', { data => $datadir }), "\n"; - $comments->close; - rename("$datadir/comments", "$datadir/comments.bak") - || warn "Failed to rename: $!"; + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations->{'datadir'}; + + # 2002-07-15 davef@tetsubo.com - bug 67950 + # Move quips to the db. + if (-e "$datadir/comments") { + print "Populating quips table from $datadir/comments...\n"; + my $comments = new IO::File("$datadir/comments", 'r') + || die "$datadir/comments: $!"; + $comments->untaint; + while (<$comments>) { + chomp; + $dbh->do("INSERT INTO quips (quip) VALUES (?)", undef, $_); } + + print "\n", install_string('update_quips', {data => $datadir}), "\n"; + $comments->close; + rename("$datadir/comments", "$datadir/comments.bak") + || warn "Failed to rename: $!"; + } } sub _use_ids_for_products_and_components { - my $dbh = Bugzilla->dbh; - # 2002-08-12 jake@bugzilla.org/bbaetz@student.usyd.edu.au - bug 43600 - # Use integer IDs for products and components. - if ($dbh->bz_column_info("products", "product")) { - print "Updating database to use product IDs.\n"; - - # First, we need to remove possible NULL entries - # NULLs may exist, but won't have been used, since all the uses of them - # are in NOT NULL fields in other tables - $dbh->do("DELETE FROM products WHERE product IS NULL"); - $dbh->do("DELETE FROM components WHERE value IS NULL"); - - $dbh->bz_add_column("products", "id", - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column("components", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("versions", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("milestones", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - $dbh->bz_add_column("bugs", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - - # The attachstatusdefs table was added in version 2.15, but - # removed again in early 2.17. If it exists now, we still need - # to perform this change with product_id because the code later on - # which converts the attachment statuses to flags depends on it. - # But we need to avoid this if the user is upgrading from 2.14 - # or earlier (because it won't be there to convert). - if ($dbh->bz_table_info("attachstatusdefs")) { - $dbh->bz_add_column("attachstatusdefs", "product_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); - } - - my %products; - my $sth = $dbh->prepare("SELECT id, product FROM products"); - $sth->execute; - while (my ($product_id, $product) = $sth->fetchrow_array()) { - if (exists $products{$product}) { - print "Ignoring duplicate product $product\n"; - $dbh->do("DELETE FROM products WHERE id = $product_id"); - next; - } - $products{$product} = 1; - $dbh->do("UPDATE components SET product_id = $product_id " . - "WHERE program = " . $dbh->quote($product)); - $dbh->do("UPDATE versions SET product_id = $product_id " . - "WHERE program = " . $dbh->quote($product)); - $dbh->do("UPDATE milestones SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)); - $dbh->do("UPDATE bugs SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)); - $dbh->do("UPDATE attachstatusdefs SET product_id = $product_id " . - "WHERE product = " . $dbh->quote($product)) - if $dbh->bz_table_info("attachstatusdefs"); - } - - print "Updating the database to use component IDs.\n"; + my $dbh = Bugzilla->dbh; + + # 2002-08-12 jake@bugzilla.org/bbaetz@student.usyd.edu.au - bug 43600 + # Use integer IDs for products and components. + if ($dbh->bz_column_info("products", "product")) { + print "Updating database to use product IDs.\n"; + + # First, we need to remove possible NULL entries + # NULLs may exist, but won't have been used, since all the uses of them + # are in NOT NULL fields in other tables + $dbh->do("DELETE FROM products WHERE product IS NULL"); + $dbh->do("DELETE FROM components WHERE value IS NULL"); + + $dbh->bz_add_column("products", "id", + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column("components", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("versions", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("milestones", "product_id", {TYPE => 'INT2', NOTNULL => 1}, + 0); + $dbh->bz_add_column("bugs", "product_id", {TYPE => 'INT2', NOTNULL => 1}, 0); + + # The attachstatusdefs table was added in version 2.15, but + # removed again in early 2.17. If it exists now, we still need + # to perform this change with product_id because the code later on + # which converts the attachment statuses to flags depends on it. + # But we need to avoid this if the user is upgrading from 2.14 + # or earlier (because it won't be there to convert). + if ($dbh->bz_table_info("attachstatusdefs")) { + $dbh->bz_add_column("attachstatusdefs", "product_id", + {TYPE => 'INT2', NOTNULL => 1}, 0); + } - $dbh->bz_add_column("components", "id", - {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - $dbh->bz_add_column("bugs", "component_id", - {TYPE => 'INT2', NOTNULL => 1}, 0); + my %products; + my $sth = $dbh->prepare("SELECT id, product FROM products"); + $sth->execute; + while (my ($product_id, $product) = $sth->fetchrow_array()) { + if (exists $products{$product}) { + print "Ignoring duplicate product $product\n"; + $dbh->do("DELETE FROM products WHERE id = $product_id"); + next; + } + $products{$product} = 1; + $dbh->do("UPDATE components SET product_id = $product_id " + . "WHERE program = " + . $dbh->quote($product)); + $dbh->do("UPDATE versions SET product_id = $product_id " + . "WHERE program = " + . $dbh->quote($product)); + $dbh->do("UPDATE milestones SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)); + $dbh->do("UPDATE bugs SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)); + $dbh->do("UPDATE attachstatusdefs SET product_id = $product_id " + . "WHERE product = " + . $dbh->quote($product)) + if $dbh->bz_table_info("attachstatusdefs"); + } - my %components; - $sth = $dbh->prepare("SELECT id, value, product_id FROM components"); - $sth->execute; - while (my ($component_id, $component, $product_id) - = $sth->fetchrow_array()) - { - if (exists $components{$component}) { - if (exists $components{$component}{$product_id}) { - print "Ignoring duplicate component $component for", - " product $product_id\n"; - $dbh->do("DELETE FROM components WHERE id = $component_id"); - next; - } - } else { - $components{$component} = {}; - } - $components{$component}{$product_id} = 1; - $dbh->do("UPDATE bugs SET component_id = $component_id " . - "WHERE component = " . $dbh->quote($component) . - " AND product_id = $product_id"); + print "Updating the database to use component IDs.\n"; + + $dbh->bz_add_column("components", "id", + {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + $dbh->bz_add_column("bugs", "component_id", {TYPE => 'INT2', NOTNULL => 1}, 0); + + my %components; + $sth = $dbh->prepare("SELECT id, value, product_id FROM components"); + $sth->execute; + while (my ($component_id, $component, $product_id) = $sth->fetchrow_array()) { + if (exists $components{$component}) { + if (exists $components{$component}{$product_id}) { + print "Ignoring duplicate component $component for", " product $product_id\n"; + $dbh->do("DELETE FROM components WHERE id = $component_id"); + next; } - print "Fixing Indexes and Uniqueness.\n"; - $dbh->bz_drop_index('milestones', 'milestones_product_idx'); - - $dbh->bz_add_index('milestones', 'milestones_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); - - $dbh->bz_drop_index('bugs', 'bugs_product_idx'); - $dbh->bz_add_index('bugs', 'bugs_product_id_idx', [qw(product_id)]); - $dbh->bz_drop_index('bugs', 'bugs_component_idx'); - $dbh->bz_add_index('bugs', 'bugs_component_id_idx', [qw(component_id)]); - - print "Removing, renaming, and retyping old product and", - " component fields.\n"; - $dbh->bz_drop_column("components", "program"); - $dbh->bz_drop_column("versions", "program"); - $dbh->bz_drop_column("milestones", "product"); - $dbh->bz_drop_column("bugs", "product"); - $dbh->bz_drop_column("bugs", "component"); - $dbh->bz_drop_column("attachstatusdefs", "product") - if $dbh->bz_table_info("attachstatusdefs"); - $dbh->bz_rename_column("products", "product", "name"); - $dbh->bz_alter_column("products", "name", - {TYPE => 'varchar(64)', NOTNULL => 1}); - $dbh->bz_rename_column("components", "value", "name"); - $dbh->bz_alter_column("components", "name", - {TYPE => 'varchar(64)', NOTNULL => 1}); - - print "Adding indexes for products and components tables.\n"; - $dbh->bz_add_index('products', 'products_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); - $dbh->bz_add_index('components', 'components_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id name)]}); - $dbh->bz_add_index('components', 'components_name_idx', [qw(name)]); + } + else { + $components{$component} = {}; + } + $components{$component}{$product_id} = 1; + $dbh->do("UPDATE bugs SET component_id = $component_id " + . "WHERE component = " + . $dbh->quote($component) + . " AND product_id = $product_id"); } + print "Fixing Indexes and Uniqueness.\n"; + $dbh->bz_drop_index('milestones', 'milestones_product_idx'); + + $dbh->bz_add_index('milestones', 'milestones_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + + $dbh->bz_drop_index('bugs', 'bugs_product_idx'); + $dbh->bz_add_index('bugs', 'bugs_product_id_idx', [qw(product_id)]); + $dbh->bz_drop_index('bugs', 'bugs_component_idx'); + $dbh->bz_add_index('bugs', 'bugs_component_id_idx', [qw(component_id)]); + + print "Removing, renaming, and retyping old product and", + " component fields.\n"; + $dbh->bz_drop_column("components", "program"); + $dbh->bz_drop_column("versions", "program"); + $dbh->bz_drop_column("milestones", "product"); + $dbh->bz_drop_column("bugs", "product"); + $dbh->bz_drop_column("bugs", "component"); + $dbh->bz_drop_column("attachstatusdefs", "product") + if $dbh->bz_table_info("attachstatusdefs"); + $dbh->bz_rename_column("products", "product", "name"); + $dbh->bz_alter_column("products", "name", + {TYPE => 'varchar(64)', NOTNULL => 1}); + $dbh->bz_rename_column("components", "value", "name"); + $dbh->bz_alter_column("components", "name", + {TYPE => 'varchar(64)', NOTNULL => 1}); + + print "Adding indexes for products and components tables.\n"; + $dbh->bz_add_index('products', 'products_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); + $dbh->bz_add_index('components', 'components_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id name)]}); + $dbh->bz_add_index('components', 'components_name_idx', [qw(name)]); + } } # Helper for the below function. @@ -1562,1211 +1609,1341 @@ sub _use_ids_for_products_and_components { # group names, _list_bits is used to fill in a list of references # to groupset bits for groups that no longer exist. sub _list_bits { - my ($num) = @_; - my $dbh = Bugzilla->dbh; - my @res; - my $curr = 1; - while (1) { - # Convert a big integer to a list of bits - my $sth = $dbh->prepare("SELECT ($num & ~$curr) > 0, + my ($num) = @_; + my $dbh = Bugzilla->dbh; + my @res; + my $curr = 1; + while (1) { + + # Convert a big integer to a list of bits + my $sth = $dbh->prepare( + "SELECT ($num & ~$curr) > 0, ($num & $curr), ($num & ~$curr), - $curr << 1"); - $sth->execute; - my ($more, $thisbit, $remain, $nval) = $sth->fetchrow_array; - push @res,"UNKNOWN<$curr>" if ($thisbit); - $curr = $nval; - $num = $remain; - last if !$more; - } - return @res; + $curr << 1" + ); + $sth->execute; + my ($more, $thisbit, $remain, $nval) = $sth->fetchrow_array; + push @res, "UNKNOWN<$curr>" if ($thisbit); + $curr = $nval; + $num = $remain; + last if !$more; + } + return @res; } sub _convert_groups_system_from_groupset { - my $dbh = Bugzilla->dbh; - # 2002-09-22 - bugreport@peshkin.net - bug 157756 - # - # If the whole groups system is new, but the installation isn't, - # convert all the old groupset groups, etc... - # - # This requires: - # 1) define groups ids in group table - # 2) populate user_group_map with grants from old groupsets - # and blessgroupsets - # 3) populate bug_group_map with data converted from old bug groupsets - # 4) convert activity logs to use group names instead of numbers - # 5) identify the admin from the old all-ones groupset - - # The groups system needs to be converted if groupset exists - if ($dbh->bz_column_info("profiles", "groupset")) { - # Some mysql versions will promote any unique key to primary key - # so all unique keys are removed first and then added back in - $dbh->bz_drop_index('groups', 'groups_bit_idx'); - $dbh->bz_drop_index('groups', 'groups_name_idx'); - my @primary_key = $dbh->primary_key(undef, undef, 'groups'); - if (@primary_key) { - $dbh->do("ALTER TABLE groups DROP PRIMARY KEY"); - } + my $dbh = Bugzilla->dbh; + + # 2002-09-22 - bugreport@peshkin.net - bug 157756 + # + # If the whole groups system is new, but the installation isn't, + # convert all the old groupset groups, etc... + # + # This requires: + # 1) define groups ids in group table + # 2) populate user_group_map with grants from old groupsets + # and blessgroupsets + # 3) populate bug_group_map with data converted from old bug groupsets + # 4) convert activity logs to use group names instead of numbers + # 5) identify the admin from the old all-ones groupset + + # The groups system needs to be converted if groupset exists + if ($dbh->bz_column_info("profiles", "groupset")) { + + # Some mysql versions will promote any unique key to primary key + # so all unique keys are removed first and then added back in + $dbh->bz_drop_index('groups', 'groups_bit_idx'); + $dbh->bz_drop_index('groups', 'groups_name_idx'); + my @primary_key = $dbh->primary_key(undef, undef, 'groups'); + if (@primary_key) { + $dbh->do("ALTER TABLE groups DROP PRIMARY KEY"); + } - $dbh->bz_add_column('groups', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - - $dbh->bz_add_index('groups', 'groups_name_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); - - # Convert all existing groupset records to map entries before removing - # groupset fields or removing "bit" from groups. - my $sth = $dbh->prepare("SELECT bit, id FROM groups WHERE bit > 0"); - $sth->execute(); - while (my ($bit, $gid) = $sth->fetchrow_array) { - # Create user_group_map membership grants for old groupsets. - # Get each user with the old groupset bit set - my $sth2 = $dbh->prepare("SELECT userid FROM profiles - WHERE (groupset & $bit) != 0"); - $sth2->execute(); - while (my ($uid) = $sth2->fetchrow_array) { - # Check to see if the user is already a member of the group - # and, if not, insert a new record. - my $query = "SELECT user_id FROM user_group_map + $dbh->bz_add_column('groups', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + $dbh->bz_add_index('groups', 'groups_name_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(name)]}); + + # Convert all existing groupset records to map entries before removing + # groupset fields or removing "bit" from groups. + my $sth = $dbh->prepare("SELECT bit, id FROM groups WHERE bit > 0"); + $sth->execute(); + while (my ($bit, $gid) = $sth->fetchrow_array) { + + # Create user_group_map membership grants for old groupsets. + # Get each user with the old groupset bit set + my $sth2 = $dbh->prepare( + "SELECT userid FROM profiles + WHERE (groupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($uid) = $sth2->fetchrow_array) { + + # Check to see if the user is already a member of the group + # and, if not, insert a new record. + my $query = "SELECT user_id FROM user_group_map WHERE group_id = $gid AND user_id = $uid AND isbless = 0"; - my $sth3 = $dbh->prepare($query); - $sth3->execute(); - if ( !$sth3->fetchrow_array() ) { - $dbh->do("INSERT INTO user_group_map + my $sth3 = $dbh->prepare($query); + $sth3->execute(); + if (!$sth3->fetchrow_array()) { + $dbh->do( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES ($uid, $gid, 0, " . GRANT_DIRECT . ")"); - } - } - # Create user can bless group grants for old groupsets, but only - # if we're upgrading from a Bugzilla that had blessing. - if($dbh->bz_column_info('profiles', 'blessgroupset')) { - # Get each user with the old blessgroupset bit set - $sth2 = $dbh->prepare("SELECT userid FROM profiles - WHERE (blessgroupset & $bit) != 0"); - $sth2->execute(); - while (my ($uid) = $sth2->fetchrow_array) { - $dbh->do("INSERT INTO user_group_map + VALUES ($uid, $gid, 0, " . GRANT_DIRECT . ")" + ); + } + } + + # Create user can bless group grants for old groupsets, but only + # if we're upgrading from a Bugzilla that had blessing. + if ($dbh->bz_column_info('profiles', 'blessgroupset')) { + + # Get each user with the old blessgroupset bit set + $sth2 = $dbh->prepare( + "SELECT userid FROM profiles + WHERE (blessgroupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($uid) = $sth2->fetchrow_array) { + $dbh->do( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES ($uid, $gid, 1, " . GRANT_DIRECT . ")"); - } - } - # Create bug_group_map records for old groupsets. - # Get each bug with the old group bit set. - $sth2 = $dbh->prepare("SELECT bug_id FROM bugs - WHERE (groupset & $bit) != 0"); - $sth2->execute(); - while (my ($bug_id) = $sth2->fetchrow_array) { - # Insert the bug, group pair into the bug_group_map. - $dbh->do("INSERT INTO bug_group_map (bug_id, group_id) - VALUES ($bug_id, $gid)"); - } + VALUES ($uid, $gid, 1, " . GRANT_DIRECT . ")" + ); } - # Replace old activity log groupset records with lists of names - # of groups. - $sth = $dbh->prepare("SELECT id FROM fielddefs - WHERE name = " . $dbh->quote('bug_group')); - $sth->execute(); - my ($bgfid) = $sth->fetchrow_array; - # Get the field id for the old groupset field - $sth = $dbh->prepare("SELECT id FROM fielddefs - WHERE name = " . $dbh->quote('groupset')); - $sth->execute(); - my ($gsid) = $sth->fetchrow_array; - # Get all bugs_activity records from groupset changes - if ($gsid) { - $sth = $dbh->prepare("SELECT bug_id, bug_when, who, added, removed - FROM bugs_activity WHERE fieldid = $gsid"); - $sth->execute(); - while (my ($bug_id, $bug_when, $who, $added, $removed) = - $sth->fetchrow_array) - { - $added ||= 0; - $removed ||= 0; - # Get names of groups added. - my $sth2 = $dbh->prepare("SELECT name FROM groups - WHERE (bit & $added) != 0 - AND (bit & $removed) = 0"); - $sth2->execute(); - my @logadd; - while (my ($n) = $sth2->fetchrow_array) { - push @logadd, $n; - } - # Get names of groups removed. - $sth2 = $dbh->prepare("SELECT name FROM groups - WHERE (bit & $removed) != 0 - AND (bit & $added) = 0"); - $sth2->execute(); - my @logrem; - while (my ($n) = $sth2->fetchrow_array) { - push @logrem, $n; - } - # Get list of group bits added that correspond to - # missing groups. - $sth2 = $dbh->prepare("SELECT ($added & ~BIT_OR(bit)) - FROM groups"); - $sth2->execute(); - my ($miss) = $sth2->fetchrow_array; - if ($miss) { - push @logadd, _list_bits($miss); - print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", - " CONTAINS DELETED GROUPS\n"; - } - # Get list of group bits deleted that correspond to - # missing groups. - $sth2 = $dbh->prepare("SELECT ($removed & ~BIT_OR(bit)) - FROM groups"); - $sth2->execute(); - ($miss) = $sth2->fetchrow_array; - if ($miss) { - push @logrem, _list_bits($miss); - print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", - " CONTAINS DELETED GROUPS\n"; - } - my $logr = ""; - my $loga = ""; - $logr = join(", ", @logrem) . '?' if @logrem; - $loga = join(", ", @logadd) . '?' if @logadd; - # Replace to old activity record with the converted data. - $dbh->do("UPDATE bugs_activity SET fieldid = $bgfid, added = " . - $dbh->quote($loga) . ", removed = " . - $dbh->quote($logr) . - " WHERE bug_id = $bug_id AND bug_when = " . - $dbh->quote($bug_when) . - " AND who = $who AND fieldid = $gsid"); - } - # Replace groupset changes with group name changes in - # profiles_activity. Get profiles_activity records for groupset. - $sth = $dbh->prepare( - "SELECT userid, profiles_when, who, newvalue, oldvalue " . - "FROM profiles_activity " . - "WHERE fieldid = $gsid"); - $sth->execute(); - while (my ($uid, $uwhen, $uwho, $added, $removed) = - $sth->fetchrow_array) - { - $added ||= 0; - $removed ||= 0; - # Get names of groups added. - my $sth2 = $dbh->prepare("SELECT name FROM groups + } + + # Create bug_group_map records for old groupsets. + # Get each bug with the old group bit set. + $sth2 = $dbh->prepare( + "SELECT bug_id FROM bugs + WHERE (groupset & $bit) != 0" + ); + $sth2->execute(); + while (my ($bug_id) = $sth2->fetchrow_array) { + + # Insert the bug, group pair into the bug_group_map. + $dbh->do( + "INSERT INTO bug_group_map (bug_id, group_id) + VALUES ($bug_id, $gid)" + ); + } + } + + # Replace old activity log groupset records with lists of names + # of groups. + $sth = $dbh->prepare( + "SELECT id FROM fielddefs + WHERE name = " . $dbh->quote('bug_group') + ); + $sth->execute(); + my ($bgfid) = $sth->fetchrow_array; + + # Get the field id for the old groupset field + $sth = $dbh->prepare( + "SELECT id FROM fielddefs + WHERE name = " . $dbh->quote('groupset') + ); + $sth->execute(); + my ($gsid) = $sth->fetchrow_array; + + # Get all bugs_activity records from groupset changes + if ($gsid) { + $sth = $dbh->prepare( + "SELECT bug_id, bug_when, who, added, removed + FROM bugs_activity WHERE fieldid = $gsid" + ); + $sth->execute(); + while (my ($bug_id, $bug_when, $who, $added, $removed) = $sth->fetchrow_array) { + $added ||= 0; + $removed ||= 0; + + # Get names of groups added. + my $sth2 = $dbh->prepare( + "SELECT name FROM groups WHERE (bit & $added) != 0 - AND (bit & $removed) = 0"); - $sth2->execute(); - my @logadd; - while (my ($n) = $sth2->fetchrow_array) { - push @logadd, $n; - } - # Get names of groups removed. - $sth2 = $dbh->prepare("SELECT name FROM groups + AND (bit & $removed) = 0" + ); + $sth2->execute(); + my @logadd; + while (my ($n) = $sth2->fetchrow_array) { + push @logadd, $n; + } + + # Get names of groups removed. + $sth2 = $dbh->prepare( + "SELECT name FROM groups WHERE (bit & $removed) != 0 - AND (bit & $added) = 0"); - $sth2->execute(); - my @logrem; - while (my ($n) = $sth2->fetchrow_array) { - push @logrem, $n; - } - my $ladd = ""; - my $lrem = ""; - $ladd = join(", ", @logadd) . '?' if @logadd; - $lrem = join(", ", @logrem) . '?' if @logrem; - # Replace profiles_activity record for groupset change - # with group list. - $dbh->do("UPDATE profiles_activity " . - "SET fieldid = $bgfid, newvalue = " . - $dbh->quote($ladd) . ", oldvalue = " . - $dbh->quote($lrem) . - " WHERE userid = $uid AND profiles_when = " . - $dbh->quote($uwhen) . - " AND who = $uwho AND fieldid = $gsid"); - } + AND (bit & $added) = 0" + ); + $sth2->execute(); + my @logrem; + while (my ($n) = $sth2->fetchrow_array) { + push @logrem, $n; } - # Identify admin group. - my ($admin_gid) = $dbh->selectrow_array( - "SELECT id FROM groups WHERE name = 'admin'"); - if (!$admin_gid) { - $dbh->do(q{INSERT INTO groups (name, description) - VALUES ('admin', 'Administrators')}); - $admin_gid = $dbh->bz_last_key('groups', 'id'); + # Get list of group bits added that correspond to + # missing groups. + $sth2 = $dbh->prepare( + "SELECT ($added & ~BIT_OR(bit)) + FROM groups" + ); + $sth2->execute(); + my ($miss) = $sth2->fetchrow_array; + if ($miss) { + push @logadd, _list_bits($miss); + print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", + " CONTAINS DELETED GROUPS\n"; } - # Find current admins - my @admins; - # Don't lose admins from DBs where Bug 157704 applies - $sth = $dbh->prepare( - "SELECT userid, (groupset & 65536), login_name " . - "FROM profiles " . - "WHERE (groupset | 65536) = 9223372036854775807"); - $sth->execute(); - while ( my ($userid, $iscomplete, $login_name) - = $sth->fetchrow_array() ) - { - # existing administrators are made members of group "admin" - print "\nWARNING - $login_name IS AN ADMIN IN SPITE OF BUG", - " 157704\n\n" if (!$iscomplete); - push(@admins, $userid) unless grep($_ eq $userid, @admins); + + # Get list of group bits deleted that correspond to + # missing groups. + $sth2 = $dbh->prepare( + "SELECT ($removed & ~BIT_OR(bit)) + FROM groups" + ); + $sth2->execute(); + ($miss) = $sth2->fetchrow_array; + if ($miss) { + push @logrem, _list_bits($miss); + print "\nWARNING - GROUPSET ACTIVITY ON BUG $bug_id", + " CONTAINS DELETED GROUPS\n"; } - # Now make all those users admins directly. They were already - # added to every other group, above, because of their groupset. - foreach my $admin_id (@admins) { - $dbh->do("INSERT INTO user_group_map - (user_id, group_id, isbless, grant_type) - VALUES (?, ?, ?, ?)", - undef, $admin_id, $admin_gid, $_, GRANT_DIRECT) - foreach (0, 1); + my $logr = ""; + my $loga = ""; + $logr = join(", ", @logrem) . '?' if @logrem; + $loga = join(", ", @logadd) . '?' if @logadd; + + # Replace to old activity record with the converted data. + $dbh->do("UPDATE bugs_activity SET fieldid = $bgfid, added = " + . $dbh->quote($loga) + . ", removed = " + . $dbh->quote($logr) + . " WHERE bug_id = $bug_id AND bug_when = " + . $dbh->quote($bug_when) + . " AND who = $who AND fieldid = $gsid"); + } + + # Replace groupset changes with group name changes in + # profiles_activity. Get profiles_activity records for groupset. + $sth + = $dbh->prepare("SELECT userid, profiles_when, who, newvalue, oldvalue " + . "FROM profiles_activity " + . "WHERE fieldid = $gsid"); + $sth->execute(); + while (my ($uid, $uwhen, $uwho, $added, $removed) = $sth->fetchrow_array) { + $added ||= 0; + $removed ||= 0; + + # Get names of groups added. + my $sth2 = $dbh->prepare( + "SELECT name FROM groups + WHERE (bit & $added) != 0 + AND (bit & $removed) = 0" + ); + $sth2->execute(); + my @logadd; + while (my ($n) = $sth2->fetchrow_array) { + push @logadd, $n; + } + + # Get names of groups removed. + $sth2 = $dbh->prepare( + "SELECT name FROM groups + WHERE (bit & $removed) != 0 + AND (bit & $added) = 0" + ); + $sth2->execute(); + my @logrem; + while (my ($n) = $sth2->fetchrow_array) { + push @logrem, $n; } + my $ladd = ""; + my $lrem = ""; + $ladd = join(", ", @logadd) . '?' if @logadd; + $lrem = join(", ", @logrem) . '?' if @logrem; + + # Replace profiles_activity record for groupset change + # with group list. + $dbh->do("UPDATE profiles_activity " + . "SET fieldid = $bgfid, newvalue = " + . $dbh->quote($ladd) + . ", oldvalue = " + . $dbh->quote($lrem) + . " WHERE userid = $uid AND profiles_when = " + . $dbh->quote($uwhen) + . " AND who = $uwho AND fieldid = $gsid"); + } + } - $dbh->bz_drop_column('profiles','groupset'); - $dbh->bz_drop_column('profiles','blessgroupset'); - $dbh->bz_drop_column('bugs','groupset'); - $dbh->bz_drop_column('groups','bit'); - $dbh->do("DELETE FROM fielddefs WHERE name = " - . $dbh->quote('groupset')); + # Identify admin group. + my ($admin_gid) + = $dbh->selectrow_array("SELECT id FROM groups WHERE name = 'admin'"); + if (!$admin_gid) { + $dbh->do( + q{INSERT INTO groups (name, description) + VALUES ('admin', 'Administrators')} + ); + $admin_gid = $dbh->bz_last_key('groups', 'id'); } + + # Find current admins + my @admins; + + # Don't lose admins from DBs where Bug 157704 applies + $sth + = $dbh->prepare("SELECT userid, (groupset & 65536), login_name " + . "FROM profiles " + . "WHERE (groupset | 65536) = 9223372036854775807"); + $sth->execute(); + while (my ($userid, $iscomplete, $login_name) = $sth->fetchrow_array()) { + + # existing administrators are made members of group "admin" + print "\nWARNING - $login_name IS AN ADMIN IN SPITE OF BUG", " 157704\n\n" + if (!$iscomplete); + push(@admins, $userid) unless grep($_ eq $userid, @admins); + } + + # Now make all those users admins directly. They were already + # added to every other group, above, because of their groupset. + foreach my $admin_id (@admins) { + $dbh->do( + "INSERT INTO user_group_map + (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)", undef, $admin_id, $admin_gid, $_, + GRANT_DIRECT + ) foreach (0, 1); + } + + $dbh->bz_drop_column('profiles', 'groupset'); + $dbh->bz_drop_column('profiles', 'blessgroupset'); + $dbh->bz_drop_column('bugs', 'groupset'); + $dbh->bz_drop_column('groups', 'bit'); + $dbh->do("DELETE FROM fielddefs WHERE name = " . $dbh->quote('groupset')); + } } sub _convert_attachment_statuses_to_flags { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + + # September 2002 myk@mozilla.org bug 98801 + # Convert the attachment statuses tables into flags tables. + if ( $dbh->bz_table_info("attachstatuses") + && $dbh->bz_table_info("attachstatusdefs")) + { + print "Converting attachment statuses to flags...\n"; + + # Get IDs for the old attachment status and new flag fields. + my ($old_field_id) + = $dbh->selectrow_array( + "SELECT id FROM fielddefs WHERE name='attachstatusdefs.name'") + || 0; + my ($new_field_id) + = $dbh->selectrow_array( + "SELECT id FROM fielddefs WHERE name = 'flagtypes.name'"); + + # Convert attachment status definitions to flag types. If more than one + # status has the same name and description, it is merged into a single + # status with multiple inclusion records. - # September 2002 myk@mozilla.org bug 98801 - # Convert the attachment statuses tables into flags tables. - if ($dbh->bz_table_info("attachstatuses") - && $dbh->bz_table_info("attachstatusdefs")) - { - print "Converting attachment statuses to flags...\n"; - - # Get IDs for the old attachment status and new flag fields. - my ($old_field_id) = $dbh->selectrow_array( - "SELECT id FROM fielddefs WHERE name='attachstatusdefs.name'") - || 0; - my ($new_field_id) = $dbh->selectrow_array( - "SELECT id FROM fielddefs WHERE name = 'flagtypes.name'"); - - # Convert attachment status definitions to flag types. If more than one - # status has the same name and description, it is merged into a single - # status with multiple inclusion records. - - my $sth = $dbh->prepare( - "SELECT id, name, description, sortkey, product_id - FROM attachstatusdefs"); - - # status definition IDs indexed by name/description - my $def_ids = {}; - - # merged IDs and the IDs they were merged into. The key is the old ID, - # the value is the new one. This allows us to give statuses the right - # ID when we convert them over to flags. This map includes IDs that - # weren't merged (in this case the old and new IDs are the same), since - # it makes the code simpler. - my $def_id_map = {}; - - $sth->execute(); - while (my ($id, $name, $desc, $sortkey, $prod_id) = - $sth->fetchrow_array()) - { - my $key = $name . $desc; - if (!$def_ids->{$key}) { - $def_ids->{$key} = $id; - my $quoted_name = $dbh->quote($name); - my $quoted_desc = $dbh->quote($desc); - $dbh->do("INSERT INTO flagtypes (id, name, description, + my $sth = $dbh->prepare( + "SELECT id, name, description, sortkey, product_id + FROM attachstatusdefs" + ); + + # status definition IDs indexed by name/description + my $def_ids = {}; + + # merged IDs and the IDs they were merged into. The key is the old ID, + # the value is the new one. This allows us to give statuses the right + # ID when we convert them over to flags. This map includes IDs that + # weren't merged (in this case the old and new IDs are the same), since + # it makes the code simpler. + my $def_id_map = {}; + + $sth->execute(); + while (my ($id, $name, $desc, $sortkey, $prod_id) = $sth->fetchrow_array()) { + my $key = $name . $desc; + if (!$def_ids->{$key}) { + $def_ids->{$key} = $id; + my $quoted_name = $dbh->quote($name); + my $quoted_desc = $dbh->quote($desc); + $dbh->do( + "INSERT INTO flagtypes (id, name, description, sortkey, target_type) VALUES ($id, $quoted_name, $quoted_desc, - $sortkey,'a')"); - } - $def_id_map->{$id} = $def_ids->{$key}; - $dbh->do("INSERT INTO flaginclusions (type_id, product_id) - VALUES ($def_id_map->{$id}, $prod_id)"); - } + $sortkey,'a')" + ); + } + $def_id_map->{$id} = $def_ids->{$key}; + $dbh->do( + "INSERT INTO flaginclusions (type_id, product_id) + VALUES ($def_id_map->{$id}, $prod_id)" + ); + } - # Note: even though we've converted status definitions, we still - # can't drop the table because we need it to convert the statuses - # themselves. - - # Convert attachment statuses to flags. To do this we select - # the statuses from the status table and then, for each one, - # figure out who set it and when they set it from the bugs - # activity table. - my $id = 0; - $sth = $dbh->prepare( - "SELECT attachstatuses.attach_id, attachstatusdefs.id, + # Note: even though we've converted status definitions, we still + # can't drop the table because we need it to convert the statuses + # themselves. + + # Convert attachment statuses to flags. To do this we select + # the statuses from the status table and then, for each one, + # figure out who set it and when they set it from the bugs + # activity table. + my $id = 0; + $sth = $dbh->prepare( + "SELECT attachstatuses.attach_id, attachstatusdefs.id, attachstatusdefs.name, attachments.bug_id FROM attachstatuses, attachstatusdefs, attachments WHERE attachstatuses.statusid = attachstatusdefs.id - AND attachstatuses.attach_id = attachments.attach_id"); + AND attachstatuses.attach_id = attachments.attach_id" + ); - # a query to determine when the attachment status was set and who set it - my $sth2 = $dbh->prepare("SELECT added, who, bug_when + # a query to determine when the attachment status was set and who set it + my $sth2 = $dbh->prepare( + "SELECT added, who, bug_when FROM bugs_activity WHERE bug_id = ? AND attach_id = ? AND fieldid = $old_field_id - ORDER BY bug_when DESC"); - - $sth->execute(); - while (my ($attach_id, $def_id, $status, $bug_id) = - $sth->fetchrow_array()) - { - ++$id; - - # Determine when the attachment status was set and who set it. - # We should always be able to find out this info from the bug - # activity, but we fall back to default values just in case. - $sth2->execute($bug_id, $attach_id); - my ($added, $who, $when); - while (($added, $who, $when) = $sth2->fetchrow_array()) { - last if $added =~ /(^|[, ]+)\Q$status\E([, ]+|$)/; - } - $who = $dbh->quote($who); # "NULL" by default if $who is undefined - $when = $when ? $dbh->quote($when) : "NOW()"; - + ORDER BY bug_when DESC" + ); - $dbh->do("INSERT INTO flags (id, type_id, status, bug_id, + $sth->execute(); + while (my ($attach_id, $def_id, $status, $bug_id) = $sth->fetchrow_array()) { + ++$id; + + # Determine when the attachment status was set and who set it. + # We should always be able to find out this info from the bug + # activity, but we fall back to default values just in case. + $sth2->execute($bug_id, $attach_id); + my ($added, $who, $when); + while (($added, $who, $when) = $sth2->fetchrow_array()) { + last if $added =~ /(^|[, ]+)\Q$status\E([, ]+|$)/; + } + $who = $dbh->quote($who); # "NULL" by default if $who is undefined + $when = $when ? $dbh->quote($when) : "NOW()"; + + + $dbh->do( + "INSERT INTO flags (id, type_id, status, bug_id, attach_id, creation_date, modification_date, requestee_id, setter_id) VALUES ($id, $def_id_map->{$def_id}, '+', $bug_id, - $attach_id, $when, $when, NULL, $who)"); - } + $attach_id, $when, $when, NULL, $who)" + ); + } - # Now that we've converted both tables we can drop them. - $dbh->bz_drop_table("attachstatuses"); - $dbh->bz_drop_table("attachstatusdefs"); + # Now that we've converted both tables we can drop them. + $dbh->bz_drop_table("attachstatuses"); + $dbh->bz_drop_table("attachstatusdefs"); - # Convert activity records for attachment statuses into records - # for flags. - $sth = $dbh->prepare("SELECT attach_id, who, bug_when, added, + # Convert activity records for attachment statuses into records + # for flags. + $sth = $dbh->prepare( + "SELECT attach_id, who, bug_when, added, removed FROM bugs_activity - WHERE fieldid = $old_field_id"); - $sth->execute(); - while (my ($attach_id, $who, $when, $old_added, $old_removed) = - $sth->fetchrow_array()) - { - my @additions = split(/[, ]+/, $old_added); - @additions = map("$_+", @additions); - my $new_added = $dbh->quote(join(", ", @additions)); - - my @removals = split(/[, ]+/, $old_removed); - @removals = map("$_+", @removals); - my $new_removed = $dbh->quote(join(", ", @removals)); - - $old_added = $dbh->quote($old_added); - $old_removed = $dbh->quote($old_removed); - $who = $dbh->quote($who); - $when = $dbh->quote($when); - - $dbh->do("UPDATE bugs_activity SET fieldid = $new_field_id, " . - "added = $new_added, removed = $new_removed " . - "WHERE attach_id = $attach_id AND who = $who " . - "AND bug_when = $when AND fieldid = $old_field_id " . - "AND added = $old_added AND removed = $old_removed"); - } + WHERE fieldid = $old_field_id" + ); + $sth->execute(); + while (my ($attach_id, $who, $when, $old_added, $old_removed) + = $sth->fetchrow_array()) + { + my @additions = split(/[, ]+/, $old_added); + @additions = map("$_+", @additions); + my $new_added = $dbh->quote(join(", ", @additions)); + + my @removals = split(/[, ]+/, $old_removed); + @removals = map("$_+", @removals); + my $new_removed = $dbh->quote(join(", ", @removals)); + + $old_added = $dbh->quote($old_added); + $old_removed = $dbh->quote($old_removed); + $who = $dbh->quote($who); + $when = $dbh->quote($when); + + $dbh->do("UPDATE bugs_activity SET fieldid = $new_field_id, " + . "added = $new_added, removed = $new_removed " + . "WHERE attach_id = $attach_id AND who = $who " + . "AND bug_when = $when AND fieldid = $old_field_id " + . "AND added = $old_added AND removed = $old_removed"); + } - # Remove the attachment status field from the field definitions. - $dbh->do("DELETE FROM fielddefs WHERE name='attachstatusdefs.name'"); + # Remove the attachment status field from the field definitions. + $dbh->do("DELETE FROM fielddefs WHERE name='attachstatusdefs.name'"); - print "done.\n"; - } + print "done.\n"; + } } sub _remove_spaces_and_commas_from_flagtypes { - my $dbh = Bugzilla->dbh; - # Get all names and IDs, to find broken ones and to - # check for collisions when renaming. - my $sth = $dbh->prepare("SELECT name, id FROM flagtypes"); - $sth->execute(); - - my %flagtypes; - my @badflagnames; - # find broken flagtype names, and populate a hash table - # to check for collisions. - while (my ($name, $id) = $sth->fetchrow_array()) { - $flagtypes{$name} = $id; - if ($name =~ /[ ,]/) { - push(@badflagnames, $name); - } + my $dbh = Bugzilla->dbh; + + # Get all names and IDs, to find broken ones and to + # check for collisions when renaming. + my $sth = $dbh->prepare("SELECT name, id FROM flagtypes"); + $sth->execute(); + + my %flagtypes; + my @badflagnames; + + # find broken flagtype names, and populate a hash table + # to check for collisions. + while (my ($name, $id) = $sth->fetchrow_array()) { + $flagtypes{$name} = $id; + if ($name =~ /[ ,]/) { + push(@badflagnames, $name); } - if (@badflagnames) { - print "Removing spaces and commas from flag names...\n"; - my ($flagname, $tryflagname); - my $sth = $dbh->prepare("UPDATE flagtypes SET name = ? WHERE id = ?"); - foreach $flagname (@badflagnames) { - print " Bad flag type name \"$flagname\" ...\n"; - # find a new name for this flagtype. - ($tryflagname = $flagname) =~ tr/ ,/__/; - # avoid collisions with existing flagtype names. - while (defined($flagtypes{$tryflagname})) { - print " ... can't rename as \"$tryflagname\" ...\n"; - $tryflagname .= "'"; - if (length($tryflagname) > 50) { - my $lastchanceflagname = (substr $tryflagname, 0, 47) . '...'; - if (defined($flagtypes{$lastchanceflagname})) { - print " ... last attempt as \"$lastchanceflagname\" still failed.'\n"; - die install_string('update_flags_bad_name', - { flag => $flagname }), "\n"; - } - $tryflagname = $lastchanceflagname; - } - } - $sth->execute($tryflagname, $flagtypes{$flagname}); - print " renamed flag type \"$flagname\" as \"$tryflagname\"\n"; - $flagtypes{$tryflagname} = $flagtypes{$flagname}; - delete $flagtypes{$flagname}; + } + if (@badflagnames) { + print "Removing spaces and commas from flag names...\n"; + my ($flagname, $tryflagname); + my $sth = $dbh->prepare("UPDATE flagtypes SET name = ? WHERE id = ?"); + foreach $flagname (@badflagnames) { + print " Bad flag type name \"$flagname\" ...\n"; + + # find a new name for this flagtype. + ($tryflagname = $flagname) =~ tr/ ,/__/; + + # avoid collisions with existing flagtype names. + while (defined($flagtypes{$tryflagname})) { + print " ... can't rename as \"$tryflagname\" ...\n"; + $tryflagname .= "'"; + if (length($tryflagname) > 50) { + my $lastchanceflagname = (substr $tryflagname, 0, 47) . '...'; + if (defined($flagtypes{$lastchanceflagname})) { + print " ... last attempt as \"$lastchanceflagname\" still failed.'\n"; + die install_string('update_flags_bad_name', {flag => $flagname}), "\n"; + } + $tryflagname = $lastchanceflagname; } - print "... done.\n"; + } + $sth->execute($tryflagname, $flagtypes{$flagname}); + print " renamed flag type \"$flagname\" as \"$tryflagname\"\n"; + $flagtypes{$tryflagname} = $flagtypes{$flagname}; + delete $flagtypes{$flagname}; } + print "... done.\n"; + } } sub _setup_usebuggroups_backward_compatibility { - my $dbh = Bugzilla->dbh; - - # Don't run this on newer Bugzillas. This is a reliable test because - # the longdescs table existed in 2.16 (which had usebuggroups) - # but not in 2.18, and this code happens between 2.16 and 2.18. - return if $dbh->bz_column_info('longdescs', 'already_wrapped'); - - # 2002-11-24 - bugreport@peshkin.net - bug 147275 - # - # If group_control_map is empty, backward-compatibility - # usebuggroups-equivalent records should be created. - my ($maps_exist) = $dbh->selectrow_array( - "SELECT DISTINCT 1 FROM group_control_map"); - if (!$maps_exist) { - print "Converting old usebuggroups controls...\n"; - # Initially populate group_control_map. - # First, get all the existing products and their groups. - my $sth = $dbh->prepare("SELECT groups.id, products.id, groups.name, + my $dbh = Bugzilla->dbh; + + # Don't run this on newer Bugzillas. This is a reliable test because + # the longdescs table existed in 2.16 (which had usebuggroups) + # but not in 2.18, and this code happens between 2.16 and 2.18. + return if $dbh->bz_column_info('longdescs', 'already_wrapped'); + + # 2002-11-24 - bugreport@peshkin.net - bug 147275 + # + # If group_control_map is empty, backward-compatibility + # usebuggroups-equivalent records should be created. + my ($maps_exist) + = $dbh->selectrow_array("SELECT DISTINCT 1 FROM group_control_map"); + if (!$maps_exist) { + print "Converting old usebuggroups controls...\n"; + + # Initially populate group_control_map. + # First, get all the existing products and their groups. + my $sth = $dbh->prepare( + "SELECT groups.id, products.id, groups.name, products.name FROM groups, products - WHERE isbuggroup != 0"); - $sth->execute(); - while (my ($groupid, $productid, $groupname, $productname) - = $sth->fetchrow_array()) - { - if ($groupname eq $productname) { - # Product and group have same name. - $dbh->do("INSERT INTO group_control_map " . - "(group_id, product_id, membercontrol, othercontrol) " . - "VALUES (?, ?, ?, ?)", undef, - ($groupid, $productid, CONTROLMAPDEFAULT, CONTROLMAPNA)); - } else { - # See if this group is a product group at all. - my $sth2 = $dbh->prepare("SELECT id FROM products - WHERE name = " .$dbh->quote($groupname)); - $sth2->execute(); - my ($id) = $sth2->fetchrow_array(); - if (!$id) { - # If there is no product with the same name as this - # group, then it is permitted for all products. - $dbh->do("INSERT INTO group_control_map " . - "(group_id, product_id, membercontrol, othercontrol) " . - "VALUES (?, ?, ?, ?)", undef, - ($groupid, $productid, CONTROLMAPSHOWN, CONTROLMAPNA)); - } - } + WHERE isbuggroup != 0" + ); + $sth->execute(); + while (my ($groupid, $productid, $groupname, $productname) + = $sth->fetchrow_array()) + { + if ($groupname eq $productname) { + + # Product and group have same name. + $dbh->do( + "INSERT INTO group_control_map " + . "(group_id, product_id, membercontrol, othercontrol) " + . "VALUES (?, ?, ?, ?)", + undef, + ($groupid, $productid, CONTROLMAPDEFAULT, CONTROLMAPNA) + ); + } + else { + # See if this group is a product group at all. + my $sth2 = $dbh->prepare( + "SELECT id FROM products + WHERE name = " . $dbh->quote($groupname) + ); + $sth2->execute(); + my ($id) = $sth2->fetchrow_array(); + if (!$id) { + + # If there is no product with the same name as this + # group, then it is permitted for all products. + $dbh->do( + "INSERT INTO group_control_map " + . "(group_id, product_id, membercontrol, othercontrol) " + . "VALUES (?, ?, ?, ?)", + undef, + ($groupid, $productid, CONTROLMAPSHOWN, CONTROLMAPNA) + ); } + } } + } } sub _remove_user_series_map { - my $dbh = Bugzilla->dbh; - # 2004-07-17 GRM - Remove "subscriptions" concept from charting, and add - # group-based security instead. - if ($dbh->bz_table_info("user_series_map")) { - # Oracle doesn't like "date" as a column name, and apparently some DBs - # don't like 'value' either. We use the changes to subscriptions as - # something to hang these renamings off. - $dbh->bz_rename_column('series_data', 'date', 'series_date'); - $dbh->bz_rename_column('series_data', 'value', 'series_value'); - - # series_categories.category_id produces a too-long column name for the - # auto-incrementing sequence (Oracle again). - $dbh->bz_rename_column('series_categories', 'category_id', 'id'); - - $dbh->bz_add_column("series", "public", - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); - - # Migrate public-ness across from user_series_map to new field - my $sth = $dbh->prepare("SELECT series_id from user_series_map " . - "WHERE user_id = 0"); - $sth->execute(); - while (my ($public_series_id) = $sth->fetchrow_array()) { - $dbh->do("UPDATE series SET public = 1 " . - "WHERE series_id = $public_series_id"); - } + my $dbh = Bugzilla->dbh; + + # 2004-07-17 GRM - Remove "subscriptions" concept from charting, and add + # group-based security instead. + if ($dbh->bz_table_info("user_series_map")) { + + # Oracle doesn't like "date" as a column name, and apparently some DBs + # don't like 'value' either. We use the changes to subscriptions as + # something to hang these renamings off. + $dbh->bz_rename_column('series_data', 'date', 'series_date'); + $dbh->bz_rename_column('series_data', 'value', 'series_value'); - $dbh->bz_drop_table("user_series_map"); + # series_categories.category_id produces a too-long column name for the + # auto-incrementing sequence (Oracle again). + $dbh->bz_rename_column('series_categories', 'category_id', 'id'); + + $dbh->bz_add_column("series", "public", + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + + # Migrate public-ness across from user_series_map to new field + my $sth = $dbh->prepare( + "SELECT series_id from user_series_map " . "WHERE user_id = 0"); + $sth->execute(); + while (my ($public_series_id) = $sth->fetchrow_array()) { + $dbh->do( + "UPDATE series SET public = 1 " . "WHERE series_id = $public_series_id"); } + + $dbh->bz_drop_table("user_series_map"); + } } sub _copy_old_charts_into_database { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations()->{'datadir'}; - # 2003-06-26 Copy the old charting data into the database, and create the - # queries that will keep it all running. When the old charting system goes - # away, if this code ever runs, it'll just find no files and do nothing. - my $series_exists = $dbh->selectrow_array("SELECT 1 FROM series " . - $dbh->sql_limit(1)); - if (!$series_exists && -d "$datadir/mining" && -e "$datadir/mining/-All-") { - print "Migrating old chart data into database...\n"; - - # We prepare the handle to insert the series data - my $seriesdatasth = $dbh->prepare( - "INSERT INTO series_data (series_id, series_date, series_value) - VALUES (?, ?, ?)"); - - my $deletesth = $dbh->prepare( - "DELETE FROM series_data WHERE series_id = ? AND series_date = ?"); - - my $groupmapsth = $dbh->prepare( - "INSERT INTO category_group_map (category_id, group_id) - VALUES (?, ?)"); - - # Fields in the data file (matches the current collectstats.pl) - my @statuses = - qw(NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED VERIFIED CLOSED); - my @resolutions = - qw(FIXED INVALID WONTFIX LATER REMIND DUPLICATE WORKSFORME MOVED); - my @fields = (@statuses, @resolutions); - - # We have a localization problem here. Where do we get these values? - my $all_name = "-All-"; - my $open_name = "All Open"; - - $dbh->bz_start_transaction(); - my $products = $dbh->selectall_arrayref("SELECT name FROM products"); - - foreach my $product ((map { $_->[0] } @$products), "-All-") { - print "$product:\n"; - # First, create the series - my %queries; - my %seriesids; - - my $query_prod = ""; - if ($product ne "-All-") { - $query_prod = "product=" . html_quote($product) . "&"; - } + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations()->{'datadir'}; + + # 2003-06-26 Copy the old charting data into the database, and create the + # queries that will keep it all running. When the old charting system goes + # away, if this code ever runs, it'll just find no files and do nothing. + my $series_exists + = $dbh->selectrow_array("SELECT 1 FROM series " . $dbh->sql_limit(1)); + if (!$series_exists && -d "$datadir/mining" && -e "$datadir/mining/-All-") { + print "Migrating old chart data into database...\n"; + + # We prepare the handle to insert the series data + my $seriesdatasth = $dbh->prepare( + "INSERT INTO series_data (series_id, series_date, series_value) + VALUES (?, ?, ?)" + ); - # The query for statuses is different to that for resolutions. - $queries{$_} = ($query_prod . "bug_status=$_") foreach (@statuses); - $queries{$_} = ($query_prod . "resolution=$_") - foreach (@resolutions); - - foreach my $field (@fields) { - # Create a Series for each field in this product. - my $series = new Bugzilla::Series(undef, $product, $all_name, - $field, undef, 1, - $queries{$field}, 1); - $series->writeToDatabase(); - $seriesids{$field} = $series->{'series_id'}; - } + my $deletesth = $dbh->prepare( + "DELETE FROM series_data WHERE series_id = ? AND series_date = ?"); - # We also add a new query for "Open", so that migrated products get - # the same set as new products (see editproducts.cgi.) - my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"); - my $query = join("&", map { "bug_status=$_" } @openedstatuses); - my $series = new Bugzilla::Series(undef, $product, $all_name, - $open_name, undef, 1, - $query_prod . $query, 1); - $series->writeToDatabase(); - $seriesids{$open_name} = $series->{'series_id'}; - - # Now, we attempt to read in historical data, if any - # Convert the name in the same way that collectstats.pl does - my $product_file = $product; - $product_file =~ s/\//-/gs; - $product_file = "$datadir/mining/$product_file"; - - # There are many reasons that this might fail (e.g. no stats - # for this product), so we don't worry if it does. - my $in = new IO::File($product_file) or next; - - # The data files should be in a standard format, even for old - # Bugzillas, because of the conversion code further up this file. - my %data; - my $last_date = ""; - - my @lines = <$in>; - while (my $line = shift @lines) { - if ($line =~ /^(\d+\|.*)/) { - my @numbers = split(/\||\r/, $1); - - # Only take the first line for each date; it was possible to - # run collectstats.pl more than once in a day. - next if $numbers[0] eq $last_date; - - for my $i (0 .. $#fields) { - # $numbers[0] is the date - $data{$fields[$i]}{$numbers[0]} = $numbers[$i + 1]; - - # Keep a total of the number of open bugs for this day - if (grep { $_ eq $fields[$i] } @openedstatuses) { - $data{$open_name}{$numbers[0]} += $numbers[$i + 1]; - } - } - - $last_date = $numbers[0]; - } - } + my $groupmapsth = $dbh->prepare( + "INSERT INTO category_group_map (category_id, group_id) + VALUES (?, ?)" + ); - $in->close; - - my $total_items = (scalar(@fields) + 1) - * scalar(keys %{ $data{'NEW'} }); - my $count = 0; - foreach my $field (@fields, $open_name) { - # Insert values into series_data: series_id, date, value - my %fielddata = %{$data{$field}}; - foreach my $date (keys %fielddata) { - # We need to delete in case the text file had duplicate - # entries in it. - $deletesth->execute($seriesids{$field}, $date); - - # We prepared this above - $seriesdatasth->execute($seriesids{$field}, - $date, $fielddata{$date} || 0); - indicate_progress({ total => $total_items, - current => ++$count, every => 100 }); - } - } + # Fields in the data file (matches the current collectstats.pl) + my @statuses = qw(NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED VERIFIED CLOSED); + my @resolutions + = qw(FIXED INVALID WONTFIX LATER REMIND DUPLICATE WORKSFORME MOVED); + my @fields = (@statuses, @resolutions); + + # We have a localization problem here. Where do we get these values? + my $all_name = "-All-"; + my $open_name = "All Open"; - # Create the groupsets for the category - my $category_id = - $dbh->selectrow_array("SELECT id FROM series_categories " . - "WHERE name = " . $dbh->quote($product)); - my $product_id = - $dbh->selectrow_array("SELECT id FROM products " . - "WHERE name = " . $dbh->quote($product)); - - if (defined($category_id) && defined($product_id)) { - - # Get all the mandatory groups for this product - my $group_ids = - $dbh->selectcol_arrayref("SELECT group_id " . - "FROM group_control_map " . - "WHERE product_id = $product_id " . - "AND (membercontrol = " . CONTROLMAPMANDATORY . - " OR othercontrol = " . CONTROLMAPMANDATORY . ")"); - - foreach my $group_id (@$group_ids) { - $groupmapsth->execute($category_id, $group_id); - } + $dbh->bz_start_transaction(); + my $products = $dbh->selectall_arrayref("SELECT name FROM products"); + + foreach my $product ((map { $_->[0] } @$products), "-All-") { + print "$product:\n"; + + # First, create the series + my %queries; + my %seriesids; + + my $query_prod = ""; + if ($product ne "-All-") { + $query_prod = "product=" . html_quote($product) . "&"; + } + + # The query for statuses is different to that for resolutions. + $queries{$_} = ($query_prod . "bug_status=$_") foreach (@statuses); + $queries{$_} = ($query_prod . "resolution=$_") foreach (@resolutions); + + foreach my $field (@fields) { + + # Create a Series for each field in this product. + my $series = new Bugzilla::Series(undef, $product, $all_name, $field, undef, 1, + $queries{$field}, 1); + $series->writeToDatabase(); + $seriesids{$field} = $series->{'series_id'}; + } + + # We also add a new query for "Open", so that migrated products get + # the same set as new products (see editproducts.cgi.) + my @openedstatuses = ("UNCONFIRMED", "NEW", "ASSIGNED", "REOPENED"); + my $query = join("&", map {"bug_status=$_"} @openedstatuses); + my $series + = new Bugzilla::Series(undef, $product, $all_name, $open_name, undef, 1, + $query_prod . $query, 1); + $series->writeToDatabase(); + $seriesids{$open_name} = $series->{'series_id'}; + + # Now, we attempt to read in historical data, if any + # Convert the name in the same way that collectstats.pl does + my $product_file = $product; + $product_file =~ s/\//-/gs; + $product_file = "$datadir/mining/$product_file"; + + # There are many reasons that this might fail (e.g. no stats + # for this product), so we don't worry if it does. + my $in = new IO::File($product_file) or next; + + # The data files should be in a standard format, even for old + # Bugzillas, because of the conversion code further up this file. + my %data; + my $last_date = ""; + + my @lines = <$in>; + while (my $line = shift @lines) { + if ($line =~ /^(\d+\|.*)/) { + my @numbers = split(/\||\r/, $1); + + # Only take the first line for each date; it was possible to + # run collectstats.pl more than once in a day. + next if $numbers[0] eq $last_date; + + for my $i (0 .. $#fields) { + + # $numbers[0] is the date + $data{$fields[$i]}{$numbers[0]} = $numbers[$i + 1]; + + # Keep a total of the number of open bugs for this day + if (grep { $_ eq $fields[$i] } @openedstatuses) { + $data{$open_name}{$numbers[0]} += $numbers[$i + 1]; } + } + + $last_date = $numbers[0]; } + } + + $in->close; + + my $total_items = (scalar(@fields) + 1) * scalar(keys %{$data{'NEW'}}); + my $count = 0; + foreach my $field (@fields, $open_name) { - $dbh->bz_commit_transaction(); + # Insert values into series_data: series_id, date, value + my %fielddata = %{$data{$field}}; + foreach my $date (keys %fielddata) { + + # We need to delete in case the text file had duplicate + # entries in it. + $deletesth->execute($seriesids{$field}, $date); + + # We prepared this above + $seriesdatasth->execute($seriesids{$field}, $date, $fielddata{$date} || 0); + indicate_progress({total => $total_items, current => ++$count, every => 100}); + } + } + + # Create the groupsets for the category + my $category_id = $dbh->selectrow_array( + "SELECT id FROM series_categories " . "WHERE name = " . $dbh->quote($product)); + my $product_id = $dbh->selectrow_array( + "SELECT id FROM products " . "WHERE name = " . $dbh->quote($product)); + + if (defined($category_id) && defined($product_id)) { + + # Get all the mandatory groups for this product + my $group_ids + = $dbh->selectcol_arrayref("SELECT group_id " + . "FROM group_control_map " + . "WHERE product_id = $product_id " + . "AND (membercontrol = " + . CONTROLMAPMANDATORY + . " OR othercontrol = " + . CONTROLMAPMANDATORY + . ")"); + + foreach my $group_id (@$group_ids) { + $groupmapsth->execute($category_id, $group_id); + } + } } + + $dbh->bz_commit_transaction(); + } } sub _add_user_group_map_grant_type { - my $dbh = Bugzilla->dbh; - # 2004-04-12 - Keep regexp-based group permissions up-to-date - Bug 240325 - if ($dbh->bz_column_info("user_group_map", "isderived")) { - $dbh->bz_add_column('user_group_map', 'grant_type', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); - $dbh->do("DELETE FROM user_group_map WHERE isderived != 0"); - $dbh->do("UPDATE user_group_map SET grant_type = " . GRANT_DIRECT); - $dbh->bz_drop_column("user_group_map", "isderived"); - - $dbh->bz_drop_index('user_group_map', 'user_group_map_user_id_idx'); - $dbh->bz_add_index('user_group_map', 'user_group_map_user_id_idx', - {TYPE => 'UNIQUE', - FIELDS => [qw(user_id group_id grant_type isbless)]}); - } + my $dbh = Bugzilla->dbh; + + # 2004-04-12 - Keep regexp-based group permissions up-to-date - Bug 240325 + if ($dbh->bz_column_info("user_group_map", "isderived")) { + $dbh->bz_add_column('user_group_map', 'grant_type', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); + $dbh->do("DELETE FROM user_group_map WHERE isderived != 0"); + $dbh->do("UPDATE user_group_map SET grant_type = " . GRANT_DIRECT); + $dbh->bz_drop_column("user_group_map", "isderived"); + + $dbh->bz_drop_index('user_group_map', 'user_group_map_user_id_idx'); + $dbh->bz_add_index('user_group_map', 'user_group_map_user_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(user_id group_id grant_type isbless)]}); + } } sub _add_group_group_map_grant_type { - my $dbh = Bugzilla->dbh; - # 2004-07-16 - Make it possible to have group-group relationships other than - # membership and bless. - if ($dbh->bz_column_info("group_group_map", "isbless")) { - $dbh->bz_add_column('group_group_map', 'grant_type', - {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); - $dbh->do("UPDATE group_group_map SET grant_type = " . - "IF(isbless, " . GROUP_BLESS . ", " . - GROUP_MEMBERSHIP . ")"); - $dbh->bz_drop_index('group_group_map', 'group_group_map_member_id_idx'); - $dbh->bz_drop_column("group_group_map", "isbless"); - $dbh->bz_add_index('group_group_map', 'group_group_map_member_id_idx', - {TYPE => 'UNIQUE', - FIELDS => [qw(member_id grantor_id grant_type)]}); - } + my $dbh = Bugzilla->dbh; + + # 2004-07-16 - Make it possible to have group-group relationships other than + # membership and bless. + if ($dbh->bz_column_info("group_group_map", "isbless")) { + $dbh->bz_add_column('group_group_map', 'grant_type', + {TYPE => 'INT1', NOTNULL => 1, DEFAULT => '0'}); + $dbh->do("UPDATE group_group_map SET grant_type = " + . "IF(isbless, " + . GROUP_BLESS . ", " + . GROUP_MEMBERSHIP + . ")"); + $dbh->bz_drop_index('group_group_map', 'group_group_map_member_id_idx'); + $dbh->bz_drop_column("group_group_map", "isbless"); + $dbh->bz_add_index( + 'group_group_map', + 'group_group_map_member_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(member_id grantor_id grant_type)]} + ); + } } sub _add_longdescs_already_wrapped { - my $dbh = Bugzilla->dbh; - # 2005-01-29 - mkanat@bugzilla.org - if (!$dbh->bz_column_info('longdescs', 'already_wrapped')) { - # Old, pre-wrapped comments should not be auto-wrapped - $dbh->bz_add_column('longdescs', 'already_wrapped', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, 1); - # If an old comment doesn't have a newline in the first 81 characters, - # (or doesn't contain a newline at all) and it contains a space, - # then it's probably a mis-wrapped comment and we should wrap it - # at display-time. - print "Fixing old, mis-wrapped comments...\n"; - $dbh->do(q{UPDATE longdescs SET already_wrapped = 0 + my $dbh = Bugzilla->dbh; + + # 2005-01-29 - mkanat@bugzilla.org + if (!$dbh->bz_column_info('longdescs', 'already_wrapped')) { + + # Old, pre-wrapped comments should not be auto-wrapped + $dbh->bz_add_column('longdescs', 'already_wrapped', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}, 1); + + # If an old comment doesn't have a newline in the first 81 characters, + # (or doesn't contain a newline at all) and it contains a space, + # then it's probably a mis-wrapped comment and we should wrap it + # at display-time. + print "Fixing old, mis-wrapped comments...\n"; + $dbh->do( + q{UPDATE longdescs SET already_wrapped = 0 WHERE (} . $dbh->sql_position(q{'\n'}, 'thetext') . q{ > 81 OR } . $dbh->sql_position(q{'\n'}, 'thetext') . q{ = 0) - AND SUBSTRING(thetext FROM 1 FOR 80) LIKE '% %'}); - } + AND SUBSTRING(thetext FROM 1 FOR 80) LIKE '% %'} + ); + } } sub _convert_attachments_filename_from_mediumtext { - my $dbh = Bugzilla->dbh; - # 2002 November, myk@mozilla.org, bug 178841: - # - # Convert the "attachments.filename" column from a ridiculously large - # "mediumtext" to a much more sensible "varchar(100)". Also takes - # the opportunity to remove paths from existing filenames, since they - # shouldn't be there for security. Buggy browsers include them, - # and attachment.cgi now takes them out, but old ones need converting. - my $ref = $dbh->bz_column_info("attachments", "filename"); - if ($ref->{TYPE} ne 'varchar(100)') { - print "Removing paths from filenames in attachments table..."; - - my $sth = $dbh->prepare("SELECT attach_id, filename FROM attachments " . - "WHERE " . $dbh->sql_position(q{'/'}, 'filename') . " > 0 OR " . - $dbh->sql_position(q{'\\\\'}, 'filename') . " > 0"); - $sth->execute; - - while (my ($attach_id, $filename) = $sth->fetchrow_array) { - $filename =~ s/^.*[\/\\]//; - my $quoted_filename = $dbh->quote($filename); - $dbh->do("UPDATE attachments SET filename = $quoted_filename " . - "WHERE attach_id = $attach_id"); - } + my $dbh = Bugzilla->dbh; + + # 2002 November, myk@mozilla.org, bug 178841: + # + # Convert the "attachments.filename" column from a ridiculously large + # "mediumtext" to a much more sensible "varchar(100)". Also takes + # the opportunity to remove paths from existing filenames, since they + # shouldn't be there for security. Buggy browsers include them, + # and attachment.cgi now takes them out, but old ones need converting. + my $ref = $dbh->bz_column_info("attachments", "filename"); + if ($ref->{TYPE} ne 'varchar(100)') { + print "Removing paths from filenames in attachments table..."; + + my $sth + = $dbh->prepare("SELECT attach_id, filename FROM attachments " + . "WHERE " + . $dbh->sql_position(q{'/'}, 'filename') + . " > 0 OR " + . $dbh->sql_position(q{'\\\\'}, 'filename') + . " > 0"); + $sth->execute; + + while (my ($attach_id, $filename) = $sth->fetchrow_array) { + $filename =~ s/^.*[\/\\]//; + my $quoted_filename = $dbh->quote($filename); + $dbh->do("UPDATE attachments SET filename = $quoted_filename " + . "WHERE attach_id = $attach_id"); + } - print "Done.\n"; + print "Done.\n"; - $dbh->bz_alter_column("attachments", "filename", - {TYPE => 'varchar(100)', NOTNULL => 1}); - } + $dbh->bz_alter_column("attachments", "filename", + {TYPE => 'varchar(100)', NOTNULL => 1}); + } } sub _rename_votes_count_and_force_group_refresh { - my $dbh = Bugzilla->dbh; - # 2003-04-27 - bugzilla@chimpychompy.org (GavinS) - # - # Bug 180086 (http://bugzilla.mozilla.org/show_bug.cgi?id=180086) - # - # Renaming the 'count' column in the votes table because Sybase doesn't - # like it - return if !$dbh->bz_table_info('votes'); - return if $dbh->bz_column_info('votes', 'count'); - $dbh->bz_rename_column('votes', 'count', 'vote_count'); + my $dbh = Bugzilla->dbh; + + # 2003-04-27 - bugzilla@chimpychompy.org (GavinS) + # + # Bug 180086 (http://bugzilla.mozilla.org/show_bug.cgi?id=180086) + # + # Renaming the 'count' column in the votes table because Sybase doesn't + # like it + return if !$dbh->bz_table_info('votes'); + return if $dbh->bz_column_info('votes', 'count'); + $dbh->bz_rename_column('votes', 'count', 'vote_count'); } sub _fix_group_with_empty_name { - my $dbh = Bugzilla->dbh; - # 2005-01-12 Nick Barnes bug 278010 - # Rename any group which has an empty name. - # Note that there can be at most one such group (because of - # the SQL index on the name column). - my ($emptygroupid) = $dbh->selectrow_array( - "SELECT id FROM groups where name = ''"); - if ($emptygroupid) { - # There is a group with an empty name; find a name to rename it - # as. Must avoid collisions with existing names. Start with - # group_$gid and add _ if necessary. - my $trycount = 0; - my $trygroupname; - my $sth = $dbh->prepare("SELECT 1 FROM groups where name = ?"); - my $name_exists = 1; - - while ($name_exists) { - $trygroupname = "group_$emptygroupid"; - if ($trycount > 0) { - $trygroupname .= "_$trycount"; - } - $name_exists = $dbh->selectrow_array($sth, undef, $trygroupname); - $trycount++; - } - $dbh->do("UPDATE groups SET name = ? WHERE id = ?", - undef, $trygroupname, $emptygroupid); - print "Group $emptygroupid had an empty name; renamed as", - " '$trygroupname'.\n"; + my $dbh = Bugzilla->dbh; + + # 2005-01-12 Nick Barnes bug 278010 + # Rename any group which has an empty name. + # Note that there can be at most one such group (because of + # the SQL index on the name column). + my ($emptygroupid) + = $dbh->selectrow_array("SELECT id FROM groups where name = ''"); + if ($emptygroupid) { + + # There is a group with an empty name; find a name to rename it + # as. Must avoid collisions with existing names. Start with + # group_$gid and add _ if necessary. + my $trycount = 0; + my $trygroupname; + my $sth = $dbh->prepare("SELECT 1 FROM groups where name = ?"); + my $name_exists = 1; + + while ($name_exists) { + $trygroupname = "group_$emptygroupid"; + if ($trycount > 0) { + $trygroupname .= "_$trycount"; + } + $name_exists = $dbh->selectrow_array($sth, undef, $trygroupname); + $trycount++; } + $dbh->do("UPDATE groups SET name = ? WHERE id = ?", + undef, $trygroupname, $emptygroupid); + print "Group $emptygroupid had an empty name; renamed as", + " '$trygroupname'.\n"; + } } # A helper for the emailprefs subs below sub _clone_email_event { - my ($source, $target) = @_; - my $dbh = Bugzilla->dbh; + my ($source, $target) = @_; + my $dbh = Bugzilla->dbh; - $dbh->do("INSERT INTO email_setting (user_id, relationship, event) + $dbh->do( + "INSERT INTO email_setting (user_id, relationship, event) SELECT user_id, relationship, $target FROM email_setting - WHERE event = $source"); + WHERE event = $source" + ); } sub _migrate_email_prefs_to_new_table { - my $dbh = Bugzilla->dbh; - # 2005-03-29 - gerv@gerv.net - bug 73665. - # Migrate email preferences to new email prefs table. - if ($dbh->bz_column_info("profiles", "emailflags")) { - print "Migrating email preferences to new table...\n"; - - # These are the "roles" and "reasons" from the original code, mapped to - # the new terminology of relationships and events. - my %relationships = ("Owner" => REL_ASSIGNEE, - "Reporter" => REL_REPORTER, - "QAcontact" => REL_QA, - "CClist" => REL_CC, - # REL_VOTER was "4" before it was moved to an - # extension. - "Voter" => 4); - - my %events = ("Removeme" => EVT_ADDED_REMOVED, - "Comments" => EVT_COMMENT, - "Attachments" => EVT_ATTACHMENT, - "Status" => EVT_PROJ_MANAGEMENT, - "Resolved" => EVT_OPENED_CLOSED, - "Keywords" => EVT_KEYWORD, - "CC" => EVT_CC, - "Other" => EVT_OTHER, - "Unconfirmed" => EVT_UNCONFIRMED); - - # Request preferences - my %requestprefs = ("FlagRequestee" => EVT_FLAG_REQUESTED, - "FlagRequester" => EVT_REQUESTED_FLAG); - - # We run the below code in a transaction to speed things up. - $dbh->bz_start_transaction(); - - # Select all emailflags flag strings - my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); - my $sth = $dbh->prepare("SELECT userid, emailflags FROM profiles"); - $sth->execute(); - my $i = 0; - - while (my ($userid, $flagstring) = $sth->fetchrow_array()) { - $i++; - indicate_progress({ total => $total, current => $i, every => 10 }); - # If the user has never logged in since emailprefs arrived, and the - # temporary code to give them a default string never ran, then - # $flagstring will be null. In this case, they just get all mail. - $flagstring ||= ""; - - # The 255 param is here, because without a third param, split will - # trim any trailing null fields, which causes Perl to eject lots of - # warnings. Any suitably large number would do. - my %emailflags = split(/~/, $flagstring, 255); - - my $sth2 = $dbh->prepare("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - "$userid, ?, ?)"); - foreach my $relationship (keys %relationships) { - foreach my $event (keys %events) { - my $key = "email$relationship$event"; - if (!exists($emailflags{$key}) - || $emailflags{$key} eq 'on') - { - $sth2->execute($relationships{$relationship}, - $events{$event}); - } - } - } - # Note that in the old system, the value of "excludeself" is - # assumed to be off if the preference does not exist in the - # user's list, unlike other preferences whose value is - # assumed to be on if they do not exist. - # - # This preference has changed from global to per-relationship. - if (!exists($emailflags{'ExcludeSelf'}) - || $emailflags{'ExcludeSelf'} ne 'on') - { - foreach my $relationship (keys %relationships) { - $dbh->do("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - $userid . ", " . - $relationships{$relationship}. ", " . - EVT_CHANGED_BY_ME . ")"); - } - } + my $dbh = Bugzilla->dbh; + + # 2005-03-29 - gerv@gerv.net - bug 73665. + # Migrate email preferences to new email prefs table. + if ($dbh->bz_column_info("profiles", "emailflags")) { + print "Migrating email preferences to new table...\n"; + + # These are the "roles" and "reasons" from the original code, mapped to + # the new terminology of relationships and events. + my %relationships = ( + "Owner" => REL_ASSIGNEE, + "Reporter" => REL_REPORTER, + "QAcontact" => REL_QA, + "CClist" => REL_CC, + + # REL_VOTER was "4" before it was moved to an + # extension. + "Voter" => 4 + ); - foreach my $key (keys %requestprefs) { - if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { - $dbh->do("INSERT into email_setting " . - "(user_id, relationship, event) VALUES (" . - $userid . ", " . REL_ANY . ", " . - $requestprefs{$key} . ")"); - } - } - } - print "\n"; + my %events = ( + "Removeme" => EVT_ADDED_REMOVED, + "Comments" => EVT_COMMENT, + "Attachments" => EVT_ATTACHMENT, + "Status" => EVT_PROJ_MANAGEMENT, + "Resolved" => EVT_OPENED_CLOSED, + "Keywords" => EVT_KEYWORD, + "CC" => EVT_CC, + "Other" => EVT_OTHER, + "Unconfirmed" => EVT_UNCONFIRMED + ); - # EVT_ATTACHMENT_DATA should initially have identical settings to - # EVT_ATTACHMENT. - _clone_email_event(EVT_ATTACHMENT, EVT_ATTACHMENT_DATA); + # Request preferences + my %requestprefs = ( + "FlagRequestee" => EVT_FLAG_REQUESTED, + "FlagRequester" => EVT_REQUESTED_FLAG + ); + + # We run the below code in a transaction to speed things up. + $dbh->bz_start_transaction(); - $dbh->bz_commit_transaction(); - $dbh->bz_drop_column("profiles", "emailflags"); + # Select all emailflags flag strings + my $total = $dbh->selectrow_array('SELECT COUNT(*) FROM profiles'); + my $sth = $dbh->prepare("SELECT userid, emailflags FROM profiles"); + $sth->execute(); + my $i = 0; + + while (my ($userid, $flagstring) = $sth->fetchrow_array()) { + $i++; + indicate_progress({total => $total, current => $i, every => 10}); + + # If the user has never logged in since emailprefs arrived, and the + # temporary code to give them a default string never ran, then + # $flagstring will be null. In this case, they just get all mail. + $flagstring ||= ""; + + # The 255 param is here, because without a third param, split will + # trim any trailing null fields, which causes Perl to eject lots of + # warnings. Any suitably large number would do. + my %emailflags = split(/~/, $flagstring, 255); + + my $sth2 + = $dbh->prepare("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . "$userid, ?, ?)"); + foreach my $relationship (keys %relationships) { + foreach my $event (keys %events) { + my $key = "email$relationship$event"; + if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { + $sth2->execute($relationships{$relationship}, $events{$event}); + } + } + } + + # Note that in the old system, the value of "excludeself" is + # assumed to be off if the preference does not exist in the + # user's list, unlike other preferences whose value is + # assumed to be on if they do not exist. + # + # This preference has changed from global to per-relationship. + if (!exists($emailflags{'ExcludeSelf'}) || $emailflags{'ExcludeSelf'} ne 'on') { + foreach my $relationship (keys %relationships) { + $dbh->do("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . $userid . ", " + . $relationships{$relationship} . ", " + . EVT_CHANGED_BY_ME + . ")"); + } + } + + foreach my $key (keys %requestprefs) { + if (!exists($emailflags{$key}) || $emailflags{$key} eq 'on') { + $dbh->do("INSERT into email_setting " + . "(user_id, relationship, event) VALUES (" + . $userid . ", " + . REL_ANY . ", " + . $requestprefs{$key} + . ")"); + } + } } + print "\n"; + + # EVT_ATTACHMENT_DATA should initially have identical settings to + # EVT_ATTACHMENT. + _clone_email_event(EVT_ATTACHMENT, EVT_ATTACHMENT_DATA); + + $dbh->bz_commit_transaction(); + $dbh->bz_drop_column("profiles", "emailflags"); + } } sub _initialize_new_email_prefs { - my $dbh = Bugzilla->dbh; - # Check for any "new" email settings that wouldn't have been ported over - # during the block above. Since these settings would have otherwise - # fallen under EVT_OTHER, we'll just clone those settings. That way if - # folks have already disabled all of that mail, there won't be any change. - my %events = ( - "Dependency Tree Changes" => EVT_DEPEND_BLOCK, - "Product/Component Changes" => EVT_COMPONENT, - ); - - foreach my $desc (keys %events) { - my $event = $events{$desc}; - my $have_events = $dbh->selectrow_array( - "SELECT 1 FROM email_setting WHERE event = $event " - . $dbh->sql_limit(1)); - - if (!$have_events) { - # No settings in the table yet, so we assume that this is the - # first time it's being set. - print "Initializing \"$desc\" email_setting ...\n"; - _clone_email_event(EVT_OTHER, $event); - } + my $dbh = Bugzilla->dbh; + + # Check for any "new" email settings that wouldn't have been ported over + # during the block above. Since these settings would have otherwise + # fallen under EVT_OTHER, we'll just clone those settings. That way if + # folks have already disabled all of that mail, there won't be any change. + my %events = ( + "Dependency Tree Changes" => EVT_DEPEND_BLOCK, + "Product/Component Changes" => EVT_COMPONENT, + ); + + foreach my $desc (keys %events) { + my $event = $events{$desc}; + my $have_events = $dbh->selectrow_array( + "SELECT 1 FROM email_setting WHERE event = $event " . $dbh->sql_limit(1)); + + if (!$have_events) { + + # No settings in the table yet, so we assume that this is the + # first time it's being set. + print "Initializing \"$desc\" email_setting ...\n"; + _clone_email_event(EVT_OTHER, $event); } + } } sub _change_all_mysql_booleans_to_tinyint { - my $dbh = Bugzilla->dbh; - # 2005-03-27: Standardize all boolean fields to plain "tinyint" - if ( $dbh->isa('Bugzilla::DB::Mysql') ) { - # This is a change to make things consistent with Schema, so we use - # direct-database access methods. - my $quip_info_sth = $dbh->column_info(undef, undef, 'quips', '%'); - my $quips_cols = $quip_info_sth->fetchall_hashref("COLUMN_NAME"); - my $approved_col = $quips_cols->{'approved'}; - if ( $approved_col->{TYPE_NAME} eq 'TINYINT' - and $approved_col->{COLUMN_SIZE} == 1 ) - { - # series.public could have been renamed to series.is_public, - # and so wouldn't need to be fixed manually. - if ($dbh->bz_column_info('series', 'public')) { - $dbh->bz_alter_column_raw('series', 'public', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '0'}); - } - $dbh->bz_alter_column_raw('bug_status', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('rep_platform', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('resolution', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('op_sys', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('bug_severity', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('priority', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - $dbh->bz_alter_column_raw('quips', 'approved', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); - } - } + my $dbh = Bugzilla->dbh; + + # 2005-03-27: Standardize all boolean fields to plain "tinyint" + if ($dbh->isa('Bugzilla::DB::Mysql')) { + + # This is a change to make things consistent with Schema, so we use + # direct-database access methods. + my $quip_info_sth = $dbh->column_info(undef, undef, 'quips', '%'); + my $quips_cols = $quip_info_sth->fetchall_hashref("COLUMN_NAME"); + my $approved_col = $quips_cols->{'approved'}; + if ( $approved_col->{TYPE_NAME} eq 'TINYINT' + and $approved_col->{COLUMN_SIZE} == 1) + { + # series.public could have been renamed to series.is_public, + # and so wouldn't need to be fixed manually. + if ($dbh->bz_column_info('series', 'public')) { + $dbh->bz_alter_column_raw('series', 'public', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '0'}); + } + $dbh->bz_alter_column_raw('bug_status', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('rep_platform', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('resolution', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('op_sys', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('bug_severity', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('priority', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + $dbh->bz_alter_column_raw('quips', 'approved', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => '1'}); + } + } } # A helper for the below function. sub _de_dup_version { - my ($product_id, $version) = @_; - my $dbh = Bugzilla->dbh; - print "Fixing duplicate version $version in product_id $product_id...\n"; - $dbh->do('DELETE FROM versions WHERE product_id = ? AND value = ?', - undef, $product_id, $version); - $dbh->do('INSERT INTO versions (product_id, value) VALUES (?,?)', - undef, $product_id, $version); + my ($product_id, $version) = @_; + my $dbh = Bugzilla->dbh; + print "Fixing duplicate version $version in product_id $product_id...\n"; + $dbh->do('DELETE FROM versions WHERE product_id = ? AND value = ?', + undef, $product_id, $version); + $dbh->do('INSERT INTO versions (product_id, value) VALUES (?,?)', + undef, $product_id, $version); } sub _add_versions_product_id_index { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_index_info('versions', 'versions_product_id_idx')) { - my $dup_versions = $dbh->selectall_arrayref( - 'SELECT product_id, value FROM versions - GROUP BY product_id, value HAVING COUNT(value) > 1', {Slice=>{}}); - foreach my $dup_version (@$dup_versions) { - _de_dup_version($dup_version->{product_id}, $dup_version->{value}); - } - - $dbh->bz_add_index('versions', 'versions_product_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_index_info('versions', 'versions_product_id_idx')) { + my $dup_versions = $dbh->selectall_arrayref( + 'SELECT product_id, value FROM versions + GROUP BY product_id, value HAVING COUNT(value) > 1', {Slice => {}} + ); + foreach my $dup_version (@$dup_versions) { + _de_dup_version($dup_version->{product_id}, $dup_version->{value}); } + + $dbh->bz_add_index('versions', 'versions_product_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(product_id value)]}); + } } sub _fix_whine_queries_title_and_op_sys_value { - my $dbh = Bugzilla->dbh; - if (!exists $dbh->bz_column_info('whine_queries', 'title')->{DEFAULT}) { - # The below change actually has nothing to do with the whine_queries - # change, it just has to be contained within a schema change so that - # it doesn't run every time we run checksetup. - - # Old Bugzillas have "other" as an OS choice, new ones have "Other" - # (capital O). - print "Setting any 'other' op_sys to 'Other'...\n"; - $dbh->do('UPDATE op_sys SET value = ? WHERE value = ?', - undef, "Other", "other"); - $dbh->do('UPDATE bugs SET op_sys = ? WHERE op_sys = ?', - undef, "Other", "other"); - if (Bugzilla->params->{'defaultopsys'} eq 'other') { - # We can't actually fix the param here, because WriteParams() will - # make $datadir/params unwriteable to the webservergroup. - # It's too much of an ugly hack to copy the permission-fixing code - # down to here. (It would create more potential future bugs than - # it would solve problems.) - print "WARNING: Your 'defaultopsys' param is set to 'other', but" - . " Bugzilla now\n" - . " uses 'Other' (capital O).\n"; - } - - # Add a DEFAULT to whine_queries stuff so that editwhines.cgi - # works on PostgreSQL. - $dbh->bz_alter_column('whine_queries', 'title', {TYPE => 'varchar(128)', - NOTNULL => 1, DEFAULT => "''"}); + my $dbh = Bugzilla->dbh; + if (!exists $dbh->bz_column_info('whine_queries', 'title')->{DEFAULT}) { + + # The below change actually has nothing to do with the whine_queries + # change, it just has to be contained within a schema change so that + # it doesn't run every time we run checksetup. + + # Old Bugzillas have "other" as an OS choice, new ones have "Other" + # (capital O). + print "Setting any 'other' op_sys to 'Other'...\n"; + $dbh->do('UPDATE op_sys SET value = ? WHERE value = ?', undef, "Other", + "other"); + $dbh->do('UPDATE bugs SET op_sys = ? WHERE op_sys = ?', undef, "Other", + "other"); + if (Bugzilla->params->{'defaultopsys'} eq 'other') { + + # We can't actually fix the param here, because WriteParams() will + # make $datadir/params unwriteable to the webservergroup. + # It's too much of an ugly hack to copy the permission-fixing code + # down to here. (It would create more potential future bugs than + # it would solve problems.) + print "WARNING: Your 'defaultopsys' param is set to 'other', but" + . " Bugzilla now\n" + . " uses 'Other' (capital O).\n"; } + + # Add a DEFAULT to whine_queries stuff so that editwhines.cgi + # works on PostgreSQL. + $dbh->bz_alter_column('whine_queries', 'title', + {TYPE => 'varchar(128)', NOTNULL => 1, DEFAULT => "''"}); + } } sub _fix_attachments_submitter_id_idx { - my $dbh = Bugzilla->dbh; - # 2005-06-29 bugreport@peshkin.net, bug 299156 - if ($dbh->bz_index_info('attachments', 'attachments_submitter_id_idx') - && (scalar(@{$dbh->bz_index_info('attachments', - 'attachments_submitter_id_idx' - )->{FIELDS}}) < 2)) - { - $dbh->bz_drop_index('attachments', 'attachments_submitter_id_idx'); - } - $dbh->bz_add_index('attachments', 'attachments_submitter_id_idx', - [qw(submitter_id bug_id)]); + my $dbh = Bugzilla->dbh; + + # 2005-06-29 bugreport@peshkin.net, bug 299156 + if ( + $dbh->bz_index_info('attachments', 'attachments_submitter_id_idx') + && ( + scalar(@{ + $dbh->bz_index_info('attachments', 'attachments_submitter_id_idx')->{FIELDS} + }) < 2 + ) + ) + { + $dbh->bz_drop_index('attachments', 'attachments_submitter_id_idx'); + } + $dbh->bz_add_index('attachments', 'attachments_submitter_id_idx', + [qw(submitter_id bug_id)]); } sub _copy_attachments_thedata_to_attach_data { - my $dbh = Bugzilla->dbh; - # 2005-08-25 - bugreport@peshkin.net - Bug 305333 - if ($dbh->bz_column_info("attachments", "thedata")) { - print "Migrating attachment data to its own table...\n"; - print "(This may take a very long time)\n"; - $dbh->do("INSERT INTO attach_data (id, thedata) - SELECT attach_id, thedata FROM attachments"); - $dbh->bz_drop_column("attachments", "thedata"); - } + my $dbh = Bugzilla->dbh; + + # 2005-08-25 - bugreport@peshkin.net - Bug 305333 + if ($dbh->bz_column_info("attachments", "thedata")) { + print "Migrating attachment data to its own table...\n"; + print "(This may take a very long time)\n"; + $dbh->do( + "INSERT INTO attach_data (id, thedata) + SELECT attach_id, thedata FROM attachments" + ); + $dbh->bz_drop_column("attachments", "thedata"); + } } sub _fix_broken_all_closed_series { - my $dbh = Bugzilla->dbh; - - # 2005-11-26 - wurblzap@gmail.com - Bug 300473 - # Repair broken automatically generated series queries for non-open bugs. - my $broken_series_indicator = - 'field0-0-0=resolution&type0-0-0=notequals&value0-0-0=---'; - my $broken_nonopen_series = - $dbh->selectall_arrayref("SELECT series_id, query FROM series - WHERE query LIKE '$broken_series_indicator%'"); - if (@$broken_nonopen_series) { - print 'Repairing broken series...'; - my $sth_nuke = - $dbh->prepare('DELETE FROM series_data WHERE series_id = ?'); - # This statement is used to repair a series by replacing the broken - # query with the correct one. - my $sth_repair = - $dbh->prepare('UPDATE series SET query = ? WHERE series_id = ?'); - # The corresponding series for open bugs look like one of these two - # variations (bug 225687 changed the order of bug states). - # This depends on the set of bug states representing open bugs not - # to have changed since series creation. - my $open_bugs_query_base_old = - join("&", map { "bug_status=" . url_quote($_) } - ('UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED')); - my $open_bugs_query_base_new = - join("&", map { "bug_status=" . url_quote($_) } - ('NEW', 'REOPENED', 'ASSIGNED', 'UNCONFIRMED')); - my $sth_openbugs_series = - $dbh->prepare("SELECT series_id FROM series WHERE query IN (?, ?)"); - # Statement to find the series which has collected the most data. - my $sth_data_collected = - $dbh->prepare('SELECT count(*) FROM series_data - WHERE series_id = ?'); - # Statement to select a broken non-open bugs count data entry. - my $sth_select_broken_nonopen_data = - $dbh->prepare('SELECT series_date, series_value FROM series_data' . - ' WHERE series_id = ?'); - # Statement to select an open bugs count data entry. - my $sth_select_open_data = - $dbh->prepare('SELECT series_value FROM series_data' . - ' WHERE series_id = ? AND series_date = ?'); - # Statement to fix a broken non-open bugs count data entry. - my $sth_fix_broken_nonopen_data = - $dbh->prepare('UPDATE series_data SET series_value = ?' . - ' WHERE series_id = ? AND series_date = ?'); - # Statement to delete an unfixable broken non-open bugs count data - # entry. - my $sth_delete_broken_nonopen_data = - $dbh->prepare('DELETE FROM series_data' . - ' WHERE series_id = ? AND series_date = ?'); - foreach (@$broken_nonopen_series) { - my ($broken_series_id, $nonopen_bugs_query) = @$_; - - # Determine the product-and-component part of the query. - if ($nonopen_bugs_query =~ /^$broken_series_indicator(.*)$/) { - my $prodcomp = $1; - - # If there is more than one series for the corresponding - # open-bugs series, we pick the one with the most data, - # which should be the one which was generated on creation. - # It's a pity we can't do subselects. - $sth_openbugs_series->execute( - $open_bugs_query_base_old . $prodcomp, - $open_bugs_query_base_new . $prodcomp); - - my ($found_open_series_id, $datacount) = (undef, -1); - foreach my $open_ser_id ($sth_openbugs_series->fetchrow_array) { - $sth_data_collected->execute($open_ser_id); - my ($this_datacount) = $sth_data_collected->fetchrow_array; - if ($this_datacount > $datacount) { - $datacount = $this_datacount; - $found_open_series_id = $open_ser_id; - } - } - - if ($found_open_series_id) { - # Move along corrupted series data and correct it. The - # corruption consists of it being the number of all bugs - # instead of the number of non-open bugs, so we calculate - # the correct count by subtracting the number of open bugs. - # If there is no corresponding open-bugs count for some - # reason (shouldn't happen), we drop the data entry. - print " $broken_series_id..."; - $sth_select_broken_nonopen_data->execute($broken_series_id); - while (my $rowref = - $sth_select_broken_nonopen_data->fetchrow_arrayref) - { - my ($date, $broken_value) = @$rowref; - my ($openbugs_value) = - $dbh->selectrow_array($sth_select_open_data, undef, - $found_open_series_id, $date); - if (defined($openbugs_value)) { - $sth_fix_broken_nonopen_data->execute - ($broken_value - $openbugs_value, - $broken_series_id, $date); - } - else { - print <dbh; + + # 2005-11-26 - wurblzap@gmail.com - Bug 300473 + # Repair broken automatically generated series queries for non-open bugs. + my $broken_series_indicator + = 'field0-0-0=resolution&type0-0-0=notequals&value0-0-0=---'; + my $broken_nonopen_series = $dbh->selectall_arrayref( + "SELECT series_id, query FROM series + WHERE query LIKE '$broken_series_indicator%'" + ); + if (@$broken_nonopen_series) { + print 'Repairing broken series...'; + my $sth_nuke = $dbh->prepare('DELETE FROM series_data WHERE series_id = ?'); + + # This statement is used to repair a series by replacing the broken + # query with the correct one. + my $sth_repair + = $dbh->prepare('UPDATE series SET query = ? WHERE series_id = ?'); + + # The corresponding series for open bugs look like one of these two + # variations (bug 225687 changed the order of bug states). + # This depends on the set of bug states representing open bugs not + # to have changed since series creation. + my $open_bugs_query_base_old = join("&", + map { "bug_status=" . url_quote($_) } + ('UNCONFIRMED', 'NEW', 'ASSIGNED', 'REOPENED')); + my $open_bugs_query_base_new = join("&", + map { "bug_status=" . url_quote($_) } + ('NEW', 'REOPENED', 'ASSIGNED', 'UNCONFIRMED')); + my $sth_openbugs_series + = $dbh->prepare("SELECT series_id FROM series WHERE query IN (?, ?)"); + + # Statement to find the series which has collected the most data. + my $sth_data_collected = $dbh->prepare( + 'SELECT count(*) FROM series_data + WHERE series_id = ?' + ); + + # Statement to select a broken non-open bugs count data entry. + my $sth_select_broken_nonopen_data = $dbh->prepare( + 'SELECT series_date, series_value FROM series_data' . ' WHERE series_id = ?'); + + # Statement to select an open bugs count data entry. + my $sth_select_open_data = $dbh->prepare('SELECT series_value FROM series_data' + . ' WHERE series_id = ? AND series_date = ?'); + + # Statement to fix a broken non-open bugs count data entry. + my $sth_fix_broken_nonopen_data + = $dbh->prepare('UPDATE series_data SET series_value = ?' + . ' WHERE series_id = ? AND series_date = ?'); + + # Statement to delete an unfixable broken non-open bugs count data + # entry. + my $sth_delete_broken_nonopen_data = $dbh->prepare( + 'DELETE FROM series_data' . ' WHERE series_id = ? AND series_date = ?'); + foreach (@$broken_nonopen_series) { + my ($broken_series_id, $nonopen_bugs_query) = @$_; + + # Determine the product-and-component part of the query. + if ($nonopen_bugs_query =~ /^$broken_series_indicator(.*)$/) { + my $prodcomp = $1; + + # If there is more than one series for the corresponding + # open-bugs series, we pick the one with the most data, + # which should be the one which was generated on creation. + # It's a pity we can't do subselects. + $sth_openbugs_series->execute($open_bugs_query_base_old . $prodcomp, + $open_bugs_query_base_new . $prodcomp); + + my ($found_open_series_id, $datacount) = (undef, -1); + foreach my $open_ser_id ($sth_openbugs_series->fetchrow_array) { + $sth_data_collected->execute($open_ser_id); + my ($this_datacount) = $sth_data_collected->fetchrow_array; + if ($this_datacount > $datacount) { + $datacount = $this_datacount; + $found_open_series_id = $open_ser_id; + } + } + + if ($found_open_series_id) { + + # Move along corrupted series data and correct it. The + # corruption consists of it being the number of all bugs + # instead of the number of non-open bugs, so we calculate + # the correct count by subtracting the number of open bugs. + # If there is no corresponding open-bugs count for some + # reason (shouldn't happen), we drop the data entry. + print " $broken_series_id..."; + $sth_select_broken_nonopen_data->execute($broken_series_id); + while (my $rowref = $sth_select_broken_nonopen_data->fetchrow_arrayref) { + my ($date, $broken_value) = @$rowref; + my ($openbugs_value) + = $dbh->selectrow_array($sth_select_open_data, undef, $found_open_series_id, + $date); + if (defined($openbugs_value)) { + $sth_fix_broken_nonopen_data->execute($broken_value - $openbugs_value, + $broken_series_id, $date); + } + else { + print <execute - ($broken_series_id, $date); - } - } - - # Fix the broken query so that it collects correct data - # in the future. - $nonopen_bugs_query =~ - s/^$broken_series_indicator/field0-0-0=resolution&type0-0-0=regexp&value0-0-0=./; - $sth_repair->execute($nonopen_bugs_query, - $broken_series_id); - } - else { - print <execute($broken_series_id, $date); + } + } + + # Fix the broken query so that it collects correct data + # in the future. + $nonopen_bugs_query + =~ s/^$broken_series_indicator/field0-0-0=resolution&type0-0-0=regexp&value0-0-0=./; + $sth_repair->execute($nonopen_bugs_query, $broken_series_id); + } + else { + print <dbh; + my $dbh = Bugzilla->dbh; - my $regex_groups_exist = $dbh->selectrow_array( - "SELECT 1 FROM groups WHERE userregexp = '' " . $dbh->sql_limit(1)); - return if !$regex_groups_exist; + my $regex_groups_exist = $dbh->selectrow_array( + "SELECT 1 FROM groups WHERE userregexp = '' " . $dbh->sql_limit(1)); + return if !$regex_groups_exist; - my $regex_derivations = $dbh->selectrow_array( - 'SELECT 1 FROM user_group_map WHERE grant_type = ' . GRANT_REGEXP - . ' ' . $dbh->sql_limit(1)); - return if $regex_derivations; + my $regex_derivations + = $dbh->selectrow_array('SELECT 1 FROM user_group_map WHERE grant_type = ' + . GRANT_REGEXP . ' ' + . $dbh->sql_limit(1)); + return if $regex_derivations; - print "Deriving regex group memberships...\n"; + print "Deriving regex group memberships...\n"; - # Re-evaluate all regexps, to keep them up-to-date. - my $sth = $dbh->prepare( - "SELECT profiles.userid, profiles.login_name, groups.id, + # Re-evaluate all regexps, to keep them up-to-date. + my $sth = $dbh->prepare( + "SELECT profiles.userid, profiles.login_name, groups.id, groups.userregexp, user_group_map.group_id FROM (profiles CROSS JOIN groups) LEFT JOIN user_group_map ON user_group_map.user_id = profiles.userid AND user_group_map.group_id = groups.id AND user_group_map.grant_type = ? - WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL"); + WHERE userregexp != '' OR user_group_map.group_id IS NOT NULL" + ); - my $sth_add = $dbh->prepare( - "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, 0, " . GRANT_REGEXP . ")"); + my $sth_add = $dbh->prepare( + "INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, 0, " . GRANT_REGEXP . ")" + ); - my $sth_del = $dbh->prepare( - "DELETE FROM user_group_map + my $sth_del = $dbh->prepare( + "DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = 0 - AND grant_type = " . GRANT_REGEXP); + AND grant_type = " . GRANT_REGEXP + ); - $sth->execute(GRANT_REGEXP); - while (my ($uid, $login, $gid, $rexp, $present) = - $sth->fetchrow_array()) - { - if ($login =~ m/$rexp/i) { - $sth_add->execute($uid, $gid) unless $present; - } else { - $sth_del->execute($uid, $gid) if $present; - } + $sth->execute(GRANT_REGEXP); + while (my ($uid, $login, $gid, $rexp, $present) = $sth->fetchrow_array()) { + if ($login =~ m/$rexp/i) { + $sth_add->execute($uid, $gid) unless $present; + } + else { + $sth_del->execute($uid, $gid) if $present; } + } } sub _clean_control_characters_from_short_desc { - my $dbh = Bugzilla->dbh; - - # Fixup for Bug 101380 - # "Newlines, nulls, leading/trailing spaces are getting into summaries" - - my $controlchar_bugs = - $dbh->selectall_arrayref("SELECT short_desc, bug_id FROM bugs WHERE " . - $dbh->sql_regexp('short_desc', "'[[:cntrl:]]'")); - if (scalar(@$controlchar_bugs)) { - my $msg = 'Cleaning control characters from bug summaries...'; - my $found = 0; - foreach (@$controlchar_bugs) { - my ($short_desc, $bug_id) = @$_; - my $clean_short_desc = clean_text($short_desc); - if ($clean_short_desc ne $short_desc) { - print $msg if !$found; - $found = 1; - print " $bug_id..."; - $dbh->do("UPDATE bugs SET short_desc = ? WHERE bug_id = ?", - undef, $clean_short_desc, $bug_id); - } - } - print " done.\n" if $found; + my $dbh = Bugzilla->dbh; + + # Fixup for Bug 101380 + # "Newlines, nulls, leading/trailing spaces are getting into summaries" + + my $controlchar_bugs + = $dbh->selectall_arrayref("SELECT short_desc, bug_id FROM bugs WHERE " + . $dbh->sql_regexp('short_desc', "'[[:cntrl:]]'")); + if (scalar(@$controlchar_bugs)) { + my $msg = 'Cleaning control characters from bug summaries...'; + my $found = 0; + foreach (@$controlchar_bugs) { + my ($short_desc, $bug_id) = @$_; + my $clean_short_desc = clean_text($short_desc); + if ($clean_short_desc ne $short_desc) { + print $msg if !$found; + $found = 1; + print " $bug_id..."; + $dbh->do("UPDATE bugs SET short_desc = ? WHERE bug_id = ?", + undef, $clean_short_desc, $bug_id); + } } + print " done.\n" if $found; + } } sub _stop_storing_inactive_flags { - my $dbh = Bugzilla->dbh; - # 2006-03-02 LpSolit@gmail.com - Bug 322285 - # Do not store inactive flags in the DB anymore. - if ($dbh->bz_column_info('flags', 'id')->{'TYPE'} eq 'INT3') { - # We first have to remove all existing inactive flags. - if ($dbh->bz_column_info('flags', 'is_active')) { - $dbh->do('DELETE FROM flags WHERE is_active = 0'); - } + my $dbh = Bugzilla->dbh; - # Now we convert the id column to the auto_increment format. - $dbh->bz_alter_column('flags', 'id', - {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + # 2006-03-02 LpSolit@gmail.com - Bug 322285 + # Do not store inactive flags in the DB anymore. + if ($dbh->bz_column_info('flags', 'id')->{'TYPE'} eq 'INT3') { - # And finally, we remove the is_active column. - $dbh->bz_drop_column('flags', 'is_active'); + # We first have to remove all existing inactive flags. + if ($dbh->bz_column_info('flags', 'is_active')) { + $dbh->do('DELETE FROM flags WHERE is_active = 0'); } + + # Now we convert the id column to the auto_increment format. + $dbh->bz_alter_column('flags', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + + # And finally, we remove the is_active column. + $dbh->bz_drop_column('flags', 'is_active'); + } } sub _change_short_desc_from_mediumtext_to_varchar { - my $dbh = Bugzilla->dbh; - # short_desc should not be a mediumtext, fix anything longer than 255 chars. - if($dbh->bz_column_info('bugs', 'short_desc')->{TYPE} eq 'MEDIUMTEXT') { - # Move extremely long summaries into a comment ("from" the Reporter), - # and then truncate the summary. - my $long_summary_bugs = $dbh->selectall_arrayref( - 'SELECT bug_id, short_desc, reporter - FROM bugs WHERE CHAR_LENGTH(short_desc) > 255'); - - if (@$long_summary_bugs) { - print "\n", install_string('update_summary_truncated'); - my $comment_sth = $dbh->prepare( - 'INSERT INTO longdescs (bug_id, who, thetext, bug_when) - VALUES (?, ?, ?, NOW())'); - my $desc_sth = $dbh->prepare('UPDATE bugs SET short_desc = ? - WHERE bug_id = ?'); - my @affected_bugs; - foreach my $bug (@$long_summary_bugs) { - my ($bug_id, $summary, $reporter_id) = @$bug; - my $summary_comment = - install_string('update_summary_truncate_comment', - { summary => $summary }); - $comment_sth->execute($bug_id, $reporter_id, $summary_comment); - my $short_summary = substr($summary, 0, 252) . "..."; - $desc_sth->execute($short_summary, $bug_id); - push(@affected_bugs, $bug_id); - } - print join(', ', @affected_bugs) . "\n\n"; - } + my $dbh = Bugzilla->dbh; + + # short_desc should not be a mediumtext, fix anything longer than 255 chars. + if ($dbh->bz_column_info('bugs', 'short_desc')->{TYPE} eq 'MEDIUMTEXT') { - $dbh->bz_alter_column('bugs', 'short_desc', {TYPE => 'varchar(255)', - NOTNULL => 1}); + # Move extremely long summaries into a comment ("from" the Reporter), + # and then truncate the summary. + my $long_summary_bugs = $dbh->selectall_arrayref( + 'SELECT bug_id, short_desc, reporter + FROM bugs WHERE CHAR_LENGTH(short_desc) > 255' + ); + + if (@$long_summary_bugs) { + print "\n", install_string('update_summary_truncated'); + my $comment_sth = $dbh->prepare( + 'INSERT INTO longdescs (bug_id, who, thetext, bug_when) + VALUES (?, ?, ?, NOW())' + ); + my $desc_sth = $dbh->prepare( + 'UPDATE bugs SET short_desc = ? + WHERE bug_id = ?' + ); + my @affected_bugs; + foreach my $bug (@$long_summary_bugs) { + my ($bug_id, $summary, $reporter_id) = @$bug; + my $summary_comment + = install_string('update_summary_truncate_comment', {summary => $summary}); + $comment_sth->execute($bug_id, $reporter_id, $summary_comment); + my $short_summary = substr($summary, 0, 252) . "..."; + $desc_sth->execute($short_summary, $bug_id); + push(@affected_bugs, $bug_id); + } + print join(', ', @affected_bugs) . "\n\n"; } + + $dbh->bz_alter_column('bugs', 'short_desc', + {TYPE => 'varchar(255)', NOTNULL => 1}); + } } sub _move_namedqueries_linkinfooter_to_its_own_table { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info("namedqueries", "linkinfooter")) { - # Move link-in-footer information into a table of its own. - my $sth_read = $dbh->prepare('SELECT id, userid + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info("namedqueries", "linkinfooter")) { + + # Move link-in-footer information into a table of its own. + my $sth_read = $dbh->prepare( + 'SELECT id, userid FROM namedqueries - WHERE linkinfooter = 1'); - my $sth_write = $dbh->prepare('INSERT INTO namedqueries_link_in_footer - (namedquery_id, user_id) VALUES (?, ?)'); - $sth_read->execute(); - while (my ($id, $userid) = $sth_read->fetchrow_array()) { - $sth_write->execute($id, $userid); - } - $dbh->bz_drop_column("namedqueries", "linkinfooter"); + WHERE linkinfooter = 1' + ); + my $sth_write = $dbh->prepare( + 'INSERT INTO namedqueries_link_in_footer + (namedquery_id, user_id) VALUES (?, ?)' + ); + $sth_read->execute(); + while (my ($id, $userid) = $sth_read->fetchrow_array()) { + $sth_write->execute($id, $userid); } + $dbh->bz_drop_column("namedqueries", "linkinfooter"); + } } sub _add_classifications_sortkey { - my $dbh = Bugzilla->dbh; - # 2006-07-07 olav@bkor.dhs.org - Bug 277377 - # Add a sortkey to the classifications - if (!$dbh->bz_column_info('classifications', 'sortkey')) { - $dbh->bz_add_column('classifications', 'sortkey', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - - my $class_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM classifications ORDER BY name'); - my $sth = $dbh->prepare('UPDATE classifications SET sortkey = ? ' . - 'WHERE id = ?'); - my $sortkey = 0; - foreach my $class_id (@$class_ids) { - $sth->execute($sortkey, $class_id); - $sortkey += 100; - } + my $dbh = Bugzilla->dbh; + + # 2006-07-07 olav@bkor.dhs.org - Bug 277377 + # Add a sortkey to the classifications + if (!$dbh->bz_column_info('classifications', 'sortkey')) { + $dbh->bz_add_column('classifications', 'sortkey', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + + my $class_ids + = $dbh->selectcol_arrayref('SELECT id FROM classifications ORDER BY name'); + my $sth + = $dbh->prepare('UPDATE classifications SET sortkey = ? ' . 'WHERE id = ?'); + my $sortkey = 0; + foreach my $class_id (@$class_ids) { + $sth->execute($sortkey, $class_id); + $sortkey += 100; } + } } sub _move_data_nomail_into_db { - my $dbh = Bugzilla->dbh; - my $datadir = bz_locations()->{'datadir'}; - # 2006-07-14 karl@kornel.name - Bug 100953 - # If a nomail file exists, move its contents into the DB - $dbh->bz_add_column('profiles', 'disable_mail', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }); - if (-e "$datadir/nomail") { - # We have a data/nomail file, read it in and delete it - my %nomail; - print "Found a data/nomail file. Moving nomail entries into DB...\n"; - my $nomail_file = new IO::File("$datadir/nomail", 'r'); - while (<$nomail_file>) { - $nomail{trim($_)} = 1; - } - $nomail_file->close; + my $dbh = Bugzilla->dbh; + my $datadir = bz_locations()->{'datadir'}; + + # 2006-07-14 karl@kornel.name - Bug 100953 + # If a nomail file exists, move its contents into the DB + $dbh->bz_add_column('profiles', 'disable_mail', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + if (-e "$datadir/nomail") { + + # We have a data/nomail file, read it in and delete it + my %nomail; + print "Found a data/nomail file. Moving nomail entries into DB...\n"; + my $nomail_file = new IO::File("$datadir/nomail", 'r'); + while (<$nomail_file>) { + $nomail{trim($_)} = 1; + } + $nomail_file->close; - # Go through each entry read. If a user exists, set disable_mail. - my $query = $dbh->prepare('UPDATE profiles + # Go through each entry read. If a user exists, set disable_mail. + my $query = $dbh->prepare( + 'UPDATE profiles SET disable_mail = 1 - WHERE userid = ?'); - foreach my $user_to_check (keys %nomail) { - my $uid = $dbh->selectrow_array( - 'SELECT userid FROM profiles WHERE login_name = ?', - undef, $user_to_check); - next if !$uid; - print "\tDisabling email for user $user_to_check\n"; - $query->execute($uid); - delete $nomail{$user_to_check}; - } - - # If there are any nomail entries remaining, move them to nomail.bad - # and say something to the user. - if (scalar(keys %nomail)) { - print "\n", install_string('update_nomail_bad', - { data => $datadir }), "\n"; - my $nomail_bad = new IO::File("$datadir/nomail.bad", '>>'); - foreach my $unknown_user (keys %nomail) { - print "\t$unknown_user\n"; - print $nomail_bad "$unknown_user\n"; - delete $nomail{$unknown_user}; - } - $nomail_bad->close; - print "\n"; - } + WHERE userid = ?' + ); + foreach my $user_to_check (keys %nomail) { + my $uid + = $dbh->selectrow_array('SELECT userid FROM profiles WHERE login_name = ?', + undef, $user_to_check); + next if !$uid; + print "\tDisabling email for user $user_to_check\n"; + $query->execute($uid); + delete $nomail{$user_to_check}; + } - # Now that we don't need it, get rid of the nomail file. - unlink "$datadir/nomail"; + # If there are any nomail entries remaining, move them to nomail.bad + # and say something to the user. + if (scalar(keys %nomail)) { + print "\n", install_string('update_nomail_bad', {data => $datadir}), "\n"; + my $nomail_bad = new IO::File("$datadir/nomail.bad", '>>'); + foreach my $unknown_user (keys %nomail) { + print "\t$unknown_user\n"; + print $nomail_bad "$unknown_user\n"; + delete $nomail{$unknown_user}; + } + $nomail_bad->close; + print "\n"; } + + # Now that we don't need it, get rid of the nomail file. + unlink "$datadir/nomail"; + } } sub _update_longdescs_who_index { - my $dbh = Bugzilla->dbh; - # When doing a search on who posted a comment, longdescs is joined - # against the bugs table. So we need an index on both of these, - # not just on "who". - my $who_index = $dbh->bz_index_info('longdescs', 'longdescs_who_idx'); - if (!$who_index || scalar @{$who_index->{FIELDS}} == 1) { - # If the index doesn't exist, this will harmlessly do nothing. - $dbh->bz_drop_index('longdescs', 'longdescs_who_idx'); - $dbh->bz_add_index('longdescs', 'longdescs_who_idx', [qw(who bug_id)]); - } + my $dbh = Bugzilla->dbh; + + # When doing a search on who posted a comment, longdescs is joined + # against the bugs table. So we need an index on both of these, + # not just on "who". + my $who_index = $dbh->bz_index_info('longdescs', 'longdescs_who_idx'); + if (!$who_index || scalar @{$who_index->{FIELDS}} == 1) { + + # If the index doesn't exist, this will harmlessly do nothing. + $dbh->bz_drop_index('longdescs', 'longdescs_who_idx'); + $dbh->bz_add_index('longdescs', 'longdescs_who_idx', [qw(who bug_id)]); + } } sub _fix_uppercase_custom_field_names { - # Before the final release of 3.0, custom fields could be - # created with mixed-case names. - my $dbh = Bugzilla->dbh; - my $fields = $dbh->selectall_arrayref( - 'SELECT name, type FROM fielddefs WHERE custom = 1'); - foreach my $row (@$fields) { - my ($name, $type) = @$row; - if ($name ne lc($name)) { - $dbh->bz_rename_column('bugs', $name, lc($name)); - $dbh->bz_rename_table($name, lc($name)) - if $type == FIELD_TYPE_SINGLE_SELECT; - $dbh->do('UPDATE fielddefs SET name = ? WHERE name = ?', - undef, lc($name), $name); - } + + # Before the final release of 3.0, custom fields could be + # created with mixed-case names. + my $dbh = Bugzilla->dbh; + my $fields = $dbh->selectall_arrayref( + 'SELECT name, type FROM fielddefs WHERE custom = 1'); + foreach my $row (@$fields) { + my ($name, $type) = @$row; + if ($name ne lc($name)) { + $dbh->bz_rename_column('bugs', $name, lc($name)); + $dbh->bz_rename_table($name, lc($name)) if $type == FIELD_TYPE_SINGLE_SELECT; + $dbh->do('UPDATE fielddefs SET name = ? WHERE name = ?', + undef, lc($name), $name); } + } } sub _fix_uppercase_index_names { - # We forgot to fix indexes in the above code. - my $dbh = Bugzilla->dbh; - my $fields = $dbh->selectcol_arrayref( - 'SELECT name FROM fielddefs WHERE type = ? AND custom = 1', - undef, FIELD_TYPE_SINGLE_SELECT); - foreach my $field (@$fields) { - my $indexes = $dbh->bz_table_indexes($field); - foreach my $name (keys %$indexes) { - next if $name eq lc($name); - my $index = $indexes->{$name}; - # Lowercase the name and everything in the definition. - my $new_name = lc($name); - my @new_fields = map {lc($_)} @{$index->{FIELDS}}; - my $new_def = {FIELDS => \@new_fields, TYPE => $index->{TYPE}}; - $new_def = \@new_fields if !$index->{TYPE}; - $dbh->bz_drop_index($field, $name); - $dbh->bz_add_index($field, $new_name, $new_def); - } + + # We forgot to fix indexes in the above code. + my $dbh = Bugzilla->dbh; + my $fields + = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs WHERE type = ? AND custom = 1', + undef, FIELD_TYPE_SINGLE_SELECT); + foreach my $field (@$fields) { + my $indexes = $dbh->bz_table_indexes($field); + foreach my $name (keys %$indexes) { + next if $name eq lc($name); + my $index = $indexes->{$name}; + + # Lowercase the name and everything in the definition. + my $new_name = lc($name); + my @new_fields = map { lc($_) } @{$index->{FIELDS}}; + my $new_def = {FIELDS => \@new_fields, TYPE => $index->{TYPE}}; + $new_def = \@new_fields if !$index->{TYPE}; + $dbh->bz_drop_index($field, $name); + $dbh->bz_add_index($field, $new_name, $new_def); } + } } sub _initialize_workflow_for_upgrade { - my $old_params = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_add_column('bug_status', 'is_open', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - - # Till now, bug statuses were not customizable. Nevertheless, local - # changes are possible and so we will try to respect these changes. - # This means: get the status of bugs having a resolution different from '' - # and mark these statuses as 'closed', even if some of these statuses are - # expected to be open statuses. Bug statuses we have no information about - # are left as 'open'. - # - # We append the default list of closed statuses *unless* we detect at least - # one closed state in the DB (i.e. with is_open = 0). This would mean that - # the DB has already been updated at least once and maybe the admin decided - # that e.g. 'RESOLVED' is now an open state, in which case we don't want to - # override this attribute. At least one bug status has to be a closed state - # anyway (due to the 'duplicate_or_move_bug_status' parameter) so it's safe - # to use this criteria. - my $num_closed_states = $dbh->selectrow_array('SELECT COUNT(*) FROM bug_status - WHERE is_open = 0'); - - if (!$num_closed_states) { - my @closed_statuses = - @{$dbh->selectcol_arrayref('SELECT DISTINCT bug_status FROM bugs - WHERE resolution != ?', undef, '')}; - @closed_statuses = - map {$dbh->quote($_)} (@closed_statuses, qw(RESOLVED VERIFIED CLOSED)); - - print "Marking closed bug statuses as such...\n"; - $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value IN (' . - join(', ', @closed_statuses) . ')'); - } - - # We only populate the workflow here if we're upgrading from a version - # before 4.0 (which is where init_workflow was added). This was the - # first schema change done for 4.0, so we check this. - return if $dbh->bz_column_info('bugs_activity', 'comment_id'); - - # Populate the status_workflow table. We do nothing if the table already - # has entries. If all bug status transitions have been deleted, the - # workflow will be restored to its default schema. - my $count = $dbh->selectrow_array('SELECT COUNT(*) FROM status_workflow'); - - if (!$count) { - # Make sure the variables below are defined as - # status_workflow.require_comment cannot be NULL. - my $create = $old_params->{'commentoncreate'} || 0; - my $confirm = $old_params->{'commentonconfirm'} || 0; - my $accept = $old_params->{'commentonaccept'} || 0; - my $resolve = $old_params->{'commentonresolve'} || 0; - my $verify = $old_params->{'commentonverify'} || 0; - my $close = $old_params->{'commentonclose'} || 0; - my $reopen = $old_params->{'commentonreopen'} || 0; - # This was till recently the only way to get back to NEW for - # confirmed bugs, so we use this parameter here. - my $reassign = $old_params->{'commentonreassign'} || 0; - - # This is the default workflow for upgrading installations. - my @workflow = ([undef, 'UNCONFIRMED', $create], - [undef, 'NEW', $create], - [undef, 'ASSIGNED', $create], - ['UNCONFIRMED', 'NEW', $confirm], - ['UNCONFIRMED', 'ASSIGNED', $accept], - ['UNCONFIRMED', 'RESOLVED', $resolve], - ['NEW', 'ASSIGNED', $accept], - ['NEW', 'RESOLVED', $resolve], - ['ASSIGNED', 'NEW', $reassign], - ['ASSIGNED', 'RESOLVED', $resolve], - ['REOPENED', 'NEW', $reassign], - ['REOPENED', 'ASSIGNED', $accept], - ['REOPENED', 'RESOLVED', $resolve], - ['RESOLVED', 'UNCONFIRMED', $reopen], - ['RESOLVED', 'REOPENED', $reopen], - ['RESOLVED', 'VERIFIED', $verify], - ['RESOLVED', 'CLOSED', $close], - ['VERIFIED', 'UNCONFIRMED', $reopen], - ['VERIFIED', 'REOPENED', $reopen], - ['VERIFIED', 'CLOSED', $close], - ['CLOSED', 'UNCONFIRMED', $reopen], - ['CLOSED', 'REOPENED', $reopen]); - - print "Now filling the 'status_workflow' table with valid bug status transitions...\n"; - my $sth_select = $dbh->prepare('SELECT id FROM bug_status WHERE value = ?'); - my $sth = $dbh->prepare('INSERT INTO status_workflow (old_status, new_status, - require_comment) VALUES (?, ?, ?)'); - - foreach my $transition (@workflow) { - my ($from, $to); - # If it's an initial state, there is no "old" value. - $from = $dbh->selectrow_array($sth_select, undef, $transition->[0]) - if $transition->[0]; - $to = $dbh->selectrow_array($sth_select, undef, $transition->[1]); - # If one of the bug statuses doesn't exist, the transition is invalid. - next if (($transition->[0] && !$from) || !$to); - - $sth->execute($from, $to, $transition->[2] ? 1 : 0); - } - } + my $old_params = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_add_column('bug_status', 'is_open', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + + # Till now, bug statuses were not customizable. Nevertheless, local + # changes are possible and so we will try to respect these changes. + # This means: get the status of bugs having a resolution different from '' + # and mark these statuses as 'closed', even if some of these statuses are + # expected to be open statuses. Bug statuses we have no information about + # are left as 'open'. + # + # We append the default list of closed statuses *unless* we detect at least + # one closed state in the DB (i.e. with is_open = 0). This would mean that + # the DB has already been updated at least once and maybe the admin decided + # that e.g. 'RESOLVED' is now an open state, in which case we don't want to + # override this attribute. At least one bug status has to be a closed state + # anyway (due to the 'duplicate_or_move_bug_status' parameter) so it's safe + # to use this criteria. + my $num_closed_states = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM bug_status + WHERE is_open = 0' + ); + + if (!$num_closed_states) { + my @closed_statuses = @{ + $dbh->selectcol_arrayref( + 'SELECT DISTINCT bug_status FROM bugs + WHERE resolution != ?', undef, '' + ) + }; + @closed_statuses + = map { $dbh->quote($_) } (@closed_statuses, qw(RESOLVED VERIFIED CLOSED)); + + print "Marking closed bug statuses as such...\n"; + $dbh->do('UPDATE bug_status SET is_open = 0 WHERE value IN (' + . join(', ', @closed_statuses) + . ')'); + } + + # We only populate the workflow here if we're upgrading from a version + # before 4.0 (which is where init_workflow was added). This was the + # first schema change done for 4.0, so we check this. + return if $dbh->bz_column_info('bugs_activity', 'comment_id'); + + # Populate the status_workflow table. We do nothing if the table already + # has entries. If all bug status transitions have been deleted, the + # workflow will be restored to its default schema. + my $count = $dbh->selectrow_array('SELECT COUNT(*) FROM status_workflow'); + + if (!$count) { + + # Make sure the variables below are defined as + # status_workflow.require_comment cannot be NULL. + my $create = $old_params->{'commentoncreate'} || 0; + my $confirm = $old_params->{'commentonconfirm'} || 0; + my $accept = $old_params->{'commentonaccept'} || 0; + my $resolve = $old_params->{'commentonresolve'} || 0; + my $verify = $old_params->{'commentonverify'} || 0; + my $close = $old_params->{'commentonclose'} || 0; + my $reopen = $old_params->{'commentonreopen'} || 0; + + # This was till recently the only way to get back to NEW for + # confirmed bugs, so we use this parameter here. + my $reassign = $old_params->{'commentonreassign'} || 0; + + # This is the default workflow for upgrading installations. + my @workflow = ( + [undef, 'UNCONFIRMED', $create], + [undef, 'NEW', $create], + [undef, 'ASSIGNED', $create], + ['UNCONFIRMED', 'NEW', $confirm], + ['UNCONFIRMED', 'ASSIGNED', $accept], + ['UNCONFIRMED', 'RESOLVED', $resolve], + ['NEW', 'ASSIGNED', $accept], + ['NEW', 'RESOLVED', $resolve], + ['ASSIGNED', 'NEW', $reassign], + ['ASSIGNED', 'RESOLVED', $resolve], + ['REOPENED', 'NEW', $reassign], + ['REOPENED', 'ASSIGNED', $accept], + ['REOPENED', 'RESOLVED', $resolve], + ['RESOLVED', 'UNCONFIRMED', $reopen], + ['RESOLVED', 'REOPENED', $reopen], + ['RESOLVED', 'VERIFIED', $verify], + ['RESOLVED', 'CLOSED', $close], + ['VERIFIED', 'UNCONFIRMED', $reopen], + ['VERIFIED', 'REOPENED', $reopen], + ['VERIFIED', 'CLOSED', $close], + ['CLOSED', 'UNCONFIRMED', $reopen], + ['CLOSED', 'REOPENED', $reopen] + ); - # Make sure the bug status used by the 'duplicate_or_move_bug_status' - # parameter has all the required transitions set. - my $dup_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; - my $status_id = $dbh->selectrow_array( - 'SELECT id FROM bug_status WHERE value = ?', undef, $dup_status); - # There's a minor chance that this status isn't in the DB. - $status_id || return; + print + "Now filling the 'status_workflow' table with valid bug status transitions...\n"; + my $sth_select = $dbh->prepare('SELECT id FROM bug_status WHERE value = ?'); + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow (old_status, new_status, + require_comment) VALUES (?, ?, ?)' + ); - my $missing_statuses = $dbh->selectcol_arrayref( - 'SELECT id FROM bug_status - LEFT JOIN status_workflow ON old_status = id - AND new_status = ? - WHERE old_status IS NULL', undef, $status_id); + foreach my $transition (@workflow) { + my ($from, $to); - my $sth = $dbh->prepare('INSERT INTO status_workflow - (old_status, new_status) VALUES (?, ?)'); + # If it's an initial state, there is no "old" value. + $from = $dbh->selectrow_array($sth_select, undef, $transition->[0]) + if $transition->[0]; + $to = $dbh->selectrow_array($sth_select, undef, $transition->[1]); - foreach my $old_status_id (@$missing_statuses) { - next if ($old_status_id == $status_id); - $sth->execute($old_status_id, $status_id); + # If one of the bug statuses doesn't exist, the transition is invalid. + next if (($transition->[0] && !$from) || !$to); + + $sth->execute($from, $to, $transition->[2] ? 1 : 0); } + } + + # Make sure the bug status used by the 'duplicate_or_move_bug_status' + # parameter has all the required transitions set. + my $dup_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; + my $status_id + = $dbh->selectrow_array('SELECT id FROM bug_status WHERE value = ?', + undef, $dup_status); + + # There's a minor chance that this status isn't in the DB. + $status_id || return; + + my $missing_statuses = $dbh->selectcol_arrayref( + 'SELECT id FROM bug_status + LEFT JOIN status_workflow ON old_status = id + AND new_status = ? + WHERE old_status IS NULL', undef, $status_id + ); + + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow + (old_status, new_status) VALUES (?, ?)' + ); + + foreach my $old_status_id (@$missing_statuses) { + next if ($old_status_id == $status_id); + $sth->execute($old_status_id, $status_id); + } } sub _make_lang_setting_dynamic { - my $dbh = Bugzilla->dbh; - my $count = $dbh->selectrow_array(q{SELECT 1 FROM setting + my $dbh = Bugzilla->dbh; + my $count = $dbh->selectrow_array( + q{SELECT 1 FROM setting WHERE name = 'lang' - AND subclass IS NULL}); - if ($count) { - $dbh->do(q{UPDATE setting SET subclass = 'Lang' WHERE name = 'lang'}); - $dbh->do(q{DELETE FROM setting_value WHERE name = 'lang'}); - } + AND subclass IS NULL} + ); + if ($count) { + $dbh->do(q{UPDATE setting SET subclass = 'Lang' WHERE name = 'lang'}); + $dbh->do(q{DELETE FROM setting_value WHERE name = 'lang'}); + } } sub _fix_attachment_modification_date { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('attachments', 'modification_time')) { - # Allow NULL values till the modification time has been set. - $dbh->bz_add_column('attachments', 'modification_time', {TYPE => 'DATETIME'}); + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('attachments', 'modification_time')) { - print "Setting the modification time for attachments...\n"; - $dbh->do('UPDATE attachments SET modification_time = creation_ts'); + # Allow NULL values till the modification time has been set. + $dbh->bz_add_column('attachments', 'modification_time', {TYPE => 'DATETIME'}); - # Now force values to be always defined. - $dbh->bz_alter_column('attachments', 'modification_time', - {TYPE => 'DATETIME', NOTNULL => 1}); + print "Setting the modification time for attachments...\n"; + $dbh->do('UPDATE attachments SET modification_time = creation_ts'); - # Update the modification time for attachments which have been modified. - my $attachments = - $dbh->selectall_arrayref('SELECT attach_id, MAX(bug_when) FROM bugs_activity - WHERE attach_id IS NOT NULL ' . - $dbh->sql_group_by('attach_id')); + # Now force values to be always defined. + $dbh->bz_alter_column('attachments', 'modification_time', + {TYPE => 'DATETIME', NOTNULL => 1}); - my $sth = $dbh->prepare('UPDATE attachments SET modification_time = ? - WHERE attach_id = ?'); - $sth->execute($_->[1], $_->[0]) foreach (@$attachments); - } - # We add this here to be sure to have the index being added, due to the original - # patch omitting it. - $dbh->bz_add_index('attachments', 'attachments_modification_time_idx', - [qw(modification_time)]); + # Update the modification time for attachments which have been modified. + my $attachments = $dbh->selectall_arrayref( + 'SELECT attach_id, MAX(bug_when) FROM bugs_activity + WHERE attach_id IS NOT NULL ' + . $dbh->sql_group_by('attach_id') + ); + + my $sth = $dbh->prepare( + 'UPDATE attachments SET modification_time = ? + WHERE attach_id = ?' + ); + $sth->execute($_->[1], $_->[0]) foreach (@$attachments); + } + + # We add this here to be sure to have the index being added, due to the original + # patch omitting it. + $dbh->bz_add_index('attachments', 'attachments_modification_time_idx', + [qw(modification_time)]); } sub _change_text_types { - my $dbh = Bugzilla->dbh; - return if - $dbh->bz_column_info('namedqueries', 'query')->{TYPE} eq 'LONGTEXT'; - _check_content_length('attachments', 'mimetype', 255, 'attach_id'); - _check_content_length('fielddefs', 'description', 255, 'id'); - _check_content_length('attachments', 'description', 255, 'attach_id'); - - $dbh->bz_alter_column('bugs', 'bug_file_loc', - { TYPE => 'MEDIUMTEXT'}); - $dbh->bz_alter_column('longdescs', 'thetext', - { TYPE => 'LONGTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('attachments', 'description', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('attachments', 'mimetype', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - # This also changes NULL to NOT NULL. - $dbh->bz_alter_column('flagtypes', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }, ''); - $dbh->bz_alter_column('fielddefs', 'description', - { TYPE => 'TINYTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('groups', 'description', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('quips', 'quip', - { TYPE => 'MEDIUMTEXT', NOTNULL => 1 }); - $dbh->bz_alter_column('namedqueries', 'query', - { TYPE => 'LONGTEXT', NOTNULL => 1 }); + my $dbh = Bugzilla->dbh; + return if $dbh->bz_column_info('namedqueries', 'query')->{TYPE} eq 'LONGTEXT'; + _check_content_length('attachments', 'mimetype', 255, 'attach_id'); + _check_content_length('fielddefs', 'description', 255, 'id'); + _check_content_length('attachments', 'description', 255, 'attach_id'); + + $dbh->bz_alter_column('bugs', 'bug_file_loc', {TYPE => 'MEDIUMTEXT'}); + $dbh->bz_alter_column('longdescs', 'thetext', + {TYPE => 'LONGTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('attachments', 'description', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('attachments', 'mimetype', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + + # This also changes NULL to NOT NULL. + $dbh->bz_alter_column('flagtypes', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, ''); + $dbh->bz_alter_column('fielddefs', 'description', + {TYPE => 'TINYTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('groups', 'description', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('quips', 'quip', {TYPE => 'MEDIUMTEXT', NOTNULL => 1}); + $dbh->bz_alter_column('namedqueries', 'query', + {TYPE => 'LONGTEXT', NOTNULL => 1}); } sub _check_content_length { - my ($table_name, $field_name, $max_length, $id_field) = @_; - my $dbh = Bugzilla->dbh; - my %contents = @{ $dbh->selectcol_arrayref( - "SELECT $id_field, $field_name FROM $table_name - WHERE CHAR_LENGTH($field_name) > ?", {Columns=>[1,2]}, $max_length) }; - - if (scalar keys %contents) { - my $error = install_string('install_data_too_long', - { column => $field_name, - id_column => $id_field, - table => $table_name, - max_length => $max_length }); - foreach my $id (keys %contents) { - my $string = $contents{$id}; - # Don't dump the whole string--it could be 16MB. - if (length($string) > 80) { - $string = substr($string, 0, 30) . "..." - . substr($string, -30) . "\n"; - } - $error .= "$id: $string\n"; - } - die $error; + my ($table_name, $field_name, $max_length, $id_field) = @_; + my $dbh = Bugzilla->dbh; + my %contents = @{ + $dbh->selectcol_arrayref( + "SELECT $id_field, $field_name FROM $table_name + WHERE CHAR_LENGTH($field_name) > ?", {Columns => [1, 2]}, $max_length + ) + }; + + if (scalar keys %contents) { + my $error = install_string( + 'install_data_too_long', + { + column => $field_name, + id_column => $id_field, + table => $table_name, + max_length => $max_length + } + ); + foreach my $id (keys %contents) { + my $string = $contents{$id}; + + # Don't dump the whole string--it could be 16MB. + if (length($string) > 80) { + $string = substr($string, 0, 30) . "..." . substr($string, -30) . "\n"; + } + $error .= "$id: $string\n"; } + die $error; + } } sub _add_foreign_keys_to_multiselects { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $names = $dbh->selectcol_arrayref( - 'SELECT name + my $names = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs - WHERE type = ' . FIELD_TYPE_MULTI_SELECT); + WHERE type = ' . FIELD_TYPE_MULTI_SELECT + ); - foreach my $name (@$names) { - $dbh->bz_add_fk("bug_$name", "bug_id", - {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}); + foreach my $name (@$names) { + $dbh->bz_add_fk("bug_$name", "bug_id", + {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}); - $dbh->bz_add_fk("bug_$name", "value", - {TABLE => $name, COLUMN => 'value', DELETE => 'RESTRICT'}); - } + $dbh->bz_add_fk("bug_$name", "value", + {TABLE => $name, COLUMN => 'value', DELETE => 'RESTRICT'}); + } } # This subroutine is used in multiple places (for times when we update @@ -3297,593 +3531,630 @@ sub _add_foreign_keys_to_multiselects { # it to update bugs_fulltext for those bug_ids instead of populating the # whole table. sub _populate_bugs_fulltext { - my $bug_ids = shift; - my $dbh = Bugzilla->dbh; - my $fulltext = $dbh->selectrow_array('SELECT 1 FROM bugs_fulltext ' - . $dbh->sql_limit(1)); - # We only populate the table if it's empty or if we've been given a - # set of bug ids. - if ($bug_ids or !$fulltext) { - $bug_ids ||= $dbh->selectcol_arrayref('SELECT bug_id FROM bugs'); - # If there are no bugs in the bugs table, there's nothing to populate. - return if !@$bug_ids; - my $num_bugs = scalar @$bug_ids; - - my $command = "INSERT"; - my $where = ""; - if ($fulltext) { - print "Updating bugs_fulltext for $num_bugs bugs...\n"; - $where = "WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids); - # It turns out that doing a REPLACE INTO is up to 10x faster - # than any other possible method of updating the table, in MySQL, - # which matters a LOT for large installations. - if ($dbh->isa('Bugzilla::DB::Mysql')) { - $command = "REPLACE"; - } - else { - $dbh->do("DELETE FROM bugs_fulltext WHERE " - . $dbh->sql_in('bug_id', $bug_ids)); - } - } - else { - print "Populating bugs_fulltext with $num_bugs entries..."; - print " (this can take a long time.)\n"; - } + my $bug_ids = shift; + my $dbh = Bugzilla->dbh; + my $fulltext + = $dbh->selectrow_array('SELECT 1 FROM bugs_fulltext ' . $dbh->sql_limit(1)); + + # We only populate the table if it's empty or if we've been given a + # set of bug ids. + if ($bug_ids or !$fulltext) { + $bug_ids ||= $dbh->selectcol_arrayref('SELECT bug_id FROM bugs'); + + # If there are no bugs in the bugs table, there's nothing to populate. + return if !@$bug_ids; + my $num_bugs = scalar @$bug_ids; + + my $command = "INSERT"; + my $where = ""; + if ($fulltext) { + print "Updating bugs_fulltext for $num_bugs bugs...\n"; + $where = "WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids); + + # It turns out that doing a REPLACE INTO is up to 10x faster + # than any other possible method of updating the table, in MySQL, + # which matters a LOT for large installations. + if ($dbh->isa('Bugzilla::DB::Mysql')) { + $command = "REPLACE"; + } + else { + $dbh->do("DELETE FROM bugs_fulltext WHERE " . $dbh->sql_in('bug_id', $bug_ids)); + } + } + else { + print "Populating bugs_fulltext with $num_bugs entries..."; + print " (this can take a long time.)\n"; + } - # As recommended by Monty Widenius for GNOME's upgrade. - # mkanat and justdave concur it'll be helpful for bmo, too. - $dbh->do('SET SESSION myisam_sort_buffer_size = 3221225472'); + # As recommended by Monty Widenius for GNOME's upgrade. + # mkanat and justdave concur it'll be helpful for bmo, too. + $dbh->do('SET SESSION myisam_sort_buffer_size = 3221225472'); - my $newline = $dbh->quote("\n"); - $dbh->do( - qq{$command INTO bugs_fulltext (bug_id, short_desc, comments, + my $newline = $dbh->quote("\n"); + $dbh->do( + qq{$command INTO bugs_fulltext (bug_id, short_desc, comments, comments_noprivate) SELECT bugs.bug_id, bugs.short_desc, } - . $dbh->sql_group_concat('longdescs.thetext', $newline, 0) - . ', ' . $dbh->sql_group_concat('nopriv.thetext', $newline, 0) . - qq{ FROM bugs + . $dbh->sql_group_concat('longdescs.thetext', $newline, 0) . ', ' + . $dbh->sql_group_concat('nopriv.thetext', $newline, 0) + . qq{ FROM bugs LEFT JOIN longdescs ON bugs.bug_id = longdescs.bug_id LEFT JOIN longdescs AS nopriv ON longdescs.comment_id = nopriv.comment_id AND nopriv.isprivate = 0 $where } - . $dbh->sql_group_by('bugs.bug_id', 'bugs.short_desc')); - } + . $dbh->sql_group_by('bugs.bug_id', 'bugs.short_desc') + ); + } } sub _fix_illegal_flag_modification_dates { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $rows = $dbh->do('UPDATE flags SET modification_date = creation_date - WHERE modification_date < creation_date'); - # If no rows are affected, $dbh->do returns 0E0 instead of 0. - print "$rows flags had an illegal modification date. Fixed!\n" if ($rows =~ /^\d+$/); + my $rows = $dbh->do( + 'UPDATE flags SET modification_date = creation_date + WHERE modification_date < creation_date' + ); + + # If no rows are affected, $dbh->do returns 0E0 instead of 0. + print "$rows flags had an illegal modification date. Fixed!\n" + if ($rows =~ /^\d+$/); } sub _add_visiblity_value_to_value_tables { - my $dbh = Bugzilla->dbh; - my @standard_fields = - qw(bug_status resolution priority bug_severity op_sys rep_platform); - my $custom_fields = $dbh->selectcol_arrayref( - 'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)', - undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT); - foreach my $field (@standard_fields, @$custom_fields) { - $dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'}); - $dbh->bz_add_index($field, "${field}_visibility_value_id_idx", - ['visibility_value_id']); - } + my $dbh = Bugzilla->dbh; + my @standard_fields + = qw(bug_status resolution priority bug_severity op_sys rep_platform); + my $custom_fields + = $dbh->selectcol_arrayref( + 'SELECT name FROM fielddefs WHERE custom = 1 AND type IN(?,?)', + undef, FIELD_TYPE_SINGLE_SELECT, FIELD_TYPE_MULTI_SELECT); + foreach my $field (@standard_fields, @$custom_fields) { + $dbh->bz_add_column($field, 'visibility_value_id', {TYPE => 'INT2'}); + $dbh->bz_add_index($field, "${field}_visibility_value_id_idx", + ['visibility_value_id']); + } } sub _add_extern_id_index { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_index_info('profiles', 'profiles_extern_id_idx')) { - # Some Bugzillas have a multiple empty strings in extern_id, - # which need to be converted to NULLs before we add the index. - $dbh->do("UPDATE profiles SET extern_id = NULL WHERE extern_id = ''"); - $dbh->bz_add_index('profiles', 'profiles_extern_id_idx', - {TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]}); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_index_info('profiles', 'profiles_extern_id_idx')) { + + # Some Bugzillas have a multiple empty strings in extern_id, + # which need to be converted to NULLs before we add the index. + $dbh->do("UPDATE profiles SET extern_id = NULL WHERE extern_id = ''"); + $dbh->bz_add_index('profiles', 'profiles_extern_id_idx', + {TYPE => 'UNIQUE', FIELDS => [qw(extern_id)]}); + } } sub _convert_disallownew_to_isactive { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('products', 'disallownew')){ - $dbh->bz_add_column('products', 'isactive', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info('products', 'disallownew')) { + $dbh->bz_add_column('products', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - # isactive is the boolean reverse of disallownew. - $dbh->do('UPDATE products SET isactive = 0 WHERE disallownew = 1'); - $dbh->do('UPDATE products SET isactive = 1 WHERE disallownew = 0'); + # isactive is the boolean reverse of disallownew. + $dbh->do('UPDATE products SET isactive = 0 WHERE disallownew = 1'); + $dbh->do('UPDATE products SET isactive = 1 WHERE disallownew = 0'); - $dbh->bz_drop_column('products','disallownew'); - } + $dbh->bz_drop_column('products', 'disallownew'); + } } sub _fix_logincookies_ipaddr { - my $dbh = Bugzilla->dbh; - return if !$dbh->bz_column_info('logincookies', 'ipaddr')->{NOTNULL}; + my $dbh = Bugzilla->dbh; + return if !$dbh->bz_column_info('logincookies', 'ipaddr')->{NOTNULL}; - $dbh->bz_alter_column('logincookies', 'ipaddr', {TYPE => 'varchar(40)'}); - $dbh->do('UPDATE logincookies SET ipaddr = NULL WHERE ipaddr = ?', - undef, '0.0.0.0'); + $dbh->bz_alter_column('logincookies', 'ipaddr', {TYPE => 'varchar(40)'}); + $dbh->do('UPDATE logincookies SET ipaddr = NULL WHERE ipaddr = ?', + undef, '0.0.0.0'); } sub _fix_invalid_custom_field_names { - my $fields = Bugzilla->fields({ custom => 1 }); + my $fields = Bugzilla->fields({custom => 1}); - foreach my $field (@$fields) { - next if $field->name =~ /^[a-zA-Z0-9_]+$/; - # The field name is illegal and can break the DB. Kill the field! - $field->set_obsolete(1); - print install_string('update_cf_invalid_name', - { field => $field->name }), "\n"; - eval { $field->remove_from_db(); }; - warn $@ if $@; - } + foreach my $field (@$fields) { + next if $field->name =~ /^[a-zA-Z0-9_]+$/; + + # The field name is illegal and can break the DB. Kill the field! + $field->set_obsolete(1); + print install_string('update_cf_invalid_name', {field => $field->name}), "\n"; + eval { $field->remove_from_db(); }; + warn $@ if $@; + } } sub _set_attachment_comment_type { - my ($type, $string) = @_; - my $dbh = Bugzilla->dbh; - # We check if there are any comments of this type already, first, - # because this is faster than a full LIKE search on the comments, - # and currently this will run every time we run checksetup. - my $test = $dbh->selectrow_array( - "SELECT 1 FROM longdescs WHERE type = $type " . $dbh->sql_limit(1)); - return [] if $test; - my %comments = @{ $dbh->selectcol_arrayref( - "SELECT comment_id, thetext FROM longdescs - WHERE thetext LIKE '$string%'", - {Columns=>[1,2]}) }; - my @comment_ids = keys %comments; - return [] if !scalar @comment_ids; - my $what = "update"; + my ($type, $string) = @_; + my $dbh = Bugzilla->dbh; + + # We check if there are any comments of this type already, first, + # because this is faster than a full LIKE search on the comments, + # and currently this will run every time we run checksetup. + my $test = $dbh->selectrow_array( + "SELECT 1 FROM longdescs WHERE type = $type " . $dbh->sql_limit(1)); + return [] if $test; + my %comments = @{ + $dbh->selectcol_arrayref( + "SELECT comment_id, thetext FROM longdescs + WHERE thetext LIKE '$string%'", {Columns => [1, 2]} + ) + }; + my @comment_ids = keys %comments; + return [] if !scalar @comment_ids; + my $what = "update"; + if ($type == CMT_ATTACHMENT_CREATED) { + $what = "creation"; + } + print "Setting the type field on attachment $what comments...\n"; + my $sth = $dbh->prepare( + 'UPDATE longdescs SET thetext = ?, type = ?, extra_data = ? + WHERE comment_id = ?' + ); + my $count = 0; + my $total = scalar @comment_ids; + foreach my $id (@comment_ids) { + $count++; + my $text = $comments{$id}; + next if $text !~ /^\Q$string\E(\d+)/; + my $attachment_id = $1; + my @lines = split("\n", $text); if ($type == CMT_ATTACHMENT_CREATED) { - $what = "creation"; + + # Now we have to remove the text up until we find a line that's + # just a single newline, because the old "Created an attachment" + # text included the attachment description underneath it, and in + # Bugzillas before 2.20, that could be wrapped into multiple lines, + # in the database. + while (1) { + my $line = shift @lines; + last if (!defined $line or trim($line) eq ''); + } } - print "Setting the type field on attachment $what comments...\n"; - my $sth = $dbh->prepare( - 'UPDATE longdescs SET thetext = ?, type = ?, extra_data = ? - WHERE comment_id = ?'); - my $count = 0; - my $total = scalar @comment_ids; - foreach my $id (@comment_ids) { - $count++; - my $text = $comments{$id}; - next if $text !~ /^\Q$string\E(\d+)/; - my $attachment_id = $1; - my @lines = split("\n", $text); - if ($type == CMT_ATTACHMENT_CREATED) { - # Now we have to remove the text up until we find a line that's - # just a single newline, because the old "Created an attachment" - # text included the attachment description underneath it, and in - # Bugzillas before 2.20, that could be wrapped into multiple lines, - # in the database. - while (1) { - my $line = shift @lines; - last if (!defined $line or trim($line) eq ''); - } - } - else { - # However, the "From update of attachment" line is always just - # one line--the first line of the comment. - shift @lines; - } - $text = join("\n", @lines); - $sth->execute($text, $type, $attachment_id, $id); - indicate_progress({ total => $total, current => $count, - every => 25 }); + else { + # However, the "From update of attachment" line is always just + # one line--the first line of the comment. + shift @lines; } - return \@comment_ids; + $text = join("\n", @lines); + $sth->execute($text, $type, $attachment_id, $id); + indicate_progress({total => $total, current => $count, every => 25}); + } + return \@comment_ids; } sub _set_attachment_comment_types { - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $created_ids = _set_attachment_comment_type( - CMT_ATTACHMENT_CREATED, 'Created an attachment (id='); - my $updated_ids = _set_attachment_comment_type( - CMT_ATTACHMENT_UPDATED, '(From update of attachment '); - $dbh->bz_commit_transaction(); - return unless (@$created_ids or @$updated_ids); - - my @comment_ids = (@$created_ids, @$updated_ids); - - my $bug_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT bug_id FROM longdescs WHERE ' - . $dbh->sql_in('comment_id', \@comment_ids)); - _populate_bugs_fulltext($bug_ids); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $created_ids = _set_attachment_comment_type(CMT_ATTACHMENT_CREATED, + 'Created an attachment (id='); + my $updated_ids = _set_attachment_comment_type(CMT_ATTACHMENT_UPDATED, + '(From update of attachment '); + $dbh->bz_commit_transaction(); + return unless (@$created_ids or @$updated_ids); + + my @comment_ids = (@$created_ids, @$updated_ids); + + my $bug_ids + = $dbh->selectcol_arrayref('SELECT DISTINCT bug_id FROM longdescs WHERE ' + . $dbh->sql_in('comment_id', \@comment_ids)); + _populate_bugs_fulltext($bug_ids); } sub _add_allows_unconfirmed_to_product_table { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('products', 'allows_unconfirmed')) { - $dbh->bz_add_column('products', 'allows_unconfirmed', - { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' }); - if ($dbh->bz_column_info('products', 'votestoconfirm')) { - $dbh->do('UPDATE products SET allows_unconfirmed = 1 - WHERE votestoconfirm > 0'); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('products', 'allows_unconfirmed')) { + $dbh->bz_add_column('products', 'allows_unconfirmed', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + if ($dbh->bz_column_info('products', 'votestoconfirm')) { + $dbh->do( + 'UPDATE products SET allows_unconfirmed = 1 + WHERE votestoconfirm > 0' + ); } + } } sub _convert_flagtypes_fks_to_set_null { - my $dbh = Bugzilla->dbh; - foreach my $column (qw(request_group_id grant_group_id)) { - my $fk = $dbh->bz_fk_info('flagtypes', $column); - if ($fk and !defined $fk->{DELETE}) { - $fk->{DELETE} = 'SET NULL'; - $dbh->bz_alter_fk('flagtypes', $column, $fk); - } + my $dbh = Bugzilla->dbh; + foreach my $column (qw(request_group_id grant_group_id)) { + my $fk = $dbh->bz_fk_info('flagtypes', $column); + if ($fk and !defined $fk->{DELETE}) { + $fk->{DELETE} = 'SET NULL'; + $dbh->bz_alter_fk('flagtypes', $column, $fk); } + } } sub _fix_decimal_types { - my $dbh = Bugzilla->dbh; - my $type = {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}; - $dbh->bz_alter_column('bugs', 'estimated_time', $type); - $dbh->bz_alter_column('bugs', 'remaining_time', $type); - $dbh->bz_alter_column('longdescs', 'work_time', $type); + my $dbh = Bugzilla->dbh; + my $type = {TYPE => 'decimal(7,2)', NOTNULL => 1, DEFAULT => '0'}; + $dbh->bz_alter_column('bugs', 'estimated_time', $type); + $dbh->bz_alter_column('bugs', 'remaining_time', $type); + $dbh->bz_alter_column('longdescs', 'work_time', $type); } sub _fix_series_creator_fk { - my $dbh = Bugzilla->dbh; - my $fk = $dbh->bz_fk_info('series', 'creator'); - if ($fk and $fk->{DELETE} eq 'SET NULL') { - $fk->{DELETE} = 'CASCADE'; - $dbh->bz_alter_fk('series', 'creator', $fk); - } + my $dbh = Bugzilla->dbh; + my $fk = $dbh->bz_fk_info('series', 'creator'); + if ($fk and $fk->{DELETE} eq 'SET NULL') { + $fk->{DELETE} = 'CASCADE'; + $dbh->bz_alter_fk('series', 'creator', $fk); + } } sub _remove_attachment_isurl { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('attachments', 'isurl')) { - # Now all attachments must have a filename. - $dbh->do('UPDATE attachments SET filename = ? WHERE isurl = 1', - undef, 'url.txt'); - $dbh->bz_drop_column('attachments', 'isurl'); - $dbh->do("DELETE FROM fielddefs WHERE name='attachments.isurl'"); - } + if ($dbh->bz_column_info('attachments', 'isurl')) { + + # Now all attachments must have a filename. + $dbh->do('UPDATE attachments SET filename = ? WHERE isurl = 1', + undef, 'url.txt'); + $dbh->bz_drop_column('attachments', 'isurl'); + $dbh->do("DELETE FROM fielddefs WHERE name='attachments.isurl'"); + } } sub _add_isactive_to_product_fields { - my $dbh = Bugzilla->dbh; - - # If we add the isactive column all values should start off as active - if (!$dbh->bz_column_info('components', 'isactive')) { - $dbh->bz_add_column('components', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } - - if (!$dbh->bz_column_info('versions', 'isactive')) { - $dbh->bz_add_column('versions', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } - - if (!$dbh->bz_column_info('milestones', 'isactive')) { - $dbh->bz_add_column('milestones', 'isactive', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - } + my $dbh = Bugzilla->dbh; + + # If we add the isactive column all values should start off as active + if (!$dbh->bz_column_info('components', 'isactive')) { + $dbh->bz_add_column('components', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } + + if (!$dbh->bz_column_info('versions', 'isactive')) { + $dbh->bz_add_column('versions', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } + + if (!$dbh->bz_column_info('milestones', 'isactive')) { + $dbh->bz_add_column('milestones', 'isactive', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + } } sub _migrate_field_visibility_value { - my $dbh = Bugzilla->dbh; - - if ($dbh->bz_column_info('fielddefs', 'visibility_value_id')) { - print "Populating new field_visibility table...\n"; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + if ($dbh->bz_column_info('fielddefs', 'visibility_value_id')) { + print "Populating new field_visibility table...\n"; - my %results = - @{ $dbh->selectcol_arrayref( - "SELECT id, visibility_value_id FROM fielddefs - WHERE visibility_value_id IS NOT NULL", - { Columns => [1,2] }) }; + $dbh->bz_start_transaction(); - my $insert_sth = - $dbh->prepare("INSERT INTO field_visibility (field_id, value_id) - VALUES (?, ?)"); + my %results = @{ + $dbh->selectcol_arrayref( + "SELECT id, visibility_value_id FROM fielddefs + WHERE visibility_value_id IS NOT NULL", {Columns => [1, 2]} + ) + }; - foreach my $id (keys %results) { - $insert_sth->execute($id, $results{$id}); - } + my $insert_sth = $dbh->prepare( + "INSERT INTO field_visibility (field_id, value_id) + VALUES (?, ?)" + ); - $dbh->bz_commit_transaction(); - $dbh->bz_drop_column('fielddefs', 'visibility_value_id'); + foreach my $id (keys %results) { + $insert_sth->execute($id, $results{$id}); } + + $dbh->bz_commit_transaction(); + $dbh->bz_drop_column('fielddefs', 'visibility_value_id'); + } } sub _fix_series_indexes { - my $dbh = Bugzilla->dbh; - return if $dbh->bz_index_info('series', 'series_category_idx'); + my $dbh = Bugzilla->dbh; + return if $dbh->bz_index_info('series', 'series_category_idx'); - $dbh->bz_drop_index('series', 'series_creator_idx'); + $dbh->bz_drop_index('series', 'series_creator_idx'); - # Fix duplicated names under the same category/subcategory before - # adding the more restrictive index. - my $duplicated_series = $dbh->selectall_arrayref( - 'SELECT s1.series_id, s1.category, s1.subcategory, s1.name + # Fix duplicated names under the same category/subcategory before + # adding the more restrictive index. + my $duplicated_series = $dbh->selectall_arrayref( + 'SELECT s1.series_id, s1.category, s1.subcategory, s1.name FROM series AS s1 INNER JOIN series AS s2 ON s1.category = s2.category AND s1.subcategory = s2.subcategory AND s1.name = s2.name - WHERE s1.series_id != s2.series_id'); - my $sth_series_update = $dbh->prepare('UPDATE series SET name = ? WHERE series_id = ?'); - my $sth_series_query = $dbh->prepare('SELECT 1 FROM series WHERE name = ? - AND category = ? AND subcategory = ?'); - - my %renamed_series; - foreach my $series (@$duplicated_series) { - my ($series_id, $category, $subcategory, $name) = @$series; - # Leave the first series alone, then rename duplicated ones. - if ($renamed_series{"${category}_${subcategory}_${name}"}++) { - print "Renaming series ${category}/${subcategory}/${name}...\n"; - my $c = 0; - my $exists = 1; - while ($exists) { - $sth_series_query->execute($name . ++$c, $category, $subcategory); - $exists = $sth_series_query->fetchrow_array; - } - $sth_series_update->execute($name . $c, $series_id); - } + WHERE s1.series_id != s2.series_id' + ); + my $sth_series_update + = $dbh->prepare('UPDATE series SET name = ? WHERE series_id = ?'); + my $sth_series_query = $dbh->prepare( + 'SELECT 1 FROM series WHERE name = ? + AND category = ? AND subcategory = ?' + ); + + my %renamed_series; + foreach my $series (@$duplicated_series) { + my ($series_id, $category, $subcategory, $name) = @$series; + + # Leave the first series alone, then rename duplicated ones. + if ($renamed_series{"${category}_${subcategory}_${name}"}++) { + print "Renaming series ${category}/${subcategory}/${name}...\n"; + my $c = 0; + my $exists = 1; + while ($exists) { + $sth_series_query->execute($name . ++$c, $category, $subcategory); + $exists = $sth_series_query->fetchrow_array; + } + $sth_series_update->execute($name . $c, $series_id); } + } - $dbh->bz_add_index('series', 'series_creator_idx', ['creator']); - $dbh->bz_add_index('series', 'series_category_idx', - {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}); + $dbh->bz_add_index('series', 'series_creator_idx', ['creator']); + $dbh->bz_add_index('series', 'series_category_idx', + {FIELDS => [qw(category subcategory name)], TYPE => 'UNIQUE'}); } sub _migrate_user_tags { - my $dbh = Bugzilla->dbh; - return unless $dbh->bz_column_info('namedqueries', 'query_type'); + my $dbh = Bugzilla->dbh; + return unless $dbh->bz_column_info('namedqueries', 'query_type'); - my $tags = $dbh->selectall_arrayref('SELECT id, userid, name, query + my $tags = $dbh->selectall_arrayref( + 'SELECT id, userid, name, query FROM namedqueries - WHERE query_type != 0'); - - my $sth_tags = $dbh->prepare( - 'INSERT INTO tag (user_id, name) VALUES (?, ?)'); - my $sth_tag_id = $dbh->prepare( - 'SELECT id FROM tag WHERE user_id = ? AND name = ?'); - my $sth_bug_tag = $dbh->prepare('INSERT INTO bug_tag (bug_id, tag_id) - VALUES (?, ?)'); - my $sth_nq = $dbh->prepare('UPDATE namedqueries SET query = ? - WHERE id = ?'); - - if (scalar @$tags) { - print install_string('update_queries_to_tags'), "\n"; + WHERE query_type != 0' + ); + + my $sth_tags = $dbh->prepare('INSERT INTO tag (user_id, name) VALUES (?, ?)'); + my $sth_tag_id + = $dbh->prepare('SELECT id FROM tag WHERE user_id = ? AND name = ?'); + my $sth_bug_tag = $dbh->prepare( + 'INSERT INTO bug_tag (bug_id, tag_id) + VALUES (?, ?)' + ); + my $sth_nq = $dbh->prepare( + 'UPDATE namedqueries SET query = ? + WHERE id = ?' + ); + + if (scalar @$tags) { + print install_string('update_queries_to_tags'), "\n"; + } + + my $total = scalar(@$tags); + my $current = 0; + + $dbh->bz_start_transaction(); + foreach my $tag (@$tags) { + my ($query_id, $user_id, $name, $query) = @$tag; + + # Tags are all lowercase. + my $tag_name = lc($name); + + $sth_tags->execute($user_id, $tag_name); + + my $tag_id = $dbh->selectrow_array($sth_tag_id, undef, $user_id, $tag_name); + + indicate_progress({current => ++$current, total => $total, every => 25}); + + my $uri = URI->new("buglist.cgi?$query", 'http'); + my $bug_id_list = $uri->query_param_delete('bug_id'); + if (!$bug_id_list) { + warn "No bug_id param for tag $name from user $user_id: $query"; + next; } + my @bug_ids = split(/[\s,]+/, $bug_id_list); - my $total = scalar(@$tags); - my $current = 0; - - $dbh->bz_start_transaction(); - foreach my $tag (@$tags) { - my ($query_id, $user_id, $name, $query) = @$tag; - # Tags are all lowercase. - my $tag_name = lc($name); - - $sth_tags->execute($user_id, $tag_name); - - my $tag_id = $dbh->selectrow_array($sth_tag_id, - undef, $user_id, $tag_name); - - indicate_progress({ current => ++$current, total => $total, - every => 25 }); + # Make sure that things like "001" get converted to "1" + @bug_ids = map { int($_) } @bug_ids; - my $uri = URI->new("buglist.cgi?$query", 'http'); - my $bug_id_list = $uri->query_param_delete('bug_id'); - if (!$bug_id_list) { - warn "No bug_id param for tag $name from user $user_id: $query"; - next; - } - my @bug_ids = split(/[\s,]+/, $bug_id_list); - # Make sure that things like "001" get converted to "1" - @bug_ids = map { int($_) } @bug_ids; - # And remove duplicates - @bug_ids = uniq @bug_ids; - foreach my $bug_id (@bug_ids) { - # If "int" above failed this might be undef. We also - # don't want to accept bug 0. - next if !$bug_id; - $sth_bug_tag->execute($bug_id, $tag_id); - } + # And remove duplicates + @bug_ids = uniq @bug_ids; + foreach my $bug_id (@bug_ids) { - # Existing tags may be used in whines, or shared with - # other users. So we convert them rather than delete them. - $uri->query_param('tag', $tag_name); - $sth_nq->execute($uri->query, $query_id); + # If "int" above failed this might be undef. We also + # don't want to accept bug 0. + next if !$bug_id; + $sth_bug_tag->execute($bug_id, $tag_id); } - $dbh->bz_commit_transaction(); + # Existing tags may be used in whines, or shared with + # other users. So we convert them rather than delete them. + $uri->query_param('tag', $tag_name); + $sth_nq->execute($uri->query, $query_id); + } - $dbh->bz_drop_column('namedqueries', 'query_type'); + $dbh->bz_commit_transaction(); + + $dbh->bz_drop_column('namedqueries', 'query_type'); } sub _populate_bug_see_also_class { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('bug_see_also', 'class')) { - # The length was incorrectly set to 64 instead of 255. - $dbh->bz_alter_column('bug_see_also', 'class', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); - return; - } + if ($dbh->bz_column_info('bug_see_also', 'class')) { - $dbh->bz_add_column('bug_see_also', 'class', - {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); + # The length was incorrectly set to 64 instead of 255. + $dbh->bz_alter_column('bug_see_also', 'class', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}); + return; + } - my $result = $dbh->selectall_arrayref( - "SELECT id, value FROM bug_see_also"); + $dbh->bz_add_column('bug_see_also', 'class', + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); - my $update_sth = - $dbh->prepare("UPDATE bug_see_also SET class = ? WHERE id = ?"); + my $result = $dbh->selectall_arrayref("SELECT id, value FROM bug_see_also"); - $dbh->bz_start_transaction(); - foreach my $see_also (@$result) { - my ($id, $value) = @$see_also; - my $class = Bugzilla::BugUrl->class_for($value); - $update_sth->execute($class, $id); - } - $dbh->bz_commit_transaction(); + my $update_sth + = $dbh->prepare("UPDATE bug_see_also SET class = ? WHERE id = ?"); + + $dbh->bz_start_transaction(); + foreach my $see_also (@$result) { + my ($id, $value) = @$see_also; + my $class = Bugzilla::BugUrl->class_for($value); + $update_sth->execute($class, $id); + } + $dbh->bz_commit_transaction(); } sub _migrate_disabledtext_boolean { - my $dbh = Bugzilla->dbh; - if (!$dbh->bz_column_info('profiles', 'is_enabled')) { - $dbh->bz_add_column("profiles", 'is_enabled', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); - $dbh->do("UPDATE profiles SET is_enabled = 0 - WHERE disabledtext != ''"); - } + my $dbh = Bugzilla->dbh; + if (!$dbh->bz_column_info('profiles', 'is_enabled')) { + $dbh->bz_add_column("profiles", 'is_enabled', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE'}); + $dbh->do( + "UPDATE profiles SET is_enabled = 0 + WHERE disabledtext != ''" + ); + } } sub _rename_tags_to_tag { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_table_info('tags')) { - # If we get here, it's because the schema created "tag" as an empty - # table while "tags" still exists. We get rid of the empty - # tag table so we can do the rename over the top of it. - $dbh->bz_drop_table('tag'); - $dbh->bz_drop_index('tags', 'tags_user_id_idx'); - $dbh->bz_rename_table('tags','tag'); - $dbh->bz_add_index('tag', 'tag_user_id_idx', - {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}); - } - if (my $bug_tag_fk = $dbh->bz_fk_info('bug_tag', 'tag_id')) { - # bz_rename_table() didn't handle FKs correctly. - if ($bug_tag_fk->{TABLE} eq 'tags') { - $bug_tag_fk->{TABLE} = 'tag'; - $dbh->bz_alter_fk('bug_tag', 'tag_id', $bug_tag_fk); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_table_info('tags')) { + + # If we get here, it's because the schema created "tag" as an empty + # table while "tags" still exists. We get rid of the empty + # tag table so we can do the rename over the top of it. + $dbh->bz_drop_table('tag'); + $dbh->bz_drop_index('tags', 'tags_user_id_idx'); + $dbh->bz_rename_table('tags', 'tag'); + $dbh->bz_add_index('tag', 'tag_user_id_idx', + {FIELDS => [qw(user_id name)], TYPE => 'UNIQUE'}); + } + if (my $bug_tag_fk = $dbh->bz_fk_info('bug_tag', 'tag_id')) { + + # bz_rename_table() didn't handle FKs correctly. + if ($bug_tag_fk->{TABLE} eq 'tags') { + $bug_tag_fk->{TABLE} = 'tag'; + $dbh->bz_alter_fk('bug_tag', 'tag_id', $bug_tag_fk); } + } } sub _on_delete_set_null_for_audit_log_userid { - my $dbh = Bugzilla->dbh; - my $fk = $dbh->bz_fk_info('audit_log', 'user_id'); - if ($fk and !defined $fk->{DELETE}) { - $fk->{DELETE} = 'SET NULL'; - $dbh->bz_alter_fk('audit_log', 'user_id', $fk); - } + my $dbh = Bugzilla->dbh; + my $fk = $dbh->bz_fk_info('audit_log', 'user_id'); + if ($fk and !defined $fk->{DELETE}) { + $fk->{DELETE} = 'SET NULL'; + $dbh->bz_alter_fk('audit_log', 'user_id', $fk); + } } sub _fix_notnull_defaults { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - $dbh->bz_alter_column('bugs', 'bug_file_loc', - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, ''); + $dbh->bz_alter_column('bugs', 'bug_file_loc', + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, ''); - my $custom_fields = Bugzilla::Field->match({ - custom => 1, type => [ FIELD_TYPE_FREETEXT, FIELD_TYPE_TEXTAREA ] + my $custom_fields + = Bugzilla::Field->match({ + custom => 1, type => [FIELD_TYPE_FREETEXT, FIELD_TYPE_TEXTAREA] }); - foreach my $field (@$custom_fields) { - if ($field->type == FIELD_TYPE_FREETEXT) { - $dbh->bz_alter_column('bugs', $field->name, - {TYPE => 'varchar(255)', NOTNULL => 1, - DEFAULT => "''"}, ''); - } - if ($field->type == FIELD_TYPE_TEXTAREA) { - $dbh->bz_alter_column('bugs', $field->name, - {TYPE => 'MEDIUMTEXT', NOTNULL => 1, - DEFAULT => "''"}, ''); - } + foreach my $field (@$custom_fields) { + if ($field->type == FIELD_TYPE_FREETEXT) { + $dbh->bz_alter_column('bugs', $field->name, + {TYPE => 'varchar(255)', NOTNULL => 1, DEFAULT => "''"}, ''); + } + if ($field->type == FIELD_TYPE_TEXTAREA) { + $dbh->bz_alter_column('bugs', $field->name, + {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => "''"}, ''); } + } } sub _fix_longdescs_primary_key { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') { - $dbh->bz_drop_related_fks('longdescs', 'comment_id'); - $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); - $dbh->bz_alter_column('longdescs', 'comment_id', - {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); - } + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info('longdescs', 'comment_id')->{TYPE} ne 'INTSERIAL') { + $dbh->bz_drop_related_fks('longdescs', 'comment_id'); + $dbh->bz_alter_column('bugs_activity', 'comment_id', {TYPE => 'INT4'}); + $dbh->bz_alter_column('longdescs', 'comment_id', + {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}); + } } sub _fix_dependencies_dupes { - my $dbh = Bugzilla->dbh; - my $blocked_idx = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx'); - if ($blocked_idx && scalar @{$blocked_idx->{'FIELDS'}} < 2) { - # Remove duplicated entries - my $dupes = $dbh->selectall_arrayref(" + my $dbh = Bugzilla->dbh; + my $blocked_idx + = $dbh->bz_index_info('dependencies', 'dependencies_blocked_idx'); + if ($blocked_idx && scalar @{$blocked_idx->{'FIELDS'}} < 2) { + + # Remove duplicated entries + my $dupes = $dbh->selectall_arrayref(" SELECT blocked, dependson, COUNT(*) AS count - FROM dependencies " . - $dbh->sql_group_by('blocked, dependson') . " - HAVING COUNT(*) > 1", - { Slice => {} }); - print "Removing duplicated entries from the 'dependencies' table...\n" if @$dupes; - foreach my $dupe (@$dupes) { - $dbh->do("DELETE FROM dependencies - WHERE blocked = ? AND dependson = ?", - undef, $dupe->{blocked}, $dupe->{dependson}); - $dbh->do("INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)", - undef, $dupe->{blocked}, $dupe->{dependson}); - } - $dbh->bz_drop_index('dependencies', 'dependencies_blocked_idx'); - $dbh->bz_add_index('dependencies', 'dependencies_blocked_idx', - { FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE' }); + FROM dependencies " . $dbh->sql_group_by('blocked, dependson') . " + HAVING COUNT(*) > 1", {Slice => {}}); + print "Removing duplicated entries from the 'dependencies' table...\n" + if @$dupes; + foreach my $dupe (@$dupes) { + $dbh->do( + "DELETE FROM dependencies + WHERE blocked = ? AND dependson = ?", undef, $dupe->{blocked}, + $dupe->{dependson} + ); + $dbh->do("INSERT INTO dependencies (blocked, dependson) VALUES (?, ?)", + undef, $dupe->{blocked}, $dupe->{dependson}); } + $dbh->bz_drop_index('dependencies', 'dependencies_blocked_idx'); + $dbh->bz_add_index('dependencies', 'dependencies_blocked_idx', + {FIELDS => [qw(blocked dependson)], TYPE => 'UNIQUE'}); + } } sub _fix_flagclusions_indexes { - my $dbh = Bugzilla->dbh; - foreach my $table ('flaginclusions', 'flagexclusions') { - my $index = $table . '_type_id_idx'; - my $idx_info = $dbh->bz_index_info($table, $index); - if ($idx_info && $idx_info->{'TYPE'} ne 'UNIQUE') { - # Remove duplicated entries - my $dupes = $dbh->selectall_arrayref(" + my $dbh = Bugzilla->dbh; + foreach my $table ('flaginclusions', 'flagexclusions') { + my $index = $table . '_type_id_idx'; + my $idx_info = $dbh->bz_index_info($table, $index); + if ($idx_info && $idx_info->{'TYPE'} ne 'UNIQUE') { + + # Remove duplicated entries + my $dupes = $dbh->selectall_arrayref(" SELECT type_id, product_id, component_id, COUNT(*) AS count - FROM $table " . - $dbh->sql_group_by('type_id, product_id, component_id') . " - HAVING COUNT(*) > 1", - { Slice => {} }); - print "Removing duplicated entries from the '$table' table...\n" if @$dupes; - foreach my $dupe (@$dupes) { - $dbh->do("DELETE FROM $table + FROM $table " + . $dbh->sql_group_by('type_id, product_id, component_id') . " + HAVING COUNT(*) > 1", {Slice => {}}); + print "Removing duplicated entries from the '$table' table...\n" if @$dupes; + foreach my $dupe (@$dupes) { + $dbh->do( + "DELETE FROM $table WHERE type_id = ? AND product_id = ? AND component_id = ?", - undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); - $dbh->do("INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)", - undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); - } - $dbh->bz_drop_index($table, $index); - $dbh->bz_add_index($table, $index, - { FIELDS => [qw(type_id product_id component_id)], - TYPE => 'UNIQUE' }); - } + undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id} + ); + $dbh->do( + "INSERT INTO $table (type_id, product_id, component_id) VALUES (?, ?, ?)", + undef, $dupe->{type_id}, $dupe->{product_id}, $dupe->{component_id}); + } + $dbh->bz_drop_index($table, $index); + $dbh->bz_add_index($table, $index, + {FIELDS => [qw(type_id product_id component_id)], TYPE => 'UNIQUE'}); } + } } sub _fix_user_api_keys_indexes { - my $dbh = Bugzilla->dbh; - - if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_key')) { - $dbh->bz_drop_index('user_api_keys', 'user_api_keys_key'); - $dbh->bz_add_index('user_api_keys', 'user_api_keys_api_key_idx', - { FIELDS => ['api_key'], TYPE => 'UNIQUE' }); - } - if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_user_id')) { - $dbh->bz_drop_index('user_api_keys', 'user_api_keys_user_id'); - $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_idx', ['user_id']); - } + my $dbh = Bugzilla->dbh; + + if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_key')) { + $dbh->bz_drop_index('user_api_keys', 'user_api_keys_key'); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_api_key_idx', + {FIELDS => ['api_key'], TYPE => 'UNIQUE'}); + } + if ($dbh->bz_index_info('user_api_keys', 'user_api_keys_user_id')) { + $dbh->bz_drop_index('user_api_keys', 'user_api_keys_user_id'); + $dbh->bz_add_index('user_api_keys', 'user_api_keys_user_id_idx', ['user_id']); + } } sub _add_attach_size { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - return if $dbh->bz_column_info('attachments', 'attach_size'); + return if $dbh->bz_column_info('attachments', 'attach_size'); - $dbh->bz_add_column('attachments', 'attach_size', - {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('attachments', 'attach_size', + {TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0}); - print "Setting attach_size...\n"; - $dbh->do(" + print "Setting attach_size...\n"; + $dbh->do(" UPDATE attachments INNER JOIN attach_data ON attach_data.id = attachments.attach_id SET attachments.attach_size = LENGTH(attach_data.thedata) @@ -3891,57 +4162,61 @@ sub _add_attach_size { } sub _fix_disable_mail { - # you can no longer have disabled accounts with enabled mail - Bugzilla->dbh->do("UPDATE profiles SET disable_mail = 1 WHERE is_enabled = 0"); + + # you can no longer have disabled accounts with enabled mail + Bugzilla->dbh->do("UPDATE profiles SET disable_mail = 1 WHERE is_enabled = 0"); } sub _add_restrict_ipaddr { - my $dbh = Bugzilla->dbh; - return if $dbh->bz_column_info('logincookies', 'restrict_ipaddr'); + my $dbh = Bugzilla->dbh; + return if $dbh->bz_column_info('logincookies', 'restrict_ipaddr'); - $dbh->bz_add_column('logincookies', 'restrict_ipaddr', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); - $dbh->do("UPDATE logincookies SET restrict_ipaddr = 1 WHERE ipaddr IS NOT NULL"); + $dbh->bz_add_column('logincookies', 'restrict_ipaddr', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); + $dbh->do( + "UPDATE logincookies SET restrict_ipaddr = 1 WHERE ipaddr IS NOT NULL"); } sub _migrate_group_owners { - my $dbh = Bugzilla->dbh; - return if $dbh->bz_column_info('groups', 'owner_user_id'); - $dbh->bz_add_column('groups', 'owner_user_id', {TYPE => 'INT3'}); - my $nobody = Bugzilla::User->new({ name => Bugzilla->params->{'nobody_user'}, cache => 1 }); - unless ($nobody) { - $nobody = Bugzilla::User->create( - { - login_name => Bugzilla->params->{'nobody_user'}, - realname => 'Nobody (ok to assign bugs to)', - cryptpassword => '*', - } - ); - } - $dbh->do('UPDATE groups SET owner_user_id = ?', undef, $nobody->id); + my $dbh = Bugzilla->dbh; + return if $dbh->bz_column_info('groups', 'owner_user_id'); + $dbh->bz_add_column('groups', 'owner_user_id', {TYPE => 'INT3'}); + my $nobody = Bugzilla::User->new( + {name => Bugzilla->params->{'nobody_user'}, cache => 1}); + unless ($nobody) { + $nobody = Bugzilla::User->create({ + login_name => Bugzilla->params->{'nobody_user'}, + realname => 'Nobody (ok to assign bugs to)', + cryptpassword => '*', + }); + } + $dbh->do('UPDATE groups SET owner_user_id = ?', undef, $nobody->id); } sub _migrate_nicknames { - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare('SELECT userid FROM profiles WHERE realname LIKE "%:%" AND is_enabled = 1 AND NOT nickname'); - $sth->execute(); - while (my ($user_id) = $sth->fetchrow_array) { - my $user = Bugzilla::User->new($user_id); - $user->set_name($user->name); - $user->update(); - } + my $dbh = Bugzilla->dbh; + my $sth + = $dbh->prepare( + 'SELECT userid FROM profiles WHERE realname LIKE "%:%" AND is_enabled = 1 AND NOT nickname' + ); + $sth->execute(); + while (my ($user_id) = $sth->fetchrow_array) { + my $user = Bugzilla::User->new($user_id); + $user->set_name($user->name); + $user->update(); + } } sub _migrate_preference_categories { - my $dbh = Bugzilla->dbh; - return if $dbh->bz_column_info('setting', 'category'); - $dbh->bz_add_column('setting', 'category', - {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'General'"}); - my @settings = @{ Bugzilla::Install::SETTINGS() }; - foreach my $params (@settings) { - $dbh->do('UPDATE setting SET category = ? WHERE name = ?', - undef, $params->{category}, $params->{name}); - } + my $dbh = Bugzilla->dbh; + return if $dbh->bz_column_info('setting', 'category'); + $dbh->bz_add_column('setting', 'category', + {TYPE => 'varchar(64)', NOTNULL => 1, DEFAULT => "'General'"}); + my @settings = @{Bugzilla::Install::SETTINGS()}; + foreach my $params (@settings) { + $dbh->do('UPDATE setting SET category = ? WHERE name = ?', + undef, $params->{category}, $params->{name}); + } } 1; diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm index cb1b1ad15..da019b760 100644 --- a/Bugzilla/Install/Filesystem.pm +++ b/Bugzilla/Install/Filesystem.pm @@ -40,10 +40,10 @@ use English qw(-no_match_vars $OSNAME); use base qw(Exporter); our @EXPORT = qw( - update_filesystem - fix_all_file_permissions - fix_dir_permissions - fix_file_permissions + update_filesystem + fix_all_file_permissions + fix_dir_permissions + fix_file_permissions ); use constant INDEX_HTML => <<'EOT'; @@ -59,12 +59,12 @@ use constant INDEX_HTML => <<'EOT'; EOT use constant HTTPD_ENV => qw( - LOCALCONFIG_ENV - BUGZILLA_UNSAFE_AUTH_DELEGATION - LOG4PERL_CONFIG_FILE - LOG4PERL_STDERR_DISABLE - USE_NYTPROF - NYTPROF_DIR + LOCALCONFIG_ENV + BUGZILLA_UNSAFE_AUTH_DELEGATION + LOG4PERL_CONFIG_FILE + LOG4PERL_STDERR_DISABLE + USE_NYTPROF + NYTPROF_DIR ); ############### @@ -72,47 +72,55 @@ use constant HTTPD_ENV => qw( ############### # Used by the permissions "constants" below. -sub _suexec { Bugzilla->localconfig->{'use_suexec'} }; -sub _group { Bugzilla->localconfig->{'webservergroup'} }; +sub _suexec { Bugzilla->localconfig->{'use_suexec'} } +sub _group { Bugzilla->localconfig->{'webservergroup'} } # Writeable by the owner only. use constant OWNER_WRITE => 0600; + # Executable by the owner only. use constant OWNER_EXECUTE => 0700; + # A directory which is only writeable by the owner. use constant DIR_OWNER_WRITE => 0700; # A cgi script that the webserver can execute. -sub WS_EXECUTE { _group() ? 0750 : 0755 }; +sub WS_EXECUTE { _group() ? 0750 : 0755 } + # A file that is read by cgi scripts, but is not ever read # directly by the webserver. -sub CGI_READ { _group() ? 0640 : 0644 }; +sub CGI_READ { _group() ? 0640 : 0644 } + # A file that is written to by cgi scripts, but is not ever # read or written directly by the webserver. -sub CGI_WRITE { _group() ? 0660 : 0666 }; +sub CGI_WRITE { _group() ? 0660 : 0666 } + # A file that is served directly by the web server. -sub WS_SERVE { (_group() and !_suexec()) ? 0640 : 0644 }; +sub WS_SERVE { (_group() and !_suexec()) ? 0640 : 0644 } # A directory whose contents can be read or served by the # webserver (so even directories containing cgi scripts # would have this permission). -sub DIR_WS_SERVE { (_group() and !_suexec()) ? 0750 : 0755 }; +sub DIR_WS_SERVE { (_group() and !_suexec()) ? 0750 : 0755 } + # A directory that is read by cgi scripts, but is never accessed # directly by the webserver -sub DIR_CGI_READ { _group() ? 0750 : 0755 }; +sub DIR_CGI_READ { _group() ? 0750 : 0755 } + # A directory that is written to by cgi scripts, but where the # scripts never needs to overwrite files created by other # users. -sub DIR_CGI_WRITE { _group() ? 0770 : 01777 }; +sub DIR_CGI_WRITE { _group() ? 0770 : 01777 } + # A directory that is written to by cgi scripts, where the # scripts need to overwrite files created by other users. -sub DIR_CGI_OVERWRITE { _group() ? 0770 : 0777 }; +sub DIR_CGI_OVERWRITE { _group() ? 0770 : 0777 } # This can be combined (using "|") with other permissions for # directories that, in addition to their normal permissions (such # as DIR_CGI_WRITE) also have content served directly from them # (or their subdirectories) to the user, via the webserver. -sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 }; +sub DIR_ALSO_WS_SERVE { _suexec() ? 0001 : 0 } sub DIR_ALSO_WS_STICKY { $OSNAME eq 'linux' ? 02000 : 0 } @@ -127,469 +135,437 @@ sub DIR_ALSO_WS_STICKY { $OSNAME eq 'linux' ? 02000 : 0 } # by this group. Otherwise someone may find it possible to change the cgis # when exploiting some security flaw somewhere (not necessarily in Bugzilla!) sub FILESYSTEM { - my $datadir = bz_locations()->{'datadir'}; - my $confdir = bz_locations()->{'confdir'}; - my $attachdir = bz_locations()->{'attachdir'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $webdotdir = bz_locations()->{'webdotdir'}; - my $templatedir = bz_locations()->{'templatedir'}; - my $libdir = bz_locations()->{'libpath'}; - my $extlib = bz_locations()->{'ext_libpath'}; - my $skinsdir = bz_locations()->{'skinsdir'}; - my $localconfig = bz_locations()->{'localconfig'}; - my $template_cache = bz_locations()->{'template_cache'}; - my $graphsdir = bz_locations()->{'graphsdir'}; - my $assetsdir = bz_locations()->{'assetsdir'}; - my $logsdir = bz_locations()->{'logsdir'}; - - # We want to set the permissions the same for all localconfig files - # across all PROJECTs, so we do something special with $localconfig, - # lower down in the permissions section. - if ($ENV{PROJECT}) { - $localconfig =~ s/\.\Q$ENV{PROJECT}\E$//; - } - - # Note: When being processed by checksetup, these have their permissions - # set in this order: %all_dirs, %recurse_dirs, %all_files. - # - # Each is processed in alphabetical order of keys, so shorter keys - # will have their permissions set before longer keys (thus setting - # the permissions on parent directories before setting permissions - # on their children). - - # --- FILE PERMISSIONS (Non-created files) --- # - my %files = ( - '*' => { perms => OWNER_WRITE }, - # Some .pl files are WS_EXECUTE because we want - # users to be able to cron them or otherwise run - # them as a secure user, like the webserver owner. - '*.cgi' => { perms => WS_EXECUTE }, - '*.psgi' => { perms => CGI_READ }, - 'whineatnews.pl' => { perms => WS_EXECUTE }, - 'collectstats.pl' => { perms => WS_EXECUTE }, - 'importxml.pl' => { perms => WS_EXECUTE }, - 'testserver.pl' => { perms => WS_EXECUTE }, - 'whine.pl' => { perms => WS_EXECUTE }, - 'email_in.pl' => { perms => WS_EXECUTE }, - 'sanitycheck.pl' => { perms => WS_EXECUTE }, - 'checksetup.pl' => { perms => OWNER_EXECUTE }, - 'runtests.pl' => { perms => OWNER_EXECUTE }, - 'jobqueue.pl' => { perms => OWNER_EXECUTE }, - 'migrate.pl' => { perms => OWNER_EXECUTE }, - 'Makefile.PL' => { perms => OWNER_EXECUTE }, - 'gen-cpanfile.pl' => { perms => OWNER_EXECUTE }, - 'jobqueue-worker.pl' => { perms => OWNER_EXECUTE }, - 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE }, - - 'bugzilla.pl' => { perms => OWNER_EXECUTE }, - 'Bugzilla.pm' => { perms => CGI_READ }, - "$localconfig*" => { perms => CGI_READ }, - 'META.*' => { perms => CGI_READ }, - 'MYMETA.*' => { perms => CGI_READ }, - 'bugzilla.dtd' => { perms => WS_SERVE }, - 'mod_perl.pl' => { perms => WS_SERVE }, - 'cvs-update.log' => { perms => WS_SERVE }, - 'scripts/sendunsentbugmail.pl' => { perms => WS_EXECUTE }, - 'docs/bugzilla.ent' => { perms => OWNER_WRITE }, - 'docs/makedocs.pl' => { perms => OWNER_EXECUTE }, - 'docs/style.css' => { perms => WS_SERVE }, - 'docs/*/rel_notes.txt' => { perms => WS_SERVE }, - 'docs/*/README.docs' => { perms => OWNER_WRITE }, - "$datadir/params" => { perms => CGI_WRITE }, - "$datadir/old-params.txt" => { perms => OWNER_WRITE }, - "$extensionsdir/create.pl" => { perms => OWNER_EXECUTE }, - "$extensionsdir/*/*.pl" => { perms => WS_EXECUTE }, - "$extensionsdir/*/bin/*" => { perms => WS_EXECUTE }, - - # google webmaster tools verification files - 'google*.html' => { perms => WS_SERVE }, - 'contribute.json' => { perms => WS_SERVE }, + my $datadir = bz_locations()->{'datadir'}; + my $confdir = bz_locations()->{'confdir'}; + my $attachdir = bz_locations()->{'attachdir'}; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $webdotdir = bz_locations()->{'webdotdir'}; + my $templatedir = bz_locations()->{'templatedir'}; + my $libdir = bz_locations()->{'libpath'}; + my $extlib = bz_locations()->{'ext_libpath'}; + my $skinsdir = bz_locations()->{'skinsdir'}; + my $localconfig = bz_locations()->{'localconfig'}; + my $template_cache = bz_locations()->{'template_cache'}; + my $graphsdir = bz_locations()->{'graphsdir'}; + my $assetsdir = bz_locations()->{'assetsdir'}; + my $logsdir = bz_locations()->{'logsdir'}; + + # We want to set the permissions the same for all localconfig files + # across all PROJECTs, so we do something special with $localconfig, + # lower down in the permissions section. + if ($ENV{PROJECT}) { + $localconfig =~ s/\.\Q$ENV{PROJECT}\E$//; + } + + # Note: When being processed by checksetup, these have their permissions + # set in this order: %all_dirs, %recurse_dirs, %all_files. + # + # Each is processed in alphabetical order of keys, so shorter keys + # will have their permissions set before longer keys (thus setting + # the permissions on parent directories before setting permissions + # on their children). + + # --- FILE PERMISSIONS (Non-created files) --- # + my %files = ( + '*' => {perms => OWNER_WRITE}, + + # Some .pl files are WS_EXECUTE because we want + # users to be able to cron them or otherwise run + # them as a secure user, like the webserver owner. + '*.cgi' => {perms => WS_EXECUTE}, + '*.psgi' => {perms => CGI_READ}, + 'whineatnews.pl' => {perms => WS_EXECUTE}, + 'collectstats.pl' => {perms => WS_EXECUTE}, + 'importxml.pl' => {perms => WS_EXECUTE}, + 'testserver.pl' => {perms => WS_EXECUTE}, + 'whine.pl' => {perms => WS_EXECUTE}, + 'email_in.pl' => {perms => WS_EXECUTE}, + 'sanitycheck.pl' => {perms => WS_EXECUTE}, + 'checksetup.pl' => {perms => OWNER_EXECUTE}, + 'runtests.pl' => {perms => OWNER_EXECUTE}, + 'jobqueue.pl' => {perms => OWNER_EXECUTE}, + 'migrate.pl' => {perms => OWNER_EXECUTE}, + 'Makefile.PL' => {perms => OWNER_EXECUTE}, + 'gen-cpanfile.pl' => {perms => OWNER_EXECUTE}, + 'jobqueue-worker.pl' => {perms => OWNER_EXECUTE}, + 'clean-bug-user-last-visit.pl' => {perms => WS_EXECUTE}, + + 'bugzilla.pl' => {perms => OWNER_EXECUTE}, + 'Bugzilla.pm' => {perms => CGI_READ}, + "$localconfig*" => {perms => CGI_READ}, + 'META.*' => {perms => CGI_READ}, + 'MYMETA.*' => {perms => CGI_READ}, + 'bugzilla.dtd' => {perms => WS_SERVE}, + 'mod_perl.pl' => {perms => WS_SERVE}, + 'cvs-update.log' => {perms => WS_SERVE}, + 'scripts/sendunsentbugmail.pl' => {perms => WS_EXECUTE}, + 'docs/bugzilla.ent' => {perms => OWNER_WRITE}, + 'docs/makedocs.pl' => {perms => OWNER_EXECUTE}, + 'docs/style.css' => {perms => WS_SERVE}, + 'docs/*/rel_notes.txt' => {perms => WS_SERVE}, + 'docs/*/README.docs' => {perms => OWNER_WRITE}, + "$datadir/params" => {perms => CGI_WRITE}, + "$datadir/old-params.txt" => {perms => OWNER_WRITE}, + "$extensionsdir/create.pl" => {perms => OWNER_EXECUTE}, + "$extensionsdir/*/*.pl" => {perms => WS_EXECUTE}, + "$extensionsdir/*/bin/*" => {perms => WS_EXECUTE}, + + # google webmaster tools verification files + 'google*.html' => {perms => WS_SERVE}, + 'contribute.json' => {perms => WS_SERVE}, + ); + + # Directories that we want to set the perms on, but not + # recurse through. These are directories we didn't create + # in checkesetup.pl. + # + # Purpose of BMO change: unknown. + my %non_recurse_dirs = ('.' => 0755, docs => DIR_WS_SERVE,); + + # This sets the permissions for each item inside each of these + # directories, including the directory itself. + # 'CVS' directories are special, though, and are never readable by + # the webserver. + my %recurse_dirs = ( + + # Writeable directories + $template_cache => {files => CGI_READ, dirs => DIR_CGI_OVERWRITE}, + $attachdir => {files => CGI_WRITE, dirs => DIR_CGI_WRITE}, + $webdotdir => {files => WS_SERVE, dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE}, + $graphsdir => {files => WS_SERVE, dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE}, + "$datadir/db" => {files => CGI_WRITE, dirs => DIR_CGI_WRITE}, + $logsdir => {files => CGI_WRITE, dirs => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY}, + $assetsdir => + {files => WS_SERVE, dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE}, + + # Readable directories + "$datadir/mining" => {files => CGI_READ, dirs => DIR_CGI_READ}, + "$libdir/Bugzilla" => {files => CGI_READ, dirs => DIR_CGI_READ}, + $extlib => {files => CGI_READ, dirs => DIR_CGI_READ}, + $templatedir => {files => CGI_READ, dirs => DIR_CGI_READ}, + + # Directories in the extensions/ dir are WS_SERVE so that + # the web/ directories can be served by the web server. + # But, for extra security, we deny direct webserver access to + # the lib/ and template/ directories of extensions. + $extensionsdir => {files => CGI_READ, dirs => DIR_WS_SERVE}, + "$extensionsdir/*/lib" => {files => CGI_READ, dirs => DIR_CGI_READ}, + "$extensionsdir/*/template" => {files => CGI_READ, dirs => DIR_CGI_READ}, + + # Content served directly by the webserver + images => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + js => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + static => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + $skinsdir => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/html' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/pdf' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/txt' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + 'docs/*/images' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + "$extensionsdir/*/web" => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + $confdir => {files => WS_SERVE, dirs => DIR_WS_SERVE,}, + + # Purpose: allow webserver to read .bzr so we execute bzr commands + # in backticks and look at the result over the web. Used to show + # bzr history. + '.bzr' => {files => WS_SERVE, dirs => DIR_WS_SERVE}, + + # Directories only for the owner, not for the webserver. + t => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + xt => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'docs/lib' => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'docs/*/xml' => {files => OWNER_WRITE, dirs => DIR_OWNER_WRITE}, + 'contrib' => {files => OWNER_EXECUTE, dirs => DIR_OWNER_WRITE,}, + 'scripts' => {files => OWNER_EXECUTE, dirs => DIR_WS_SERVE,}, + ); + + # --- FILES TO CREATE --- # + + # The name of each directory that we should actually *create*, + # pointing at its default permissions. + my %create_dirs = ( + + # This is DIR_ALSO_WS_SERVE because it contains $webdotdir and + # $assetsdir. + $datadir => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE, + + # Directories that are read-only for cgi scripts + "$datadir/mining" => DIR_CGI_READ, + "$datadir/extensions" => DIR_CGI_READ, + $extensionsdir => DIR_CGI_READ, + + # Directories that cgi scripts can write to. + "$datadir/db" => DIR_CGI_WRITE, + $attachdir => DIR_CGI_WRITE, + $graphsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + $webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + $assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, + $template_cache => DIR_CGI_WRITE, + $logsdir => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY, + + # Directories that contain content served directly by the web server. + "$skinsdir/custom" => DIR_WS_SERVE, + "$skinsdir/contrib" => DIR_WS_SERVE, + $confdir => DIR_CGI_READ, + ); + + my $yui_all_css = sub { + return join( + "\n", + map { + my $css = read_file($_); + _css_url_fix($css, $_, "skins/yui.css.list") + } read_file("skins/yui.css.list", {chomp => 1}) ); - - # Directories that we want to set the perms on, but not - # recurse through. These are directories we didn't create - # in checkesetup.pl. - # - # Purpose of BMO change: unknown. - my %non_recurse_dirs = ( - '.' => 0755, - docs => DIR_WS_SERVE, - ); - - # This sets the permissions for each item inside each of these - # directories, including the directory itself. - # 'CVS' directories are special, though, and are never readable by - # the webserver. - my %recurse_dirs = ( - # Writeable directories - $template_cache => { files => CGI_READ, - dirs => DIR_CGI_OVERWRITE }, - $attachdir => { files => CGI_WRITE, - dirs => DIR_CGI_WRITE }, - $webdotdir => { files => WS_SERVE, - dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE }, - $graphsdir => { files => WS_SERVE, - dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE }, - "$datadir/db" => { files => CGI_WRITE, - dirs => DIR_CGI_WRITE }, - $logsdir => { files => CGI_WRITE, - dirs => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY }, - $assetsdir => { files => WS_SERVE, - dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE }, - - # Readable directories - "$datadir/mining" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - "$libdir/Bugzilla" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - $extlib => { files => CGI_READ, - dirs => DIR_CGI_READ }, - $templatedir => { files => CGI_READ, - dirs => DIR_CGI_READ }, - # Directories in the extensions/ dir are WS_SERVE so that - # the web/ directories can be served by the web server. - # But, for extra security, we deny direct webserver access to - # the lib/ and template/ directories of extensions. - $extensionsdir => { files => CGI_READ, - dirs => DIR_WS_SERVE }, - "$extensionsdir/*/lib" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - "$extensionsdir/*/template" => { files => CGI_READ, - dirs => DIR_CGI_READ }, - - # Content served directly by the webserver - images => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - js => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - static => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - $skinsdir => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/html' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/pdf' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/txt' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - 'docs/*/images' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - "$extensionsdir/*/web" => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - $confdir => { files => WS_SERVE, - dirs => DIR_WS_SERVE, }, - - # Purpose: allow webserver to read .bzr so we execute bzr commands - # in backticks and look at the result over the web. Used to show - # bzr history. - '.bzr' => { files => WS_SERVE, - dirs => DIR_WS_SERVE }, - # Directories only for the owner, not for the webserver. - t => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - xt => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'docs/lib' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'docs/*/xml' => { files => OWNER_WRITE, - dirs => DIR_OWNER_WRITE }, - 'contrib' => { files => OWNER_EXECUTE, - dirs => DIR_OWNER_WRITE, }, - 'scripts' => { files => OWNER_EXECUTE, - dirs => DIR_WS_SERVE, }, - ); - - # --- FILES TO CREATE --- # - - # The name of each directory that we should actually *create*, - # pointing at its default permissions. - my %create_dirs = ( - # This is DIR_ALSO_WS_SERVE because it contains $webdotdir and - # $assetsdir. - $datadir => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE, - # Directories that are read-only for cgi scripts - "$datadir/mining" => DIR_CGI_READ, - "$datadir/extensions" => DIR_CGI_READ, - $extensionsdir => DIR_CGI_READ, - # Directories that cgi scripts can write to. - "$datadir/db" => DIR_CGI_WRITE, - $attachdir => DIR_CGI_WRITE, - $graphsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - $webdotdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - $assetsdir => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE, - $template_cache => DIR_CGI_WRITE, - $logsdir => DIR_CGI_WRITE | DIR_ALSO_WS_STICKY, - # Directories that contain content served directly by the web server. - "$skinsdir/custom" => DIR_WS_SERVE, - "$skinsdir/contrib" => DIR_WS_SERVE, - $confdir => DIR_CGI_READ, - ); - - my $yui_all_css = sub { - return join("\n", - map { - my $css = read_file($_); - _css_url_fix($css, $_, "skins/yui.css.list") - } read_file("skins/yui.css.list", { chomp => 1 }) - ); - }; - - my $yui_all_js = sub { - return join("\n", - map { scalar read_file($_) } read_file("js/yui.js.list", { chomp => 1 }) - ); - }; - - my $yui3_all_css = sub { - return join("\n", - map { - my $css = read_file($_); - _css_url_fix($css, $_, "skins/yui3.css.list") - } read_file("skins/yui3.css.list", { chomp => 1 }) - ); - }; - - my $yui3_all_js = sub { - return join("\n", - map { scalar read_file($_) } read_file("js/yui3.js.list", { chomp => 1 }) - ); - }; - - # The name of each file, pointing at its default permissions and - # default contents. - my %create_files = ( - "$datadir/extensions/additional" => { perms => CGI_READ, - contents => '' }, - # We create this file so that it always has the right owner - # and permissions. Otherwise, the webserver creates it as - # owned by itself, which can cause problems if jobqueue.pl - # or something else is not running as the webserver or root. - "$datadir/mailer.testfile" => { perms => CGI_WRITE, - contents => '' }, - "js/yui.js" => { perms => CGI_READ, - overwrite => 1, - contents => $yui_all_js }, - "skins/yui.css" => { perms => CGI_READ, - overwrite => 1, - contents => $yui_all_css }, - "js/yui3.js" => { perms => CGI_READ, - overwrite => 1, - contents => $yui3_all_js }, - "skins/yui3.css" => { perms => CGI_READ, - overwrite => 1, - contents => $yui3_all_css }, + }; + + my $yui_all_js = sub { + return join("\n", + map { scalar read_file($_) } read_file("js/yui.js.list", {chomp => 1})); + }; + + my $yui3_all_css = sub { + return join( + "\n", + map { + my $css = read_file($_); + _css_url_fix($css, $_, "skins/yui3.css.list") + } read_file("skins/yui3.css.list", {chomp => 1}) ); + }; + + my $yui3_all_js = sub { + return join("\n", + map { scalar read_file($_) } read_file("js/yui3.js.list", {chomp => 1})); + }; + + # The name of each file, pointing at its default permissions and + # default contents. + my %create_files = ( + "$datadir/extensions/additional" => {perms => CGI_READ, contents => ''}, + + # We create this file so that it always has the right owner + # and permissions. Otherwise, the webserver creates it as + # owned by itself, which can cause problems if jobqueue.pl + # or something else is not running as the webserver or root. + "$datadir/mailer.testfile" => {perms => CGI_WRITE, contents => ''}, + "js/yui.js" => {perms => CGI_READ, overwrite => 1, contents => $yui_all_js}, + "skins/yui.css" => + {perms => CGI_READ, overwrite => 1, contents => $yui_all_css}, + "js/yui3.js" => {perms => CGI_READ, overwrite => 1, contents => $yui3_all_js}, + "skins/yui3.css" => + {perms => CGI_READ, overwrite => 1, contents => $yui3_all_css}, + ); + + # Create static error pages. + $create_dirs{"errors"} = DIR_CGI_READ; + + # Because checksetup controls the creation of index.html separately + # from all other files, it gets its very own hash. + my %index_html = ('index.html' => {perms => WS_SERVE, contents => INDEX_HTML}); + + Bugzilla::Hook::process( + 'install_filesystem', + { + files => \%files, + create_dirs => \%create_dirs, + non_recurse_dirs => \%non_recurse_dirs, + recurse_dirs => \%recurse_dirs, + create_files => \%create_files, + } + ); - # Create static error pages. - $create_dirs{"errors"} = DIR_CGI_READ; + my %all_files = (%create_files, %index_html, %files); + my %all_dirs = (%create_dirs, %non_recurse_dirs); - # Because checksetup controls the creation of index.html separately - # from all other files, it gets its very own hash. - my %index_html = ( - 'index.html' => { perms => WS_SERVE, contents => INDEX_HTML } - ); + return { + create_dirs => \%create_dirs, + recurse_dirs => \%recurse_dirs, + all_dirs => \%all_dirs, - Bugzilla::Hook::process('install_filesystem', { - files => \%files, - create_dirs => \%create_dirs, - non_recurse_dirs => \%non_recurse_dirs, - recurse_dirs => \%recurse_dirs, - create_files => \%create_files, - }); - - my %all_files = (%create_files, %index_html, %files); - my %all_dirs = (%create_dirs, %non_recurse_dirs); - - return { - create_dirs => \%create_dirs, - recurse_dirs => \%recurse_dirs, - all_dirs => \%all_dirs, - - create_files => \%create_files, - index_html => \%index_html, - all_files => \%all_files, - }; + create_files => \%create_files, + index_html => \%index_html, + all_files => \%all_files, + }; } sub update_filesystem { - my ($params) = @_; - my $fs = FILESYSTEM(); - my %dirs = %{$fs->{create_dirs}}; - my %files = %{$fs->{create_files}}; - - my $datadir = bz_locations->{'datadir'}; - my $graphsdir = bz_locations->{'graphsdir'}; - my $assetsdir = bz_locations->{'assetsdir'}; - # If the graphs/ directory doesn't exist, we're upgrading from - # a version old enough that we need to update the $datadir/mining - # format. - if (-d "$datadir/mining" && !-d $graphsdir) { - _update_old_charts($datadir); + my ($params) = @_; + my $fs = FILESYSTEM(); + my %dirs = %{$fs->{create_dirs}}; + my %files = %{$fs->{create_files}}; + + my $datadir = bz_locations->{'datadir'}; + my $graphsdir = bz_locations->{'graphsdir'}; + my $assetsdir = bz_locations->{'assetsdir'}; + + # If the graphs/ directory doesn't exist, we're upgrading from + # a version old enough that we need to update the $datadir/mining + # format. + if (-d "$datadir/mining" && !-d $graphsdir) { + _update_old_charts($datadir); + } + + # By sorting the dirs, we assure that shorter-named directories + # (meaning parent directories) are always created before their + # child directories. + foreach my $dir (sort keys %dirs) { + unless (-d $dir) { + print "Creating $dir directory...\n"; + mkdir $dir or die "mkdir $dir failed: $!"; + + # For some reason, passing in the permissions to "mkdir" + # doesn't work right, but doing a "chmod" does. + chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!"; } - - # By sorting the dirs, we assure that shorter-named directories - # (meaning parent directories) are always created before their - # child directories. - foreach my $dir (sort keys %dirs) { - unless (-d $dir) { - print "Creating $dir directory...\n"; - mkdir $dir or die "mkdir $dir failed: $!"; - # For some reason, passing in the permissions to "mkdir" - # doesn't work right, but doing a "chmod" does. - chmod $dirs{$dir}, $dir or warn "Cannot chmod $dir: $!"; - } - } - - # Move the testfile if we can't write to it, so that we can re-create - # it with the correct permissions below. - my $testfile = "$datadir/mailer.testfile"; - if (-e $testfile and !-w $testfile) { - _rename_file($testfile, "$testfile.old"); - } - - # If old-params.txt exists in the root directory, move it to datadir. - my $oldparamsfile = "old_params.txt"; - if (-e $oldparamsfile) { - _rename_file($oldparamsfile, "$datadir/$oldparamsfile"); - } - - _create_files(%files); - if ($params->{index_html}) { - _create_files(%{$fs->{index_html}}); - } - elsif (-e 'index.html') { - my $templatedir = bz_locations()->{'templatedir'}; - print "*** It appears that you still have an old index.html hanging around.\n", - "Either the contents of this file should be moved into a template and\n", - "placed in the '$templatedir/en/custom' directory, or you should delete\n", - "the file.\n"; - } - - # Delete old files that no longer need to exist - - # 2001-04-29 jake@bugzilla.org - Remove oldemailtech - # http://bugzilla.mozilla.org/show_bug.cgi?id=71552 - if (-d 'shadow') { - print "Removing shadow directory...\n"; - rmtree("shadow"); - } - - if (-e "$datadir/versioncache") { - print "Removing versioncache...\n"; - unlink "$datadir/versioncache"; - } - - if (-e "$datadir/duplicates.rdf") { - print "Removing duplicates.rdf...\n"; - unlink "$datadir/duplicates.rdf"; - unlink "$datadir/duplicates-old.rdf"; - } - - if (-e "$datadir/duplicates") { - print "Removing duplicates directory...\n"; - rmtree("$datadir/duplicates"); - } - - _remove_empty_css_files(); - _convert_single_file_skins(); + } + + # Move the testfile if we can't write to it, so that we can re-create + # it with the correct permissions below. + my $testfile = "$datadir/mailer.testfile"; + if (-e $testfile and !-w $testfile) { + _rename_file($testfile, "$testfile.old"); + } + + # If old-params.txt exists in the root directory, move it to datadir. + my $oldparamsfile = "old_params.txt"; + if (-e $oldparamsfile) { + _rename_file($oldparamsfile, "$datadir/$oldparamsfile"); + } + + _create_files(%files); + if ($params->{index_html}) { + _create_files(%{$fs->{index_html}}); + } + elsif (-e 'index.html') { + my $templatedir = bz_locations()->{'templatedir'}; + print "*** It appears that you still have an old index.html hanging around.\n", + "Either the contents of this file should be moved into a template and\n", + "placed in the '$templatedir/en/custom' directory, or you should delete\n", + "the file.\n"; + } + + # Delete old files that no longer need to exist + + # 2001-04-29 jake@bugzilla.org - Remove oldemailtech + # http://bugzilla.mozilla.org/show_bug.cgi?id=71552 + if (-d 'shadow') { + print "Removing shadow directory...\n"; + rmtree("shadow"); + } + + if (-e "$datadir/versioncache") { + print "Removing versioncache...\n"; + unlink "$datadir/versioncache"; + } + + if (-e "$datadir/duplicates.rdf") { + print "Removing duplicates.rdf...\n"; + unlink "$datadir/duplicates.rdf"; + unlink "$datadir/duplicates-old.rdf"; + } + + if (-e "$datadir/duplicates") { + print "Removing duplicates directory...\n"; + rmtree("$datadir/duplicates"); + } + + _remove_empty_css_files(); + _convert_single_file_skins(); } sub _css_url_fix { - my ($content, $from, $to) = @_; - my $from_dir = dirname(File::Spec->rel2abs($from, bz_locations()->{libpath})); - my $to_dir = dirname(File::Spec->rel2abs($to, bz_locations()->{libpath})); - - return css_url_rewrite( - $content, - sub { - my ($url) = @_; - if ( $url =~ m{^(?:/|data:)} ) { - return sprintf 'url(%s)', $url; - } - else { - my $new_url = File::Spec->abs2rel( - Cwd::realpath( - File::Spec->rel2abs( $url, $from_dir ) - ), - $to_dir - ); - return sprintf "url(%s)", $new_url; - } - } - ); + my ($content, $from, $to) = @_; + my $from_dir = dirname(File::Spec->rel2abs($from, bz_locations()->{libpath})); + my $to_dir = dirname(File::Spec->rel2abs($to, bz_locations()->{libpath})); + + return css_url_rewrite( + $content, + sub { + my ($url) = @_; + if ($url =~ m{^(?:/|data:)}) { + return sprintf 'url(%s)', $url; + } + else { + my $new_url + = File::Spec->abs2rel(Cwd::realpath(File::Spec->rel2abs($url, $from_dir)), + $to_dir); + return sprintf "url(%s)", $new_url; + } + } + ); } sub _remove_empty_css_files { - my $skinsdir = bz_locations()->{'skinsdir'}; - foreach my $css_file (glob("$skinsdir/custom/*.css"), - glob("$skinsdir/contrib/*/*.css")) - { - _remove_empty_css($css_file); - } + my $skinsdir = bz_locations()->{'skinsdir'}; + foreach my $css_file (glob("$skinsdir/custom/*.css"), + glob("$skinsdir/contrib/*/*.css")) + { + _remove_empty_css($css_file); + } } # A simple helper for the update code that removes "empty" CSS files. sub _remove_empty_css { - my ($file) = @_; - my $basename = basename($file); - my $empty_contents = "/* Custom rules for $basename.\n" - . " * The rules you put here override rules in that stylesheet. */"; - if (length($empty_contents) == -s $file) { - open(my $fh, '<', $file) or warn "$file: $!"; - my $file_contents; - { local $/; $file_contents = <$fh>; } - if ($file_contents eq $empty_contents) { - print install_string('file_remove', { name => $file }), "\n"; - unlink $file or warn "$file: $!"; - } - }; + my ($file) = @_; + my $basename = basename($file); + my $empty_contents = "/* Custom rules for $basename.\n" + . " * The rules you put here override rules in that stylesheet. */"; + if (length($empty_contents) == -s $file) { + open(my $fh, '<', $file) or warn "$file: $!"; + my $file_contents; + { local $/; $file_contents = <$fh>; } + if ($file_contents eq $empty_contents) { + print install_string('file_remove', {name => $file}), "\n"; + unlink $file or warn "$file: $!"; + } + } } # We used to allow a single css file in the skins/contrib/ directory # to be a whole skin. sub _convert_single_file_skins { - my $skinsdir = bz_locations()->{'skinsdir'}; - foreach my $skin_file (glob "$skinsdir/contrib/*.css") { - my $dir_name = $skin_file; - $dir_name =~ s/\.css$//; - mkdir $dir_name or warn "$dir_name: $!"; - _rename_file($skin_file, "$dir_name/global.css"); - } + my $skinsdir = bz_locations()->{'skinsdir'}; + foreach my $skin_file (glob "$skinsdir/contrib/*.css") { + my $dir_name = $skin_file; + $dir_name =~ s/\.css$//; + mkdir $dir_name or warn "$dir_name: $!"; + _rename_file($skin_file, "$dir_name/global.css"); + } } sub _rename_file { - my ($from, $to) = @_; - print install_string('file_rename', { from => $from, to => $to }), "\n"; - if (-e $to) { - warn "$to already exists, not moving\n"; - } - else { - move($from, $to) or warn $!; - } + my ($from, $to) = @_; + print install_string('file_rename', {from => $from, to => $to}), "\n"; + if (-e $to) { + warn "$to already exists, not moving\n"; + } + else { + move($from, $to) or warn $!; + } } # A helper for the above functions. sub _create_files { - my (%files) = @_; - - # It's not necessary to sort these, but it does make the - # output of checksetup.pl look a bit nicer. - foreach my $file (sort keys %files) { - my $info = $files{$file}; - if ($info->{overwrite} or not -f $file) { - print "Creating $file...\n"; - my $fh = IO::File->new( $file, O_WRONLY | O_CREAT | O_TRUNC, $info->{perms} ) - or die "unable to write $file: $!"; - my $contents = $info->{contents}; - if (defined $contents && ref($contents) eq 'CODE') { - print $fh $contents->(); - } - elsif (defined $contents) { - print $fh $contents; - } - $fh->close; - } + my (%files) = @_; + + # It's not necessary to sort these, but it does make the + # output of checksetup.pl look a bit nicer. + foreach my $file (sort keys %files) { + my $info = $files{$file}; + if ($info->{overwrite} or not -f $file) { + print "Creating $file...\n"; + my $fh = IO::File->new($file, O_WRONLY | O_CREAT | O_TRUNC, $info->{perms}) + or die "unable to write $file: $!"; + my $contents = $info->{contents}; + if (defined $contents && ref($contents) eq 'CODE') { + print $fh $contents->(); + } + elsif (defined $contents) { + print $fh $contents; + } + $fh->close; } + } } # If you ran a REALLY old version of Bugzilla, your chart files are in the @@ -597,243 +573,266 @@ sub _create_files { # when moving it into this module, I couldn't test it so I left it almost # completely alone. sub _update_old_charts { - my ($datadir) = @_; - print "Updating old chart storage format...\n"; - foreach my $in_file (glob("$datadir/mining/*")) { - # Don't try and upgrade image or db files! - next if (($in_file =~ /\.gif$/i) || - ($in_file =~ /\.png$/i) || - ($in_file =~ /\.db$/i) || - ($in_file =~ /\.orig$/i)); - - rename("$in_file", "$in_file.orig") or next; - open(IN, "<", "$in_file.orig") or next; - open(OUT, '>', $in_file) or next; - - # Fields in the header - my @declared_fields; - - # Fields we changed to half way through by mistake - # This list comes from an old version of collectstats.pl - # This part is only for people who ran later versions of 2.11 (devel) - my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED - RESOLVED VERIFIED CLOSED); - - # Fields we actually want (matches the current collectstats.pl) - my @out_fields = qw(DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED - VERIFIED CLOSED FIXED INVALID WONTFIX LATER REMIND - DUPLICATE WORKSFORME MOVED); - - while () { - if (/^# fields?: (.*)\s$/) { - @declared_fields = map uc, (split /\||\r/, $1); - print OUT "# fields: ", join('|', @out_fields), "\n"; - } - elsif (/^(\d+\|.*)/) { - my @data = split(/\||\r/, $1); - my %data; - if (@data == @declared_fields) { - # old format - for my $i (0 .. $#declared_fields) { - $data{$declared_fields[$i]} = $data[$i]; - } - } - elsif (@data == @intermediate_fields) { - # Must have changed over at this point - for my $i (0 .. $#intermediate_fields) { - $data{$intermediate_fields[$i]} = $data[$i]; - } - } - elsif (@data == @out_fields) { - # This line's fine - it has the right number of entries - for my $i (0 .. $#out_fields) { - $data{$out_fields[$i]} = $data[$i]; - } - } - else { - print "Oh dear, input line $. of $in_file had " . - scalar(@data) . " fields\nThis was unexpected.", - " You may want to check your data files.\n"; - } - - print OUT join('|', - map { defined ($data{$_}) ? ($data{$_}) : "" } @out_fields), - "\n"; - } - else { - print OUT; - } + my ($datadir) = @_; + print "Updating old chart storage format...\n"; + foreach my $in_file (glob("$datadir/mining/*")) { + + # Don't try and upgrade image or db files! + next + if (($in_file =~ /\.gif$/i) + || ($in_file =~ /\.png$/i) + || ($in_file =~ /\.db$/i) + || ($in_file =~ /\.orig$/i)); + + rename("$in_file", "$in_file.orig") or next; + open(IN, "<", "$in_file.orig") or next; + open(OUT, '>', $in_file) or next; + + # Fields in the header + my @declared_fields; + + # Fields we changed to half way through by mistake + # This list comes from an old version of collectstats.pl + # This part is only for people who ran later versions of 2.11 (devel) + my @intermediate_fields = qw(DATE UNCONFIRMED NEW ASSIGNED REOPENED + RESOLVED VERIFIED CLOSED); + + # Fields we actually want (matches the current collectstats.pl) + my @out_fields = qw(DATE NEW ASSIGNED REOPENED UNCONFIRMED RESOLVED + VERIFIED CLOSED FIXED INVALID WONTFIX LATER REMIND + DUPLICATE WORKSFORME MOVED); + + while () { + if (/^# fields?: (.*)\s$/) { + @declared_fields = map uc, (split /\||\r/, $1); + print OUT "# fields: ", join('|', @out_fields), "\n"; + } + elsif (/^(\d+\|.*)/) { + my @data = split(/\||\r/, $1); + my %data; + if (@data == @declared_fields) { + + # old format + for my $i (0 .. $#declared_fields) { + $data{$declared_fields[$i]} = $data[$i]; + } } + elsif (@data == @intermediate_fields) { - close(IN); - close(OUT); + # Must have changed over at this point + for my $i (0 .. $#intermediate_fields) { + $data{$intermediate_fields[$i]} = $data[$i]; + } + } + elsif (@data == @out_fields) { + + # This line's fine - it has the right number of entries + for my $i (0 .. $#out_fields) { + $data{$out_fields[$i]} = $data[$i]; + } + } + else { + print "Oh dear, input line $. of $in_file had " + . scalar(@data) + . " fields\nThis was unexpected.", + " You may want to check your data files.\n"; + } + + print OUT join('|', map { defined($data{$_}) ? ($data{$_}) : "" } @out_fields), + "\n"; + } + else { + print OUT; + } } + + close(IN); + close(OUT); + } } sub fix_dir_permissions { - my ($dir) = @_; - return if ON_WINDOWS; - # Note that _get_owner_and_group is always silent here. - my ($owner_id, $group_id) = _get_owner_and_group(); - - my $perms; - my $fs = FILESYSTEM(); - if ($perms = $fs->{recurse_dirs}->{$dir}) { - _fix_perms_recursively($dir, $owner_id, $group_id, $perms); - } - elsif ($perms = $fs->{all_dirs}->{$dir}) { - _fix_perms($dir, $owner_id, $group_id, $perms); - } - else { - # Do nothing. We know nothing about this directory. - warn "Unknown directory $dir"; - } + my ($dir) = @_; + return if ON_WINDOWS; + + # Note that _get_owner_and_group is always silent here. + my ($owner_id, $group_id) = _get_owner_and_group(); + + my $perms; + my $fs = FILESYSTEM(); + if ($perms = $fs->{recurse_dirs}->{$dir}) { + _fix_perms_recursively($dir, $owner_id, $group_id, $perms); + } + elsif ($perms = $fs->{all_dirs}->{$dir}) { + _fix_perms($dir, $owner_id, $group_id, $perms); + } + else { + # Do nothing. We know nothing about this directory. + warn "Unknown directory $dir"; + } } sub fix_file_permissions { - my ($file) = @_; - return if ON_WINDOWS; - my $perms = FILESYSTEM()->{all_files}->{$file}->{perms}; - # Note that _get_owner_and_group is always silent here. - my ($owner_id, $group_id) = _get_owner_and_group(); - _fix_perms($file, $owner_id, $group_id, $perms); + my ($file) = @_; + return if ON_WINDOWS; + my $perms = FILESYSTEM()->{all_files}->{$file}->{perms}; + + # Note that _get_owner_and_group is always silent here. + my ($owner_id, $group_id) = _get_owner_and_group(); + _fix_perms($file, $owner_id, $group_id, $perms); } sub fix_all_file_permissions { - my ($output) = @_; + my ($output) = @_; - # _get_owner_and_group also checks that the webservergroup is valid. - my ($owner_id, $group_id) = _get_owner_and_group($output); + # _get_owner_and_group also checks that the webservergroup is valid. + my ($owner_id, $group_id) = _get_owner_and_group($output); - return if ON_WINDOWS; + return if ON_WINDOWS; - my $fs = FILESYSTEM(); - my %files = %{$fs->{all_files}}; - my %dirs = %{$fs->{all_dirs}}; - my %recurse_dirs = %{$fs->{recurse_dirs}}; + my $fs = FILESYSTEM(); + my %files = %{$fs->{all_files}}; + my %dirs = %{$fs->{all_dirs}}; + my %recurse_dirs = %{$fs->{recurse_dirs}}; - print get_text('install_file_perms_fix') . "\n" if $output; + print get_text('install_file_perms_fix') . "\n" if $output; - foreach my $dir (sort keys %dirs) { - next unless -d $dir; - _fix_perms($dir, $owner_id, $group_id, $dirs{$dir}); - } + foreach my $dir (sort keys %dirs) { + next unless -d $dir; + _fix_perms($dir, $owner_id, $group_id, $dirs{$dir}); + } - foreach my $pattern (sort keys %recurse_dirs) { - my $perms = $recurse_dirs{$pattern}; - # %recurse_dirs supports globs - foreach my $dir (glob $pattern) { - next unless -d $dir; - _fix_perms_recursively($dir, $owner_id, $group_id, $perms); - } + foreach my $pattern (sort keys %recurse_dirs) { + my $perms = $recurse_dirs{$pattern}; + + # %recurse_dirs supports globs + foreach my $dir (glob $pattern) { + next unless -d $dir; + _fix_perms_recursively($dir, $owner_id, $group_id, $perms); } + } - foreach my $file (sort keys %files) { - # %files supports globs - foreach my $filename (glob $file) { - # Don't touch directories. - next if -d $filename || !-e $filename; - _fix_perms($filename, $owner_id, $group_id, - $files{$file}->{perms}); - } + foreach my $file (sort keys %files) { + + # %files supports globs + foreach my $filename (glob $file) { + + # Don't touch directories. + next if -d $filename || !-e $filename; + _fix_perms($filename, $owner_id, $group_id, $files{$file}->{perms}); } + } - _fix_cvs_dirs($owner_id, '.'); + _fix_cvs_dirs($owner_id, '.'); } sub _get_owner_and_group { - my ($output) = @_; - my $group_id = _check_web_server_group($output); - return () if ON_WINDOWS; + my ($output) = @_; + my $group_id = _check_web_server_group($output); + return () if ON_WINDOWS; - my $owner_id = POSIX::getuid(); - $group_id = POSIX::getgid() unless defined $group_id; - return ($owner_id, $group_id); + my $owner_id = POSIX::getuid(); + $group_id = POSIX::getgid() unless defined $group_id; + return ($owner_id, $group_id); } # A helper for fix_all_file_permissions sub _fix_cvs_dirs { - my ($owner_id, $dir) = @_; - my $owner_gid = POSIX::getgid(); - find({ no_chdir => 1, wanted => sub { + my ($owner_id, $dir) = @_; + my $owner_gid = POSIX::getgid(); + find( + { + no_chdir => 1, + wanted => sub { my $name = $File::Find::name; - if ($File::Find::dir =~ /\/CVS/ || $_ eq '.cvsignore' - || (-d $name && $_ =~ /CVS$/)) + if ( $File::Find::dir =~ /\/CVS/ + || $_ eq '.cvsignore' + || (-d $name && $_ =~ /CVS$/)) { - my $perms = 0600; - if (-d $name) { - $perms = 0700; - } - _fix_perms($name, $owner_id, $owner_gid, $perms); + my $perms = 0600; + if (-d $name) { + $perms = 0700; + } + _fix_perms($name, $owner_id, $owner_gid, $perms); } - }}, $dir); + } + }, + $dir + ); } sub _fix_perms { - my ($name, $owner, $group, $perms) = @_; - #printf ("Changing $name to %o\n", $perms); - - # The webserver should never try to chown files. - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - chown $owner, $group, $name - or warn install_string('chown_failed', { path => $name, - error => $! }) . "\n"; - } - chmod $perms, $name - or warn install_string('chmod_failed', { path => $name, - error => $! }) . "\n"; + my ($name, $owner, $group, $perms) = @_; + + #printf ("Changing $name to %o\n", $perms); + + # The webserver should never try to chown files. + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + chown $owner, $group, $name + or warn install_string('chown_failed', {path => $name, error => $!}) . "\n"; + } + chmod $perms, $name + or warn install_string('chmod_failed', {path => $name, error => $!}) . "\n"; } sub _fix_perms_recursively { - my ($dir, $owner_id, $group_id, $perms) = @_; - # Set permissions on the directory itself. - _fix_perms($dir, $owner_id, $group_id, $perms->{dirs}); - # Now recurse through the directory and set the correct permissions - # on subdirectories and files. - find({ no_chdir => 1, wanted => sub { + my ($dir, $owner_id, $group_id, $perms) = @_; + + # Set permissions on the directory itself. + _fix_perms($dir, $owner_id, $group_id, $perms->{dirs}); + + # Now recurse through the directory and set the correct permissions + # on subdirectories and files. + find( + { + no_chdir => 1, + wanted => sub { my $name = $File::Find::name; if (-d $name) { - _fix_perms($name, $owner_id, $group_id, $perms->{dirs}); + _fix_perms($name, $owner_id, $group_id, $perms->{dirs}); } else { - _fix_perms($name, $owner_id, $group_id, $perms->{files}); + _fix_perms($name, $owner_id, $group_id, $perms->{files}); } - }}, $dir); + } + }, + $dir + ); } sub _check_web_server_group { - my ($output) = @_; - - my $group = Bugzilla->localconfig->{'webservergroup'}; - my $filename = bz_locations()->{'localconfig'}; - my $group_id; - - # If we are on Windows, webservergroup does nothing - if (ON_WINDOWS && $group && $output) { - print "\n\n" . get_text('install_webservergroup_windows') . "\n\n"; - } - - # If we're not on Windows, make sure that webservergroup isn't - # empty. - elsif (!ON_WINDOWS && !$group && $output) { - print "\n\n" . get_text('install_webservergroup_empty') . "\n\n"; - } - - # If we're not on Windows, make sure we are actually a member of - # the webservergroup. - elsif (!ON_WINDOWS && $group) { - $group_id = getgrnam($group); - ThrowCodeError('invalid_webservergroup', { group => $group }) - unless defined $group_id; - - # If on unix, see if we need to print a warning about a webservergroup - # that we can't chgrp to - if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $)))) { - print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n"; - } + my ($output) = @_; + + my $group = Bugzilla->localconfig->{'webservergroup'}; + my $filename = bz_locations()->{'localconfig'}; + my $group_id; + + # If we are on Windows, webservergroup does nothing + if (ON_WINDOWS && $group && $output) { + print "\n\n" . get_text('install_webservergroup_windows') . "\n\n"; + } + + # If we're not on Windows, make sure that webservergroup isn't + # empty. + elsif (!ON_WINDOWS && !$group && $output) { + print "\n\n" . get_text('install_webservergroup_empty') . "\n\n"; + } + + # If we're not on Windows, make sure we are actually a member of + # the webservergroup. + elsif (!ON_WINDOWS && $group) { + $group_id = getgrnam($group); + ThrowCodeError('invalid_webservergroup', {group => $group}) + unless defined $group_id; + + # If on unix, see if we need to print a warning about a webservergroup + # that we can't chgrp to + if ($output && $< != 0 && !grep($_ eq $group_id, split(" ", $)))) { + print "\n\n" . get_text('install_webservergroup_not_in') . "\n\n"; } + } - return $group_id; + return $group_id; } diff --git a/Bugzilla/Install/Localconfig.pm b/Bugzilla/Install/Localconfig.pm index 6650eca27..fad7404d8 100644 --- a/Bugzilla/Install/Localconfig.pm +++ b/Bugzilla/Install/Localconfig.pm @@ -36,276 +36,206 @@ use Sys::Hostname qw(hostname); use parent qw(Exporter); our @EXPORT_OK = qw( - read_localconfig - update_localconfig - ENV_KEYS + read_localconfig + update_localconfig + ENV_KEYS ); # might want to change this for upstream -use constant ENV_PREFIX => 'BMO_'; -use constant PARAM_OVERRIDE => qw( use_mailer_queue mail_delivery_method shadowdb shadowdbhost shadowdbport shadowdbsock ); +use constant ENV_PREFIX => 'BMO_'; +use constant PARAM_OVERRIDE => + qw( use_mailer_queue mail_delivery_method shadowdb shadowdbhost shadowdbport shadowdbsock ); sub _sensible_group { - return '' if ON_WINDOWS; - return scalar getgrgid($EGID); + return '' if ON_WINDOWS; + return scalar getgrgid($EGID); } sub _migrate_param { - my ( $name, $fallback_value ) = @_; - - return sub { - if ( Bugzilla->can('params') ) { - return Bugzilla->params->{$name} // $fallback_value; - } - else { - return $fallback_value; - } - }; + my ($name, $fallback_value) = @_; + + return sub { + if (Bugzilla->can('params')) { + return Bugzilla->params->{$name} // $fallback_value; + } + else { + return $fallback_value; + } + }; } use constant LOCALCONFIG_VARS => ( - { - name => 'create_htaccess', - default => 1, - }, - { - name => 'webservergroup', - default => \&_sensible_group, - }, - { - name => 'use_suexec', - default => 0, - }, - { - name => 'db_driver', - default => 'mysql', - }, - { - name => 'db_host', - default => 'localhost', - }, - { - name => 'db_name', - default => 'bugs', - }, - { - - name => 'db_user', - default => 'bugs', - }, - { - name => 'db_pass', - default => '', - }, - { - name => 'db_port', - default => 0, - }, - { - name => 'db_sock', - default => '', - }, - { - name => 'db_check', - default => 1, - }, - { - name => 'index_html', - default => 0, - }, - { - name => 'cvsbin', - default => sub { bin_loc('cvs') }, - }, - { - name => 'interdiffbin', - default => sub { bin_loc('interdiff') }, - }, - { - name => 'diffpath', - default => sub { dirname( bin_loc('diff') ) }, - }, - { - name => 'tct_bin', - default => sub { bin_loc('tct') }, - }, - { - name => 'site_wide_secret', - - # 64 characters is roughly the equivalent of a 384-bit key, which - # is larger than anybody would ever be able to brute-force. - default => sub { generate_random_password(64) }, - }, - { - name => 'param_override', - default => { - use_mailer_queue => undef, - mail_delivery_method => undef, - shadowdb => undef, - shadowdbhost => undef, - shadowdbport => undef, - shadowdbsock => undef, - }, - }, - { - name => 'apache_size_limit', - default => 600000, - }, - { - name => 'memcached_servers', - default => _migrate_param( "memcached_servers", "" ), - }, - { - name => 'memcached_namespace', - default => _migrate_param( "memcached_namespace", "bugzilla:" ), - }, - { - name => 'urlbase', - default => _migrate_param( "urlbase", "" ), - }, - { - name => 'canonical_urlbase', - default => '', - }, - { - name => 'attachment_base', - default => _migrate_param( "attachment_base", '' ), - }, - { - name => 'ses_username', - default => '', - }, - { - name => 'ses_password', - default => '', - }, - { - name => 'inbound_proxies', - default => _migrate_param( 'inbound_proxies', '' ), - }, - { - name => 'shadowdb_user', - default => '', - }, - { - name => 'shadowdb_pass', - default => '', - }, - { - name => 'datadog_host', - default => '', - }, - { - name => 'datadog_port', - default => 8125, + {name => 'create_htaccess', default => 1,}, + {name => 'webservergroup', default => \&_sensible_group,}, + {name => 'use_suexec', default => 0,}, + {name => 'db_driver', default => 'mysql',}, + {name => 'db_host', default => 'localhost',}, + {name => 'db_name', default => 'bugs',}, + { + + name => 'db_user', + default => 'bugs', + }, + {name => 'db_pass', default => '',}, + {name => 'db_port', default => 0,}, + {name => 'db_sock', default => '',}, + {name => 'db_check', default => 1,}, + {name => 'index_html', default => 0,}, + {name => 'cvsbin', default => sub { bin_loc('cvs') },}, + {name => 'interdiffbin', default => sub { bin_loc('interdiff') },}, + {name => 'diffpath', default => sub { dirname(bin_loc('diff')) },}, + {name => 'tct_bin', default => sub { bin_loc('tct') },}, + { + name => 'site_wide_secret', + + # 64 characters is roughly the equivalent of a 384-bit key, which + # is larger than anybody would ever be able to brute-force. + default => sub { generate_random_password(64) }, + }, + { + name => 'param_override', + default => { + use_mailer_queue => undef, + mail_delivery_method => undef, + shadowdb => undef, + shadowdbhost => undef, + shadowdbport => undef, + shadowdbsock => undef, }, + }, + {name => 'apache_size_limit', default => 600000,}, + { + name => 'memcached_servers', + default => _migrate_param("memcached_servers", ""), + }, + { + name => 'memcached_namespace', + default => _migrate_param("memcached_namespace", "bugzilla:"), + }, + {name => 'urlbase', default => _migrate_param("urlbase", ""),}, + {name => 'canonical_urlbase', default => '',}, + {name => 'attachment_base', default => _migrate_param("attachment_base", ''),}, + {name => 'ses_username', default => '',}, + {name => 'ses_password', default => '',}, + {name => 'inbound_proxies', default => _migrate_param('inbound_proxies', ''),}, + {name => 'shadowdb_user', default => '',}, + {name => 'shadowdb_pass', default => '',}, + {name => 'datadog_host', default => '',}, + {name => 'datadog_port', default => 8125,}, ); use constant ENV_KEYS => ( - (map { ENV_PREFIX . $_->{name} } LOCALCONFIG_VARS), - (map { ENV_PREFIX . $_ } PARAM_OVERRIDE), + (map { ENV_PREFIX . $_->{name} } LOCALCONFIG_VARS), + (map { ENV_PREFIX . $_ } PARAM_OVERRIDE), ); sub _read_localconfig_from_env { - my %localconfig; - - foreach my $var ( LOCALCONFIG_VARS ) { - my $name = $var->{name}; - my $key = ENV_PREFIX . $name; - if ($name eq 'param_override') { - foreach my $override (PARAM_OVERRIDE) { - my $o_key = ENV_PREFIX . $override; - $localconfig{param_override}{$override} = $ENV{$o_key}; - untaint($localconfig{param_override}{$override}); - } - } - elsif (exists $ENV{$key}) { - $localconfig{$name} = $ENV{$key}; - untaint($localconfig{$name}); - } - else { - my $default = $var->{default}; - $localconfig{$name} = ref($default) eq 'CODE' ? $default->() : $default; - untaint($localconfig{$name}); - } + my %localconfig; + + foreach my $var (LOCALCONFIG_VARS) { + my $name = $var->{name}; + my $key = ENV_PREFIX . $name; + if ($name eq 'param_override') { + foreach my $override (PARAM_OVERRIDE) { + my $o_key = ENV_PREFIX . $override; + $localconfig{param_override}{$override} = $ENV{$o_key}; + untaint($localconfig{param_override}{$override}); + } + } + elsif (exists $ENV{$key}) { + $localconfig{$name} = $ENV{$key}; + untaint($localconfig{$name}); } + else { + my $default = $var->{default}; + $localconfig{$name} = ref($default) eq 'CODE' ? $default->() : $default; + untaint($localconfig{$name}); + } + } - return \%localconfig; + return \%localconfig; } sub _read_localconfig_from_file { - my ($include_deprecated) = @_; - my $filename = bz_locations()->{'localconfig'}; - - my %localconfig; - if (-e $filename) { - my $s = new Safe; - # Some people like to store their database password in another file. - $s->permit('dofile'); - - $s->rdo($filename); - if ($@ || $!) { - my $err_msg = $@ ? $@ : $!; - die install_string('error_localconfig_read', - { error => $err_msg, localconfig => $filename }), "\n"; - } - - my @read_symbols; - if ($include_deprecated) { - # First we have to get the whole symbol table - my $safe_root = $s->root; - my %safe_package; - { no strict 'refs'; %safe_package = %{$safe_root . "::"}; } - # And now we read the contents of every var in the symbol table. - # However: - # * We only include symbols that start with an alphanumeric - # character. This excludes symbols like "_<./localconfig" - # that show up in some perls. - # * We ignore the INC symbol, which exists in every package. - # * Perl 5.10 imports a lot of random symbols that all - # contain "::", and we want to ignore those. - @read_symbols = grep { /^[A-Za-z0-1]/ and !/^INC$/ and !/::/ } - (keys %safe_package); - } - else { - @read_symbols = map($_->{name}, LOCALCONFIG_VARS); - } - foreach my $var (@read_symbols) { - my $glob = $s->varglob($var); - # We can't get the type of a variable out of a Safe automatically. - # We can only get the glob itself. So we figure out its type this - # way, by trying first a scalar, then an array, then a hash. - # - # The interesting thing is that this converts all deprecated - # array or hash vars into hashrefs or arrayrefs, but that's - # fine since as I write this all modern localconfig vars are - # actually scalars. - if (defined $$glob) { - $localconfig{$var} = $$glob; - } - elsif (@$glob) { - $localconfig{$var} = \@$glob; - } - elsif (%$glob) { - $localconfig{$var} = \%$glob; - } - } + my ($include_deprecated) = @_; + my $filename = bz_locations()->{'localconfig'}; + + my %localconfig; + if (-e $filename) { + my $s = new Safe; + + # Some people like to store their database password in another file. + $s->permit('dofile'); + + $s->rdo($filename); + if ($@ || $!) { + my $err_msg = $@ ? $@ : $!; + die install_string( + 'error_localconfig_read', {error => $err_msg, localconfig => $filename} + ), + "\n"; + } + + my @read_symbols; + if ($include_deprecated) { + + # First we have to get the whole symbol table + my $safe_root = $s->root; + my %safe_package; + { no strict 'refs'; %safe_package = %{$safe_root . "::"}; } + + # And now we read the contents of every var in the symbol table. + # However: + # * We only include symbols that start with an alphanumeric + # character. This excludes symbols like "_<./localconfig" + # that show up in some perls. + # * We ignore the INC symbol, which exists in every package. + # * Perl 5.10 imports a lot of random symbols that all + # contain "::", and we want to ignore those. + @read_symbols + = grep { /^[A-Za-z0-1]/ and !/^INC$/ and !/::/ } (keys %safe_package); } + else { + @read_symbols = map($_->{name}, LOCALCONFIG_VARS); + } + foreach my $var (@read_symbols) { + my $glob = $s->varglob($var); + + # We can't get the type of a variable out of a Safe automatically. + # We can only get the glob itself. So we figure out its type this + # way, by trying first a scalar, then an array, then a hash. + # + # The interesting thing is that this converts all deprecated + # array or hash vars into hashrefs or arrayrefs, but that's + # fine since as I write this all modern localconfig vars are + # actually scalars. + if (defined $$glob) { + $localconfig{$var} = $$glob; + } + elsif (@$glob) { + $localconfig{$var} = \@$glob; + } + elsif (%$glob) { + $localconfig{$var} = \%$glob; + } + } + } - return \%localconfig; + return \%localconfig; } sub read_localconfig { - my ($include_deprecated) = @_; - my $config = $ENV{LOCALCONFIG_ENV} - ? _read_localconfig_from_env() - : _read_localconfig_from_file($include_deprecated); + my ($include_deprecated) = @_; + my $config + = $ENV{LOCALCONFIG_ENV} + ? _read_localconfig_from_env() + : _read_localconfig_from_file($include_deprecated); - # Use the site's URL as the default Canonical URL - $config->{canonical_urlbase} //= $config->{urlbase}; + # Use the site's URL as the default Canonical URL + $config->{canonical_urlbase} //= $config->{urlbase}; - return $config; + return $config; } # @@ -333,96 +263,99 @@ sub read_localconfig { # Cute, ey? # sub update_localconfig { - my ($params) = @_; - - if ($ENV{LOCALCONFIG_ENV}) { - require Carp; - Carp::croak("update_localconfig() called with LOCALCONFIG_ENV enabled"); + my ($params) = @_; + + if ($ENV{LOCALCONFIG_ENV}) { + require Carp; + Carp::croak("update_localconfig() called with LOCALCONFIG_ENV enabled"); + } + + my $output = $params->{output} || 0; + my $answer = Bugzilla->installation_answers; + my $localconfig = read_localconfig('include deprecated'); + + my @new_vars; + foreach my $var (LOCALCONFIG_VARS) { + my $name = $var->{name}; + my $value = $localconfig->{$name}; + + # Regenerate site_wide_secret if it was made by our old, weak + # generate_random_password. Previously we used to generate + # a 256-character string for site_wide_secret. + $value = undef + if ($name eq 'site_wide_secret' and defined $value and length($value) == 256); + + if (!defined $value) { + push(@new_vars, $name); + $var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE'; + if (exists $answer->{$name}) { + $localconfig->{$name} = $answer->{$name}; + } + else { + $localconfig->{$name} = $var->{default}; + } } + } - my $output = $params->{output} || 0; - my $answer = Bugzilla->installation_answers; - my $localconfig = read_localconfig('include deprecated'); - - my @new_vars; - foreach my $var (LOCALCONFIG_VARS) { - my $name = $var->{name}; - my $value = $localconfig->{$name}; - # Regenerate site_wide_secret if it was made by our old, weak - # generate_random_password. Previously we used to generate - # a 256-character string for site_wide_secret. - $value = undef if ($name eq 'site_wide_secret' and defined $value - and length($value) == 256); - - if (!defined $value) { - push(@new_vars, $name); - $var->{default} = &{$var->{default}} if ref($var->{default}) eq 'CODE'; - if (exists $answer->{$name}) { - $localconfig->{$name} = $answer->{$name}; - } - else { - $localconfig->{$name} = $var->{default}; - } - } - } + if (!$localconfig->{'interdiffbin'} && $output) { + print "\n", install_string('patchutils_missing'), "\n"; + } - if (!$localconfig->{'interdiffbin'} && $output) { - print "\n", install_string('patchutils_missing'), "\n"; - } + my @old_vars; + foreach my $var (keys %$localconfig) { + push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS); + } - my @old_vars; - foreach my $var (keys %$localconfig) { - push(@old_vars, $var) if !grep($_->{name} eq $var, LOCALCONFIG_VARS); - } + my $filename = bz_locations->{'localconfig'}; - my $filename = bz_locations->{'localconfig'}; - - # Ensure output is sorted and deterministic - local $Data::Dumper::Sortkeys = 1; - - # Move any custom or old variables into a separate file. - if (scalar @old_vars) { - my $filename_old = "$filename.old"; - open(my $old_file, ">>:utf8", $filename_old) - or die "$filename_old: $!"; - local $Data::Dumper::Purity = 1; - foreach my $var (@old_vars) { - print $old_file Data::Dumper->Dump([$localconfig->{$var}], - ["*$var"]) . "\n\n"; - } - close $old_file; - my $oldstuff = join(', ', @old_vars); - print install_string('lc_old_vars', - { localconfig => $filename, old_file => $filename_old, - vars => $oldstuff }), "\n"; - } + # Ensure output is sorted and deterministic + local $Data::Dumper::Sortkeys = 1; - # Re-write localconfig - open(my $fh, ">:utf8", $filename) or die "$filename: $!"; - foreach my $var (LOCALCONFIG_VARS) { - my $name = $var->{name}; - my $desc = install_string("localconfig_$name", { root => ROOT_USER }); - chomp($desc); - # Make the description into a comment. - $desc =~ s/^/# /mg; - print $fh $desc, "\n", - Data::Dumper->Dump([$localconfig->{$name}], - ["*$name"]), "\n"; - } - - if (@new_vars) { - my $newstuff = join(', ', @new_vars); - print "\n"; - print colored(install_string('lc_new_vars', { localconfig => $filename, - new_vars => wrap_hard($newstuff, 70) }), - COLOR_ERROR), "\n"; - exit unless $params->{use_defaults}; + # Move any custom or old variables into a separate file. + if (scalar @old_vars) { + my $filename_old = "$filename.old"; + open(my $old_file, ">>:utf8", $filename_old) or die "$filename_old: $!"; + local $Data::Dumper::Purity = 1; + foreach my $var (@old_vars) { + print $old_file Data::Dumper->Dump([$localconfig->{$var}], ["*$var"]) . "\n\n"; } - - # Reset the cache for Bugzilla->localconfig so that it will be re-read - delete Bugzilla->process_cache->{localconfig}; - - return { old_vars => \@old_vars, new_vars => \@new_vars }; + close $old_file; + my $oldstuff = join(', ', @old_vars); + print install_string('lc_old_vars', + {localconfig => $filename, old_file => $filename_old, vars => $oldstuff}), + "\n"; + } + + # Re-write localconfig + open(my $fh, ">:utf8", $filename) or die "$filename: $!"; + foreach my $var (LOCALCONFIG_VARS) { + my $name = $var->{name}; + my $desc = install_string("localconfig_$name", {root => ROOT_USER}); + chomp($desc); + + # Make the description into a comment. + $desc =~ s/^/# /mg; + print $fh $desc, "\n", Data::Dumper->Dump([$localconfig->{$name}], ["*$name"]), + "\n"; + } + + if (@new_vars) { + my $newstuff = join(', ', @new_vars); + print "\n"; + print colored( + install_string( + 'lc_new_vars', {localconfig => $filename, new_vars => wrap_hard($newstuff, 70)} + ), + COLOR_ERROR + ), + "\n"; + exit unless $params->{use_defaults}; + } + + # Reset the cache for Bugzilla->localconfig so that it will be re-read + delete Bugzilla->process_cache->{localconfig}; + + return {old_vars => \@old_vars, new_vars => \@new_vars}; } 1; diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm index beed721f3..4b11d83a1 100644 --- a/Bugzilla/Install/Requirements.pm +++ b/Bugzilla/Install/Requirements.pm @@ -30,14 +30,14 @@ use parent qw(Exporter); use autodie; our @EXPORT = qw( - FEATURE_FILES - - check_cpan_requirements - check_cpan_feature - check_all_cpan_features - check_webdotbase - check_font_file - map_files_to_features + FEATURE_FILES + + check_cpan_requirements + check_cpan_feature + check_all_cpan_features + check_webdotbase + check_font_file + map_files_to_features ); our $checking_for_indent = 0; @@ -52,11 +52,11 @@ use constant TABLE_WIDTH => 71; # The keys are the names of the modules, the values are what the module # is called in the output of "apachectl -t -D DUMP_MODULES". use constant APACHE_MODULES => { - mod_headers => 'headers_module', - mod_env => 'env_module', - mod_expires => 'expires_module', - mod_rewrite => 'rewrite_module', - mod_version => 'version_module' + mod_headers => 'headers_module', + mod_env => 'env_module', + mod_expires => 'expires_module', + mod_rewrite => 'rewrite_module', + mod_version => 'version_module' }; # These are all of the binaries that we could possibly use that can @@ -69,235 +69,255 @@ use constant APACHE => qw(apachectl httpd apache2 apache); # If we don't find any of the above binaries in the normal PATH, # these are extra places we look. -use constant APACHE_PATH => [qw( +use constant APACHE_PATH => [ + qw( /usr/sbin /usr/local/sbin /usr/libexec /usr/local/libexec -)]; + ) +]; # This maps features to the files that require that feature in order # to compile. It is used by t/001compile.t and mod_perl.pl. use constant FEATURE_FILES => ( - jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'], - xmlrpc => ['Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi', - 'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm'], - rest => ['Bugzilla/API/Server.pm', 'rest.cgi', 'Bugzilla/API/*/*.pm', - 'Bugzilla/API/*/Server.pm', 'Bugzilla/API/*/Resource/*.pm'], - csp => ['Bugzilla/CGI/ContentSecurityPolicy.pm'], - psgi => ['app.psgi'], - moving => ['importxml.pl'], - auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'], - auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'], - documentation => ['docs/makedocs.pl'], - inbound_email => ['email_in.pl'], - jobqueue => ['Bugzilla/Job/*', 'Bugzilla/JobQueue.pm', - 'Bugzilla/JobQueue/*', 'jobqueue.pl'], - patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'], - updates => ['Bugzilla/Update.pm'], - mfa => ['Bugzilla/MFA/*.pm'], - memcached => ['Bugzilla/Memcache.pm'], - s3 => ['Bugzilla/S3.pm', 'Bugzilla/S3/Bucket.pm', 'Bugzilla/Attachment/S3.pm'] + jsonrpc => ['Bugzilla/WebService/Server/JSONRPC.pm', 'jsonrpc.cgi'], + xmlrpc => [ + 'Bugzilla/WebService/Server/XMLRPC.pm', 'xmlrpc.cgi', + 'Bugzilla/WebService.pm', 'Bugzilla/WebService/*.pm' + ], + rest => [ + 'Bugzilla/API/Server.pm', 'rest.cgi', + 'Bugzilla/API/*/*.pm', 'Bugzilla/API/*/Server.pm', + 'Bugzilla/API/*/Resource/*.pm' + ], + csp => ['Bugzilla/CGI/ContentSecurityPolicy.pm'], + psgi => ['app.psgi'], + moving => ['importxml.pl'], + auth_ldap => ['Bugzilla/Auth/Verify/LDAP.pm'], + auth_radius => ['Bugzilla/Auth/Verify/RADIUS.pm'], + documentation => ['docs/makedocs.pl'], + inbound_email => ['email_in.pl'], + jobqueue => [ + 'Bugzilla/Job/*', 'Bugzilla/JobQueue.pm', + 'Bugzilla/JobQueue/*', 'jobqueue.pl' + ], + patch_viewer => ['Bugzilla/Attachment/PatchReader.pm'], + updates => ['Bugzilla/Update.pm'], + mfa => ['Bugzilla/MFA/*.pm'], + memcached => ['Bugzilla/Memcache.pm'], + s3 => ['Bugzilla/S3.pm', 'Bugzilla/S3/Bucket.pm', 'Bugzilla/Attachment/S3.pm'] ); sub check_all_cpan_features { - my ($meta, $dirs, $output) = @_; - my %report; - - local $checking_for_indent = 2; - - print "\nOptional features:\n" if $output; - my @features = sort { $a->identifier cmp $b->identifier } $meta->features; - foreach my $feature (@features) { - next if $feature->identifier eq 'features'; - printf "Feature '%s': %s\n", $feature->identifier, $feature->description if $output; - my $result = check_cpan_feature($feature, $dirs, $output); - print "\n" if $output; - - $report{$feature->identifier} = { - description => $feature->description, - result => $result, - }; - } + my ($meta, $dirs, $output) = @_; + my %report; + + local $checking_for_indent = 2; + + print "\nOptional features:\n" if $output; + my @features = sort { $a->identifier cmp $b->identifier } $meta->features; + foreach my $feature (@features) { + next if $feature->identifier eq 'features'; + printf "Feature '%s': %s\n", $feature->identifier, $feature->description + if $output; + my $result = check_cpan_feature($feature, $dirs, $output); + print "\n" if $output; - return \%report; + $report{$feature->identifier} + = {description => $feature->description, result => $result,}; + } + + return \%report; } sub check_cpan_feature { - my ($feature, $dirs, $output) = @_; + my ($feature, $dirs, $output) = @_; - return _check_prereqs($feature->prereqs, $dirs, $output); + return _check_prereqs($feature->prereqs, $dirs, $output); } sub check_cpan_requirements { - my ($meta, $dirs, $output) = @_; + my ($meta, $dirs, $output) = @_; - my $result = _check_prereqs($meta->effective_prereqs, $dirs, $output); - print colored(install_string('installation_failed'), COLOR_ERROR), "\n" if !$result->{ok} && $output; - return $result; + my $result = _check_prereqs($meta->effective_prereqs, $dirs, $output); + print colored(install_string('installation_failed'), COLOR_ERROR), "\n" + if !$result->{ok} && $output; + return $result; } sub _check_prereqs { - my ($prereqs, $dirs, $output) = @_; - $dirs //= \@INC; - my $reqs = Bugzilla::CPAN->cpan_requirements($prereqs); - my @found; - my @missing; - - foreach my $module (sort $reqs->required_modules) { - my $ok = _check_module($reqs, $module, $dirs, $output); - if ($ok) { - push @found, $module; - } - else { - push @missing, $module; - } + my ($prereqs, $dirs, $output) = @_; + $dirs //= \@INC; + my $reqs = Bugzilla::CPAN->cpan_requirements($prereqs); + my @found; + my @missing; + + foreach my $module (sort $reqs->required_modules) { + my $ok = _check_module($reqs, $module, $dirs, $output); + if ($ok) { + push @found, $module; + } + else { + push @missing, $module; } + } - return { ok => (@missing == 0), found => \@found, missing => \@missing }; + return {ok => (@missing == 0), found => \@found, missing => \@missing}; } sub _check_module { - my ($reqs, $module, $dirs, $output) = @_; - my $required_version = $reqs->requirements_for_module($module); - - if ($module eq 'perl') { - my $ok = $reqs->accepts_module($module, $]); - _checking_for({package => "perl", found => $], wanted => $required_version, ok => $ok}) if $output; - return $ok; - } else { - my $metadata = Module::Metadata->new_from_module($module, inc => $dirs); - my $version = eval { $metadata->version }; - my $ok = $metadata && $version && $reqs->accepts_module($module, $version || 0); - _checking_for({package => $module, $version ? ( found => $version ) : (), wanted => $required_version, ok => $ok}) if $output; - - return $ok; - } + my ($reqs, $module, $dirs, $output) = @_; + my $required_version = $reqs->requirements_for_module($module); + + if ($module eq 'perl') { + my $ok = $reqs->accepts_module($module, $]); + _checking_for( + {package => "perl", found => $], wanted => $required_version, ok => $ok}) + if $output; + return $ok; + } + else { + my $metadata = Module::Metadata->new_from_module($module, inc => $dirs); + my $version = eval { $metadata->version }; + my $ok = $metadata && $version && $reqs->accepts_module($module, $version || 0); + _checking_for({ + package => $module, + $version ? (found => $version) : (), + wanted => $required_version, + ok => $ok + }) + if $output; + + return $ok; + } } sub _get_apachectl { - foreach my $bin_name (APACHE) { - my $bin = bin_loc($bin_name); - return $bin if $bin; - } - # Try again with a possibly different path. - foreach my $bin_name (APACHE) { - my $bin = bin_loc($bin_name, APACHE_PATH); - return $bin if $bin; - } - return undef; + foreach my $bin_name (APACHE) { + my $bin = bin_loc($bin_name); + return $bin if $bin; + } + + # Try again with a possibly different path. + foreach my $bin_name (APACHE) { + my $bin = bin_loc($bin_name, APACHE_PATH); + return $bin if $bin; + } + return undef; } sub check_webdotbase { - my ($output) = @_; + my ($output) = @_; - my $webdotbase = Bugzilla->localconfig->{'webdotbase'}; - return 1 if $webdotbase =~ /^https?:/; + my $webdotbase = Bugzilla->localconfig->{'webdotbase'}; + return 1 if $webdotbase =~ /^https?:/; - my $return; - $return = 1 if -x $webdotbase; + my $return; + $return = 1 if -x $webdotbase; - if ($output) { - _checking_for({ package => 'GraphViz', ok => $return }); - } + if ($output) { + _checking_for({package => 'GraphViz', ok => $return}); + } - if (!$return) { - print install_string('bad_executable', { bin => $webdotbase }), "\n"; - } + if (!$return) { + print install_string('bad_executable', {bin => $webdotbase}), "\n"; + } + + my $webdotdir = bz_locations()->{'webdotdir'}; - my $webdotdir = bz_locations()->{'webdotdir'}; - # Check .htaccess allows access to generated images - if (-e "$webdotdir/.htaccess") { - my $htaccess = new IO::File("$webdotdir/.htaccess", 'r') - || die "$webdotdir/.htaccess: " . $!; - if (!grep(/ \\\.png\$/, $htaccess->getlines)) { - print STDERR install_string('webdot_bad_htaccess', - { dir => $webdotdir }), "\n"; - } - $htaccess->close; + # Check .htaccess allows access to generated images + if (-e "$webdotdir/.htaccess") { + my $htaccess = new IO::File("$webdotdir/.htaccess", 'r') + || die "$webdotdir/.htaccess: " . $!; + if (!grep(/ \\\.png\$/, $htaccess->getlines)) { + print STDERR install_string('webdot_bad_htaccess', {dir => $webdotdir}), "\n"; } + $htaccess->close; + } - return $return; + return $return; } sub check_font_file { - my ($output) = @_; + my ($output) = @_; - my $font_file = Bugzilla->localconfig->{'font_file'}; + my $font_file = Bugzilla->localconfig->{'font_file'}; - my $readable; - $readable = 1 if -r $font_file; + my $readable; + $readable = 1 if -r $font_file; - my $ttf; - $ttf = 1 if $font_file =~ /\.(ttf|otf)$/; + my $ttf; + $ttf = 1 if $font_file =~ /\.(ttf|otf)$/; - if ($output) { - _checking_for({ package => 'Font file', ok => $readable && $ttf}); - } + if ($output) { + _checking_for({package => 'Font file', ok => $readable && $ttf}); + } - if (!$readable) { - print install_string('bad_font_file', { file => $font_file }), "\n"; - } - elsif (!$ttf) { - print install_string('bad_font_file_name', { file => $font_file }), "\n"; - } + if (!$readable) { + print install_string('bad_font_file', {file => $font_file}), "\n"; + } + elsif (!$ttf) { + print install_string('bad_font_file_name', {file => $font_file}), "\n"; + } - return $readable && $ttf; + return $readable && $ttf; } sub _checking_for { - my ($params) = @_; - my ($package, $ok, $wanted, $blacklisted, $found) = - @$params{qw(package ok wanted blacklisted found)}; - - my $ok_string = $ok ? install_string('module_ok') : ''; - - # If we're actually checking versions (like for Perl modules), then - # we have some rather complex logic to determine what we want to - # show. If we're not checking versions (like for GraphViz) we just - # show "ok" or "not found". - if (exists $params->{found}) { - my $found_string; - # We do a string compare in case it's non-numeric. We make sure - # it's not a version object as negative versions are forbidden. - if ($found && !ref($found) && $found eq '-1') { - $found_string = install_string('module_not_found'); - } - elsif ($found) { - $found_string = install_string('module_found', { ver => $found }); - } - else { - $found_string = install_string('module_unknown_version'); - } - $ok_string = $ok ? "$ok_string: $found_string" : $found_string; + my ($params) = @_; + my ($package, $ok, $wanted, $blacklisted, $found) + = @$params{qw(package ok wanted blacklisted found)}; + + my $ok_string = $ok ? install_string('module_ok') : ''; + + # If we're actually checking versions (like for Perl modules), then + # we have some rather complex logic to determine what we want to + # show. If we're not checking versions (like for GraphViz) we just + # show "ok" or "not found". + if (exists $params->{found}) { + my $found_string; + + # We do a string compare in case it's non-numeric. We make sure + # it's not a version object as negative versions are forbidden. + if ($found && !ref($found) && $found eq '-1') { + $found_string = install_string('module_not_found'); } - elsif (!$ok) { - $ok_string = install_string('module_not_found'); + elsif ($found) { + $found_string = install_string('module_found', {ver => $found}); } - - my $black_string = $blacklisted ? install_string('blacklisted') : ''; - my $want_string = $wanted ? "$wanted" : install_string('any'); - - my $str = sprintf "%s %20s %-11s $ok_string $black_string\n", - ( ' ' x $checking_for_indent ) . install_string('checking_for'), - $package, "($want_string)"; - print $ok ? $str : colored($str, COLOR_ERROR); + else { + $found_string = install_string('module_unknown_version'); + } + $ok_string = $ok ? "$ok_string: $found_string" : $found_string; + } + elsif (!$ok) { + $ok_string = install_string('module_not_found'); + } + + my $black_string = $blacklisted ? install_string('blacklisted') : ''; + my $want_string = $wanted ? "$wanted" : install_string('any'); + + my $str = sprintf "%s %20s %-11s $ok_string $black_string\n", + (' ' x $checking_for_indent) . install_string('checking_for'), $package, + "($want_string)"; + print $ok ? $str : colored($str, COLOR_ERROR); } # This does a reverse mapping for FEATURE_FILES. sub map_files_to_features { - my %features = FEATURE_FILES; - my %files; - foreach my $feature (keys %features) { - my @my_files = @{ $features{$feature} }; - foreach my $pattern (@my_files) { - foreach my $file (glob $pattern) { - $files{$file} = $feature; - } - } + my %features = FEATURE_FILES; + my %files; + foreach my $feature (keys %features) { + my @my_files = @{$features{$feature}}; + foreach my $pattern (@my_files) { + foreach my $file (glob $pattern) { + $files{$file} = $feature; + } } - return \%files; + } + return \%files; } 1; diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm index b6d28d9c7..a5522e134 100644 --- a/Bugzilla/Install/Util.pm +++ b/Bugzilla/Install/Util.pm @@ -27,367 +27,391 @@ use PerlIO; use base qw(Exporter); our @EXPORT_OK = qw( - bin_loc - get_version_and_os - extension_code_files - i_am_persistent - indicate_progress - install_string - include_languages - success - template_include_path - init_console + bin_loc + get_version_and_os + extension_code_files + i_am_persistent + indicate_progress + install_string + include_languages + success + template_include_path + init_console ); sub bin_loc { - my ($bin, $path) = @_; - # This module is not needed most of the time and is a bit slow, - # so we only load it when calling bin_loc(). - require ExtUtils::MM; - - # If the binary is a full path... - if ($bin =~ m{[/\\]}) { - return MM->maybe_command($bin) || ''; - } - - # Otherwise we look for it in the path in a cross-platform way. - my @path = $path ? @$path : File::Spec->path; - foreach my $dir (@path) { - next if !-d $dir; - my $full_path = File::Spec->catfile($dir, $bin); - # MM is an alias for ExtUtils::MM. maybe_command is nice - # because it checks .com, .bat, .exe (etc.) on Windows. - my $command = MM->maybe_command($full_path); - return $command if $command; - } - - return ''; + my ($bin, $path) = @_; + + # This module is not needed most of the time and is a bit slow, + # so we only load it when calling bin_loc(). + require ExtUtils::MM; + + # If the binary is a full path... + if ($bin =~ m{[/\\]}) { + return MM->maybe_command($bin) || ''; + } + + # Otherwise we look for it in the path in a cross-platform way. + my @path = $path ? @$path : File::Spec->path; + foreach my $dir (@path) { + next if !-d $dir; + my $full_path = File::Spec->catfile($dir, $bin); + + # MM is an alias for ExtUtils::MM. maybe_command is nice + # because it checks .com, .bat, .exe (etc.) on Windows. + my $command = MM->maybe_command($full_path); + return $command if $command; + } + + return ''; } sub get_version_and_os { - # Display version information - my @os_details = POSIX::uname; - # 0 is the name of the OS, 2 is the major version, - my $os_name = $os_details[0] . ' ' . $os_details[2]; - if (ON_WINDOWS) { - require Win32; - $os_name = Win32::GetOSName(); - } - # $os_details[3] is the minor version. - return { bz_ver => BUGZILLA_VERSION, - perl_ver => sprintf('%vd', $^V), - os_name => $os_name, - os_ver => $os_details[3] }; + + # Display version information + my @os_details = POSIX::uname; + + # 0 is the name of the OS, 2 is the major version, + my $os_name = $os_details[0] . ' ' . $os_details[2]; + if (ON_WINDOWS) { + require Win32; + $os_name = Win32::GetOSName(); + } + + # $os_details[3] is the minor version. + return { + bz_ver => BUGZILLA_VERSION, + perl_ver => sprintf('%vd', $^V), + os_name => $os_name, + os_ver => $os_details[3] + }; } sub _extension_paths { - my $dir = bz_locations()->{'extensionsdir'}; - my @extension_items = glob("$dir/*"); - my @paths; - foreach my $item (@extension_items) { - my $basename = basename($item); - # Skip CVS directories and any hidden files/dirs. - next if ($basename eq 'CVS' or $basename =~ /^\./); - if (-d $item) { - if (!-e "$item/disabled") { - push(@paths, $item); - } - } - elsif ($item =~ /\.pm$/i) { - push(@paths, $item); - } - } - return @paths; + my $dir = bz_locations()->{'extensionsdir'}; + my @extension_items = glob("$dir/*"); + my @paths; + foreach my $item (@extension_items) { + my $basename = basename($item); + + # Skip CVS directories and any hidden files/dirs. + next if ($basename eq 'CVS' or $basename =~ /^\./); + if (-d $item) { + if (!-e "$item/disabled") { + push(@paths, $item); + } + } + elsif ($item =~ /\.pm$/i) { + push(@paths, $item); + } + } + return @paths; } sub extension_code_files { - my ($requirements_only) = @_; - my @files; - foreach my $path (_extension_paths()) { - my @load_files; - if (-d $path) { - my $extension_file = "$path/Extension.pm"; - my $config_file = "$path/Config.pm"; - if (-e $extension_file) { - push(@load_files, $extension_file); - } - if (-e $config_file) { - push(@load_files, $config_file); - } - - # Don't load Extension.pm if we just want Config.pm and - # we found both. - if ($requirements_only and scalar(@load_files) == 2) { - shift(@load_files); - } - } - else { - push(@load_files, $path); - } - next if !scalar(@load_files); - # We know that these paths are safe, because they came from - # extensionsdir and we checked them specifically for their format. - # Also, the only thing we ever do with them is pass them to "require". - trick_taint($_) foreach @load_files; - push(@files, \@load_files); + my ($requirements_only) = @_; + my @files; + foreach my $path (_extension_paths()) { + my @load_files; + if (-d $path) { + my $extension_file = "$path/Extension.pm"; + my $config_file = "$path/Config.pm"; + if (-e $extension_file) { + push(@load_files, $extension_file); + } + if (-e $config_file) { + push(@load_files, $config_file); + } + + # Don't load Extension.pm if we just want Config.pm and + # we found both. + if ($requirements_only and scalar(@load_files) == 2) { + shift(@load_files); + } } + else { + push(@load_files, $path); + } + next if !scalar(@load_files); + + # We know that these paths are safe, because they came from + # extensionsdir and we checked them specifically for their format. + # Also, the only thing we ever do with them is pass them to "require". + trick_taint($_) foreach @load_files; + push(@files, \@load_files); + } - return (\@files); + return (\@files); } sub indicate_progress { - my ($params) = @_; - my $current = $params->{current}; - my $total = $params->{total}; - my $every = $params->{every} || 1; - - print "." if !($current % $every); - if ($current == $total || $current % ($every * 60) == 0) { - print "$current/$total (" . int($current * 100 / $total) . "%)\n"; - } + my ($params) = @_; + my $current = $params->{current}; + my $total = $params->{total}; + my $every = $params->{every} || 1; + + print "." if !($current % $every); + if ($current == $total || $current % ($every * 60) == 0) { + print "$current/$total (" . int($current * 100 / $total) . "%)\n"; + } } sub feature_description { - my ($feature_name) = @_; - eval { - my $meta = Bugzilla::CPAN->cpan_meta; + my ($feature_name) = @_; + eval { + my $meta = Bugzilla::CPAN->cpan_meta; - return $meta->feature($feature_name)->description - } or warn $@; + return $meta->feature($feature_name)->description; + } or warn $@; } sub install_string { - my ($string_id, $vars) = @_; - _cache()->{install_string_path} ||= template_include_path(); - my $path = _cache()->{install_string_path}; - - my $string_template; - # Find the first template that defines this string. - foreach my $dir (@$path) { - my $base = "$dir/setup/strings"; - $string_template = _get_string_from_file($string_id, "$base.txt.pl") - if !defined $string_template; - last if defined $string_template; - } - - die "No language defines the string '$string_id'" - if !defined $string_template; - - utf8::decode($string_template) if !utf8::is_utf8($string_template); - - $vars ||= {}; - my @replace_keys = keys %$vars; - foreach my $key (@replace_keys) { - my $replacement = $vars->{$key}; - die "'$key' in '$string_id' is tainted: '$replacement'" - if tainted($replacement); - # We don't want people to start getting clever and inserting - # ##variable## into their values. So we check if any other - # key is listed in the *replacement* string, before doing - # the replacement. This is mostly to protect programmers from - # making mistakes. - if (grep($replacement =~ /##$key##/, @replace_keys)) { - die "Unsafe replacement for '$key' in '$string_id': '$replacement'"; - } - $string_template =~ s/\Q##$key##\E/$replacement/g; - } - - return $string_template; + my ($string_id, $vars) = @_; + _cache()->{install_string_path} ||= template_include_path(); + my $path = _cache()->{install_string_path}; + + my $string_template; + + # Find the first template that defines this string. + foreach my $dir (@$path) { + my $base = "$dir/setup/strings"; + $string_template = _get_string_from_file($string_id, "$base.txt.pl") + if !defined $string_template; + last if defined $string_template; + } + + die "No language defines the string '$string_id'" if !defined $string_template; + + utf8::decode($string_template) if !utf8::is_utf8($string_template); + + $vars ||= {}; + my @replace_keys = keys %$vars; + foreach my $key (@replace_keys) { + my $replacement = $vars->{$key}; + die "'$key' in '$string_id' is tainted: '$replacement'" + if tainted($replacement); + + # We don't want people to start getting clever and inserting + # ##variable## into their values. So we check if any other + # key is listed in the *replacement* string, before doing + # the replacement. This is mostly to protect programmers from + # making mistakes. + if (grep($replacement =~ /##$key##/, @replace_keys)) { + die "Unsafe replacement for '$key' in '$string_id': '$replacement'"; + } + $string_template =~ s/\Q##$key##\E/$replacement/g; + } + + return $string_template; } sub _wanted_languages { - my ($requested, @wanted); - - # Checking SERVER_SOFTWARE is the same as i_am_cgi() in Bugzilla::Util. - if (exists $ENV{'SERVER_SOFTWARE'}) { - my $cgi = eval { Bugzilla->cgi } || eval { require CGI; return CGI->new() }; - $requested = $cgi->http('Accept-Language') || ''; - my $lang = $cgi->cookie('LANG'); - push(@wanted, $lang) if $lang; - } - else { - $requested = get_console_locale(); - } - - push(@wanted, _sort_accept_language($requested)); - return \@wanted; + my ($requested, @wanted); + + # Checking SERVER_SOFTWARE is the same as i_am_cgi() in Bugzilla::Util. + if (exists $ENV{'SERVER_SOFTWARE'}) { + my $cgi = eval { Bugzilla->cgi } || eval { require CGI; return CGI->new() }; + $requested = $cgi->http('Accept-Language') || ''; + my $lang = $cgi->cookie('LANG'); + push(@wanted, $lang) if $lang; + } + else { + $requested = get_console_locale(); + } + + push(@wanted, _sort_accept_language($requested)); + return \@wanted; } sub _wanted_to_actual_languages { - my ($wanted, $supported) = @_; - - my @actual; - foreach my $lang (@$wanted) { - # If we support the language we want, or *any version* of - # the language we want, it gets pushed into @actual. - # - # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and - # 'en-uk', but not the other way around. (This is unfortunately - # not very clearly stated in those RFC; see comment just over 14.5 - # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) - my @found = grep(/^\Q$lang\E(-.+)?$/i, @$supported); - push(@actual, @found) if @found; - } + my ($wanted, $supported) = @_; - # We always include English at the bottom if it's not there, even if - # it wasn't selected by the user. - if (!grep($_ eq 'en', @actual)) { - push(@actual, 'en'); - } + my @actual; + foreach my $lang (@$wanted) { - return \@actual; + # If we support the language we want, or *any version* of + # the language we want, it gets pushed into @actual. + # + # Per RFC 1766 and RFC 2616, things like 'en' match 'en-us' and + # 'en-uk', but not the other way around. (This is unfortunately + # not very clearly stated in those RFC; see comment just over 14.5 + # in http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.4) + my @found = grep(/^\Q$lang\E(-.+)?$/i, @$supported); + push(@actual, @found) if @found; + } + + # We always include English at the bottom if it's not there, even if + # it wasn't selected by the user. + if (!grep($_ eq 'en', @actual)) { + push(@actual, 'en'); + } + + return \@actual; } sub supported_languages { - my $cache = _cache(); - return $cache->{supported_languages} if $cache->{supported_languages}; - - my @dirs = glob(bz_locations()->{'templatedir'} . "/*"); - my @languages; - foreach my $dir (@dirs) { - # It's a language directory only if it contains "default" or - # "custom". This auto-excludes CVS directories as well. - next if (!-d "$dir/default" and !-d "$dir/custom"); - my $lang = basename($dir); - # Check for language tag format conforming to RFC 1766. - next unless $lang =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/; - push(@languages, $lang); - } + my $cache = _cache(); + return $cache->{supported_languages} if $cache->{supported_languages}; + + my @dirs = glob(bz_locations()->{'templatedir'} . "/*"); + my @languages; + foreach my $dir (@dirs) { - $cache->{supported_languages} = \@languages; - return \@languages; + # It's a language directory only if it contains "default" or + # "custom". This auto-excludes CVS directories as well. + next if (!-d "$dir/default" and !-d "$dir/custom"); + my $lang = basename($dir); + + # Check for language tag format conforming to RFC 1766. + next unless $lang =~ /^[a-zA-Z]{1,8}(-[a-zA-Z]{1,8})?$/; + push(@languages, $lang); + } + + $cache->{supported_languages} = \@languages; + return \@languages; } sub include_languages { - my ($params) = @_; - - # Basically, the way this works is that we have a list of languages - # that we *want*, and a list of languages that Bugzilla actually - # supports. If there is only one language installed, we take it. - my $supported = supported_languages(); - return @$supported if @$supported == 1; - - my $wanted; - if ($params->{language}) { - # We can pass several languages at once as an arrayref - # or a single language. - $wanted = $params->{language}; - $wanted = [$wanted] unless ref $wanted; - } - else { - $wanted = _wanted_languages(); - } - my $actual = _wanted_to_actual_languages($wanted, $supported); - return @$actual; + my ($params) = @_; + + # Basically, the way this works is that we have a list of languages + # that we *want*, and a list of languages that Bugzilla actually + # supports. If there is only one language installed, we take it. + my $supported = supported_languages(); + return @$supported if @$supported == 1; + + my $wanted; + if ($params->{language}) { + + # We can pass several languages at once as an arrayref + # or a single language. + $wanted = $params->{language}; + $wanted = [$wanted] unless ref $wanted; + } + else { + $wanted = _wanted_languages(); + } + my $actual = _wanted_to_actual_languages($wanted, $supported); + return @$actual; } # Used by template_include_path sub _template_lang_directories { - my ($languages, $templatedir) = @_; - - my @add = qw(custom default); - my $project = bz_locations->{'project'}; - unshift(@add, $project) if $project; - - my @result; - foreach my $lang (@$languages) { - foreach my $dir (@add) { - my $full_dir = "$templatedir/$lang/$dir"; - if (-d $full_dir) { - trick_taint($full_dir); - push(@result, $full_dir); - } - } - } - return @result; + my ($languages, $templatedir) = @_; + + my @add = qw(custom default); + my $project = bz_locations->{'project'}; + unshift(@add, $project) if $project; + + my @result; + foreach my $lang (@$languages) { + foreach my $dir (@add) { + my $full_dir = "$templatedir/$lang/$dir"; + if (-d $full_dir) { + trick_taint($full_dir); + push(@result, $full_dir); + } + } + } + return @result; } # Used by template_include_path. sub _template_base_directories { - my @template_dirs; + my @template_dirs; - foreach my $path (_extension_paths()) { - next if !-d $path; - if ( -d "$path/template") { - push(@template_dirs, "$path/template"); - } + foreach my $path (_extension_paths()) { + next if !-d $path; + if (-d "$path/template") { + push(@template_dirs, "$path/template"); } + } - state $bz_locations = bz_locations(); - push(@template_dirs, $bz_locations->{'templatedir'}); - return \@template_dirs; + state $bz_locations = bz_locations(); + push(@template_dirs, $bz_locations->{'templatedir'}); + return \@template_dirs; } sub template_include_path { - my ($params) = @_; - my @used_languages = include_languages($params); - # Now, we add template directories in the order they will be searched: - my $template_dirs = _template_base_directories(); - - my @include_path; - foreach my $template_dir (@$template_dirs) { - my @lang_dirs = _template_lang_directories(\@used_languages, - $template_dir); - # Hooks get each set of extension directories separately. - if ($params->{hook}) { - push(@include_path, \@lang_dirs); - } - # Whereas everything else just gets a whole INCLUDE_PATH. - else { - push(@include_path, @lang_dirs); - } + my ($params) = @_; + my @used_languages = include_languages($params); + + # Now, we add template directories in the order they will be searched: + my $template_dirs = _template_base_directories(); + + my @include_path; + foreach my $template_dir (@$template_dirs) { + my @lang_dirs = _template_lang_directories(\@used_languages, $template_dir); + + # Hooks get each set of extension directories separately. + if ($params->{hook}) { + push(@include_path, \@lang_dirs); } - return \@include_path; + + # Whereas everything else just gets a whole INCLUDE_PATH. + else { + push(@include_path, @lang_dirs); + } + } + return \@include_path; } # This is taken straight from Sort::Versions 1.5, which is not included # with perl by default. sub vers_cmp { - my ($a, $b) = @_; - - # Remove leading zeroes - Bug 344661 - $a =~ s/^0*(\d.+)/$1/; - $b =~ s/^0*(\d.+)/$1/; - - my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); - my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); - - my ($A, $B); - while (@A and @B) { - $A = shift @A; - $B = shift @B; - if ($A eq '-' and $B eq '-') { - next; - } elsif ( $A eq '-' ) { - return -1; - } elsif ( $B eq '-') { - return 1; - } elsif ($A eq '.' and $B eq '.') { - next; - } elsif ( $A eq '.' ) { - return -1; - } elsif ( $B eq '.' ) { - return 1; - } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { - if ($A =~ /^0/ || $B =~ /^0/) { - return $A cmp $B if $A cmp $B; - } else { - return $A <=> $B if $A <=> $B; - } - } else { - $A = uc $A; - $B = uc $B; - return $A cmp $B if $A cmp $B; - } + my ($a, $b) = @_; + + # Remove leading zeroes - Bug 344661 + $a =~ s/^0*(\d.+)/$1/; + $b =~ s/^0*(\d.+)/$1/; + + my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g); + my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g); + + my ($A, $B); + while (@A and @B) { + $A = shift @A; + $B = shift @B; + if ($A eq '-' and $B eq '-') { + next; + } + elsif ($A eq '-') { + return -1; + } + elsif ($B eq '-') { + return 1; + } + elsif ($A eq '.' and $B eq '.') { + next; + } + elsif ($A eq '.') { + return -1; + } + elsif ($B eq '.') { + return 1; + } + elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) { + if ($A =~ /^0/ || $B =~ /^0/) { + return $A cmp $B if $A cmp $B; + } + else { + return $A <=> $B if $A <=> $B; + } + } + else { + $A = uc $A; + $B = uc $B; + return $A cmp $B if $A cmp $B; } - @A <=> @B; + } + @A <=> @B; } sub no_checksetup_from_cgi { - print "Content-Type: text/html; charset=UTF-8\r\n\r\n"; - print install_string('no_checksetup_from_cgi'); - exit; + print "Content-Type: text/html; charset=UTF-8\r\n\r\n"; + print install_string('no_checksetup_from_cgi'); + exit; } ###################### @@ -396,160 +420,169 @@ sub no_checksetup_from_cgi { # Used by install_string sub _get_string_from_file { - my ($string_id, $file) = @_; - # If we already loaded the file, then use its copy from the cache. - if (my $strings = _cache()->{strings_from_file}->{$file}) { - return $strings->{$string_id}; - } - - # This module is only needed by checksetup.pl, - # so only load it when needed. - require Safe; - - return undef if !-e $file; - my $safe = new Safe; - $safe->rdo($file); - my %strings = %{$safe->varglob('strings')}; - _cache()->{strings_from_file}->{$file} = \%strings; - return $strings{$string_id}; + my ($string_id, $file) = @_; + + # If we already loaded the file, then use its copy from the cache. + if (my $strings = _cache()->{strings_from_file}->{$file}) { + return $strings->{$string_id}; + } + + # This module is only needed by checksetup.pl, + # so only load it when needed. + require Safe; + + return undef if !-e $file; + my $safe = new Safe; + $safe->rdo($file); + my %strings = %{$safe->varglob('strings')}; + _cache()->{strings_from_file}->{$file} = \%strings; + return $strings{$string_id}; } # Make an ordered list out of a HTTP Accept-Language header (see RFC 2616, 14.4) # We ignore '*' and ;q=0 # For languages with the same priority q the order remains unchanged. sub _sort_accept_language { - sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} } - my $accept_language = $_[0]; - - # clean up string. - $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g; - my @qlanguages; - my @languages; - foreach(split /,/, $accept_language) { - if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) { - my $lang = $1; - my $qvalue = $2; - $qvalue = 1 if not defined $qvalue; - next if $qvalue == 0; - $qvalue = 1 if $qvalue > 1; - push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang}); - } - } - - return map($_->{'language'}, (sort sortQvalue @qlanguages)); + sub sortQvalue { $b->{'qvalue'} <=> $a->{'qvalue'} } + my $accept_language = $_[0]; + + # clean up string. + $accept_language =~ s/[^A-Za-z;q=0-9\.\-,]//g; + my @qlanguages; + my @languages; + foreach (split /,/, $accept_language) { + if (m/([A-Za-z\-]+)(?:;q=(\d(?:\.\d+)))?/) { + my $lang = $1; + my $qvalue = $2; + $qvalue = 1 if not defined $qvalue; + next if $qvalue == 0; + $qvalue = 1 if $qvalue > 1; + push(@qlanguages, {'qvalue' => $qvalue, 'language' => $lang}); + } + } + + return map($_->{'language'}, (sort sortQvalue @qlanguages)); } sub get_console_locale { - require Locale::Language; - my $locale = setlocale(LC_CTYPE); - my $language; - # Some distros set e.g. LC_CTYPE = fr_CH.UTF-8. We clean it up. - if ($locale =~ /^([^\.]+)/) { - $locale = $1; - } - $locale =~ s/_/-/; - # It's pretty sure that there is no language pack of the form fr-CH - # installed, so we also include fr as a wanted language. - if ($locale =~ /^(\S+)\-/) { - $language = $1; - $locale .= ",$language"; - } - else { - $language = $locale; - } - - # Some OSs or distributions may have setlocale return a string of the form - # German_Germany.1252 (this example taken from a Windows XP system), which - # is unsuitable for our needs because Bugzilla works on language codes. - # We try and convert them here. - if ($language = Locale::Language::language2code($language)) { - $locale .= ",$language"; - } - - return $locale; + require Locale::Language; + my $locale = setlocale(LC_CTYPE); + my $language; + + # Some distros set e.g. LC_CTYPE = fr_CH.UTF-8. We clean it up. + if ($locale =~ /^([^\.]+)/) { + $locale = $1; + } + $locale =~ s/_/-/; + + # It's pretty sure that there is no language pack of the form fr-CH + # installed, so we also include fr as a wanted language. + if ($locale =~ /^(\S+)\-/) { + $language = $1; + $locale .= ",$language"; + } + else { + $language = $locale; + } + + # Some OSs or distributions may have setlocale return a string of the form + # German_Germany.1252 (this example taken from a Windows XP system), which + # is unsuitable for our needs because Bugzilla works on language codes. + # We try and convert them here. + if ($language = Locale::Language::language2code($language)) { + $locale .= ",$language"; + } + + return $locale; } sub set_output_encoding { - # If we've already set an encoding layer on STDOUT, don't - # add another one. - my @stdout_layers = PerlIO::get_layers(STDOUT); - return if grep(/^encoding/, @stdout_layers); - - my $encoding; - if (ON_WINDOWS and eval { require Win32::Console }) { - # Although setlocale() works on Windows, it doesn't always return - # the current *console's* encoding. So we use OutputCP here instead, - # when we can. - $encoding = Win32::Console::OutputCP(); - } - else { - my $locale = setlocale(LC_CTYPE); - if ($locale =~ /\.([^\.]+)$/) { - $encoding = $1; - } - } - $encoding = "cp$encoding" if ON_WINDOWS; - $encoding = Encode::resolve_alias($encoding) if $encoding; - if ($encoding and $encoding !~ /utf-8/i) { - binmode STDOUT, ":encoding($encoding)"; - binmode STDERR, ":encoding($encoding)"; - } - else { - binmode STDOUT, ':utf8'; - binmode STDERR, ':utf8'; - } + # If we've already set an encoding layer on STDOUT, don't + # add another one. + my @stdout_layers = PerlIO::get_layers(STDOUT); + return if grep(/^encoding/, @stdout_layers); + + my $encoding; + if (ON_WINDOWS and eval { require Win32::Console }) { + + # Although setlocale() works on Windows, it doesn't always return + # the current *console's* encoding. So we use OutputCP here instead, + # when we can. + $encoding = Win32::Console::OutputCP(); + } + else { + my $locale = setlocale(LC_CTYPE); + if ($locale =~ /\.([^\.]+)$/) { + $encoding = $1; + } + } + $encoding = "cp$encoding" if ON_WINDOWS; + + $encoding = Encode::resolve_alias($encoding) if $encoding; + if ($encoding and $encoding !~ /utf-8/i) { + binmode STDOUT, ":encoding($encoding)"; + binmode STDERR, ":encoding($encoding)"; + } + else { + binmode STDOUT, ':utf8'; + binmode STDERR, ':utf8'; + } } sub init_console { - eval { ON_WINDOWS && require Win32::Console::ANSI; }; - $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT); - $SIG{__DIE__} = \&_console_die; - prevent_windows_dialog_boxes(); - set_output_encoding(); + eval { ON_WINDOWS && require Win32::Console::ANSI; }; + $ENV{'ANSI_COLORS_DISABLED'} = 1 if ($@ || !-t *STDOUT); + $SIG{__DIE__} = \&_console_die; + prevent_windows_dialog_boxes(); + set_output_encoding(); } sub _console_die { - my ($message) = @_; - # $^S means "we are in an eval" - if ($^S) { - die $message; - } - # Remove newlines from the message before we color it, and then - # add them back in on display. Otherwise the ANSI escape code - # for resetting the color comes after the newline, and Perl thinks - # that it should put "at Bugzilla/Install.pm line 1234" after the - # message. - $message =~ s/\n+$//; - # We put quotes around the message to stringify any object exceptions, - # like Template::Exception. - die colored("$message", COLOR_ERROR) . "\n"; + my ($message) = @_; + + # $^S means "we are in an eval" + if ($^S) { + die $message; + } + + # Remove newlines from the message before we color it, and then + # add them back in on display. Otherwise the ANSI escape code + # for resetting the color comes after the newline, and Perl thinks + # that it should put "at Bugzilla/Install.pm line 1234" after the + # message. + $message =~ s/\n+$//; + + # We put quotes around the message to stringify any object exceptions, + # like Template::Exception. + die colored("$message", COLOR_ERROR) . "\n"; } sub success { - my ($message) = @_; - print colored($message, COLOR_SUCCESS), "\n"; + my ($message) = @_; + print colored($message, COLOR_SUCCESS), "\n"; } sub prevent_windows_dialog_boxes { - # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183 - # and prevents Perl modules from popping up dialog boxes, particularly - # during checksetup (since loading DBD::Oracle during checksetup when - # Oracle isn't installed causes a scary popup and pauses checksetup). - # - # Win32::API ships with ActiveState by default, though there could - # theoretically be a Windows installation without it, I suppose. - if (ON_WINDOWS and eval { require Win32::API }) { - # Call kernel32.SetErrorMode with arguments that mean: - # "The system does not display the critical-error-handler message box. - # Instead, the system sends the error to the calling process." and - # "A child process inherits the error mode of its parent process." - my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode', - 'I', 'I'); - my $SEM_FAILCRITICALERRORS = 0x0001; - my $SEM_NOGPFAULTERRORBOX = 0x0002; - $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX); - } + + # This code comes from http://bugs.activestate.com/show_bug.cgi?id=82183 + # and prevents Perl modules from popping up dialog boxes, particularly + # during checksetup (since loading DBD::Oracle during checksetup when + # Oracle isn't installed causes a scary popup and pauses checksetup). + # + # Win32::API ships with ActiveState by default, though there could + # theoretically be a Windows installation without it, I suppose. + if (ON_WINDOWS and eval { require Win32::API }) { + + # Call kernel32.SetErrorMode with arguments that mean: + # "The system does not display the critical-error-handler message box. + # Instead, the system sends the error to the calling process." and + # "A child process inherits the error mode of its parent process." + my $SetErrorMode = Win32::API->new('kernel32', 'SetErrorMode', 'I', 'I'); + my $SEM_FAILCRITICALERRORS = 0x0001; + my $SEM_NOGPFAULTERRORBOX = 0x0002; + $SetErrorMode->Call($SEM_FAILCRITICALERRORS | $SEM_NOGPFAULTERRORBOX); + } } # This is like request_cache, but it's used only by installation code @@ -561,20 +594,20 @@ use constant _cache => {}; ############################## sub trick_taint { - require Carp; - Carp::confess("Undef to trick_taint") unless defined $_[0]; - my $match = $_[0] =~ /^(.*)$/s; - $_[0] = $match ? $1 : undef; - return (defined($_[0])); + require Carp; + Carp::confess("Undef to trick_taint") unless defined $_[0]; + my $match = $_[0] =~ /^(.*)$/s; + $_[0] = $match ? $1 : undef; + return (defined($_[0])); } sub trim { - my ($str) = @_; - if ($str) { - $str =~ s/^\s+//g; - $str =~ s/\s+$//g; - } - return $str; + my ($str) = @_; + if ($str) { + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; } __END__ diff --git a/Bugzilla/Job/BugMail.pm b/Bugzilla/Job/BugMail.pm index b4887c470..ebc884a32 100644 --- a/Bugzilla/Job/BugMail.pm +++ b/Bugzilla/Job/BugMail.pm @@ -15,8 +15,8 @@ use Bugzilla::BugMail; BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; } sub process_job { - my ($class, $arg) = @_; - Bugzilla::BugMail::dequeue($arg->{vars}); + my ($class, $arg) = @_; + Bugzilla::BugMail::dequeue($arg->{vars}); } 1; diff --git a/Bugzilla/Job/Mailer.pm b/Bugzilla/Job/Mailer.pm index a376a9256..d73e4f009 100644 --- a/Bugzilla/Job/Mailer.pm +++ b/Bugzilla/Job/Mailer.pm @@ -17,40 +17,42 @@ BEGIN { eval "use base qw(TheSchwartz::Worker)"; } # The longest we expect a job to possibly take, in seconds. use constant grab_for => 300; + # We don't want email to fail permanently very easily. Retry for 30 days. use constant max_retries => 725; # The first few retries happen quickly, but after that we wait an hour for # each retry. sub retry_delay { - my ($class, $num_retries) = @_; - if ($num_retries < 5) { - return (10, 30, 60, 300, 600)[$num_retries]; - } - # One hour - return 60*60; + my ($class, $num_retries) = @_; + if ($num_retries < 5) { + return (10, 30, 60, 300, 600)[$num_retries]; + } + + # One hour + return 60 * 60; } sub work { - my ($class, $job) = @_; - eval { $class->process_job($job->arg) }; - if (my $error = $@) { - if ($error eq EMAIL_LIMIT_EXCEPTION) { - $job->declined(); - } - else { - $job->failed($error); - } - undef $@; + my ($class, $job) = @_; + eval { $class->process_job($job->arg) }; + if (my $error = $@) { + if ($error eq EMAIL_LIMIT_EXCEPTION) { + $job->declined(); } else { - $job->completed; + $job->failed($error); } + undef $@; + } + else { + $job->completed; + } } sub process_job { - my ($class, $arg) = @_; - MessageToMTA($arg->{msg}, 1); + my ($class, $arg) = @_; + MessageToMTA($arg->{msg}, 1); } 1; diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm index a78a4d0ae..cb8a2aa49 100644 --- a/Bugzilla/JobQueue.pm +++ b/Bugzilla/JobQueue.pm @@ -23,128 +23,127 @@ use base qw(TheSchwartz); # This maps job names for Bugzilla::JobQueue to the appropriate modules. # If you add new types of jobs, you should add a mapping here. -use constant JOB_MAP => { - send_mail => 'Bugzilla::Job::Mailer', - bug_mail => 'Bugzilla::Job::BugMail', -}; +use constant JOB_MAP => + {send_mail => 'Bugzilla::Job::Mailer', bug_mail => 'Bugzilla::Job::BugMail',}; # Without a driver cache TheSchwartz opens a new database connection # for each email it sends. This cached connection doesn't persist # across requests. -use constant DRIVER_CACHE_TIME => 300; # 5 minutes +use constant DRIVER_CACHE_TIME => 300; # 5 minutes # To avoid memory leak/fragmentation, a worker process won't process more than # MAX_MESSAGES messages. use constant MAX_MESSAGES => 75; sub job_map { - if (!defined(Bugzilla->request_cache->{job_map})) { - my $job_map = JOB_MAP; - Bugzilla::Hook::process('job_map', { job_map => $job_map }); - Bugzilla->request_cache->{job_map} = $job_map; - } + if (!defined(Bugzilla->request_cache->{job_map})) { + my $job_map = JOB_MAP; + Bugzilla::Hook::process('job_map', {job_map => $job_map}); + Bugzilla->request_cache->{job_map} = $job_map; + } - return Bugzilla->request_cache->{job_map}; + return Bugzilla->request_cache->{job_map}; } sub new { - my $class = shift; - - if (!Bugzilla->feature('jobqueue')) { - ThrowCodeError('feature_disabled', { feature => 'jobqueue' }); - } - - my $lc = Bugzilla->localconfig; - # We need to use the main DB as TheSchwartz module is going - # to write to it. - my $self = $class->SUPER::new( - databases => [{ - dsn => Bugzilla->dbh_main->dsn, - user => $lc->{db_user}, - pass => $lc->{db_pass}, - prefix => 'ts_', - }], - driver_cache_expiration => DRIVER_CACHE_TIME, - ); - - return $self; + my $class = shift; + + if (!Bugzilla->feature('jobqueue')) { + ThrowCodeError('feature_disabled', {feature => 'jobqueue'}); + } + + my $lc = Bugzilla->localconfig; + + # We need to use the main DB as TheSchwartz module is going + # to write to it. + my $self = $class->SUPER::new( + databases => [{ + dsn => Bugzilla->dbh_main->dsn, + user => $lc->{db_user}, + pass => $lc->{db_pass}, + prefix => 'ts_', + }], + driver_cache_expiration => DRIVER_CACHE_TIME, + ); + + return $self; } # A way to get access to the underlying databases directly. sub bz_databases { - my $self = shift; - my @hashes = keys %{ $self->{databases} }; - return map { $self->driver_for($_) } @hashes; + my $self = shift; + my @hashes = keys %{$self->{databases}}; + return map { $self->driver_for($_) } @hashes; } # inserts a job into the queue to be processed and returns immediately sub insert { - my $self = shift; - my $job = shift; + my $self = shift; + my $job = shift; - my $mapped_job = Bugzilla::JobQueue->job_map()->{$job}; - ThrowCodeError('jobqueue_no_job_mapping', { job => $job }) - if !$mapped_job; - unshift(@_, $mapped_job); + my $mapped_job = Bugzilla::JobQueue->job_map()->{$job}; + ThrowCodeError('jobqueue_no_job_mapping', {job => $job}) if !$mapped_job; + unshift(@_, $mapped_job); - my $retval = $self->SUPER::insert(@_); - # XXX Need to get an error message here if insert fails, but - # I don't see any way to do that in TheSchwartz. - ThrowCodeError('jobqueue_insert_failed', { job => $job, errmsg => $@ }) - if !$retval; + my $retval = $self->SUPER::insert(@_); - return $retval; + # XXX Need to get an error message here if insert fails, but + # I don't see any way to do that in TheSchwartz. + ThrowCodeError('jobqueue_insert_failed', {job => $job, errmsg => $@}) + if !$retval; + + return $retval; } sub debug { - my ($self, @args) = @_; - my $caller_pkg = caller; - local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 1; - my $logger = Log::Log4perl->get_logger($caller_pkg); - if ($args[0] && $args[0] eq "TheSchwartz::work_once found no jobs") { - $logger->trace(@args); - } - else { - $logger->info(@args); - } + my ($self, @args) = @_; + my $caller_pkg = caller; + local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 1; + my $logger = Log::Log4perl->get_logger($caller_pkg); + if ($args[0] && $args[0] eq "TheSchwartz::work_once found no jobs") { + $logger->trace(@args); + } + else { + $logger->info(@args); + } } sub work { - my ($self, $delay) = @_; - $delay ||= 1; - my $loop = IO::Async::Loop->new; - my $timer = IO::Async::Timer::Periodic->new( - first_interval => 0, - interval => $delay, - reschedule => 'drift', - on_tick => sub { $self->work_once } - ); - DEBUG("working every $delay seconds"); - $loop->add($timer); - $timer->start; - Future->wait_any(map { catch_signal($_) } qw( INT TERM HUP ))->get; - $timer->stop; - $loop->remove($timer); + my ($self, $delay) = @_; + $delay ||= 1; + my $loop = IO::Async::Loop->new; + my $timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => $delay, + reschedule => 'drift', + on_tick => sub { $self->work_once } + ); + DEBUG("working every $delay seconds"); + $loop->add($timer); + $timer->start; + Future->wait_any(map { catch_signal($_) } qw( INT TERM HUP ))->get; + $timer->stop; + $loop->remove($timer); } # Clear the request cache at the start of each run. sub work_once { - my $self = shift; - my $val = $self->SUPER::work_once(@_); - Bugzilla::Hook::process('request_cleanup'); - Bugzilla::Bug->CLEANUP; - Bugzilla->clear_request_cache(); - return $val; + my $self = shift; + my $val = $self->SUPER::work_once(@_); + Bugzilla::Hook::process('request_cleanup'); + Bugzilla::Bug->CLEANUP; + Bugzilla->clear_request_cache(); + return $val; } # Never process more than MAX_MESSAGES in one batch, to avoid memory # leak/fragmentation issues. sub work_until_done { - my $self = shift; - my $count = 0; - while ($count++ < MAX_MESSAGES) { - $self->work_once or last; - } + my $self = shift; + my $count = 0; + while ($count++ < MAX_MESSAGES) { + $self->work_once or last; + } } 1; diff --git a/Bugzilla/JobQueue/Runner.pm b/Bugzilla/JobQueue/Runner.pm index 0177de40a..8d840fc75 100644 --- a/Bugzilla/JobQueue/Runner.pm +++ b/Bugzilla/JobQueue/Runner.pm @@ -48,206 +48,205 @@ our $initscript = 'bugzilla-queue'; # only thing it uses from gd_preconfig is the "pidfile" # config parameter. sub gd_preconfig { - my $self = shift; + my $self = shift; - my $pidfile = $self->{gd_args}{pidfile}; - if ( !$pidfile ) { - $pidfile = catfile(tmpdir(), $self->{gd_progname} . '.pid'); - } - return ( pidfile => $pidfile ); + my $pidfile = $self->{gd_args}{pidfile}; + if (!$pidfile) { + $pidfile = catfile(tmpdir(), $self->{gd_progname} . '.pid'); + } + return (pidfile => $pidfile); } # All config other than the pidfile has to be done in gd_getopt # in order for it to be set up early enough. sub gd_getopt { - my $self = shift; + my $self = shift; - $self->SUPER::gd_getopt(); + $self->SUPER::gd_getopt(); - if ( $self->{gd_args}{progname} ) { - $self->{gd_progname} = $self->{gd_args}{progname}; - } - else { - $self->{gd_progname} = basename($PROGRAM_NAME); - } + if ($self->{gd_args}{progname}) { + $self->{gd_progname} = $self->{gd_args}{progname}; + } + else { + $self->{gd_progname} = basename($PROGRAM_NAME); + } - # There are places that Daemon Generic's new() uses $PROGRAM_NAME instead of - # gd_progname, which it really shouldn't, but this hack fixes it. - $self->{_original_program_name} = $PROGRAM_NAME; + # There are places that Daemon Generic's new() uses $PROGRAM_NAME instead of + # gd_progname, which it really shouldn't, but this hack fixes it. + $self->{_original_program_name} = $PROGRAM_NAME; - ## no critic (Variables::RequireLocalizedPunctuationVars) - $PROGRAM_NAME = $self->{gd_progname}; - ## use critic + ## no critic (Variables::RequireLocalizedPunctuationVars) + $PROGRAM_NAME = $self->{gd_progname}; + ## use critic } sub gd_postconfig { - my $self = shift; + my $self = shift; - # See the hack above in gd_getopt. This just reverses it - # in case anything else needs the accurate $0. - ## no critic (Variables::RequireLocalizedPunctuationVars) - $PROGRAM_NAME = delete $self->{_original_program_name}; - ## use critic + # See the hack above in gd_getopt. This just reverses it + # in case anything else needs the accurate $0. + ## no critic (Variables::RequireLocalizedPunctuationVars) + $PROGRAM_NAME = delete $self->{_original_program_name}; + ## use critic } sub gd_more_opt { - my $self = shift; - return ( - 'pidfile=s' => \$self->{gd_args}{pidfile}, - 'n=s' => \$self->{gd_args}{progname}, - 'jobs|j=i' => \$self->{gd_args}{jobs}, - ); + my $self = shift; + return ( + 'pidfile=s' => \$self->{gd_args}{pidfile}, + 'n=s' => \$self->{gd_args}{progname}, + 'jobs|j=i' => \$self->{gd_args}{jobs}, + ); } sub gd_usage { - pod2usage( { -verbose => 0, -exitval => 'NOEXIT' } ); - return 0; + pod2usage({-verbose => 0, -exitval => 'NOEXIT'}); + return 0; } sub gd_can_install { - my $self = shift; + my $self = shift; + + my $source_file = "scripts/$initscript.rhel"; + my $dest_file = "$initd/$initscript"; + my $sysconfig = '/etc/sysconfig'; + my $config_file = "$sysconfig/$initscript"; - my $source_file = "scripts/$initscript.rhel"; - my $dest_file = "$initd/$initscript"; - my $sysconfig = '/etc/sysconfig'; - my $config_file = "$sysconfig/$initscript"; + if (!-x $chkconfig || !-d $initd) { + return $self->SUPER::gd_can_install(@_); + } - if ( !-x $chkconfig || !-d $initd ) { - return $self->SUPER::gd_can_install(@_); + return sub { + if (!-w $initd) { + print "You must run the 'install' command as root.\n"; + return; + } + if (-e $dest_file) { + print "$initscript already in $initd.\n"; + } + else { + copy($source_file, $dest_file) + or die "Could not copy $source_file to $dest_file: $!"; + chmod 0755, $dest_file or die "Could not change permissions on $dest_file: $!"; } - return sub { - if ( !-w $initd ) { - print "You must run the 'install' command as root.\n"; - return; - } - if ( -e $dest_file ) { - print "$initscript already in $initd.\n"; - } - else { - copy( $source_file, $dest_file ) - or die "Could not copy $source_file to $dest_file: $!"; - chmod 0755, $dest_file - or die "Could not change permissions on $dest_file: $!"; - } - - system $chkconfig, '--add', $initscript; - print "$initscript installed.", " To start the daemon, do \"$dest_file start\" as root.\n"; - - if ( -d $sysconfig and -w $sysconfig ) { - if ( -e $config_file ) { - print "$config_file already exists.\n"; - return; - } - - open my $config_fh, '>', $config_file; - my $directory = abs_path( dirname( $self->{_original_program_name} ) ); - my $owner_id = ( stat $self->{_original_program_name} )[4]; - my $owner = getpwuid $owner_id; - print $config_fh <<"END"; + system $chkconfig, '--add', $initscript; + print "$initscript installed.", + " To start the daemon, do \"$dest_file start\" as root.\n"; + + if (-d $sysconfig and -w $sysconfig) { + if (-e $config_file) { + print "$config_file already exists.\n"; + return; + } + + open my $config_fh, '>', $config_file; + my $directory = abs_path(dirname($self->{_original_program_name})); + my $owner_id = (stat $self->{_original_program_name})[4]; + my $owner = getpwuid $owner_id; + print $config_fh <<"END"; #!/bin/sh BUGZILLA="$directory" USER=$owner END - close $config_fh; - } - else { - print "Please edit $dest_file to configure the daemon.\n"; - } - } + close $config_fh; + } + else { + print "Please edit $dest_file to configure the daemon.\n"; + } + } } sub gd_can_uninstall { - my $self = shift; - - if ( -x $chkconfig and -d $initd ) { - return sub { - if ( !-e "$initd/$initscript" ) { - print "$initscript not installed.\n"; - return; - } - system $chkconfig, '--del', $initscript; - print "$initscript disabled.", " To stop it, run: $initd/$initscript stop\n"; - } - } + my $self = shift; - return $self->SUPER::gd_can_install(@_); + if (-x $chkconfig and -d $initd) { + return sub { + if (!-e "$initd/$initscript") { + print "$initscript not installed.\n"; + return; + } + system $chkconfig, '--del', $initscript; + print "$initscript disabled.", " To stop it, run: $initd/$initscript stop\n"; + } + } + + return $self->SUPER::gd_can_install(@_); } sub gd_check { - my $self = shift; - - # Get a count of all the jobs currently in the queue. - my $jq = Bugzilla->job_queue(); - my @dbs = $jq->bz_databases(); - my $count = 0; - foreach my $driver (@dbs) { - $count += $driver->select_one( 'SELECT COUNT(*) FROM ts_job', [] ); - } - print get_text( 'job_queue_depth', { count => $count } ) . "\n"; + my $self = shift; + + # Get a count of all the jobs currently in the queue. + my $jq = Bugzilla->job_queue(); + my @dbs = $jq->bz_databases(); + my $count = 0; + foreach my $driver (@dbs) { + $count += $driver->select_one('SELECT COUNT(*) FROM ts_job', []); + } + print get_text('job_queue_depth', {count => $count}) . "\n"; } # override this to use IO::Async. sub gd_setup_signals { - my $self = shift; - my @signals = qw( INT HUP TERM ); - $self->{_signal_future} = Future->wait_any( map { catch_signal( $_, $_ ) } @signals ); + my $self = shift; + my @signals = qw( INT HUP TERM ); + $self->{_signal_future} + = Future->wait_any(map { catch_signal($_, $_) } @signals); } sub gd_other_cmd { - my ($self) = shift; - if ( $ARGV[0] eq 'once' ) { - Bugzilla::JobQueue::Worker->run('work_once'); - exit; - } + my ($self) = shift; + if ($ARGV[0] eq 'once') { + Bugzilla::JobQueue::Worker->run('work_once'); + exit; + } - $self->SUPER::gd_other_cmd(); + $self->SUPER::gd_other_cmd(); } sub gd_quit_event { FATAL('gd_quit_event() should never be called') } sub gd_reconfig_event { FATAL('gd_reconfig_event() should never be called') } sub gd_run { - my $self = shift; - my $jobs = $self->{gd_args}{jobs} // 1; - my $signal_f = $self->{_signal_future}; - my $workers_f = fmap_void { $self->run_worker() } - concurrent => $jobs, - generate => sub { !$signal_f->is_ready }; - - # This is so the process shows up in (h)top in a useful way. - local $PROGRAM_NAME = "$self->{gd_progname} [supervisor]"; - Future->wait_any($signal_f, $workers_f)->get; - unlink $self->{gd_pidfile}; - exit 0; + my $self = shift; + my $jobs = $self->{gd_args}{jobs} // 1; + my $signal_f = $self->{_signal_future}; + my $workers_f = fmap_void { $self->run_worker() } + concurrent => $jobs, + generate => sub { !$signal_f->is_ready }; + + # This is so the process shows up in (h)top in a useful way. + local $PROGRAM_NAME = "$self->{gd_progname} [supervisor]"; + Future->wait_any($signal_f, $workers_f)->get; + unlink $self->{gd_pidfile}; + exit 0; } # This executes the script "jobqueue-worker.pl" # $EXECUTABLE_NAME is the name of the perl interpreter. sub run_worker { - my ( $self ) = @_; - - my $script = catfile( bz_locations->{cgi_path}, 'jobqueue-worker.pl' ); - my @command = ( $EXECUTABLE_NAME, $script); - if ( $self->{gd_args}{progname} ) { - push @command, '--name' => "$self->{gd_args}{progname} [worker]"; - } - - my $loop = IO::Async::Loop->new; - my $exit_f = $loop->new_future; - my $worker = IO::Async::Process->new( - command => \@command, - on_finish => on_finish($exit_f), - on_exception => on_exception( 'jobqueue worker', $exit_f ) - ); - $exit_f->on_cancel( - sub { - DEBUG('terminate worker'); - $worker->kill('TERM'); - } - ); - $loop->add($worker); - return $exit_f; + my ($self) = @_; + + my $script = catfile(bz_locations->{cgi_path}, 'jobqueue-worker.pl'); + my @command = ($EXECUTABLE_NAME, $script); + if ($self->{gd_args}{progname}) { + push @command, '--name' => "$self->{gd_args}{progname} [worker]"; + } + + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my $worker = IO::Async::Process->new( + command => \@command, + on_finish => on_finish($exit_f), + on_exception => on_exception('jobqueue worker', $exit_f) + ); + $exit_f->on_cancel(sub { + DEBUG('terminate worker'); + $worker->kill('TERM'); + }); + $loop->add($worker); + return $exit_f; } 1; diff --git a/Bugzilla/JobQueue/Worker.pm b/Bugzilla/JobQueue/Worker.pm index db8ebe35e..b0c0826eb 100644 --- a/Bugzilla/JobQueue/Worker.pm +++ b/Bugzilla/JobQueue/Worker.pm @@ -14,17 +14,17 @@ use Bugzilla::Logging; use Module::Runtime qw(require_module); sub run { - my ( $class, $fn ) = @_; - DEBUG("Starting up for $fn"); - my $jq = Bugzilla->job_queue(); + my ($class, $fn) = @_; + DEBUG("Starting up for $fn"); + my $jq = Bugzilla->job_queue(); - DEBUG('Loading jobqueue modules'); - foreach my $module ( values %{ Bugzilla::JobQueue->job_map() } ) { - DEBUG("JobQueue can do $module"); - require_module($module); - $jq->can_do($module); - } - $jq->$fn; + DEBUG('Loading jobqueue modules'); + foreach my $module (values %{Bugzilla::JobQueue->job_map()}) { + DEBUG("JobQueue can do $module"); + require_module($module); + $jq->can_do($module); + } + $jq->$fn; } 1; diff --git a/Bugzilla/Keyword.pm b/Bugzilla/Keyword.pm index 61038f602..35dc2f594 100644 --- a/Bugzilla/Keyword.pm +++ b/Bugzilla/Keyword.pm @@ -23,76 +23,78 @@ use Bugzilla::Util; use constant IS_CONFIG => 1; use constant DB_COLUMNS => qw( - keyworddefs.id - keyworddefs.name - keyworddefs.description - keyworddefs.is_active + keyworddefs.id + keyworddefs.name + keyworddefs.description + keyworddefs.is_active ); use constant DB_TABLE => 'keyworddefs'; use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - is_active => \&_check_is_active, + name => \&_check_name, + description => \&_check_description, + is_active => \&_check_is_active, }; use constant UPDATE_COLUMNS => qw( - name - description - is_active + name + description + is_active ); ############################### #### Accessors ###### ############################### -sub description { return $_[0]->{'description'}; } +sub description { return $_[0]->{'description'}; } sub bug_count { - my ($self) = @_; - return $self->{'bug_count'} if defined $self->{'bug_count'}; - ($self->{'bug_count'}) = - Bugzilla->dbh->selectrow_array( - 'SELECT COUNT(*) FROM keywords WHERE keywordid = ?', - undef, $self->id); - return $self->{'bug_count'}; + my ($self) = @_; + return $self->{'bug_count'} if defined $self->{'bug_count'}; + ($self->{'bug_count'}) + = Bugzilla->dbh->selectrow_array( + 'SELECT COUNT(*) FROM keywords WHERE keywordid = ?', + undef, $self->id); + return $self->{'bug_count'}; } ############################### #### Mutators ##### ############################### -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('is_active', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } ############################### #### Subroutines ###### ############################### sub get_all_with_bug_count { - my $class = shift; - my $dbh = Bugzilla->dbh; - my $keywords = - $dbh->selectall_arrayref('SELECT ' - . join(', ', $class->_get_db_columns) . ', + my $class = shift; + my $dbh = Bugzilla->dbh; + my $keywords = $dbh->selectall_arrayref( + 'SELECT ' . join(', ', $class->_get_db_columns) . ', COUNT(keywords.bug_id) AS bug_count FROM keyworddefs LEFT JOIN keywords - ON keyworddefs.id = keywords.keywordid ' . - $dbh->sql_group_by('keyworddefs.id', - 'keyworddefs.name, - keyworddefs.description') . ' - ORDER BY keyworddefs.name', {'Slice' => {}}); - if (!$keywords) { - return []; - } - - foreach my $keyword (@$keywords) { - bless($keyword, $class); - } - return $keywords; + ON keyworddefs.id = keywords.keywordid ' + . $dbh->sql_group_by( + 'keyworddefs.id', 'keyworddefs.name, + keyworddefs.description' + ) . ' + ORDER BY keyworddefs.name', + {'Slice' => {}} + ); + if (!$keywords) { + return []; + } + + foreach my $keyword (@$keywords) { + bless($keyword, $class); + } + return $keywords; } ############################### @@ -100,33 +102,33 @@ sub get_all_with_bug_count { ############################### sub _check_name { - my ($self, $name) = @_; - - $name = trim($name); - if (!defined $name or $name eq "") { - ThrowUserError("keyword_blank_name"); - } - if ($name =~ /[\s,]/) { - ThrowUserError("keyword_invalid_name"); - } - - # We only want to validate the non-existence of the name if - # we're creating a new Keyword or actually renaming the keyword. - if (!ref($self) || $self->name ne $name) { - my $keyword = new Bugzilla::Keyword({ name => $name }); - ThrowUserError("keyword_already_exists", { name => $name }) if $keyword; - } - - return $name; + my ($self, $name) = @_; + + $name = trim($name); + if (!defined $name or $name eq "") { + ThrowUserError("keyword_blank_name"); + } + if ($name =~ /[\s,]/) { + ThrowUserError("keyword_invalid_name"); + } + + # We only want to validate the non-existence of the name if + # we're creating a new Keyword or actually renaming the keyword. + if (!ref($self) || $self->name ne $name) { + my $keyword = new Bugzilla::Keyword({name => $name}); + ThrowUserError("keyword_already_exists", {name => $name}) if $keyword; + } + + return $name; } sub _check_description { - my ($self, $desc) = @_; - $desc = trim($desc); - if (!defined $desc or $desc eq '') { - ThrowUserError("keyword_blank_description"); - } - return $desc; + my ($self, $desc) = @_; + $desc = trim($desc); + if (!defined $desc or $desc eq '') { + ThrowUserError("keyword_blank_description"); + } + return $desc; } sub _check_is_active { return $_[1] ? 1 : 0 } diff --git a/Bugzilla/Logging.pm b/Bugzilla/Logging.pm index f334435fc..22c46b31c 100644 --- a/Bugzilla/Logging.pm +++ b/Bugzilla/Logging.pm @@ -18,105 +18,109 @@ use English qw(-no_match_vars $PROGRAM_NAME); use Taint::Util qw(untaint); sub logfile { - my ($class, $name) = @_; + my ($class, $name) = @_; - my $file = rel2abs(catfile(bz_locations->{logsdir}, $name)); - untaint($file); - return $file; + my $file = rel2abs(catfile(bz_locations->{logsdir}, $name)); + untaint($file); + return $file; } sub fields { - return Log::Log4perl::MDC->get_context->{fields} //= {}; + return Log::Log4perl::MDC->get_context->{fields} //= {}; } BEGIN { - my $file = $ENV{LOG4PERL_CONFIG_FILE} // 'log4perl-syslog.conf'; - Log::Log4perl::Logger::create_custom_level('NOTICE', 'WARN', 5, 2); - Log::Log4perl->init(rel2abs($file, bz_locations->{confdir})); - TRACE("logging enabled in $PROGRAM_NAME"); + my $file = $ENV{LOG4PERL_CONFIG_FILE} // 'log4perl-syslog.conf'; + Log::Log4perl::Logger::create_custom_level('NOTICE', 'WARN', 5, 2); + Log::Log4perl->init(rel2abs($file, bz_locations->{confdir})); + TRACE("logging enabled in $PROGRAM_NAME"); } # this is copied from Log::Log4perl's :easy handling, # except we also export NOTICE. sub import { - my $caller_pkg = caller; - - return 1 if $Log::Log4perl::IMPORT_CALLED{$caller_pkg}++; - - # Define default logger object in caller's package - my $logger = Log::Log4perl->get_logger("$caller_pkg"); - - # Define DEBUG, INFO, etc. routines in caller's package - for (qw(TRACE DEBUG INFO NOTICE WARN ERROR FATAL ALWAYS)) { - my $level = $_; - $level = 'OFF' if $level eq 'ALWAYS'; - my $lclevel = lc $_; - Log::Log4perl::easy_closure_create( - $caller_pkg, - $_, - sub { - Log::Log4perl::Logger::init_warn() - unless $Log::Log4perl::Logger::INITIALIZED or $Log::Log4perl::Logger::NON_INIT_WARNED; - $logger->{$level}->( $logger, @_, $level ); - }, - $logger - ); - } - - # Define LOGCROAK, LOGCLUCK, etc. routines in caller's package - for (qw(LOGCROAK LOGCLUCK LOGCARP LOGCONFESS)) { - my $method = 'Log::Log4perl::Logger::' . lc $_; - - Log::Log4perl::easy_closure_create( - $caller_pkg, - $_, - sub { - unshift @_, $logger; - goto &$method; - }, - $logger - ); - } - - # Define LOGDIE, LOGWARN - Log::Log4perl::easy_closure_create( - $caller_pkg, - 'LOGDIE', - sub { - Log::Log4perl::Logger::init_warn() - unless $Log::Log4perl::Logger::INITIALIZED or $Log::Log4perl::Logger::NON_INIT_WARNED; - $logger->{FATAL}->( $logger, @_, 'FATAL' ); - $Log::Log4perl::LOGDIE_MESSAGE_ON_STDERR - ? CORE::die( Log::Log4perl::Logger::callerline( join '', @_ ) ) - : exit $Log::Log4perl::LOGEXIT_CODE; - }, - $logger - ); + my $caller_pkg = caller; + + return 1 if $Log::Log4perl::IMPORT_CALLED{$caller_pkg}++; + # Define default logger object in caller's package + my $logger = Log::Log4perl->get_logger("$caller_pkg"); + + # Define DEBUG, INFO, etc. routines in caller's package + for (qw(TRACE DEBUG INFO NOTICE WARN ERROR FATAL ALWAYS)) { + my $level = $_; + $level = 'OFF' if $level eq 'ALWAYS'; + my $lclevel = lc $_; Log::Log4perl::easy_closure_create( - $caller_pkg, - 'LOGEXIT', - sub { - Log::Log4perl::Logger::init_warn() - unless $Log::Log4perl::Logger::INITIALIZED or $Log::Log4perl::Logger::NON_INIT_WARNED; - $logger->{FATAL}->( $logger, @_, 'FATAL' ); - exit $Log::Log4perl::LOGEXIT_CODE; - }, - $logger + $caller_pkg, + $_, + sub { + Log::Log4perl::Logger::init_warn() + unless $Log::Log4perl::Logger::INITIALIZED + or $Log::Log4perl::Logger::NON_INIT_WARNED; + $logger->{$level}->($logger, @_, $level); + }, + $logger ); + } + + # Define LOGCROAK, LOGCLUCK, etc. routines in caller's package + for (qw(LOGCROAK LOGCLUCK LOGCARP LOGCONFESS)) { + my $method = 'Log::Log4perl::Logger::' . lc $_; Log::Log4perl::easy_closure_create( - $caller_pkg, - 'LOGWARN', - sub { - Log::Log4perl::Logger::init_warn() - unless $Log::Log4perl::Logger::INITIALIZED or $Log::Log4perl::Logger::NON_INIT_WARNED; - $logger->{WARN}->( $logger, @_, 'WARN' ); - CORE::warn( Log::Log4perl::Logger::callerline( join '', @_ ) ) - if $Log::Log4perl::LOGDIE_MESSAGE_ON_STDERR; - }, - $logger + $caller_pkg, + $_, + sub { + unshift @_, $logger; + goto &$method; + }, + $logger ); + } + + # Define LOGDIE, LOGWARN + Log::Log4perl::easy_closure_create( + $caller_pkg, + 'LOGDIE', + sub { + Log::Log4perl::Logger::init_warn() + unless $Log::Log4perl::Logger::INITIALIZED + or $Log::Log4perl::Logger::NON_INIT_WARNED; + $logger->{FATAL}->($logger, @_, 'FATAL'); + $Log::Log4perl::LOGDIE_MESSAGE_ON_STDERR + ? CORE::die(Log::Log4perl::Logger::callerline(join '', @_)) + : exit $Log::Log4perl::LOGEXIT_CODE; + }, + $logger + ); + + Log::Log4perl::easy_closure_create( + $caller_pkg, + 'LOGEXIT', + sub { + Log::Log4perl::Logger::init_warn() + unless $Log::Log4perl::Logger::INITIALIZED + or $Log::Log4perl::Logger::NON_INIT_WARNED; + $logger->{FATAL}->($logger, @_, 'FATAL'); + exit $Log::Log4perl::LOGEXIT_CODE; + }, + $logger + ); + + Log::Log4perl::easy_closure_create( + $caller_pkg, + 'LOGWARN', + sub { + Log::Log4perl::Logger::init_warn() + unless $Log::Log4perl::Logger::INITIALIZED + or $Log::Log4perl::Logger::NON_INIT_WARNED; + $logger->{WARN}->($logger, @_, 'WARN'); + CORE::warn(Log::Log4perl::Logger::callerline(join '', @_)) + if $Log::Log4perl::LOGDIE_MESSAGE_ON_STDERR; + }, + $logger + ); } 1; diff --git a/Bugzilla/MFA.pm b/Bugzilla/MFA.pm index 43c722364..4ce03817d 100644 --- a/Bugzilla/MFA.pm +++ b/Bugzilla/MFA.pm @@ -12,152 +12,156 @@ use strict; use warnings; use Bugzilla::RNG qw( irand ); -use Bugzilla::Token qw( issue_short_lived_session_token set_token_extra_data get_token_extra_data delete_token ); +use Bugzilla::Token + qw( issue_short_lived_session_token set_token_extra_data get_token_extra_data delete_token ); use Bugzilla::Util qw( trick_taint ); sub new { - my ($class, $user) = @_; - return bless({ user => $user }, $class); + my ($class, $user) = @_; + return bless({user => $user}, $class); } sub new_from { - my ($class, $user, $mfa) = @_; - $mfa //= ''; - if ($mfa eq 'TOTP') { - require Bugzilla::MFA::TOTP; - return Bugzilla::MFA::TOTP->new($user); - } - elsif ($mfa eq 'Duo' && Bugzilla->params->{duo_host}) { - require Bugzilla::MFA::Duo; - return Bugzilla::MFA::Duo->new($user); - } - else { - require Bugzilla::MFA::Dummy; - return Bugzilla::MFA::Dummy->new($user); - } + my ($class, $user, $mfa) = @_; + $mfa //= ''; + if ($mfa eq 'TOTP') { + require Bugzilla::MFA::TOTP; + return Bugzilla::MFA::TOTP->new($user); + } + elsif ($mfa eq 'Duo' && Bugzilla->params->{duo_host}) { + require Bugzilla::MFA::Duo; + return Bugzilla::MFA::Duo->new($user); + } + else { + require Bugzilla::MFA::Dummy; + return Bugzilla::MFA::Dummy->new($user); + } } # abstract methods # called during enrollment -sub enroll {} +sub enroll { } # api call, returns required data to user-prefs enrollment page -sub enroll_api {} +sub enroll_api { } # called after the user has confirmed enrollment -sub enrolled {} +sub enrolled { } # display page with verification prompt -sub prompt {} +sub prompt { } # throws errors if code is invalid -sub check {} +sub check { } # if true verifcation can happen inline (during enrollment/pref changes) # if false then the mfa provider requires an intermediate verification page -sub can_verify_inline { 0 } +sub can_verify_inline {0} # verification sub verify_prompt { - my ($self, $event) = @_; - my $user = delete $event->{user} // Bugzilla->user; - - # generate token and attach mfa data - my $token = issue_short_lived_session_token('mfa', $user); - set_token_extra_data($token, $event); - - # trigger provider verification - my $token_field = $event->{postback}->{token_field} // 'mfa_token'; - $event->{postback}->{fields}->{$token_field} = $token; - $self->prompt($event); - exit; + my ($self, $event) = @_; + my $user = delete $event->{user} // Bugzilla->user; + + # generate token and attach mfa data + my $token = issue_short_lived_session_token('mfa', $user); + set_token_extra_data($token, $event); + + # trigger provider verification + my $token_field = $event->{postback}->{token_field} // 'mfa_token'; + $event->{postback}->{fields}->{$token_field} = $token; + $self->prompt($event); + exit; } sub verify_token { - my ($self, $token) = @_; + my ($self, $token) = @_; - # check token - my ($user_id) = Bugzilla::Token::GetTokenData($token); - my $user = Bugzilla::User->check({ id => $user_id, cache => 1 }); + # check token + my ($user_id) = Bugzilla::Token::GetTokenData($token); + my $user = Bugzilla::User->check({id => $user_id, cache => 1}); - # verify mfa - $self->verify_check(Bugzilla->input_params); + # verify mfa + $self->verify_check(Bugzilla->input_params); - # return event data - my $event = get_token_extra_data($token); - delete_token($token); - if (!$event) { - print Bugzilla->cgi->redirect('index.cgi'); - exit; - } - return $event; + # return event data + my $event = get_token_extra_data($token); + delete_token($token); + if (!$event) { + print Bugzilla->cgi->redirect('index.cgi'); + exit; + } + return $event; } sub verify_check { - my ($self, $params) = @_; - $params->{code} //= ''; - - # recovery code verification - if (length($params->{code}) == 9) { - my $code = $params->{code}; - foreach my $i (1..10) { - my $key = "recovery.$i"; - if (($self->property_get($key) // '') eq $code) { - $self->property_delete($key); - return; - } - } + my ($self, $params) = @_; + $params->{code} //= ''; + + # recovery code verification + if (length($params->{code}) == 9) { + my $code = $params->{code}; + foreach my $i (1 .. 10) { + my $key = "recovery.$i"; + if (($self->property_get($key) // '') eq $code) { + $self->property_delete($key); + return; + } } + } - # mfa verification - $self->check($params); + # mfa verification + $self->check($params); } # methods sub generate_recovery_codes { - my ($self) = @_; - - my @codes; - foreach my $i (1..10) { - # generate 9 digit code - my $code; - $code .= irand(10) for 1..9; - push @codes, $code; - # store (replacing existing) - $self->property_set("recovery.$i", $code); - } + my ($self) = @_; + + my @codes; + foreach my $i (1 .. 10) { + + # generate 9 digit code + my $code; + $code .= irand(10) for 1 .. 9; + push @codes, $code; + + # store (replacing existing) + $self->property_set("recovery.$i", $code); + } - return \@codes; + return \@codes; } # helpers sub property_get { - my ($self, $name) = @_; - trick_taint($name); - return scalar Bugzilla->dbh->selectrow_array( - "SELECT value FROM profile_mfa WHERE user_id = ? AND name = ?", - undef, $self->{user}->id, $name); + my ($self, $name) = @_; + trick_taint($name); + return + scalar Bugzilla->dbh->selectrow_array( + "SELECT value FROM profile_mfa WHERE user_id = ? AND name = ?", + undef, $self->{user}->id, $name); } sub property_set { - my ($self, $name, $value) = @_; - trick_taint($name); - trick_taint($value); - Bugzilla->dbh->do( - "INSERT INTO profile_mfa (user_id, name, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?", - undef, $self->{user}->id, $name, $value, $value); + my ($self, $name, $value) = @_; + trick_taint($name); + trick_taint($value); + Bugzilla->dbh->do( + "INSERT INTO profile_mfa (user_id, name, value) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE value = ?", + undef, $self->{user}->id, $name, $value, $value + ); } sub property_delete { - my ($self, $name) = @_; - trick_taint($name); - Bugzilla->dbh->do( - "DELETE FROM profile_mfa WHERE user_id = ? AND name = ?", - undef, $self->{user}->id, $name); + my ($self, $name) = @_; + trick_taint($name); + Bugzilla->dbh->do("DELETE FROM profile_mfa WHERE user_id = ? AND name = ?", + undef, $self->{user}->id, $name); } 1; diff --git a/Bugzilla/MFA/Dummy.pm b/Bugzilla/MFA/Dummy.pm index 03fbe76b5..0ba7a79a6 100644 --- a/Bugzilla/MFA/Dummy.pm +++ b/Bugzilla/MFA/Dummy.pm @@ -18,12 +18,12 @@ use base 'Bugzilla::MFA'; # it provides no 2fa protection at all, but prevents crashing. sub prompt { - my ($self, $vars) = @_; - my $template = Bugzilla->template; + my ($self, $vars) = @_; + my $template = Bugzilla->template; - print Bugzilla->cgi->header(); - $template->process('mfa/dummy/verify.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + print Bugzilla->cgi->header(); + $template->process('mfa/dummy/verify.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } 1; diff --git a/Bugzilla/MFA/Duo.pm b/Bugzilla/MFA/Duo.pm index 19590944b..6b026f55b 100644 --- a/Bugzilla/MFA/Duo.pm +++ b/Bugzilla/MFA/Duo.pm @@ -18,58 +18,57 @@ use Bugzilla::DuoWeb; use Bugzilla::Error; sub can_verify_inline { - return 0; + return 0; } sub enroll { - my ($self, $params) = @_; + my ($self, $params) = @_; - # verify that the user is enrolled with duo - my $client = Bugzilla::DuoAPI->new( - Bugzilla->params->{duo_ikey}, - Bugzilla->params->{duo_skey}, - Bugzilla->params->{duo_host} - ); - my $response = $client->json_api_call('POST', '/auth/v2/preauth', { username => $params->{username} }); + # verify that the user is enrolled with duo + my $client = Bugzilla::DuoAPI->new( + Bugzilla->params->{duo_ikey}, + Bugzilla->params->{duo_skey}, + Bugzilla->params->{duo_host} + ); + my $response = $client->json_api_call('POST', '/auth/v2/preauth', + {username => $params->{username}}); - # not enrolled - show a nice error page instead of just throwing - unless ($response->{result} eq 'auth' || $response->{result} eq 'allow') { - print Bugzilla->cgi->header(); - my $template = Bugzilla->template; - $template->process('mfa/duo/not_enrolled.html.tmpl', { email => $params->{username} }) - || ThrowTemplateError($template->error()); - exit; - } + # not enrolled - show a nice error page instead of just throwing + unless ($response->{result} eq 'auth' || $response->{result} eq 'allow') { + print Bugzilla->cgi->header(); + my $template = Bugzilla->template; + $template->process('mfa/duo/not_enrolled.html.tmpl', + {email => $params->{username}}) + || ThrowTemplateError($template->error()); + exit; + } - $self->property_set('user', $params->{username}); + $self->property_set('user', $params->{username}); } sub prompt { - my ($self, $vars) = @_; - my $template = Bugzilla->template; + my ($self, $vars) = @_; + my $template = Bugzilla->template; - $vars->{sig_request} = Bugzilla::DuoWeb::sign_request( - Bugzilla->params->{duo_ikey}, - Bugzilla->params->{duo_skey}, - Bugzilla->params->{duo_akey}, - $self->property_get('user'), - ); + $vars->{sig_request} = Bugzilla::DuoWeb::sign_request( + Bugzilla->params->{duo_ikey}, Bugzilla->params->{duo_skey}, + Bugzilla->params->{duo_akey}, $self->property_get('user'), + ); - print Bugzilla->cgi->header(); - $template->process('mfa/duo/verify.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + print Bugzilla->cgi->header(); + $template->process('mfa/duo/verify.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } sub check { - my ($self, $params) = @_; + my ($self, $params) = @_; - return if Bugzilla::DuoWeb::verify_response( - Bugzilla->params->{duo_ikey}, - Bugzilla->params->{duo_skey}, - Bugzilla->params->{duo_akey}, - $params->{sig_response} + return + if Bugzilla::DuoWeb::verify_response( + Bugzilla->params->{duo_ikey}, Bugzilla->params->{duo_skey}, + Bugzilla->params->{duo_akey}, $params->{sig_response} ); - ThrowUserError('mfa_bad_code'); + ThrowUserError('mfa_bad_code'); } 1; diff --git a/Bugzilla/MFA/TOTP.pm b/Bugzilla/MFA/TOTP.pm index 131dea676..2398fcbeb 100644 --- a/Bugzilla/MFA/TOTP.pm +++ b/Bugzilla/MFA/TOTP.pm @@ -21,60 +21,61 @@ use GD::Barcode::QRcode; use MIME::Base64 qw( encode_base64 ); sub can_verify_inline { - return 1; + return 1; } sub _auth { - my ($self) = @_; - return Auth::GoogleAuth->new({ - secret => $self->property_get('secret') // $self->property_get('secret.temp'), - issuer => template_var('terms')->{BugzillaTitle}, - key_id => $self->{user}->login, - }); + my ($self) = @_; + return Auth::GoogleAuth->new({ + secret => $self->property_get('secret') // $self->property_get('secret.temp'), + issuer => template_var('terms')->{BugzillaTitle}, + key_id => $self->{user}->login, + }); } sub enroll_api { - my ($self) = @_; - - # create a new secret for the user - # store it in secret.temp to avoid overwriting a valid secret - $self->property_set('secret.temp', generate_random_password(16)); - - # build the qr code - my $auth = $self->_auth(); - my $otpauth = $auth->qr_code(undef, undef, undef, 1); - my $png = GD::Barcode::QRcode->new($otpauth, { Version => 10, ModuleSize => 3 })->plot()->png(); - return { png => encode_base64($png), secret32 => $auth->secret32 }; + my ($self) = @_; + + # create a new secret for the user + # store it in secret.temp to avoid overwriting a valid secret + $self->property_set('secret.temp', generate_random_password(16)); + + # build the qr code + my $auth = $self->_auth(); + my $otpauth = $auth->qr_code(undef, undef, undef, 1); + my $png = GD::Barcode::QRcode->new($otpauth, {Version => 10, ModuleSize => 3}) + ->plot()->png(); + return {png => encode_base64($png), secret32 => $auth->secret32}; } sub enrolled { - my ($self) = @_; + my ($self) = @_; - # make the temporary secret permanent - $self->property_set('secret', $self->property_get('secret.temp')); - $self->property_delete('secret.temp'); + # make the temporary secret permanent + $self->property_set('secret', $self->property_get('secret.temp')); + $self->property_delete('secret.temp'); } sub prompt { - my ($self, $vars) = @_; - my $template = Bugzilla->template; + my ($self, $vars) = @_; + my $template = Bugzilla->template; - print Bugzilla->cgi->header(); - $template->process('mfa/totp/verify.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + print Bugzilla->cgi->header(); + $template->process('mfa/totp/verify.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } sub check { - my ($self, $params) = @_; - my $code = $params->{code}; - return if $self->_auth()->verify($code, 1); - - if ($params->{mfa_action} && $params->{mfa_action} eq 'enable') { - ThrowUserError('mfa_totp_bad_enrollment_code'); - } - else { - ThrowUserError('mfa_bad_code'); - } + my ($self, $params) = @_; + my $code = $params->{code}; + return if $self->_auth()->verify($code, 1); + + if ($params->{mfa_action} && $params->{mfa_action} eq 'enable') { + ThrowUserError('mfa_totp_bad_enrollment_code'); + } + else { + ThrowUserError('mfa_bad_code'); + } } 1; diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm index c9a458b47..9a2023a8c 100644 --- a/Bugzilla/Mailer.pm +++ b/Bugzilla/Mailer.pm @@ -32,259 +32,265 @@ use Try::Tiny; # deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send # to disable these warnings. BEGIN { - $Return::Value::NO_CLUCK = 1; + $Return::Value::NO_CLUCK = 1; } use Email::Send; use Sys::Hostname; use Bugzilla::Version qw(vers_cmp); sub MessageToMTA { - my ($msg, $send_now) = (@_); - my $method = Bugzilla->get_param_with_override('mail_delivery_method'); - return if $method eq 'None'; - - if (Bugzilla->get_param_with_override('use_mailer_queue') and !$send_now) { - Bugzilla->job_queue->insert('send_mail', { msg => $msg }); - return; + my ($msg, $send_now) = (@_); + my $method = Bugzilla->get_param_with_override('mail_delivery_method'); + return if $method eq 'None'; + + if (Bugzilla->get_param_with_override('use_mailer_queue') and !$send_now) { + Bugzilla->job_queue->insert('send_mail', {msg => $msg}); + return; + } + + my $dbh = Bugzilla->dbh; + + my $email; + if (ref $msg) { + $email = $msg; + } + else { + # RFC 2822 requires us to have CRLF for our line endings and + # Email::MIME doesn't do this for us until 1.911. We use \015 (CR) and \012 (LF) + # directly because Perl translates "\n" depending on what platform + # you're running on. See http://perldoc.perl.org/perlport.html#Newlines + # We check for multiple CRs because of this Template-Toolkit bug: + # https://rt.cpan.org/Ticket/Display.html?id=43345 + if (vers_cmp($Email::MIME::VERSION, 1.911) == -1) { + $msg =~ s/(?:\015+)?\012/\015\012/msg; } - my $dbh = Bugzilla->dbh; + $email = Email::MIME->new($msg); + } - my $email; - if (ref $msg) { - $email = $msg; - } - else { - # RFC 2822 requires us to have CRLF for our line endings and - # Email::MIME doesn't do this for us until 1.911. We use \015 (CR) and \012 (LF) - # directly because Perl translates "\n" depending on what platform - # you're running on. See http://perldoc.perl.org/perlport.html#Newlines - # We check for multiple CRs because of this Template-Toolkit bug: - # https://rt.cpan.org/Ticket/Display.html?id=43345 - if (vers_cmp($Email::MIME::VERSION, 1.911) == -1) { - $msg =~ s/(?:\015+)?\012/\015\012/msg; - } - - $email = Email::MIME->new($msg); - } + # Ensure that we are not sending emails too quickly to recipients. + if (Bugzilla->get_param_with_override('use_mailer_queue') + && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR)) + { + $dbh->do("DELETE FROM email_rates WHERE message_ts < " + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR')); - # Ensure that we are not sending emails too quickly to recipients. - if (Bugzilla->get_param_with_override('use_mailer_queue') - && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR)) - { - $dbh->do( - "DELETE FROM email_rates WHERE message_ts < " - . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR')); + my $recipient = $email->header('To'); - my $recipient = $email->header('To'); - - if (EMAIL_LIMIT_PER_MINUTE) { - my $minute_rate = $dbh->selectrow_array( - "SELECT COUNT(*) + if (EMAIL_LIMIT_PER_MINUTE) { + my $minute_rate = $dbh->selectrow_array( + "SELECT COUNT(*) FROM email_rates WHERE recipient = ? AND message_ts >= " - . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'MINUTE'), - undef, - $recipient); - if ($minute_rate >= EMAIL_LIMIT_PER_MINUTE) { - die EMAIL_LIMIT_EXCEPTION; - } - } - if (EMAIL_LIMIT_PER_HOUR) { - my $hour_rate = $dbh->selectrow_array( - "SELECT COUNT(*) + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'MINUTE'), undef, + $recipient + ); + if ($minute_rate >= EMAIL_LIMIT_PER_MINUTE) { + die EMAIL_LIMIT_EXCEPTION; + } + } + if (EMAIL_LIMIT_PER_HOUR) { + my $hour_rate = $dbh->selectrow_array( + "SELECT COUNT(*) FROM email_rates WHERE recipient = ? AND message_ts >= " - . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR'), - undef, - $recipient); - if ($hour_rate >= EMAIL_LIMIT_PER_HOUR) { - die EMAIL_LIMIT_EXCEPTION; - } - } + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '1', 'HOUR'), undef, + $recipient + ); + if ($hour_rate >= EMAIL_LIMIT_PER_HOUR) { + die EMAIL_LIMIT_EXCEPTION; + } } + } - # We add this header to uniquely identify all email that we - # send as coming from this Bugzilla installation. - # - $email->header_set('X-Bugzilla-URL', Bugzilla->localconfig->{urlbase}); - - # We add this header to mark the mail as "auto-generated" and - # thus to hopefully avoid auto replies. - $email->header_set('Auto-Submitted', 'auto-generated'); - - # MIME-Version must be set otherwise some mailsystems ignore the charset - $email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version'); - - # Encode the headers correctly in quoted-printable - foreach my $header ($email->header_names) { - my @values = $email->header($header); - # We don't recode headers that happen multiple times. - next if scalar(@values) > 1; - if (my $value = $values[0]) { - if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($value)) { - utf8::decode($value); - } - - # avoid excessive line wrapping done by Encode. - local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998; - - my $encoded = encode('MIME-Q', $value); - $email->header_set($header, $encoded); - } - } + # We add this header to uniquely identify all email that we + # send as coming from this Bugzilla installation. + # + $email->header_set('X-Bugzilla-URL', Bugzilla->localconfig->{urlbase}); - my $from = $email->header('From'); + # We add this header to mark the mail as "auto-generated" and + # thus to hopefully avoid auto replies. + $email->header_set('Auto-Submitted', 'auto-generated'); - my ($hostname, @args); - my $mailer_class = $method; - if ($method eq "Sendmail") { - $mailer_class = 'Bugzilla::Send::Sendmail'; - if (ON_WINDOWS) { - $Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE; - } - push @args, "-i"; - # We want to make sure that we pass *only* an email address. - if ($from) { - my ($email_obj) = Email::Address->parse($from); - if ($email_obj) { - my $from_email = $email_obj->address; - push(@args, "-f$from_email") if $from_email; - } - } - } - else { - # Sendmail will automatically append our hostname to the From - # address, but other mailers won't. - my $urlbase = Bugzilla->localconfig->{urlbase}; - $urlbase =~ m|//([^:/]+)[:/]?|; - $hostname = $1; - $from .= "\@$hostname" if $from !~ /@/; - $email->header_set('From', $from); - - # Sendmail adds a Date: header also, but others may not. - if (!defined $email->header('Date')) { - $email->header_set('Date', time2str("%a, %d %b %Y %T %z", time())); - } - } + # MIME-Version must be set otherwise some mailsystems ignore the charset + $email->header_set('MIME-Version', '1.0') if !$email->header('MIME-Version'); - # For tracking/diagnostic purposes, add our hostname - my $generated_by = $email->header('X-Generated-By') || ''; - if ($generated_by =~ tr/\/// < 3) { - $email->header_set('X-Generated-By' => $generated_by . '/' . hostname() . "($$)"); - } + # Encode the headers correctly in quoted-printable + foreach my $header ($email->header_names) { + my @values = $email->header($header); - if ($method eq "SMTP") { - push @args, Host => Bugzilla->params->{"smtpserver"}, - username => Bugzilla->params->{"smtp_username"}, - password => Bugzilla->params->{"smtp_password"}, - Hello => $hostname, - Debug => Bugzilla->params->{'smtp_debug'}; - } + # We don't recode headers that happen multiple times. + next if scalar(@values) > 1; + if (my $value = $values[0]) { + if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($value)) { + utf8::decode($value); + } - Bugzilla::Hook::process('mailer_before_send', - { email => $email, mailer_args => \@args }); - - try { - my $to = $email->header('to') or die qq{Unable to find "To:" address\n}; - my @recipients = Email::Address->parse($to); - die qq{Unable to parse "To:" address - $to\n} unless @recipients; - die qq{Did not expect more than one "To:" address in $to\n} if @recipients > 1; - my $recipient = $recipients[0]; - my $badhosts = Bugzilla::Bloomfilter->lookup("badhosts"); - if ($badhosts && $badhosts->test($recipient->host)) { - WARN("Attempted to send email to address in badhosts: $to"); - $email->header_set(to => ''); - } - elsif ($recipient->host =~ /\.(?:bugs|tld)$/) { - WARN("Attempted to send email to fake address: $to"); - $email->header_set(to => ''); - } - } catch { - ERROR($_); - }; - - # Allow for extensions to to drop the bugmail by clearing the 'to' header - return if $email->header('to') eq ''; - - $email->walk_parts(sub { - my ($part) = @_; - return if $part->parts > 1; # Top-level - my $content_type = $part->content_type || ''; - $content_type =~ /charset=['"](.+)['"]/; - # If no charset is defined or is the default us-ascii, - # then we encode the email to UTF-8 if Bugzilla has utf8 enabled. - # XXX - This is a hack to workaround bug 723944. - if (!$1 || $1 eq 'us-ascii') { - my $body = $part->body; - if (Bugzilla->params->{'utf8'}) { - $part->charset_set('UTF-8'); - # encoding_set works only with bytes, not with utf8 strings. - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } - } - $part->encoding_set('quoted-printable') if !is_7bit_clean($body); - } - }); - - if ($method eq "Test") { - my $filename = bz_locations()->{'datadir'} . '/mailer.testfile'; - open TESTFILE, '>>', $filename; - # From - is required to be a valid mbox file. - print TESTFILE "\n\nFrom - " . $email->header('Date') . "\n" . $email->as_string; - close TESTFILE; - } - else { - # This is useful for both Sendmail and Qmail, so we put it out here. - local $ENV{PATH} = SENDMAIL_PATH; - my $mailer = Email::Send->new({ mailer => $mailer_class, - mailer_args => \@args }); - my $retval = $mailer->send($email); - ThrowCodeError('mail_send_error', { msg => $retval, mail => $email }) - if !$retval; - } + # avoid excessive line wrapping done by Encode. + local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998; - # insert into email_rates - if (Bugzilla->get_param_with_override('use_mailer_queue') - && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR)) - { - $dbh->do( - "INSERT INTO email_rates(recipient, message_ts) VALUES (?, LOCALTIMESTAMP(0))", - undef, - $email->header('To') - ); + my $encoded = encode('MIME-Q', $value); + $email->header_set($header, $encoded); } -} + } -# Builds header suitable for use as a threading marker in email notifications -sub build_thread_marker { - my ($bug_id, $user_id, $is_new) = @_; + my $from = $email->header('From'); - if (!defined $user_id) { - $user_id = Bugzilla->user->id; + my ($hostname, @args); + my $mailer_class = $method; + if ($method eq "Sendmail") { + $mailer_class = 'Bugzilla::Send::Sendmail'; + if (ON_WINDOWS) { + $Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE; } - - my $sitespec = '@' . Bugzilla->localconfig->{urlbase}; - $sitespec =~ s/:\/\//\./; # Make the protocol look like part of the domain - $sitespec =~ s/^([^:\/]+):(\d+)/$1/; # Remove a port number, to relocate - if ($2) { - $sitespec = "-$2$sitespec"; # Put the port number back in, before the '@' + push @args, "-i"; + + # We want to make sure that we pass *only* an email address. + if ($from) { + my ($email_obj) = Email::Address->parse($from); + if ($email_obj) { + my $from_email = $email_obj->address; + push(@args, "-f$from_email") if $from_email; + } } - - my $threadingmarker = "References: "; - if ($is_new) { - $threadingmarker .= "\nMessage-ID: "; + } + else { + # Sendmail will automatically append our hostname to the From + # address, but other mailers won't. + my $urlbase = Bugzilla->localconfig->{urlbase}; + $urlbase =~ m|//([^:/]+)[:/]?|; + $hostname = $1; + $from .= "\@$hostname" if $from !~ /@/; + $email->header_set('From', $from); + + # Sendmail adds a Date: header also, but others may not. + if (!defined $email->header('Date')) { + $email->header_set('Date', time2str("%a, %d %b %Y %T %z", time())); + } + } + + # For tracking/diagnostic purposes, add our hostname + my $generated_by = $email->header('X-Generated-By') || ''; + if ($generated_by =~ tr/\/// < 3) { + $email->header_set( + 'X-Generated-By' => $generated_by . '/' . hostname() . "($$)"); + } + + if ($method eq "SMTP") { + push @args, + Host => Bugzilla->params->{"smtpserver"}, + username => Bugzilla->params->{"smtp_username"}, + password => Bugzilla->params->{"smtp_password"}, + Hello => $hostname, + Debug => Bugzilla->params->{'smtp_debug'}; + } + + Bugzilla::Hook::process('mailer_before_send', + {email => $email, mailer_args => \@args}); + + try { + my $to = $email->header('to') or die qq{Unable to find "To:" address\n}; + my @recipients = Email::Address->parse($to); + die qq{Unable to parse "To:" address - $to\n} unless @recipients; + die qq{Did not expect more than one "To:" address in $to\n} if @recipients > 1; + my $recipient = $recipients[0]; + my $badhosts = Bugzilla::Bloomfilter->lookup("badhosts"); + if ($badhosts && $badhosts->test($recipient->host)) { + WARN("Attempted to send email to address in badhosts: $to"); + $email->header_set(to => ''); + } + elsif ($recipient->host =~ /\.(?:bugs|tld)$/) { + WARN("Attempted to send email to fake address: $to"); + $email->header_set(to => ''); } - else { - my $rand_bits = generate_random_password(10); - $threadingmarker .= "\nMessage-ID: " . - "\nIn-Reply-To: "; + } + catch { + ERROR($_); + }; + + # Allow for extensions to to drop the bugmail by clearing the 'to' header + return if $email->header('to') eq ''; + + $email->walk_parts(sub { + my ($part) = @_; + return if $part->parts > 1; # Top-level + my $content_type = $part->content_type || ''; + $content_type =~ /charset=['"](.+)['"]/; + + # If no charset is defined or is the default us-ascii, + # then we encode the email to UTF-8 if Bugzilla has utf8 enabled. + # XXX - This is a hack to workaround bug 723944. + if (!$1 || $1 eq 'us-ascii') { + my $body = $part->body; + if (Bugzilla->params->{'utf8'}) { + $part->charset_set('UTF-8'); + + # encoding_set works only with bytes, not with utf8 strings. + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); + } + } + $part->encoding_set('quoted-printable') if !is_7bit_clean($body); } + }); + + if ($method eq "Test") { + my $filename = bz_locations()->{'datadir'} . '/mailer.testfile'; + open TESTFILE, '>>', $filename; + + # From - is required to be a valid mbox file. + print TESTFILE "\n\nFrom - " + . $email->header('Date') . "\n" + . $email->as_string; + close TESTFILE; + } + else { + # This is useful for both Sendmail and Qmail, so we put it out here. + local $ENV{PATH} = SENDMAIL_PATH; + my $mailer = Email::Send->new({mailer => $mailer_class, mailer_args => \@args}); + my $retval = $mailer->send($email); + ThrowCodeError('mail_send_error', {msg => $retval, mail => $email}) if !$retval; + } + + # insert into email_rates + if (Bugzilla->get_param_with_override('use_mailer_queue') + && (EMAIL_LIMIT_PER_MINUTE || EMAIL_LIMIT_PER_HOUR)) + { + $dbh->do( + "INSERT INTO email_rates(recipient, message_ts) VALUES (?, LOCALTIMESTAMP(0))", + undef, $email->header('To') + ); + } +} - return $threadingmarker; +# Builds header suitable for use as a threading marker in email notifications +sub build_thread_marker { + my ($bug_id, $user_id, $is_new) = @_; + + if (!defined $user_id) { + $user_id = Bugzilla->user->id; + } + + my $sitespec = '@' . Bugzilla->localconfig->{urlbase}; + $sitespec =~ s/:\/\//\./; # Make the protocol look like part of the domain + $sitespec =~ s/^([^:\/]+):(\d+)/$1/; # Remove a port number, to relocate + if ($2) { + $sitespec = "-$2$sitespec"; # Put the port number back in, before the '@' + } + + my $threadingmarker = "References: "; + if ($is_new) { + $threadingmarker .= "\nMessage-ID: "; + } + else { + my $rand_bits = generate_random_password(10); + $threadingmarker .= "\nMessage-ID: " + . "\nIn-Reply-To: "; + } + + return $threadingmarker; } 1; diff --git a/Bugzilla/Markdown/GFM.pm b/Bugzilla/Markdown/GFM.pm index 367dc7a53..437122093 100644 --- a/Bugzilla/Markdown/GFM.pm +++ b/Bugzilla/Markdown/GFM.pm @@ -17,55 +17,55 @@ use Bugzilla::Markdown::GFM::Node; our @EXPORT_OK = qw(cmark_markdown_to_html); my %OPTIONS = ( - default => 0, - sourcepos => ( 1 << 1 ), - hardbreaks => ( 1 << 2 ), - safe => ( 1 << 3 ), - nobreaks => ( 1 << 4 ), - normalize => ( 1 << 8 ), - validate_utf8 => ( 1 << 9 ), - smart => ( 1 << 10 ), - github_pre_lang => ( 1 << 11 ), - liberal_html_tag => ( 1 << 12 ), - footnotes => ( 1 << 13 ), - strikethrough_double_tilde => ( 1 << 14 ), - table_prefer_style_attributes => ( 1 << 15 ), + default => 0, + sourcepos => (1 << 1), + hardbreaks => (1 << 2), + safe => (1 << 3), + nobreaks => (1 << 4), + normalize => (1 << 8), + validate_utf8 => (1 << 9), + smart => (1 << 10), + github_pre_lang => (1 << 11), + liberal_html_tag => (1 << 12), + footnotes => (1 << 13), + strikethrough_double_tilde => (1 << 14), + table_prefer_style_attributes => (1 << 15), ); my $FFI = FFI::Platypus->new( - lib => [grep { not -l $_ } Alien::libcmark_gfm->dynamic_libs], -); + lib => [grep { not -l $_ } Alien::libcmark_gfm->dynamic_libs],); $FFI->custom_type( - markdown_options_t => { - native_type => 'int', - native_to_perl => sub { - my ($options) = @_; - my $result = {}; - foreach my $key (keys %OPTIONS) { - $result->{$key} = ($options & $OPTIONS{$key}) != 0; - } - return $result; - }, - perl_to_native => sub { - my ($options) = @_; - my $result = 0; - foreach my $key (keys %OPTIONS) { - if ($options->{$key}) { - $result |= $OPTIONS{$key}; - } - } - return $result; + markdown_options_t => { + native_type => 'int', + native_to_perl => sub { + my ($options) = @_; + my $result = {}; + foreach my $key (keys %OPTIONS) { + $result->{$key} = ($options & $OPTIONS{$key}) != 0; + } + return $result; + }, + perl_to_native => sub { + my ($options) = @_; + my $result = 0; + foreach my $key (keys %OPTIONS) { + if ($options->{$key}) { + $result |= $OPTIONS{$key}; } + } + return $result; } + } ); -$FFI->attach(cmark_markdown_to_html => ['opaque', 'int', 'markdown_options_t'] => 'string', - sub { - my $c_func = shift; - my($markdown, $markdown_length) = scalar_to_buffer $_[0]; - return $c_func->($markdown, $markdown_length, $_[1]); - } +$FFI->attach( + cmark_markdown_to_html => ['opaque', 'int', 'markdown_options_t'] => 'string', + sub { + my $c_func = shift; + my ($markdown, $markdown_length) = scalar_to_buffer $_[0]; + return $c_func->($markdown, $markdown_length, $_[1]); + } ); # This has to happen after something from the main lib is loaded diff --git a/Bugzilla/Markdown/GFM/Node.pm b/Bugzilla/Markdown/GFM/Node.pm index da5af1a68..934cb4055 100644 --- a/Bugzilla/Markdown/GFM/Node.pm +++ b/Bugzilla/Markdown/GFM/Node.pm @@ -5,27 +5,25 @@ use strict; use warnings; sub SETUP { - my ($class, $FFI) = @_; + my ($class, $FFI) = @_; - $FFI->custom_type( - markdown_node_t => { - native_type => 'opaque', - native_to_perl => sub { - bless \$_[0], $class if $_[0]; - }, - perl_to_native => sub { ${ $_[0] } }, - } - ); + $FFI->custom_type( + markdown_node_t => { + native_type => 'opaque', + native_to_perl => sub { + bless \$_[0], $class if $_[0]; + }, + perl_to_native => sub { ${$_[0]} }, + } + ); - $FFI->attach( - [ cmark_node_free => 'DESTROY' ], - [ 'markdown_node_t' ] => 'void' - ); + $FFI->attach([cmark_node_free => 'DESTROY'], ['markdown_node_t'] => 'void'); - $FFI->attach( - [ cmark_render_html => 'render_html' ], - [ 'markdown_node_t', 'markdown_options_t', 'markdown_syntax_extension_list_t'] => 'string', - ); + $FFI->attach( + [cmark_render_html => 'render_html'], + ['markdown_node_t', 'markdown_options_t', + 'markdown_syntax_extension_list_t'] => 'string', + ); } 1; diff --git a/Bugzilla/Markdown/GFM/Parser.pm b/Bugzilla/Markdown/GFM/Parser.pm index 5307b49c1..7e16b2b31 100644 --- a/Bugzilla/Markdown/GFM/Parser.pm +++ b/Bugzilla/Markdown/GFM/Parser.pm @@ -7,79 +7,74 @@ use warnings; use FFI::Platypus::Buffer qw( scalar_to_buffer buffer_to_scalar ); sub new { - my ($class, $options) = @_; - my $extensions = delete $options->{extensions} // []; - my $parser = $class->_new($options); - $parser->{_options} = $options; - - eval { - foreach my $name (@$extensions) { - my $extension = Bugzilla::Markdown::GFM::SyntaxExtension->find($name) - or die "unknown extension: $name"; - $parser->attach_syntax_extension($extension); - } - }; - - return $parser; + my ($class, $options) = @_; + my $extensions = delete $options->{extensions} // []; + my $parser = $class->_new($options); + $parser->{_options} = $options; + + eval { + foreach my $name (@$extensions) { + my $extension = Bugzilla::Markdown::GFM::SyntaxExtension->find($name) + or die "unknown extension: $name"; + $parser->attach_syntax_extension($extension); + } + }; + + return $parser; } sub render_html { - my ($self, $markdown) = @_; - $self->feed($markdown); - my $node = $self->finish; - return $node->render_html($self->{_options}, $self->get_syntax_extensions); + my ($self, $markdown) = @_; + $self->feed($markdown); + my $node = $self->finish; + return $node->render_html($self->{_options}, $self->get_syntax_extensions); } sub SETUP { - my ($class, $FFI) = @_; - - $FFI->custom_type( - markdown_parser_t => { - native_type => 'opaque', - native_to_perl => sub { - bless { _pointer => $_[0] }, $class; - }, - perl_to_native => sub { $_[0]->{_pointer} }, - } - ); - - $FFI->attach( - [ cmark_parser_new => '_new' ], - [ 'markdown_options_t' ] => 'markdown_parser_t', - sub { - my $c_func = shift; - return $c_func->($_[1]); - } - ); - - $FFI->attach( - [ cmark_parser_free => 'DESTROY' ], - [ 'markdown_parser_t' ] => 'void' - ); - - $FFI->attach( - [ cmark_parser_feed => 'feed'], - ['markdown_parser_t', 'opaque', 'int'] => 'void', - sub { - my $c_func = shift; - $c_func->($_[0], scalar_to_buffer $_[1]); - } - ); - - $FFI->attach( - [ cmark_parser_finish => 'finish' ], - [ 'markdown_parser_t' ] => 'markdown_node_t', - ); - - $FFI->attach( - [ cmark_parser_attach_syntax_extension => 'attach_syntax_extension' ], - [ 'markdown_parser_t', 'markdown_syntax_extension_t' ] => 'void', - ); - - $FFI->attach( - [ cmark_parser_get_syntax_extensions => 'get_syntax_extensions' ], - [ 'markdown_parser_t' ] => 'markdown_syntax_extension_list_t', - ); + my ($class, $FFI) = @_; + + $FFI->custom_type( + markdown_parser_t => { + native_type => 'opaque', + native_to_perl => sub { + bless {_pointer => $_[0]}, $class; + }, + perl_to_native => sub { $_[0]->{_pointer} }, + } + ); + + $FFI->attach( + [cmark_parser_new => '_new'], + ['markdown_options_t'] => 'markdown_parser_t', + sub { + my $c_func = shift; + return $c_func->($_[1]); + } + ); + + $FFI->attach([cmark_parser_free => 'DESTROY'], ['markdown_parser_t'] => 'void'); + + $FFI->attach( + [cmark_parser_feed => 'feed'], + ['markdown_parser_t', 'opaque', 'int'] => 'void', + sub { + my $c_func = shift; + $c_func->($_[0], scalar_to_buffer $_[1]); + } + ); + + $FFI->attach([cmark_parser_finish => 'finish'], + ['markdown_parser_t'] => 'markdown_node_t',); + + $FFI->attach( + [cmark_parser_attach_syntax_extension => 'attach_syntax_extension'], + ['markdown_parser_t', 'markdown_syntax_extension_t'] => 'void', + ); + + $FFI->attach( + [cmark_parser_get_syntax_extensions => 'get_syntax_extensions'], + ['markdown_parser_t'] => 'markdown_syntax_extension_list_t', + ); } 1; diff --git a/Bugzilla/Markdown/GFM/SyntaxExtension.pm b/Bugzilla/Markdown/GFM/SyntaxExtension.pm index 56efa177a..213980485 100644 --- a/Bugzilla/Markdown/GFM/SyntaxExtension.pm +++ b/Bugzilla/Markdown/GFM/SyntaxExtension.pm @@ -5,25 +5,25 @@ use strict; use warnings; sub SETUP { - my ($class, $FFI) = @_; + my ($class, $FFI) = @_; - $FFI->custom_type( - markdown_syntax_extension_t => { - native_type => 'opaque', - native_to_perl => sub { - bless \$_[0], $class if $_[0]; - }, - perl_to_native => sub { $_[0] ? ${ $_[0] } : 0 }, - } - ); - $FFI->attach( - [ cmark_find_syntax_extension => 'find' ], - [ 'string' ] => 'markdown_syntax_extension_t', - sub { - my $c_func = shift; - return $c_func->($_[1]); - } - ); + $FFI->custom_type( + markdown_syntax_extension_t => { + native_type => 'opaque', + native_to_perl => sub { + bless \$_[0], $class if $_[0]; + }, + perl_to_native => sub { $_[0] ? ${$_[0]} : 0 }, + } + ); + $FFI->attach( + [cmark_find_syntax_extension => 'find'], + ['string'] => 'markdown_syntax_extension_t', + sub { + my $c_func = shift; + return $c_func->($_[1]); + } + ); } 1; diff --git a/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm b/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm index 06a9798c2..963dec26d 100644 --- a/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm +++ b/Bugzilla/Markdown/GFM/SyntaxExtensionList.pm @@ -5,17 +5,17 @@ use strict; use warnings; sub SETUP { - my ($class, $FFI) = @_; + my ($class, $FFI) = @_; - $FFI->custom_type( - markdown_syntax_extension_list_t => { - native_type => 'opaque', - native_to_perl => sub { - bless \$_[0], $class if $_[0]; - }, - perl_to_native => sub { $_[0] ? ${ $_[0] } : 0 }, - } - ); + $FFI->custom_type( + markdown_syntax_extension_list_t => { + native_type => 'opaque', + native_to_perl => sub { + bless \$_[0], $class if $_[0]; + }, + perl_to_native => sub { $_[0] ? ${$_[0]} : 0 }, + } + ); } 1; diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm index 6bbef080a..eda30de23 100644 --- a/Bugzilla/Memcached.pm +++ b/Bugzilla/Memcached.pm @@ -22,336 +22,335 @@ use Encode; use Sys::Syslog qw(:DEFAULT); # memcached keys have a maximum length of 250 bytes -use constant MAX_KEY_LENGTH => 250; +use constant MAX_KEY_LENGTH => 250; use constant RATE_LIMIT_PREFIX => "rate:"; *new = \&_new; sub _new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $self = {}; - - # always return an object to simplify calling code when memcached is - # disabled. - my $servers = Bugzilla->localconfig->{memcached_servers}; - if (Bugzilla->feature('memcached') && $servers) { - $self->{namespace} = Bugzilla->localconfig->{memcached_namespace}; - TRACE("connecting servers: $servers, namespace: $self->{namespace}"); - $self->{memcached} = Cache::Memcached::Fast->new( - { - servers => [ _parse_memcached_server_list($servers) ], - namespace => $self->{namespace}, - max_size => 1024 * 1024 * 4, - max_failures => 1, - failure_timeout => 60, - io_timeout => 0.2, - connect_timeout => 0.2, - } - ); - my $versions = $self->{memcached}->server_versions; - if (keys %$versions) { - # this is needed to ensure forked processes don't start out with a connected memcached socket. - $self->{memcached}->disconnect_all; - } - else { - WARN("No memcached servers"); - } + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $self = {}; + + # always return an object to simplify calling code when memcached is + # disabled. + my $servers = Bugzilla->localconfig->{memcached_servers}; + if (Bugzilla->feature('memcached') && $servers) { + $self->{namespace} = Bugzilla->localconfig->{memcached_namespace}; + TRACE("connecting servers: $servers, namespace: $self->{namespace}"); + $self->{memcached} = Cache::Memcached::Fast->new({ + servers => [_parse_memcached_server_list($servers)], + namespace => $self->{namespace}, + max_size => 1024 * 1024 * 4, + max_failures => 1, + failure_timeout => 60, + io_timeout => 0.2, + connect_timeout => 0.2, + }); + my $versions = $self->{memcached}->server_versions; + if (keys %$versions) { + +# this is needed to ensure forked processes don't start out with a connected memcached socket. + $self->{memcached}->disconnect_all; } else { - TRACE("memcached feature is not enabled"); + WARN("No memcached servers"); } - return bless($self, $class); + } + else { + TRACE("memcached feature is not enabled"); + } + return bless($self, $class); } sub _parse_memcached_server_list { - my ($server_list) = @_; - my @servers = split(/[, ]+/, trim($server_list)); + my ($server_list) = @_; + my @servers = split(/[, ]+/, trim($server_list)); - return map { /:[0-9]+$/s ? $_ : "$_:11211" } @servers; + return map { /:[0-9]+$/s ? $_ : "$_:11211" } @servers; } sub enabled { - return $_[0]->{memcached} ? 1 : 0; + return $_[0]->{memcached} ? 1 : 0; } sub set { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key, value => $value } - if (exists $args->{key}) { - $self->_set($args->{key}, $args->{value}); - } - - # { table => $table, id => $id, name => $name, data => $data } - elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) { - # For caching of Bugzilla::Object, we have to be able to clear the - # cached values when given either the object's id or name. - my ($table, $id, $name, $data) = @$args{qw(table id name data)}; - $self->_set("$table.id.$id", $data); - if (defined $name) { - $self->_set("$table.name_id.$name", $id); - $self->_set("$table.id_name.$id", $name); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key, value => $value } + if (exists $args->{key}) { + $self->_set($args->{key}, $args->{value}); + } + + # { table => $table, id => $id, name => $name, data => $data } + elsif (exists $args->{table} && exists $args->{id} && exists $args->{name}) { + + # For caching of Bugzilla::Object, we have to be able to clear the + # cached values when given either the object's id or name. + my ($table, $id, $name, $data) = @$args{qw(table id name data)}; + $self->_set("$table.id.$id", $data); + if (defined $name) { + $self->_set("$table.name_id.$name", $id); + $self->_set("$table.id_name.$id", $name); } + } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set", - params => [ 'key', 'table' ] }); - } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set", params => ['key', 'table']}); + } } sub get { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key } - if (exists $args->{key}) { - return $self->_get($args->{key}); - } - - # { table => $table, id => $id } - elsif (exists $args->{table} && exists $args->{id}) { - my ($table, $id) = @$args{qw(table id)}; - return $self->_get("$table.id.$id"); - } - - # { table => $table, name => $name } - elsif (exists $args->{table} && exists $args->{name}) { - my ($table, $name) = @$args{qw(table name)}; - return unless my $id = $self->_get("$table.name_id.$name"); - return $self->_get("$table.id.$id"); - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get", - params => [ 'key', 'table' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key } + if (exists $args->{key}) { + return $self->_get($args->{key}); + } + + # { table => $table, id => $id } + elsif (exists $args->{table} && exists $args->{id}) { + my ($table, $id) = @$args{qw(table id)}; + return $self->_get("$table.id.$id"); + } + + # { table => $table, name => $name } + elsif (exists $args->{table} && exists $args->{name}) { + my ($table, $name) = @$args{qw(table name)}; + return unless my $id = $self->_get("$table.name_id.$name"); + return $self->_get("$table.id.$id"); + } + + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::get", params => ['key', 'table']}); + } } sub set_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - - if (exists $args->{key}) { - return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_config", - params => [ 'key' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + if (exists $args->{key}) { + return $self->_set($self->_config_prefix . '.' . $args->{key}, $args->{data}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set_config", params => ['key']}); + } } sub get_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - - if (exists $args->{key}) { - return $self->_get($self->_config_prefix . '.' . $args->{key}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::get_config", - params => [ 'key' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + if (exists $args->{key}) { + return $self->_get($self->_config_prefix . '.' . $args->{key}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::get_config", params => ['key']}); + } } sub set_bloomfilter { - my ($self, $args) = @_; - return unless $self->{memcached}; - if (exists $args->{name}) { - return $self->_set($self->_bloomfilter_prefix . '.' . $args->{name}, $args->{filter}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_bloomfilter", - params => [ 'name' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + if (exists $args->{name}) { + return $self->_set($self->_bloomfilter_prefix . '.' . $args->{name}, + $args->{filter}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set_bloomfilter", params => ['name']}); + } } sub get_bloomfilter { - my ($self, $args) = @_; - return unless $self->{memcached}; - if (exists $args->{name}) { - return $self->_get($self->_bloomfilter_prefix . '.' . $args->{name}); - } - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::set_bloomfilter", - params => [ 'name' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + if (exists $args->{name}) { + return $self->_get($self->_bloomfilter_prefix . '.' . $args->{name}); + } + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::set_bloomfilter", params => ['name']}); + } } sub clear_bloomfilter { - my ($self, $args) = @_; - return unless $self->{memcached}; - if ($args && exists $args->{name}) { - $self->_delete($self->_config_prefix . '.' . $args->{name}); - } - else { - $self->_inc_prefix("bloomfilter"); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + if ($args && exists $args->{name}) { + $self->_delete($self->_config_prefix . '.' . $args->{name}); + } + else { + $self->_inc_prefix("bloomfilter"); + } } sub clear { - my ($self, $args) = @_; - return unless $self->{memcached}; - - # { key => $key } - if (exists $args->{key}) { - $self->_delete($args->{key}); - } - - # { table => $table, id => $id } - elsif (exists $args->{table} && exists $args->{id}) { - my ($table, $id) = @$args{qw(table id)}; - my $name = $self->_get("$table.id_name.$id"); - $self->_delete("$table.id.$id"); - $self->_delete("$table.name_id.$name") if defined $name; - $self->_delete("$table.id_name.$id"); - } - - # { table => $table, name => $name } - elsif (exists $args->{table} && exists $args->{name}) { - my ($table, $name) = @$args{qw(table name)}; - return unless my $id = $self->_get("$table.name_id.$name"); - $self->_delete("$table.id.$id"); - $self->_delete("$table.name_id.$name"); - $self->_delete("$table.id_name.$id"); - } - - else { - ThrowCodeError('params_required', { function => "Bugzilla::Memcached::clear", - params => [ 'key', 'table' ] }); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + + # { key => $key } + if (exists $args->{key}) { + $self->_delete($args->{key}); + } + + # { table => $table, id => $id } + elsif (exists $args->{table} && exists $args->{id}) { + my ($table, $id) = @$args{qw(table id)}; + my $name = $self->_get("$table.id_name.$id"); + $self->_delete("$table.id.$id"); + $self->_delete("$table.name_id.$name") if defined $name; + $self->_delete("$table.id_name.$id"); + } + + # { table => $table, name => $name } + elsif (exists $args->{table} && exists $args->{name}) { + my ($table, $name) = @$args{qw(table name)}; + return unless my $id = $self->_get("$table.name_id.$name"); + $self->_delete("$table.id.$id"); + $self->_delete("$table.name_id.$name"); + $self->_delete("$table.id_name.$id"); + } + + else { + ThrowCodeError('params_required', + {function => "Bugzilla::Memcached::clear", params => ['key', 'table']}); + } } sub should_rate_limit { - my ($self, $name, $rate_max, $rate_seconds, $tries) = @_; - my $prefix = RATE_LIMIT_PREFIX . $name . ':'; - my $memcached = $self->{memcached}; - - return 0 unless $name; - return 0 unless $memcached; - - $tries //= 4; - - for my $try (1 .. $tries) { - my $now = time; - my ($key, @keys) = map { $prefix . ( $now - $_ ) } 0 .. $rate_seconds; - $memcached->add($key, 0, $rate_seconds+1); - my $tokens = $memcached->get_multi(@keys); - my $cas = $memcached->gets($key); - $tokens->{$key} = $cas->[1]++; - return 1 if sum(values %$tokens) >= $rate_max; - return 0 if $memcached->cas($key, @$cas, $rate_seconds+1); - WARN("retry for $prefix (try $try of $tries)"); - } - return 0; + my ($self, $name, $rate_max, $rate_seconds, $tries) = @_; + my $prefix = RATE_LIMIT_PREFIX . $name . ':'; + my $memcached = $self->{memcached}; + + return 0 unless $name; + return 0 unless $memcached; + + $tries //= 4; + + for my $try (1 .. $tries) { + my $now = time; + my ($key, @keys) = map { $prefix . ($now - $_) } 0 .. $rate_seconds; + $memcached->add($key, 0, $rate_seconds + 1); + my $tokens = $memcached->get_multi(@keys); + my $cas = $memcached->gets($key); + $tokens->{$key} = $cas->[1]++; + return 1 if sum(values %$tokens) >= $rate_max; + return 0 if $memcached->cas($key, @$cas, $rate_seconds + 1); + WARN("retry for $prefix (try $try of $tries)"); + } + return 0; } sub clear_all { - my ($self) = @_; - return unless $self->{memcached}; - $self->_inc_prefix("global"); + my ($self) = @_; + return unless $self->{memcached}; + $self->_inc_prefix("global"); } sub clear_config { - my ($self, $args) = @_; - return unless $self->{memcached}; - if ($args && exists $args->{key}) { - $self->_delete($self->_config_prefix . '.' . $args->{key}); - } - else { - $self->_inc_prefix("config"); - } + my ($self, $args) = @_; + return unless $self->{memcached}; + if ($args && exists $args->{key}) { + $self->_delete($self->_config_prefix . '.' . $args->{key}); + } + else { + $self->_inc_prefix("config"); + } } # in order to clear all our keys, we add a prefix to all our keys. when we # need to "clear" all current keys, we increment the prefix. sub _prefix { - my ($self, $name) = @_; - # we don't want to change prefixes in the middle of a request - my $request_cache = Bugzilla->request_cache; - my $request_cache_key = "memcached_prefix_$name"; - if (!$request_cache->{$request_cache_key}) { - my $memcached = $self->{memcached}; - my $prefix = $memcached->get($name); - if (!$prefix) { - $prefix = time(); - if (!$memcached->add($name, $prefix)) { - # if this failed, either another process set the prefix, or - # memcached is down. assume we lost the race, and get the new - # value. if that fails, memcached is down so use a dummy - # prefix for this request. - $prefix = $memcached->get($name) || 0; - } - } - $request_cache->{$request_cache_key} = $prefix; - } - return $request_cache->{$request_cache_key}; -} + my ($self, $name) = @_; -sub _inc_prefix { - my ($self, $name) = @_; + # we don't want to change prefixes in the middle of a request + my $request_cache = Bugzilla->request_cache; + my $request_cache_key = "memcached_prefix_$name"; + if (!$request_cache->{$request_cache_key}) { my $memcached = $self->{memcached}; - if (!$memcached->incr($name, 1)) { - $memcached->add($name, time()); + my $prefix = $memcached->get($name); + if (!$prefix) { + $prefix = time(); + if (!$memcached->add($name, $prefix)) { + + # if this failed, either another process set the prefix, or + # memcached is down. assume we lost the race, and get the new + # value. if that fails, memcached is down so use a dummy + # prefix for this request. + $prefix = $memcached->get($name) || 0; + } } - delete Bugzilla->request_cache->{"memcached_prefix_$name"}; + $request_cache->{$request_cache_key} = $prefix; + } + return $request_cache->{$request_cache_key}; +} - # BMO - log that we've wiped the cache - TRACE("$name cache cleared"); +sub _inc_prefix { + my ($self, $name) = @_; + my $memcached = $self->{memcached}; + if (!$memcached->incr($name, 1)) { + $memcached->add($name, time()); + } + delete Bugzilla->request_cache->{"memcached_prefix_$name"}; + + # BMO - log that we've wiped the cache + TRACE("$name cache cleared"); } sub _global_prefix { - return $_[0]->_prefix("global"); + return $_[0]->_prefix("global"); } sub _config_prefix { - return $_[0]->_prefix("config"); + return $_[0]->_prefix("config"); } sub _bloomfilter_prefix { - return $_[0]->_prefix("bloomfilter"); + return $_[0]->_prefix("bloomfilter"); } sub _encode_key { - my ($self, $key) = @_; - $key = $self->_global_prefix . '.' . uri_escape_utf8($key); - trick_taint($key) if defined $key; - return length($self->{namespace} . $key) > MAX_KEY_LENGTH - ? undef - : $key; + my ($self, $key) = @_; + $key = $self->_global_prefix . '.' . uri_escape_utf8($key); + trick_taint($key) if defined $key; + return length($self->{namespace} . $key) > MAX_KEY_LENGTH ? undef : $key; } sub _set { - my ($self, $key, $value) = @_; - if (blessed($value)) { - # we don't support blessed objects - ThrowCodeError('param_invalid', { function => "Bugzilla::Memcached::set", - param => "value" }); - } + my ($self, $key, $value) = @_; + if (blessed($value)) { + + # we don't support blessed objects + ThrowCodeError('param_invalid', + {function => "Bugzilla::Memcached::set", param => "value"}); + } - my $enc_key = $self->_encode_key($key) - or return; - TRACE("set $enc_key"); - return $self->{memcached}->set($enc_key, $value); + my $enc_key = $self->_encode_key($key) or return; + TRACE("set $enc_key"); + return $self->{memcached}->set($enc_key, $value); } sub _get { - my ($self, $key) = @_; + my ($self, $key) = @_; - my $enc_key = $self->_encode_key($key) - or return; - my $val = $self->{memcached}->get($enc_key); - TRACE("get $enc_key: " . (defined $val ? "HIT" : "MISS")); - return $val; + my $enc_key = $self->_encode_key($key) or return; + my $val = $self->{memcached}->get($enc_key); + TRACE("get $enc_key: " . (defined $val ? "HIT" : "MISS")); + return $val; } sub _delete { - my ($self, $key) = @_; - $key = $self->_encode_key($key) - or return; - return $self->{memcached}->delete($key); + my ($self, $key) = @_; + $key = $self->_encode_key($key) or return; + return $self->{memcached}->delete($key); } 1; diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm index 925a01355..74bce149f 100644 --- a/Bugzilla/Migrate.pm +++ b/Bugzilla/Migrate.pm @@ -20,7 +20,7 @@ use Bugzilla::Install::Requirements (); use Bugzilla::Install::Util qw(indicate_progress); use Bugzilla::Product; use Bugzilla::Util qw(get_text trim generate_random_password); -use Bugzilla::User (); +use Bugzilla::User (); use Bugzilla::Status (); use Bugzilla::Version; @@ -37,10 +37,10 @@ use constant REQUIRED_MODULES => []; use constant NON_COMMENT_FIELDS => (); use constant CONFIG_VARS => ( - { - name => 'translate_fields', - default => {}, - desc => <<'END', + { + name => 'translate_fields', + default => {}, + desc => <<'END', # This maps field names in your bug-tracker to Bugzilla field names. If a field # has the same name in your bug-tracker and Bugzilla (case-insensitively), it # doesn't need a mapping here. If a field isn't listed here and doesn't have @@ -64,11 +64,11 @@ use constant CONFIG_VARS => ( # variable by default, then that field will be automatically created by # the migrator and you don't have to worry about it. END - }, - { - name => 'translate_values', - default => {}, - desc => <<'END', + }, + { + name => 'translate_values', + default => {}, + desc => <<'END', # This configuration variable allows you to say that a particular field # value in your current bug-tracker should be translated to a different # value when it's imported into Bugzilla. @@ -108,22 +108,22 @@ END # # Values that don't get translated will be imported as-is. END - }, - { - name => 'starting_bug_id', - default => 0, - desc => <<'END', + }, + { + name => 'starting_bug_id', + default => 0, + desc => <<'END', # What bug ID do you want the first imported bug to get? If you set this to # 0, then the imported bug ids will just start right after the current # bug ids. If you use this configuration variable, you must make sure that # nobody else is using your Bugzilla while you run the migration, or a new # bug filed by a user might take this ID instead. END - }, - { - name => 'timezone', - default => 'local', - desc => <<'END', + }, + { + name => 'timezone', + default => 'local', + desc => <<'END', # If migrate.pl comes across any dates without timezones, while doing the # migration, what timezone should we assume those dates are in? # The best format for this variable is something like "America/Los Angeles". @@ -133,7 +133,7 @@ END # The special value "local" means "use the same timezone as the system I # am running this script on now". END - }, + }, ); use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc); @@ -143,42 +143,44 @@ use constant USER_FIELDS => qw(user assigned_to qa_contact reporter cc); ######################### sub do_migration { - my $self = shift; - my $dbh = Bugzilla->dbh; - # On MySQL, setting serial values implicitly commits a transaction, - # so we want to do it up here, outside of any transaction. This also - # has the advantage of loading the config before anything else is done. - if ($self->config('starting_bug_id')) { - $dbh->bz_set_next_serial_value('bugs', 'bug_id', - $self->config('starting_bug_id')); - } - $dbh->bz_start_transaction(); - - # Read Other Database - my $users = $self->users; - my $products = $self->products; - my $bugs = $self->bugs; - $self->after_read(); - - $self->translate_all_bugs($bugs); - - Bugzilla->set_user(Bugzilla::User->super_user); - - # Insert into Bugzilla - $self->before_insert(); - $self->insert_users($users); - $self->insert_products($products); - $self->create_custom_fields(); - $self->create_legal_values($bugs); - $self->insert_bugs($bugs); - $self->after_insert(); - if ($self->dry_run) { - $dbh->bz_rollback_transaction(); - $self->reset_serial_values(); - } - else { - $dbh->bz_commit_transaction(); - } + my $self = shift; + my $dbh = Bugzilla->dbh; + + # On MySQL, setting serial values implicitly commits a transaction, + # so we want to do it up here, outside of any transaction. This also + # has the advantage of loading the config before anything else is done. + if ($self->config('starting_bug_id')) { + $dbh->bz_set_next_serial_value('bugs', 'bug_id', + $self->config('starting_bug_id')); + } + $dbh->bz_start_transaction(); + + # Read Other Database + my $users = $self->users; + my $products = $self->products; + my $bugs = $self->bugs; + $self->after_read(); + + $self->translate_all_bugs($bugs); + + Bugzilla->set_user(Bugzilla::User->super_user); + + # Insert into Bugzilla + $self->before_insert(); + $self->insert_users($users); + $self->insert_products($products); + $self->create_custom_fields(); + $self->create_legal_values($bugs); + $self->insert_bugs($bugs); + $self->after_insert(); + + if ($self->dry_run) { + $dbh->bz_rollback_transaction(); + $self->reset_serial_values(); + } + else { + $dbh->bz_commit_transaction(); + } } ################ @@ -186,24 +188,23 @@ sub do_migration { ################ sub new { - my ($class) = @_; - my $self = { }; - bless $self, $class; - return $self; + my ($class) = @_; + my $self = {}; + bless $self, $class; + return $self; } sub load { - my ($class, $from) = @_; - my $libdir = bz_locations()->{libpath}; - my @migration_modules = glob("$libdir/Bugzilla/Migrate/*"); - my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } - @migration_modules; - if (!$module) { - ThrowUserError('migrate_from_invalid', { from => $from }); - } - require $module; - my $canonical_name = _canonical_name($module); - return "Bugzilla::Migrate::$canonical_name"->new; + my ($class, $from) = @_; + my $libdir = bz_locations()->{libpath}; + my @migration_modules = glob("$libdir/Bugzilla/Migrate/*"); + my ($module) = grep { basename($_) =~ /^\Q$from\E\.pm$/i } @migration_modules; + if (!$module) { + ThrowUserError('migrate_from_invalid', {from => $from}); + } + require $module; + my $canonical_name = _canonical_name($module); + return "Bugzilla::Migrate::$canonical_name"->new; } ############# @@ -211,67 +212,67 @@ sub load { ############# sub name { - my $self = shift; - return _canonical_name(ref $self); + my $self = shift; + return _canonical_name(ref $self); } sub dry_run { - my ($self, $value) = @_; - if (scalar(@_) > 1) { - $self->{dry_run} = $value; - } - return $self->{dry_run} || 0; + my ($self, $value) = @_; + if (scalar(@_) > 1) { + $self->{dry_run} = $value; + } + return $self->{dry_run} || 0; } sub verbose { - my ($self, $value) = @_; - if (scalar(@_) > 1) { - $self->{verbose} = $value; - } - return $self->{verbose} || 0; + my ($self, $value) = @_; + if (scalar(@_) > 1) { + $self->{verbose} = $value; + } + return $self->{verbose} || 0; } sub debug { - my ($self, $value, $level) = @_; - $level ||= 1; - if ($self->verbose >= $level) { - $value = Dumper($value) if ref $value; - print STDERR $value, "\n"; - } + my ($self, $value, $level) = @_; + $level ||= 1; + if ($self->verbose >= $level) { + $value = Dumper($value) if ref $value; + print STDERR $value, "\n"; + } } sub bug_fields { - my $self = shift; - $self->{bug_fields} ||= Bugzilla->fields({ by_name => 1 }); - return $self->{bug_fields}; + my $self = shift; + $self->{bug_fields} ||= Bugzilla->fields({by_name => 1}); + return $self->{bug_fields}; } sub users { - my $self = shift; - if (!exists $self->{users}) { - print get_text('migrate_reading_users'), "\n"; - $self->{users} = $self->_read_users(); - } - return $self->{users}; + my $self = shift; + if (!exists $self->{users}) { + print get_text('migrate_reading_users'), "\n"; + $self->{users} = $self->_read_users(); + } + return $self->{users}; } sub products { - my $self = shift; - if (!exists $self->{products}) { - print get_text('migrate_reading_products'), "\n"; - $self->{products} = $self->_read_products(); - } - return $self->{products}; + my $self = shift; + if (!exists $self->{products}) { + print get_text('migrate_reading_products'), "\n"; + $self->{products} = $self->_read_products(); + } + return $self->{products}; } sub bugs { - my $self = shift; - if (!exists $self->{bugs}) { - print get_text('migrate_reading_bugs'), "\n"; - $self->{bugs} = $self->_read_bugs(); - } - return $self->{bugs}; + my $self = shift; + if (!exists $self->{bugs}) { + print get_text('migrate_reading_bugs'), "\n"; + $self->{bugs} = $self->_read_bugs(); + } + return $self->{bugs}; } ########### @@ -279,49 +280,49 @@ sub bugs { ########### sub check_requirements { - my $self = shift; - my $missing = Bugzilla::Install::Requirements::_check_missing( - $self->REQUIRED_MODULES, 1); - my %results = ( - apache => [], - pass => @$missing ? 0 : 1, - missing => $missing, - any_missing => @$missing ? 1 : 0, - hide_all => 1, - # These are just for compatibility with print_module_instructions - one_dbd => 1, - optional => [], - ); - Bugzilla::Install::Requirements::print_module_instructions( - \%results, 1); - exit(1) if @$missing; + my $self = shift; + my $missing + = Bugzilla::Install::Requirements::_check_missing($self->REQUIRED_MODULES, 1); + my %results = ( + apache => [], + pass => @$missing ? 0 : 1, + missing => $missing, + any_missing => @$missing ? 1 : 0, + hide_all => 1, + + # These are just for compatibility with print_module_instructions + one_dbd => 1, + optional => [], + ); + Bugzilla::Install::Requirements::print_module_instructions(\%results, 1); + exit(1) if @$missing; } sub reset_serial_values { - my $self = shift; - return if $self->{serial_values_reset}; - my $dbh = Bugzilla->dbh; - my %reset = ( - 'bugs' => 'bug_id', - 'attachments' => 'attach_id', - 'profiles' => 'userid', - 'longdescs' => 'comment_id', - 'products' => 'id', - 'components' => 'id', - 'versions' => 'id', - 'milestones' => 'id', - ); - my @select_fields = grep { $_->is_select } (values %{ $self->bug_fields }); - foreach my $field (@select_fields) { - next if $field->is_abnormal; - $reset{$field->name} = 'id'; - } - - while (my ($table, $column) = each %reset) { - $dbh->bz_set_next_serial_value($table, $column); - } - - $self->{serial_values_reset} = 1; + my $self = shift; + return if $self->{serial_values_reset}; + my $dbh = Bugzilla->dbh; + my %reset = ( + 'bugs' => 'bug_id', + 'attachments' => 'attach_id', + 'profiles' => 'userid', + 'longdescs' => 'comment_id', + 'products' => 'id', + 'components' => 'id', + 'versions' => 'id', + 'milestones' => 'id', + ); + my @select_fields = grep { $_->is_select } (values %{$self->bug_fields}); + foreach my $field (@select_fields) { + next if $field->is_abnormal; + $reset{$field->name} = 'id'; + } + + while (my ($table, $column) = each %reset) { + $dbh->bz_set_next_serial_value($table, $column); + } + + $self->{serial_values_reset} = 1; } ################### @@ -329,160 +330,167 @@ sub reset_serial_values { ################### sub translate_all_bugs { - my ($self, $bugs) = @_; - print get_text('migrate_translating_bugs'), "\n"; - # We modify the array in place so that $self->bugs will return the - # modified bugs, in case $self->before_insert wants them. - my $num_bugs = scalar(@$bugs); - for (my $i = 0; $i < $num_bugs; $i++) { - $bugs->[$i] = $self->translate_bug($bugs->[$i]); - } + my ($self, $bugs) = @_; + print get_text('migrate_translating_bugs'), "\n"; + + # We modify the array in place so that $self->bugs will return the + # modified bugs, in case $self->before_insert wants them. + my $num_bugs = scalar(@$bugs); + for (my $i = 0; $i < $num_bugs; $i++) { + $bugs->[$i] = $self->translate_bug($bugs->[$i]); + } } sub translate_bug { - my ($self, $fields) = @_; - my (%bug, %other_fields); - my $original_status; - foreach my $field (keys %$fields) { - my $value = delete $fields->{$field}; - my $bz_field = $self->translate_field($field); - if ($bz_field) { - $bug{$bz_field} = $self->translate_value($bz_field, $value); - if ($bz_field eq 'bug_status') { - $original_status = $value; - } - } - else { - $other_fields{$field} = $value; - } + my ($self, $fields) = @_; + my (%bug, %other_fields); + my $original_status; + foreach my $field (keys %$fields) { + my $value = delete $fields->{$field}; + my $bz_field = $self->translate_field($field); + if ($bz_field) { + $bug{$bz_field} = $self->translate_value($bz_field, $value); + if ($bz_field eq 'bug_status') { + $original_status = $value; + } } - - if (defined $original_status and !defined $bug{resolution} - and $self->map_value('bug_status_resolution', $original_status)) - { - $bug{resolution} = $self->map_value('bug_status_resolution', - $original_status); + else { + $other_fields{$field} = $value; } + } - $bug{comment} = $self->_generate_description(\%bug, \%other_fields); + if ( defined $original_status + and !defined $bug{resolution} + and $self->map_value('bug_status_resolution', $original_status)) + { + $bug{resolution} = $self->map_value('bug_status_resolution', $original_status); + } - return wantarray ? (\%bug, \%other_fields) : \%bug; + $bug{comment} = $self->_generate_description(\%bug, \%other_fields); + + return wantarray ? (\%bug, \%other_fields) : \%bug; } sub _generate_description { - my ($self, $bug, $fields) = @_; - - my $description = ""; - foreach my $field (sort keys %$fields) { - next if grep($_ eq $field, $self->NON_COMMENT_FIELDS); - my $value = delete $fields->{$field}; - next if $value eq ''; - $description .= "$field: $value\n"; - } - $description .= "\n" if $description; - - return $description . $bug->{comment}; + my ($self, $bug, $fields) = @_; + + my $description = ""; + foreach my $field (sort keys %$fields) { + next if grep($_ eq $field, $self->NON_COMMENT_FIELDS); + my $value = delete $fields->{$field}; + next if $value eq ''; + $description .= "$field: $value\n"; + } + $description .= "\n" if $description; + + return $description . $bug->{comment}; } sub translate_field { - my ($self, $field) = @_; - my $mapped = $self->config('translate_fields')->{$field}; - return $mapped if defined $mapped; - ($mapped) = grep { lc($_) eq lc($field) } (keys %{ $self->bug_fields }); - return $mapped; + my ($self, $field) = @_; + my $mapped = $self->config('translate_fields')->{$field}; + return $mapped if defined $mapped; + ($mapped) = grep { lc($_) eq lc($field) } (keys %{$self->bug_fields}); + return $mapped; } sub parse_date { - my ($self, $date) = @_; - my @time = strptime($date); - # Handle times with timezones that strptime doesn't know about. - if (!scalar @time) { - $date =~ s/\s+\S+$//; - @time = strptime($date); + my ($self, $date) = @_; + my @time = strptime($date); + + # Handle times with timezones that strptime doesn't know about. + if (!scalar @time) { + $date =~ s/\s+\S+$//; + @time = strptime($date); + } + my $tz; + if ($time[6]) { + $tz = Bugzilla->local_timezone->offset_as_string($time[6]); + } + else { + $tz = $self->config('timezone'); + $tz =~ s/\s/_/g; + if ($tz eq 'local') { + $tz = Bugzilla->local_timezone; } - my $tz; - if ($time[6]) { - $tz = Bugzilla->local_timezone->offset_as_string($time[6]); - } - else { - $tz = $self->config('timezone'); - $tz =~ s/\s/_/g; - if ($tz eq 'local') { - $tz = Bugzilla->local_timezone; - } - } - my $dt = DateTime->new({ - year => $time[5] + 1900, - month => $time[4] + 1, - day => $time[3], - hour => $time[2], - minute => $time[1], - second => int($time[0]), - time_zone => $tz, - }); - $dt->set_time_zone(Bugzilla->local_timezone); - return $dt->iso8601; + } + my $dt = DateTime->new({ + year => $time[5] + 1900, + month => $time[4] + 1, + day => $time[3], + hour => $time[2], + minute => $time[1], + second => int($time[0]), + time_zone => $tz, + }); + $dt->set_time_zone(Bugzilla->local_timezone); + return $dt->iso8601; } sub translate_value { - my ($self, $field, $value) = @_; + my ($self, $field, $value) = @_; - if (!defined $value) { - warn("Got undefined value for $field\n"); - $value = ''; - } + if (!defined $value) { + warn("Got undefined value for $field\n"); + $value = ''; + } - if (ref($value) eq 'ARRAY') { - return [ map($self->translate_value($field, $_), @$value) ]; - } + if (ref($value) eq 'ARRAY') { + return [map($self->translate_value($field, $_), @$value)]; + } - if (defined $self->map_value($field, $value)) { - return $self->map_value($field, $value); - } - - if (grep($_ eq $field, USER_FIELDS)) { - if (defined $self->map_value('user', $value)) { - return $self->map_value('user', $value); - } - } + if (defined $self->map_value($field, $value)) { + return $self->map_value($field, $value); + } - my $field_obj = $self->bug_fields->{$field}; - if ($field eq 'creation_ts' - or $field eq 'delta_ts' - or ($field_obj and - ($field_obj->type == FIELD_TYPE_DATETIME - or $field_obj->type == FIELD_TYPE_DATE))) - { - $value = trim($value); - return undef if !$value; - return $self->parse_date($value); + if (grep($_ eq $field, USER_FIELDS)) { + if (defined $self->map_value('user', $value)) { + return $self->map_value('user', $value); } - - return $value; + } + + my $field_obj = $self->bug_fields->{$field}; + if ( + $field eq 'creation_ts' + or $field eq 'delta_ts' + or ( + $field_obj + and + ($field_obj->type == FIELD_TYPE_DATETIME or $field_obj->type == FIELD_TYPE_DATE) + ) + ) + { + $value = trim($value); + return undef if !$value; + return $self->parse_date($value); + } + + return $value; } sub map_value { - my ($self, $field, $value) = @_; - return $self->_value_map->{$field}->{lc($value)}; + my ($self, $field, $value) = @_; + return $self->_value_map->{$field}->{lc($value)}; } sub _value_map { - my $self = shift; - if (!defined $self->{_value_map}) { - # Lowercase all values to make them case-insensitive. - my %map; - my $translation = $self->config('translate_values'); - foreach my $field (keys %$translation) { - my $value_mapping = $translation->{$field}; - foreach my $value (keys %$value_mapping) { - $map{$field}->{lc($value)} = $value_mapping->{$value}; - } - } - $self->{_value_map} = \%map; + my $self = shift; + if (!defined $self->{_value_map}) { + + # Lowercase all values to make them case-insensitive. + my %map; + my $translation = $self->config('translate_values'); + foreach my $field (keys %$translation) { + my $value_mapping = $translation->{$field}; + foreach my $value (keys %$value_mapping) { + $map{$field}->{lc($value)} = $value_mapping->{$value}; + } } - return $self->{_value_map}; + $self->{_value_map} = \%map; + } + return $self->{_value_map}; } ################# @@ -490,386 +498,401 @@ sub _value_map { ################# sub config { - my ($self, $var) = @_; - if (!exists $self->{config}) { - $self->{config} = $self->read_config; - } - return $self->{config}->{$var}; + my ($self, $var) = @_; + if (!exists $self->{config}) { + $self->{config} = $self->read_config; + } + return $self->{config}->{$var}; } sub config_file_name { - my $self = shift; - my $name = $self->name; - my $dir = bz_locations()->{datadir}; - return "$dir/migrate-$name.cfg" + my $self = shift; + my $name = $self->name; + my $dir = bz_locations()->{datadir}; + return "$dir/migrate-$name.cfg"; } sub read_config { - my ($self) = @_; - my $file = $self->config_file_name; - if (!-e $file) { - $self->write_config(); - ThrowUserError('migrate_config_created', { file => $file }); - } - open(my $fh, "<", $file) || die "$file: $!"; - my $safe = new Safe; - $safe->rdo($file); - my @read_symbols = map($_->{name}, $self->CONFIG_VARS); - my %config; - foreach my $var (@read_symbols) { - my $glob = $safe->varglob($var); - $config{$var} = $$glob; - } - return \%config; + my ($self) = @_; + my $file = $self->config_file_name; + if (!-e $file) { + $self->write_config(); + ThrowUserError('migrate_config_created', {file => $file}); + } + open(my $fh, "<", $file) || die "$file: $!"; + my $safe = new Safe; + $safe->rdo($file); + my @read_symbols = map($_->{name}, $self->CONFIG_VARS); + my %config; + foreach my $var (@read_symbols) { + my $glob = $safe->varglob($var); + $config{$var} = $$glob; + } + return \%config; } sub write_config { - my ($self) = @_; - my $file = $self->config_file_name; - open(my $fh, ">", $file) || die "$file: $!"; - # Fixed indentation - local $Data::Dumper::Indent = 1; - local $Data::Dumper::Quotekeys = 0; - local $Data::Dumper::Sortkeys = 1; - foreach my $var ($self->CONFIG_VARS) { - print $fh "\n", $var->{desc}, - Data::Dumper->Dump([$var->{default}], [$var->{name}]); - } - close($fh); + my ($self) = @_; + my $file = $self->config_file_name; + open(my $fh, ">", $file) || die "$file: $!"; + + # Fixed indentation + local $Data::Dumper::Indent = 1; + local $Data::Dumper::Quotekeys = 0; + local $Data::Dumper::Sortkeys = 1; + foreach my $var ($self->CONFIG_VARS) { + print $fh "\n", $var->{desc}, + Data::Dumper->Dump([$var->{default}], [$var->{name}]); + } + close($fh); } #################################### # Default Implementations of Hooks # #################################### -sub after_insert {} -sub before_insert {} -sub after_read {} +sub after_insert { } +sub before_insert { } +sub after_read { } ############# # Inserters # ############# sub insert_users { - my ($self, $users) = @_; - foreach my $user (@$users) { - next if new Bugzilla::User({ name => $user->{login_name} }); - my $generated_password; - if (!defined $user->{cryptpassword}) { - $generated_password = lc(generate_random_password()); - $user->{cryptpassword} = $generated_password; - } - my $created = Bugzilla::User->create($user); - print get_text('migrate_user_created', - { created => $created, - password => $generated_password }), "\n"; + my ($self, $users) = @_; + foreach my $user (@$users) { + next if new Bugzilla::User({name => $user->{login_name}}); + my $generated_password; + if (!defined $user->{cryptpassword}) { + $generated_password = lc(generate_random_password()); + $user->{cryptpassword} = $generated_password; } + my $created = Bugzilla::User->create($user); + print get_text('migrate_user_created', + {created => $created, password => $generated_password}), + "\n"; + } } # XXX This should also insert Classifications. sub insert_products { - my ($self, $products) = @_; - foreach my $product (@$products) { - my $components = delete $product->{components}; - - my $created_prod = new Bugzilla::Product({ name => $product->{name} }); - if (!$created_prod) { - $created_prod = Bugzilla::Product->create($product); - print get_text('migrate_product_created', - { created => $created_prod }), "\n"; - } + my ($self, $products) = @_; + foreach my $product (@$products) { + my $components = delete $product->{components}; + + my $created_prod = new Bugzilla::Product({name => $product->{name}}); + if (!$created_prod) { + $created_prod = Bugzilla::Product->create($product); + print get_text('migrate_product_created', {created => $created_prod}), "\n"; + } - foreach my $component (@$components) { - next if new Bugzilla::Component({ product => $created_prod, - name => $component->{name} }); - my $created_comp = Bugzilla::Component->create( - { %$component, product => $created_prod }); - print ' ', get_text('migrate_component_created', - { comp => $created_comp, - product => $created_prod }), "\n"; - } + foreach my $component (@$components) { + next + if new Bugzilla::Component({ + product => $created_prod, name => $component->{name} + }); + my $created_comp + = Bugzilla::Component->create({%$component, product => $created_prod}); + print ' ', + get_text('migrate_component_created', + {comp => $created_comp, product => $created_prod}), + "\n"; } + } } sub create_custom_fields { - my $self = shift; - foreach my $field (keys %{ $self->CUSTOM_FIELDS }) { - next if new Bugzilla::Field({ name => $field }); - my %values = %{ $self->CUSTOM_FIELDS->{$field} }; - # We set these all here for the dry-run case. - my $created = { %values, name => $field, custom => 1 }; - if (!$self->dry_run) { - $created = Bugzilla::Field->create($created); - } - print get_text('migrate_field_created', { field => $created }), "\n"; + my $self = shift; + foreach my $field (keys %{$self->CUSTOM_FIELDS}) { + next if new Bugzilla::Field({name => $field}); + my %values = %{$self->CUSTOM_FIELDS->{$field}}; + + # We set these all here for the dry-run case. + my $created = {%values, name => $field, custom => 1}; + if (!$self->dry_run) { + $created = Bugzilla::Field->create($created); } - delete $self->{bug_fields}; + print get_text('migrate_field_created', {field => $created}), "\n"; + } + delete $self->{bug_fields}; } sub create_legal_values { - my ($self, $bugs) = @_; - my @select_fields = grep($_->is_select, values %{ $self->bug_fields }); - - # Get all the values in use on all the bugs we're importing. - my (%values, %product_values); - foreach my $bug (@$bugs) { - foreach my $field (@select_fields) { - my $name = $field->name; - next if !defined $bug->{$name}; - $values{$name}->{$bug->{$name}} = 1; - } - foreach my $field (qw(version target_milestone)) { - # Fix per-product bug values here, because it's easier than - # doing it during _insert_bugs. - if (!defined $bug->{$field} or trim($bug->{$field}) eq '') { - my $accessor = $field; - $accessor =~ s/^target_//; $accessor .= "s"; - my $product = Bugzilla::Product->check($bug->{product}); - $bug->{$field} = $product->$accessor->[0]->name; - next; - } - $product_values{$bug->{product}}->{$field}->{$bug->{$field}} = 1; - } - } + my ($self, $bugs) = @_; + my @select_fields = grep($_->is_select, values %{$self->bug_fields}); + # Get all the values in use on all the bugs we're importing. + my (%values, %product_values); + foreach my $bug (@$bugs) { foreach my $field (@select_fields) { - next if $field->is_abnormal; - my $name = $field->name; - foreach my $value (keys %{ $values{$name} }) { - next if Bugzilla::Field::Choice->type($field)->new({ name => $value }); - Bugzilla::Field::Choice->type($field)->create({ value => $value }); - print get_text('migrate_value_created', - { field => $field, value => $value }), "\n"; - } + my $name = $field->name; + next if !defined $bug->{$name}; + $values{$name}->{$bug->{$name}} = 1; } + foreach my $field (qw(version target_milestone)) { + + # Fix per-product bug values here, because it's easier than + # doing it during _insert_bugs. + if (!defined $bug->{$field} or trim($bug->{$field}) eq '') { + my $accessor = $field; + $accessor =~ s/^target_//; + $accessor .= "s"; + my $product = Bugzilla::Product->check($bug->{product}); + $bug->{$field} = $product->$accessor->[0]->name; + next; + } + $product_values{$bug->{product}}->{$field}->{$bug->{$field}} = 1; + } + } + + foreach my $field (@select_fields) { + next if $field->is_abnormal; + my $name = $field->name; + foreach my $value (keys %{$values{$name}}) { + next if Bugzilla::Field::Choice->type($field)->new({name => $value}); + Bugzilla::Field::Choice->type($field)->create({value => $value}); + print get_text('migrate_value_created', {field => $field, value => $value}), + "\n"; + } + } + + foreach my $product (keys %product_values) { + my $prod_obj = Bugzilla::Product->check($product); + foreach my $version (keys %{$product_values{$product}->{version}}) { + next if new Bugzilla::Version({product => $prod_obj, name => $version}); + my $created + = Bugzilla::Version->create({product => $prod_obj, value => $version}); + my $field = $self->bug_fields->{version}; + print get_text('migrate_value_created', + {product => $prod_obj, field => $field, value => $created->name}), + "\n"; + } + foreach my $milestone (keys %{$product_values{$product}->{target_milestone}}) { + next if new Bugzilla::Milestone({product => $prod_obj, name => $milestone}); + my $created + = Bugzilla::Milestone->create({product => $prod_obj, value => $milestone}); + my $field = $self->bug_fields->{target_milestone}; + print get_text('migrate_value_created', + {product => $prod_obj, field => $field, value => $created->name}), + "\n"; - foreach my $product (keys %product_values) { - my $prod_obj = Bugzilla::Product->check($product); - foreach my $version (keys %{ $product_values{$product}->{version} }) { - next if new Bugzilla::Version({ product => $prod_obj, - name => $version }); - my $created = Bugzilla::Version->create({ product => $prod_obj, - value => $version }); - my $field = $self->bug_fields->{version}; - print get_text('migrate_value_created', { product => $prod_obj, - field => $field, - value => $created->name }), "\n"; - } - foreach my $milestone (keys %{ $product_values{$product}->{target_milestone} }) { - next if new Bugzilla::Milestone({ product => $prod_obj, - name => $milestone }); - my $created = Bugzilla::Milestone->create( - { product => $prod_obj, value => $milestone }); - my $field = $self->bug_fields->{target_milestone}; - print get_text('migrate_value_created', { product => $prod_obj, - field => $field, - value => $created->name }), "\n"; - - } } + } } sub insert_bugs { - my ($self, $bugs) = @_; - my $dbh = Bugzilla->dbh; - print get_text('migrate_creating_bugs'), "\n"; - - my $init_statuses = Bugzilla::Status->can_change_to(); - my %allowed_statuses = map { lc($_->name) => 1 } @$init_statuses; - # Bypass the question of whether or not we can file UNCONFIRMED - # in any product by simply picking a non-UNCONFIRMED status as our - # default for bugs that don't have a status specified. - my $default_status = first { $_->name ne 'UNCONFIRMED' } @$init_statuses; - # Use the first resolution that's not blank. - my $default_resolution = - first { $_->name ne '' } - @{ $self->bug_fields->{resolution}->legal_values }; - - # Set the values of any required drop-down fields that aren't set. - my @standard_drop_downs = grep { !$_->custom and $_->is_select } - (values %{ $self->bug_fields }); - # Make bug_status get set before resolution. - @standard_drop_downs = sort { $a->name cmp $b->name } @standard_drop_downs; - # Cache all statuses for setting the resolution. - my %statuses = map { lc($_->name) => $_ } Bugzilla::Status->get_all; - - my $total = scalar @$bugs; - my $count = 1; - foreach my $bug (@$bugs) { - my $comments = delete $bug->{comments}; - my $history = delete $bug->{history}; - my $attachments = delete $bug->{attachments}; - - $self->debug($bug, 3); - - foreach my $field (@standard_drop_downs) { - next if $field->is_abnormal; - my $field_name = $field->name; - if (!defined $bug->{$field_name}) { - # If there's a default value for this, then just let create() - # pick it. - next if grep($_->is_default, @{ $field->legal_values }); - # Otherwise, pick the first valid value if this is a required - # field. - if ($field_name eq 'bug_status') { - $bug->{bug_status} = $default_status; - } - elsif ($field_name eq 'resolution') { - my $status = $statuses{lc($bug->{bug_status})}; - if (!$status->is_open) { - $bug->{resolution} = $default_resolution; - } - } - else { - $bug->{$field_name} = $field->legal_values->[0]->name; - } - } + my ($self, $bugs) = @_; + my $dbh = Bugzilla->dbh; + print get_text('migrate_creating_bugs'), "\n"; + + my $init_statuses = Bugzilla::Status->can_change_to(); + my %allowed_statuses = map { lc($_->name) => 1 } @$init_statuses; + + # Bypass the question of whether or not we can file UNCONFIRMED + # in any product by simply picking a non-UNCONFIRMED status as our + # default for bugs that don't have a status specified. + my $default_status = first { $_->name ne 'UNCONFIRMED' } @$init_statuses; + + # Use the first resolution that's not blank. + my $default_resolution = first { $_->name ne '' } + @{$self->bug_fields->{resolution}->legal_values}; + + # Set the values of any required drop-down fields that aren't set. + my @standard_drop_downs + = grep { !$_->custom and $_->is_select } (values %{$self->bug_fields}); + + # Make bug_status get set before resolution. + @standard_drop_downs = sort { $a->name cmp $b->name } @standard_drop_downs; + + # Cache all statuses for setting the resolution. + my %statuses = map { lc($_->name) => $_ } Bugzilla::Status->get_all; + + my $total = scalar @$bugs; + my $count = 1; + foreach my $bug (@$bugs) { + my $comments = delete $bug->{comments}; + my $history = delete $bug->{history}; + my $attachments = delete $bug->{attachments}; + + $self->debug($bug, 3); + + foreach my $field (@standard_drop_downs) { + next if $field->is_abnormal; + my $field_name = $field->name; + if (!defined $bug->{$field_name}) { + + # If there's a default value for this, then just let create() + # pick it. + next if grep($_->is_default, @{$field->legal_values}); + + # Otherwise, pick the first valid value if this is a required + # field. + if ($field_name eq 'bug_status') { + $bug->{bug_status} = $default_status; } - - my $product = Bugzilla::Product->check($bug->{product}); - - # If this isn't a legal starting status, or if the bug has a - # resolution, then those will have to be set after creating the bug. - # We make them into objects so that we can normalize their names. - my ($set_status, $set_resolution); - if (defined $bug->{resolution}) { - $set_resolution = Bugzilla::Field::Choice->type('resolution') - ->new({ name => delete $bug->{resolution} }); + elsif ($field_name eq 'resolution') { + my $status = $statuses{lc($bug->{bug_status})}; + if (!$status->is_open) { + $bug->{resolution} = $default_resolution; + } } - if (!$allowed_statuses{lc($bug->{bug_status})}) { - $set_status = new Bugzilla::Status({ name => $bug->{bug_status} }); - # Set the starting status to some status that Bugzilla will - # accept. We're going to overwrite it immediately afterward. - $bug->{bug_status} = $default_status; + else { + $bug->{$field_name} = $field->legal_values->[0]->name; } + } + } - # If we're in dry-run mode, our custom fields haven't been created - # yet, so we shouldn't try to set them on creation. - if ($self->dry_run) { - foreach my $field (keys %{ $self->CUSTOM_FIELDS }) { - delete $bug->{$field}; - } - } + my $product = Bugzilla::Product->check($bug->{product}); + + # If this isn't a legal starting status, or if the bug has a + # resolution, then those will have to be set after creating the bug. + # We make them into objects so that we can normalize their names. + my ($set_status, $set_resolution); + if (defined $bug->{resolution}) { + $set_resolution = Bugzilla::Field::Choice->type('resolution') + ->new({name => delete $bug->{resolution}}); + } + if (!$allowed_statuses{lc($bug->{bug_status})}) { + $set_status = new Bugzilla::Status({name => $bug->{bug_status}}); + + # Set the starting status to some status that Bugzilla will + # accept. We're going to overwrite it immediately afterward. + $bug->{bug_status} = $default_status; + } - # File the bug as the reporter. - my $super_user = Bugzilla->user; - my $reporter = Bugzilla::User->check($bug->{reporter}); - # Allow the user to file a bug in any product, no matter his current - # permissions. - $reporter->{groups} = $super_user->groups; - Bugzilla->set_user($reporter); - my $created = Bugzilla::Bug->create($bug); - $self->debug('Created bug ' . $created->id); - Bugzilla->set_user($super_user); - - if (defined $bug->{creation_ts}) { - $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ? + # If we're in dry-run mode, our custom fields haven't been created + # yet, so we shouldn't try to set them on creation. + if ($self->dry_run) { + foreach my $field (keys %{$self->CUSTOM_FIELDS}) { + delete $bug->{$field}; + } + } + + # File the bug as the reporter. + my $super_user = Bugzilla->user; + my $reporter = Bugzilla::User->check($bug->{reporter}); + + # Allow the user to file a bug in any product, no matter his current + # permissions. + $reporter->{groups} = $super_user->groups; + Bugzilla->set_user($reporter); + my $created = Bugzilla::Bug->create($bug); + $self->debug('Created bug ' . $created->id); + Bugzilla->set_user($super_user); + + if (defined $bug->{creation_ts}) { + $dbh->do( + 'UPDATE bugs SET creation_ts = ?, delta_ts = ? WHERE bug_id = ?', undef, $bug->{creation_ts}, - $bug->{creation_ts}, $created->id); - } - if (defined $bug->{delta_ts}) { - $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', - undef, $bug->{delta_ts}, $created->id); - } - # We don't need to send email for imported bugs. - $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?', - undef, $created->id); - - # We don't use set_ and update() because that would create - # a bugs_activity entry that we don't want. - if ($set_status) { - $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_id = ?', - undef, $set_status->name, $created->id); - } - if ($set_resolution) { - $dbh->do('UPDATE bugs SET resolution = ? WHERE bug_id = ?', - undef, $set_resolution->name, $created->id); - } + $bug->{creation_ts}, $created->id + ); + } + if (defined $bug->{delta_ts}) { + $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?', + undef, $bug->{delta_ts}, $created->id); + } - $self->_insert_comments($created, $comments); - $self->_insert_history($created, $history); - $self->_insert_attachments($created, $attachments); + # We don't need to send email for imported bugs. + $dbh->do('UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?', + undef, $created->id); - # bugs_fulltext isn't transactional, so if we're in a dry-run we - # need to delete anything that we put in there. - if ($self->dry_run) { - $dbh->do('DELETE FROM bugs_fulltext WHERE bug_id = ?', - undef, $created->id); - } + # We don't use set_ and update() because that would create + # a bugs_activity entry that we don't want. + if ($set_status) { + $dbh->do('UPDATE bugs SET bug_status = ? WHERE bug_id = ?', + undef, $set_status->name, $created->id); + } + if ($set_resolution) { + $dbh->do('UPDATE bugs SET resolution = ? WHERE bug_id = ?', + undef, $set_resolution->name, $created->id); + } - if (!$self->verbose) { - indicate_progress({ current => $count++, every => 5, total => $total }); - } + $self->_insert_comments($created, $comments); + $self->_insert_history($created, $history); + $self->_insert_attachments($created, $attachments); + + # bugs_fulltext isn't transactional, so if we're in a dry-run we + # need to delete anything that we put in there. + if ($self->dry_run) { + $dbh->do('DELETE FROM bugs_fulltext WHERE bug_id = ?', undef, $created->id); + } + + if (!$self->verbose) { + indicate_progress({current => $count++, every => 5, total => $total}); } + } } sub _insert_comments { - my ($self, $bug, $comments) = @_; - return if !$comments; - $self->debug(' Inserting comments:', 2); - foreach my $comment (@$comments) { - $self->debug($comment, 3); - my %copy = %$comment; - # XXX In the future, if we have a Bugzilla::Comment->create, this - # should use it. - my $who = Bugzilla::User->check(delete $copy{who}); - $copy{who} = $who->id; - $copy{bug_id} = $bug->id; - $self->_do_table_insert('longdescs', \%copy); - $self->debug(" Inserted comment from " . $who->login, 2); - } - $bug->_sync_fulltext( update_comments => 1 ); + my ($self, $bug, $comments) = @_; + return if !$comments; + $self->debug(' Inserting comments:', 2); + foreach my $comment (@$comments) { + $self->debug($comment, 3); + my %copy = %$comment; + + # XXX In the future, if we have a Bugzilla::Comment->create, this + # should use it. + my $who = Bugzilla::User->check(delete $copy{who}); + $copy{who} = $who->id; + $copy{bug_id} = $bug->id; + $self->_do_table_insert('longdescs', \%copy); + $self->debug(" Inserted comment from " . $who->login, 2); + } + $bug->_sync_fulltext(update_comments => 1); } sub _insert_history { - my ($self, $bug, $history) = @_; - return if !$history; - $self->debug(' Inserting history:', 2); - foreach my $item (@$history) { - $self->debug($item, 3); - my $who = Bugzilla::User->check($item->{who}); - LogActivityEntry($bug->id, $item->{field}, $item->{removed}, - $item->{added}, $who->id, $item->{bug_when}); - $self->debug(" $item->{field} change from " . $who->login, 2); - } + my ($self, $bug, $history) = @_; + return if !$history; + $self->debug(' Inserting history:', 2); + foreach my $item (@$history) { + $self->debug($item, 3); + my $who = Bugzilla::User->check($item->{who}); + LogActivityEntry($bug->id, $item->{field}, $item->{removed}, $item->{added}, + $who->id, $item->{bug_when}); + $self->debug(" $item->{field} change from " . $who->login, 2); + } } sub _insert_attachments { - my ($self, $bug, $attachments) = @_; - return if !$attachments; - $self->debug(' Inserting attachments:', 2); - foreach my $attachment (@$attachments) { - $self->debug($attachment, 3); - # Make sure that our pointer is at the beginning of the file, - # because usually it will be at the end, having just been fully - # written to. - if (ref $attachment->{data}) { - $attachment->{data}->seek(0, SEEK_SET); - } - - my $submitter = Bugzilla::User->check(delete $attachment->{submitter}); - my $super_user = Bugzilla->user; - # Make sure the submitter can attach this attachment no matter what. - $submitter->{groups} = $super_user->groups; - Bugzilla->set_user($submitter); - my $created = - Bugzilla::Attachment->create({ %$attachment, bug => $bug }); - $self->debug(' Attachment ' . $created->description . ' from ' - . $submitter->login, 2); - Bugzilla->set_user($super_user); + my ($self, $bug, $attachments) = @_; + return if !$attachments; + $self->debug(' Inserting attachments:', 2); + foreach my $attachment (@$attachments) { + $self->debug($attachment, 3); + + # Make sure that our pointer is at the beginning of the file, + # because usually it will be at the end, having just been fully + # written to. + if (ref $attachment->{data}) { + $attachment->{data}->seek(0, SEEK_SET); } + + my $submitter = Bugzilla::User->check(delete $attachment->{submitter}); + my $super_user = Bugzilla->user; + + # Make sure the submitter can attach this attachment no matter what. + $submitter->{groups} = $super_user->groups; + Bugzilla->set_user($submitter); + my $created = Bugzilla::Attachment->create({%$attachment, bug => $bug}); + $self->debug( + ' Attachment ' . $created->description . ' from ' . $submitter->login, 2); + Bugzilla->set_user($super_user); + } } sub _do_table_insert { - my ($self, $table, $hash) = @_; - my @fields = keys %$hash; - my @questions = ('?') x @fields; - my @values = map { $hash->{$_} } @fields; - my $field_sql = join(',', @fields); - my $question_sql = join(',', @questions); - Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)", - undef, @values); + my ($self, $table, $hash) = @_; + my @fields = keys %$hash; + my @questions = ('?') x @fields; + my @values = map { $hash->{$_} } @fields; + my $field_sql = join(',', @fields); + my $question_sql = join(',', @questions); + Bugzilla->dbh->do("INSERT INTO $table ($field_sql) VALUES ($question_sql)", + undef, @values); } ###################### @@ -877,11 +900,11 @@ sub _do_table_insert { ###################### sub _canonical_name { - my ($module) = @_; - $module =~ s{::}{/}g; - $module = basename($module); - $module =~ s/\.pm$//g; - return $module; + my ($module) = @_; + $module =~ s{::}{/}g; + $module = basename($module); + $module =~ s/\.pm$//g; + return $module; } 1; diff --git a/Bugzilla/Migrate/Gnats.pm b/Bugzilla/Migrate/Gnats.pm index 4ac9cd925..a562abf12 100644 --- a/Bugzilla/Migrate/Gnats.pm +++ b/Bugzilla/Migrate/Gnats.pm @@ -25,88 +25,87 @@ use List::MoreUtils qw(firstidx); use List::Util qw(first); use constant REQUIRED_MODULES => [ - { - package => 'Email-Simple-FromHandle', - module => 'Email::Simple::FromHandle', - # This version added seekable handles. - version => 0.050, - }, + { + package => 'Email-Simple-FromHandle', + module => 'Email::Simple::FromHandle', + + # This version added seekable handles. + version => 0.050, + }, ]; use constant FIELD_MAP => { - 'Number' => 'bug_id', - 'Category' => 'product', - 'Synopsis' => 'short_desc', - 'Responsible' => 'assigned_to', - 'State' => 'bug_status', - 'Class' => 'cf_type', - 'Classification' => '', - 'Originator' => 'reporter', - 'Arrival-Date' => 'creation_ts', - 'Last-Modified' => 'delta_ts', - 'Release' => 'version', - 'Severity' => 'bug_severity', - 'Description' => 'comment', + 'Number' => 'bug_id', + 'Category' => 'product', + 'Synopsis' => 'short_desc', + 'Responsible' => 'assigned_to', + 'State' => 'bug_status', + 'Class' => 'cf_type', + 'Classification' => '', + 'Originator' => 'reporter', + 'Arrival-Date' => 'creation_ts', + 'Last-Modified' => 'delta_ts', + 'Release' => 'version', + 'Severity' => 'bug_severity', + 'Description' => 'comment', }; use constant VALUE_MAP => { - bug_severity => { - 'serious' => 'major', - 'cosmetic' => 'trivial', - 'new-feature' => 'enhancement', - 'non-critical' => 'normal', - }, - bug_status => { - 'open' => 'CONFIRMED', - 'analyzed' => 'IN_PROGRESS', - 'suspended' => 'RESOLVED', - 'feedback' => 'RESOLVED', - 'released' => 'VERIFIED', - }, - bug_status_resolution => { - 'feedback' => 'FIXED', - 'released' => 'FIXED', - 'closed' => 'FIXED', - 'suspended' => 'LATER', - }, - priority => { - 'medium' => 'Normal', - }, + bug_severity => { + 'serious' => 'major', + 'cosmetic' => 'trivial', + 'new-feature' => 'enhancement', + 'non-critical' => 'normal', + }, + bug_status => { + 'open' => 'CONFIRMED', + 'analyzed' => 'IN_PROGRESS', + 'suspended' => 'RESOLVED', + 'feedback' => 'RESOLVED', + 'released' => 'VERIFIED', + }, + bug_status_resolution => { + 'feedback' => 'FIXED', + 'released' => 'FIXED', + 'closed' => 'FIXED', + 'suspended' => 'LATER', + }, + priority => {'medium' => 'Normal',}, }; use constant GNATS_CONFIG_VARS => ( - { - name => 'gnats_path', - default => '/var/lib/gnats', - desc => < 'gnats_path', + default => '/var/lib/gnats', + desc => < 'default_email_domain', - default => 'example.com', - desc => <<'END', + }, + { + name => 'default_email_domain', + default => 'example.com', + desc => <<'END', # Some GNATS users do not have full email addresses, but Bugzilla requires # every user to have an email address. What domain should be appended to # usernames that don't have emails, to make them into email addresses? # (For example, if you leave this at the default, "unknown" would become # "unknown@example.com".) END - }, - { - name => 'component_name', - default => 'General', - desc => <<'END', + }, + { + name => 'component_name', + default => 'General', + desc => <<'END', # GNATS has only "Category" to classify bugs. However, Bugzilla has a # multi-level system of Products that contain Components. When importing # GNATS categories, they become a Product with one Component. What should # the name of that Component be? END - }, - { - name => 'version_regex', - default => '', - desc => <<'END', + }, + { + name => 'version_regex', + default => '', + desc => <<'END', # In GNATS, the "version" field can contain almost anything. However, in # Bugzilla, it's a drop-down, so you don't want too many choices in there. # If you specify a regular expression here, versions will be tested against @@ -115,43 +114,43 @@ END # as the version value for the bug instead of the full version value specified # in GNATS. END - }, - { - name => 'default_originator', - default => 'gnats-admin', - desc => <<'END', + }, + { + name => 'default_originator', + default => 'gnats-admin', + desc => <<'END', # Sometimes, a PR has no valid Originator, so we fall back to the From # header of the email. If the From header also isn't a valid username # (is just a name with spaces in it--we can't convert that to an email # address) then this username (which can either be a GNATS username or an # email address) will be considered to be the Originator of the PR. END - } + } ); sub CONFIG_VARS { - my $self = shift; - my @vars = (GNATS_CONFIG_VARS, $self->SUPER::CONFIG_VARS); - my $field_map = first { $_->{name} eq 'translate_fields' } @vars; - $field_map->{default} = FIELD_MAP; - my $value_map = first { $_->{name} eq 'translate_values' } @vars; - $value_map->{default} = VALUE_MAP; - return @vars; + my $self = shift; + my @vars = (GNATS_CONFIG_VARS, $self->SUPER::CONFIG_VARS); + my $field_map = first { $_->{name} eq 'translate_fields' } @vars; + $field_map->{default} = FIELD_MAP; + my $value_map = first { $_->{name} eq 'translate_values' } @vars; + $value_map->{default} = VALUE_MAP; + return @vars; } # Directories that aren't projects, or that we shouldn't be parsing use constant SKIP_DIRECTORIES => qw( - gnats-adm - gnats-queue - pending + gnats-adm + gnats-queue + pending ); use constant NON_COMMENT_FIELDS => qw( - Audit-Trail - Closed-Date - Confidential - Unformatted - attachments + Audit-Trail + Closed-Date + Confidential + Unformatted + attachments ); # Certain fields can contain things that look like fields in them, @@ -160,20 +159,16 @@ use constant NON_COMMENT_FIELDS => qw( # and wait for the next field to consider that we actually have # a field to parse. use constant END_FIELD_ORDER => qw( - Description - How-To-Repeat - Fix - Release-Note - Audit-Trail - Unformatted + Description + How-To-Repeat + Fix + Release-Note + Audit-Trail + Unformatted ); -use constant CUSTOM_FIELDS => { - cf_type => { - type => FIELD_TYPE_SINGLE_SELECT, - description => 'Type', - }, -}; +use constant CUSTOM_FIELDS => + {cf_type => {type => FIELD_TYPE_SINGLE_SELECT, description => 'Type',},}; use constant FIELD_REGEX => qr/^>(\S+):\s*(.*)$/; @@ -192,24 +187,24 @@ use constant LONG_VERSION_LENGTH => 32; ######### sub before_insert { - my $self = shift; - - # gnats_id isn't a valid User::create field, and we don't need it - # anymore now. - delete $_->{gnats_id} foreach @{ $self->users }; - - # Grab a version out of a bug for each product, so that there is a - # valid "version" argument for Bugzilla::Product->create. - foreach my $product (@{ $self->products }) { - my $bug = first { $_->{product} eq $product->{name} and $_->{version} } - @{ $self->bugs }; - if (defined $bug) { - $product->{version} = $bug->{version}; - } - else { - $product->{version} = 'unspecified'; - } + my $self = shift; + + # gnats_id isn't a valid User::create field, and we don't need it + # anymore now. + delete $_->{gnats_id} foreach @{$self->users}; + + # Grab a version out of a bug for each product, so that there is a + # valid "version" argument for Bugzilla::Product->create. + foreach my $product (@{$self->products}) { + my $bug = first { $_->{product} eq $product->{name} and $_->{version} } + @{$self->bugs}; + if (defined $bug) { + $product->{version} = $bug->{version}; + } + else { + $product->{version} = 'unspecified'; } + } } ######### @@ -217,53 +212,53 @@ sub before_insert { ######### sub _read_users { - my $self = shift; - my $path = $self->config('gnats_path'); - my $file = "$path/gnats-adm/responsible"; - $self->debug("Reading users from $file"); - my $default_domain = $self->config('default_email_domain'); - open(my $users_fh, '<', $file) || die "$file: $!"; - my @users; - foreach my $line (<$users_fh>) { - $line = trim($line); - next if $line =~ /^#/; - my ($id, $name, $email) = split(':', $line, 3); - $email ||= "$id\@$default_domain"; - # We can't call our own translate_value, because that depends on - # the existence of user_map, which doesn't exist until after - # this method. However, we still want to translate any users found. - $email = $self->SUPER::translate_value('user', $email); - push(@users, { realname => $name, login_name => $email, - gnats_id => $id }); - } - close($users_fh); - return \@users; + my $self = shift; + my $path = $self->config('gnats_path'); + my $file = "$path/gnats-adm/responsible"; + $self->debug("Reading users from $file"); + my $default_domain = $self->config('default_email_domain'); + open(my $users_fh, '<', $file) || die "$file: $!"; + my @users; + foreach my $line (<$users_fh>) { + $line = trim($line); + next if $line =~ /^#/; + my ($id, $name, $email) = split(':', $line, 3); + $email ||= "$id\@$default_domain"; + + # We can't call our own translate_value, because that depends on + # the existence of user_map, which doesn't exist until after + # this method. However, we still want to translate any users found. + $email = $self->SUPER::translate_value('user', $email); + push(@users, {realname => $name, login_name => $email, gnats_id => $id}); + } + close($users_fh); + return \@users; } sub user_map { - my $self = shift; - $self->{user_map} ||= { map { $_->{gnats_id} => $_->{login_name} } - @{ $self->users } }; - return $self->{user_map}; + my $self = shift; + $self->{user_map} + ||= {map { $_->{gnats_id} => $_->{login_name} } @{$self->users}}; + return $self->{user_map}; } sub add_user { - my ($self, $id, $email) = @_; - return if defined $self->user_map->{$id}; - $self->user_map->{$id} = $email; - push(@{ $self->users }, { login_name => $email, gnats_id => $id }); + my ($self, $id, $email) = @_; + return if defined $self->user_map->{$id}; + $self->user_map->{$id} = $email; + push(@{$self->users}, {login_name => $email, gnats_id => $id}); } sub user_to_email { - my ($self, $value) = @_; - if (defined $self->user_map->{$value}) { - $value = $self->user_map->{$value}; - } - elsif ($value !~ /@/) { - my $domain = $self->config('default_email_domain'); - $value = "$value\@$domain"; - } - return $value; + my ($self, $value) = @_; + if (defined $self->user_map->{$value}) { + $value = $self->user_map->{$value}; + } + elsif ($value !~ /@/) { + my $domain = $self->config('default_email_domain'); + $value = "$value\@$domain"; + } + return $value; } ############ @@ -271,31 +266,33 @@ sub user_to_email { ############ sub _read_products { - my $self = shift; - my $path = $self->config('gnats_path'); - my $file = "$path/gnats-adm/categories"; - $self->debug("Reading categories from $file"); - - open(my $categories_fh, '<', $file) || die "$file: $!"; - my @products; - foreach my $line (<$categories_fh>) { - $line = trim($line); - next if $line =~ /^#/; - my ($name, $description, $assigned_to, $cc) = split(':', $line, 4); - my %product = ( name => $name, description => $description ); - - my @initial_cc = split(',', $cc); - @initial_cc = @{ $self->translate_value('user', \@initial_cc) }; - $assigned_to = $self->translate_value('user', $assigned_to); - my %component = ( name => $self->config('component_name'), - description => $description, - initialowner => $assigned_to, - initial_cc => \@initial_cc ); - $product{components} = [\%component]; - push(@products, \%product); - } - close($categories_fh); - return \@products; + my $self = shift; + my $path = $self->config('gnats_path'); + my $file = "$path/gnats-adm/categories"; + $self->debug("Reading categories from $file"); + + open(my $categories_fh, '<', $file) || die "$file: $!"; + my @products; + foreach my $line (<$categories_fh>) { + $line = trim($line); + next if $line =~ /^#/; + my ($name, $description, $assigned_to, $cc) = split(':', $line, 4); + my %product = (name => $name, description => $description); + + my @initial_cc = split(',', $cc); + @initial_cc = @{$self->translate_value('user', \@initial_cc)}; + $assigned_to = $self->translate_value('user', $assigned_to); + my %component = ( + name => $self->config('component_name'), + description => $description, + initialowner => $assigned_to, + initial_cc => \@initial_cc + ); + $product{components} = [\%component]; + push(@products, \%product); + } + close($categories_fh); + return \@products; } ################ @@ -303,128 +300,131 @@ sub _read_products { ################ sub _read_bugs { - my $self = shift; - my $path = $self->config('gnats_path'); - my @directories = glob("$path/*"); - my @bugs; - foreach my $directory (@directories) { - next if !-d $directory; - my $name = basename($directory); - next if grep($_ eq $name, SKIP_DIRECTORIES); - push(@bugs, @{ $self->_parse_project($directory) }); - } - @bugs = sort { $a->{Number} <=> $b->{Number} } @bugs; - return \@bugs; + my $self = shift; + my $path = $self->config('gnats_path'); + my @directories = glob("$path/*"); + my @bugs; + foreach my $directory (@directories) { + next if !-d $directory; + my $name = basename($directory); + next if grep($_ eq $name, SKIP_DIRECTORIES); + push(@bugs, @{$self->_parse_project($directory)}); + } + @bugs = sort { $a->{Number} <=> $b->{Number} } @bugs; + return \@bugs; } sub _parse_project { - my ($self, $directory) = @_; - my @files = glob("$directory/*"); - - $self->debug("Reading Project: $directory"); - # Sometimes other files get into gnats directories. - @files = grep { basename($_) =~ /^\d+$/ } @files; - my @bugs; - my $count = 1; - my $total = scalar @files; - print basename($directory) . ":\n"; - foreach my $file (@files) { - push(@bugs, $self->_parse_bug_file($file)); - if (!$self->verbose) { - indicate_progress({ current => $count++, every => 5, - total => $total }); - } + my ($self, $directory) = @_; + my @files = glob("$directory/*"); + + $self->debug("Reading Project: $directory"); + + # Sometimes other files get into gnats directories. + @files = grep { basename($_) =~ /^\d+$/ } @files; + my @bugs; + my $count = 1; + my $total = scalar @files; + print basename($directory) . ":\n"; + foreach my $file (@files) { + push(@bugs, $self->_parse_bug_file($file)); + if (!$self->verbose) { + indicate_progress({current => $count++, every => 5, total => $total}); } - return \@bugs; + } + return \@bugs; } sub _parse_bug_file { - my ($self, $file) = @_; - $self->debug("Reading $file"); - open(my $fh, "<", $file) || die "$file: $!"; - my $email = Email::Simple::FromHandle->new($fh); - my $fields = $self->_get_gnats_field_data($email); - # We parse attachments here instead of during translate_bug, - # because otherwise we'd be taking up huge amounts of memory storing - # all the raw attachment data in memory. - $fields->{attachments} = $self->_parse_attachments($fields); - close($fh); - return $fields; + my ($self, $file) = @_; + $self->debug("Reading $file"); + open(my $fh, "<", $file) || die "$file: $!"; + my $email = Email::Simple::FromHandle->new($fh); + my $fields = $self->_get_gnats_field_data($email); + + # We parse attachments here instead of during translate_bug, + # because otherwise we'd be taking up huge amounts of memory storing + # all the raw attachment data in memory. + $fields->{attachments} = $self->_parse_attachments($fields); + close($fh); + return $fields; } sub _get_gnats_field_data { - my ($self, $email) = @_; - my ($current_field, @value_lines, %fields); - $email->reset_handle(); - my $handle = $email->handle; - foreach my $line (<$handle>) { - # If this line starts a field name - if ($line =~ FIELD_REGEX) { - my ($new_field, $rest_of_line) = ($1, $2); - - # If this is one of the last few PR fields, then make sure - # that we're getting our fields in the right order. - my $new_field_valid = 1; - my $search_for = $current_field || ''; - my $current_field_pos = firstidx { $_ eq $search_for } - END_FIELD_ORDER; - if ($current_field_pos > -1) { - my $new_field_pos = firstidx { $_ eq $new_field } - END_FIELD_ORDER; - # We accept any field, as long as it's later than this one. - $new_field_valid = $new_field_pos > $current_field_pos ? 1 : 0; - } - - if ($new_field_valid) { - if ($current_field) { - $fields{$current_field} = _handle_lines(\@value_lines); - @value_lines = (); - } - $current_field = $new_field; - $line = $rest_of_line; - } + my ($self, $email) = @_; + my ($current_field, @value_lines, %fields); + $email->reset_handle(); + my $handle = $email->handle; + foreach my $line (<$handle>) { + + # If this line starts a field name + if ($line =~ FIELD_REGEX) { + my ($new_field, $rest_of_line) = ($1, $2); + + # If this is one of the last few PR fields, then make sure + # that we're getting our fields in the right order. + my $new_field_valid = 1; + my $search_for = $current_field || ''; + my $current_field_pos = firstidx { $_ eq $search_for } + END_FIELD_ORDER; + if ($current_field_pos > -1) { + my $new_field_pos = firstidx { $_ eq $new_field } + END_FIELD_ORDER; + + # We accept any field, as long as it's later than this one. + $new_field_valid = $new_field_pos > $current_field_pos ? 1 : 0; + } + + if ($new_field_valid) { + if ($current_field) { + $fields{$current_field} = _handle_lines(\@value_lines); + @value_lines = (); } - push(@value_lines, $line) if defined $line; + $current_field = $new_field; + $line = $rest_of_line; + } } - $fields{$current_field} = _handle_lines(\@value_lines); - $fields{cc} = [$email->header('Cc')] if $email->header('Cc'); - - # If the Originator is invalid and we don't have a translation for it, - # use the From header instead. - my $originator = $self->translate_value('reporter', $fields{Originator}, - { check_only => 1 }); - if ($originator !~ Bugzilla->params->{emailregexp}) { - # We use the raw header sometimes, because it looks like "From: user" - # which Email::Address won't parse but we can still use. - my $address = $email->header('From'); - my ($parsed) = Email::Address->parse($address); - if ($parsed) { - $address = $parsed->address; - } - if ($address) { - $self->debug( - "PR $fields{Number} had an Originator that was not a valid" - . " user ($fields{Originator}). Using From ($address)" - . " instead.\n"); - my $address_email = $self->translate_value('reporter', $address, - { check_only => 1 }); - if ($address_email !~ Bugzilla->params->{emailregexp}) { - $self->debug(" From was also invalid, using default_originator.\n"); - $address = $self->config('default_originator'); - } - $fields{Originator} = $address; - } + push(@value_lines, $line) if defined $line; + } + $fields{$current_field} = _handle_lines(\@value_lines); + $fields{cc} = [$email->header('Cc')] if $email->header('Cc'); + + # If the Originator is invalid and we don't have a translation for it, + # use the From header instead. + my $originator + = $self->translate_value('reporter', $fields{Originator}, {check_only => 1}); + if ($originator !~ Bugzilla->params->{emailregexp}) { + + # We use the raw header sometimes, because it looks like "From: user" + # which Email::Address won't parse but we can still use. + my $address = $email->header('From'); + my ($parsed) = Email::Address->parse($address); + if ($parsed) { + $address = $parsed->address; } + if ($address) { + $self->debug("PR $fields{Number} had an Originator that was not a valid" + . " user ($fields{Originator}). Using From ($address)" + . " instead.\n"); + my $address_email + = $self->translate_value('reporter', $address, {check_only => 1}); + if ($address_email !~ Bugzilla->params->{emailregexp}) { + $self->debug(" From was also invalid, using default_originator.\n"); + $address = $self->config('default_originator'); + } + $fields{Originator} = $address; + } + } - $self->debug(\%fields, 3); - return \%fields; + $self->debug(\%fields, 3); + return \%fields; } sub _handle_lines { - my ($lines) = @_; - my $value = join('', @$lines); - $value =~ s/\s+$//; - return $value; + my ($lines) = @_; + my $value = join('', @$lines); + $value =~ s/\s+$//; + return $value; } #################### @@ -432,169 +432,188 @@ sub _handle_lines { #################### sub translate_bug { - my ($self, $fields) = @_; - - my ($bug, $other_fields) = $self->SUPER::translate_bug($fields); + my ($self, $fields) = @_; - $bug->{attachments} = delete $other_fields->{attachments}; + my ($bug, $other_fields) = $self->SUPER::translate_bug($fields); - if (defined $other_fields->{_add_to_comment}) { - $bug->{comment} .= delete $other_fields->{_add_to_comment}; - } + $bug->{attachments} = delete $other_fields->{attachments}; - my ($changes, $extra_comment) = - $self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'}); - - my @comments; - foreach my $change (@$changes) { - if (exists $change->{comment}) { - push(@comments, { - thetext => $change->{comment}, - who => $change->{who}, - bug_when => $change->{bug_when} }); - delete $change->{comment}; - } - } - $bug->{history} = $changes; - - if (trim($extra_comment)) { - push(@comments, { thetext => $extra_comment, who => $bug->{reporter}, - bug_when => $bug->{delta_ts} || $bug->{creation_ts} }); - } - $bug->{comments} = \@comments; + if (defined $other_fields->{_add_to_comment}) { + $bug->{comment} .= delete $other_fields->{_add_to_comment}; + } - $bug->{component} = $self->config('component_name'); - if (!$bug->{short_desc}) { - $bug->{short_desc} = NO_SUBJECT; - } + my ($changes, $extra_comment) + = $self->_parse_audit_trail($bug, $other_fields->{'Audit-Trail'}); - foreach my $attachment (@{ $bug->{attachments} || [] }) { - $attachment->{submitter} = $bug->{reporter}; - $attachment->{creation_ts} = $bug->{creation_ts}; + my @comments; + foreach my $change (@$changes) { + if (exists $change->{comment}) { + push( + @comments, + { + thetext => $change->{comment}, + who => $change->{who}, + bug_when => $change->{bug_when} + } + ); + delete $change->{comment}; } - - $self->debug($bug, 3); - return $bug; + } + $bug->{history} = $changes; + + if (trim($extra_comment)) { + push( + @comments, + { + thetext => $extra_comment, + who => $bug->{reporter}, + bug_when => $bug->{delta_ts} || $bug->{creation_ts} + } + ); + } + $bug->{comments} = \@comments; + + $bug->{component} = $self->config('component_name'); + if (!$bug->{short_desc}) { + $bug->{short_desc} = NO_SUBJECT; + } + + foreach my $attachment (@{$bug->{attachments} || []}) { + $attachment->{submitter} = $bug->{reporter}; + $attachment->{creation_ts} = $bug->{creation_ts}; + } + + $self->debug($bug, 3); + return $bug; } sub _parse_audit_trail { - my ($self, $bug, $audit_trail) = @_; - return [] if !trim($audit_trail); - $self->debug(" Parsing audit trail...", 2); - - if ($audit_trail !~ /^\S+-Changed-\S+:/ms) { - # This is just a comment from the bug's creator. - $self->debug(" Audit trail is just a comment.", 2); - return ([], $audit_trail); + my ($self, $bug, $audit_trail) = @_; + return [] if !trim($audit_trail); + $self->debug(" Parsing audit trail...", 2); + + if ($audit_trail !~ /^\S+-Changed-\S+:/ms) { + + # This is just a comment from the bug's creator. + $self->debug(" Audit trail is just a comment.", 2); + return ([], $audit_trail); + } + + my (@changes, %current_data, $current_column, $on_why); + my $extra_comment = ''; + my $current_field; + my @all_lines = split("\n", $audit_trail); + foreach my $line (@all_lines) { + + # GNATS history looks like: + # Status-Changed-From-To: open->closed + # Status-Changed-By: jack + # Status-Changed-When: Mon May 12 14:46:59 2003 + # Status-Changed-Why: + # This is some comment here about the change. + if ($line =~ /^(\S+)-Changed-(\S+):(.*)/) { + my ($field, $column, $value) = ($1, $2, $3); + my $bz_field = $self->translate_field($field); + + # If it's not a field we're importing, we don't care about + # its history. + next if !$bz_field; + + # GNATS doesn't track values for description changes, + # unfortunately, and that's the only information we'd be able to + # use in Bugzilla for the audit trail on that field. + next if $bz_field eq 'comment'; + $current_field = $bz_field if !$current_field; + if ($bz_field ne $current_field) { + $self->_store_audit_change(\@changes, $current_field, \%current_data); + %current_data = (); + $current_field = $bz_field; + } + $value = trim($value); + $self->debug(" $bz_field $column: $value", 3); + if ($column eq 'From-To') { + my ($from, $to) = split('->', $value, 2); + + # Sometimes there's just a - instead of a -> between the values. + if (!defined($to)) { + ($from, $to) = split('-', $value, 2); + } + $current_data{added} = $to; + $current_data{removed} = $from; + } + elsif ($column eq 'By') { + my $email = $self->translate_value('user', $value); + + # Sometimes we hit users in the audit trail that we haven't + # seen anywhere else. + $current_data{who} = $email; + } + elsif ($column eq 'When') { + $current_data{bug_when} = $self->parse_date($value); + } + if ($column eq 'Why') { + $value = '' if !defined $value; + $current_data{comment} = $value; + $on_why = 1; + } + else { + $on_why = 0; + } } + elsif ($on_why) { - my (@changes, %current_data, $current_column, $on_why); - my $extra_comment = ''; - my $current_field; - my @all_lines = split("\n", $audit_trail); - foreach my $line (@all_lines) { - # GNATS history looks like: - # Status-Changed-From-To: open->closed - # Status-Changed-By: jack - # Status-Changed-When: Mon May 12 14:46:59 2003 - # Status-Changed-Why: - # This is some comment here about the change. - if ($line =~ /^(\S+)-Changed-(\S+):(.*)/) { - my ($field, $column, $value) = ($1, $2, $3); - my $bz_field = $self->translate_field($field); - # If it's not a field we're importing, we don't care about - # its history. - next if !$bz_field; - # GNATS doesn't track values for description changes, - # unfortunately, and that's the only information we'd be able to - # use in Bugzilla for the audit trail on that field. - next if $bz_field eq 'comment'; - $current_field = $bz_field if !$current_field; - if ($bz_field ne $current_field) { - $self->_store_audit_change( - \@changes, $current_field, \%current_data); - %current_data = (); - $current_field = $bz_field; - } - $value = trim($value); - $self->debug(" $bz_field $column: $value", 3); - if ($column eq 'From-To') { - my ($from, $to) = split('->', $value, 2); - # Sometimes there's just a - instead of a -> between the values. - if (!defined($to)) { - ($from, $to) = split('-', $value, 2); - } - $current_data{added} = $to; - $current_data{removed} = $from; - } - elsif ($column eq 'By') { - my $email = $self->translate_value('user', $value); - # Sometimes we hit users in the audit trail that we haven't - # seen anywhere else. - $current_data{who} = $email; - } - elsif ($column eq 'When') { - $current_data{bug_when} = $self->parse_date($value); - } - if ($column eq 'Why') { - $value = '' if !defined $value; - $current_data{comment} = $value; - $on_why = 1; - } - else { - $on_why = 0; - } - } - elsif ($on_why) { - # "Why" lines are indented four characters. - $line =~ s/^\s{4}//; - $current_data{comment} .= "$line\n"; - } - else { - $self->debug( - "Extra Audit-Trail line on $bug->{product} $bug->{bug_id}:" - . " $line\n", 2); - $extra_comment .= "$line\n"; - } + # "Why" lines are indented four characters. + $line =~ s/^\s{4}//; + $current_data{comment} .= "$line\n"; + } + else { + $self->debug( + "Extra Audit-Trail line on $bug->{product} $bug->{bug_id}:" . " $line\n", 2); + $extra_comment .= "$line\n"; } - $self->_store_audit_change(\@changes, $current_field, \%current_data); - return (\@changes, $extra_comment); + } + $self->_store_audit_change(\@changes, $current_field, \%current_data); + return (\@changes, $extra_comment); } sub _store_audit_change { - my ($self, $changes, $old_field, $current_data) = @_; - - $current_data->{field} = $old_field; - $current_data->{removed} = - $self->translate_value($old_field, $current_data->{removed}); - $current_data->{added} = - $self->translate_value($old_field, $current_data->{added}); - push(@$changes, { %$current_data }); + my ($self, $changes, $old_field, $current_data) = @_; + + $current_data->{field} = $old_field; + $current_data->{removed} + = $self->translate_value($old_field, $current_data->{removed}); + $current_data->{added} + = $self->translate_value($old_field, $current_data->{added}); + push(@$changes, {%$current_data}); } sub _parse_attachments { - my ($self, $fields) = @_; - my $unformatted = delete $fields->{'Unformatted'}; - my $gnats_boundary = GNATS_BOUNDARY; - # A sanity checker to make sure that we're parsing attachments right. - my $num_attachments = 0; - $num_attachments++ while ($unformatted =~ /\Q$gnats_boundary\E/g); - # Sometimes there's a GNATS_BOUNDARY that is on the same line as other data. - $unformatted =~ s/(\S\s*)\Q$gnats_boundary\E$/$1\n$gnats_boundary/mg; - # Often the "Unformatted" section starts with stuff before - # ----gnatsweb-attachment---- that isn't necessary. - $unformatted =~ s/^\s*From:.+?Reply-to:[^\n]+//s; - $unformatted = trim($unformatted); - return [] if !$unformatted; - $self->debug('Reading attachments...', 2); - my $boundary = generate_random_password(48); - $unformatted =~ s/\Q$gnats_boundary\E/--$boundary/g; - # Sometimes the whole Unformatted section is indented by exactly - # one space, and needs to be fixed. - if ($unformatted =~ /--\Q$boundary\E\n /) { - $unformatted =~ s/^ //mg; - } - $unformatted = <{'Unformatted'}; + my $gnats_boundary = GNATS_BOUNDARY; + + # A sanity checker to make sure that we're parsing attachments right. + my $num_attachments = 0; + $num_attachments++ while ($unformatted =~ /\Q$gnats_boundary\E/g); + + # Sometimes there's a GNATS_BOUNDARY that is on the same line as other data. + $unformatted =~ s/(\S\s*)\Q$gnats_boundary\E$/$1\n$gnats_boundary/mg; + + # Often the "Unformatted" section starts with stuff before + # ----gnatsweb-attachment---- that isn't necessary. + $unformatted =~ s/^\s*From:.+?Reply-to:[^\n]+//s; + $unformatted = trim($unformatted); + return [] if !$unformatted; + $self->debug('Reading attachments...', 2); + my $boundary = generate_random_password(48); + $unformatted =~ s/\Q$gnats_boundary\E/--$boundary/g; + + # Sometimes the whole Unformatted section is indented by exactly + # one space, and needs to be fixed. + if ($unformatted =~ /--\Q$boundary\E\n /) { + $unformatted =~ s/^ //mg; + } + $unformatted = <parts; - # Remove the fake body. - my $part1 = shift @parts; - if ($part1->body) { - $self->debug(" Additional Unformatted data found on " - . $fields->{Category} . " bug " . $fields->{Number}); - $self->debug($part1->body, 3); - $fields->{_add_comment} .= "\n\nUnformatted:\n" . $part1->body; - } + my $email = new Email::MIME(\$unformatted); + my @parts = $email->parts; + + # Remove the fake body. + my $part1 = shift @parts; + if ($part1->body) { + $self->debug(" Additional Unformatted data found on " + . $fields->{Category} . " bug " + . $fields->{Number}); + $self->debug($part1->body, 3); + $fields->{_add_comment} .= "\n\nUnformatted:\n" . $part1->body; + } + + my @attachments; + foreach my $part (@parts) { + $self->debug(' Parsing attachment: ' . $part->filename); + my $temp_fh = IO::File->new_tmpfile or die("Can't create tempfile: $!"); + $temp_fh->binmode; + print $temp_fh $part->body; + my $content_type = $part->content_type; + $content_type =~ s/; name=.+$//; + my $attachment = { + filename => $part->filename, + description => $part->filename, + mimetype => $content_type, + data => $temp_fh + }; + $self->debug($attachment, 3); + push(@attachments, $attachment); + } + + if (scalar(@attachments) ne $num_attachments) { + warn "WARNING: Expected $num_attachments attachments but got " + . scalar(@attachments) . "\n"; + $self->debug($unformatted, 3); + } + return \@attachments; +} - my @attachments; - foreach my $part (@parts) { - $self->debug(' Parsing attachment: ' . $part->filename); - my $temp_fh = IO::File->new_tmpfile or die ("Can't create tempfile: $!"); - $temp_fh->binmode; - print $temp_fh $part->body; - my $content_type = $part->content_type; - $content_type =~ s/; name=.+$//; - my $attachment = { filename => $part->filename, - description => $part->filename, - mimetype => $content_type, - data => $temp_fh }; - $self->debug($attachment, 3); - push(@attachments, $attachment); +sub translate_value { + my $self = shift; + my ($field, $value, $options) = @_; + my $original_value = $value; + $options ||= {}; + + if (!ref($value) and grep($_ eq $field, $self->USER_FIELDS)) { + if ($value =~ /(\S+\@\S+)/) { + $value = $1; + $value =~ s/^$//; } + else { + # Sometimes names have extra stuff on the end like "(Somebody's Name)" + $value =~ s/\s+\(.+\)$//; - if (scalar(@attachments) ne $num_attachments) { - warn "WARNING: Expected $num_attachments attachments but got " - . scalar(@attachments) . "\n" ; - $self->debug($unformatted, 3); + # Sometimes user fields look like "(user)" instead of just "user". + $value =~ s/^\((.+)\)$/$1/; + $value = trim($value); } - return \@attachments; -} + } -sub translate_value { - my $self = shift; - my ($field, $value, $options) = @_; - my $original_value = $value; - $options ||= {}; - - if (!ref($value) and grep($_ eq $field, $self->USER_FIELDS)) { - if ($value =~ /(\S+\@\S+)/) { - $value = $1; - $value =~ s/^$//; - } - else { - # Sometimes names have extra stuff on the end like "(Somebody's Name)" - $value =~ s/\s+\(.+\)$//; - # Sometimes user fields look like "(user)" instead of just "user". - $value =~ s/^\((.+)\)$/$1/; - $value = trim($value); - } + if ($field eq 'version' and $value ne '') { + my $version_re = $self->config('version_regex'); + if ($version_re and $value =~ $version_re) { + $value = $1; } - if ($field eq 'version' and $value ne '') { - my $version_re = $self->config('version_regex'); - if ($version_re and $value =~ $version_re) { - $value = $1; - } - # In the GNATS that I tested this with, there were many extremely long - # values for "version" that caused some import problems (they were - # longer than the max allowed version value). So if the version value - # is longer than 32 characters, pull out the first thing that looks - # like a version number. - elsif (length($value) > LONG_VERSION_LENGTH) { - $value =~ s/^.+?\b(\d[\w\.]+)\b.+$/$1/; - } + # In the GNATS that I tested this with, there were many extremely long + # values for "version" that caused some import problems (they were + # longer than the max allowed version value). So if the version value + # is longer than 32 characters, pull out the first thing that looks + # like a version number. + elsif (length($value) > LONG_VERSION_LENGTH) { + $value =~ s/^.+?\b(\d[\w\.]+)\b.+$/$1/; } + } + + my @args = @_; + $args[1] = $value; - my @args = @_; + $value = $self->SUPER::translate_value(@args); + return $value if ref $value; + + if (grep($_ eq $field, $self->USER_FIELDS)) { + my $from_value = $value; + $value = $self->user_to_email($value); $args[1] = $value; + # If we got something new from user_to_email, do any necessary + # translation of it. $value = $self->SUPER::translate_value(@args); - return $value if ref $value; - - if (grep($_ eq $field, $self->USER_FIELDS)) { - my $from_value = $value; - $value = $self->user_to_email($value); - $args[1] = $value; - # If we got something new from user_to_email, do any necessary - # translation of it. - $value = $self->SUPER::translate_value(@args); - if (!$options->{check_only}) { - $self->add_user($from_value, $value); - } + if (!$options->{check_only}) { + $self->add_user($from_value, $value); } + } - return $value; + return $value; } 1; diff --git a/Bugzilla/Milestone.pm b/Bugzilla/Milestone.pm index 2f10e1f00..277ae14e1 100644 --- a/Bugzilla/Milestone.pm +++ b/Bugzilla/Milestone.pm @@ -25,140 +25,140 @@ use Scalar::Util qw(blessed); use constant DEFAULT_SORTKEY => 0; -use constant DB_TABLE => 'milestones'; +use constant DB_TABLE => 'milestones'; use constant NAME_FIELD => 'value'; use constant LIST_ORDER => 'sortkey, value'; use constant DB_COLUMNS => qw( - id - value - product_id - sortkey - isactive + id + value + product_id + sortkey + isactive ); -use constant REQUIRED_FIELD_MAP => { - product_id => 'product', -}; +use constant REQUIRED_FIELD_MAP => {product_id => 'product',}; use constant UPDATE_COLUMNS => qw( - value - sortkey - isactive + value + sortkey + isactive ); use constant VALIDATORS => { - product => \&_check_product, - sortkey => \&_check_sortkey, - value => \&_check_value, - isactive => \&Bugzilla::Object::check_boolean, + product => \&_check_product, + sortkey => \&_check_sortkey, + value => \&_check_value, + isactive => \&Bugzilla::Object::check_boolean, }; -use constant VALIDATOR_DEPENDENCIES => { - value => ['product'], -}; +use constant VALIDATOR_DEPENDENCIES => {value => ['product'],}; ################################ sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $product; - if (ref $param) { - $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 value = ?'; - 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) { + $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 value = ?'; + my @values = ($product->id, $name); + $param = {condition => $condition, values => \@values}; + } - unshift @_, $param; - return $class->SUPER::new(@_); + unshift @_, $param; + return $class->SUPER::new(@_); } sub run_create_validators { - my $class = shift; - my $params = $class->SUPER::run_create_validators(@_); - my $product = delete $params->{product}; - $params->{product_id} = $product->id; - return $params; + my $class = shift; + my $params = $class->SUPER::run_create_validators(@_); + my $product = delete $params->{product}; + $params->{product_id} = $product->id; + return $params; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - my $changes = $self->SUPER::update(@_); - - if (exists $changes->{value}) { - # The milestone value is stored in the bugs table instead of its ID. - $dbh->do('UPDATE bugs SET target_milestone = ? - WHERE target_milestone = ? AND product_id = ?', - undef, ($self->name, $changes->{value}->[0], $self->product_id)); - - # The default milestone also stores the value instead of the ID. - $dbh->do('UPDATE products SET defaultmilestone = ? - WHERE id = ? AND defaultmilestone = ?', - undef, ($self->name, $self->product_id, $changes->{value}->[0])); - Bugzilla->memcached->clear({ table => 'products', id => $self->product_id }); - } - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - - return $changes; + my $self = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + my $changes = $self->SUPER::update(@_); + + if (exists $changes->{value}) { + + # The milestone value is stored in the bugs table instead of its ID. + $dbh->do( + 'UPDATE bugs SET target_milestone = ? + WHERE target_milestone = ? AND product_id = ?', undef, + ($self->name, $changes->{value}->[0], $self->product_id) + ); + + # The default milestone also stores the value instead of the ID. + $dbh->do( + 'UPDATE products SET defaultmilestone = ? + WHERE id = ? AND defaultmilestone = ?', undef, + ($self->name, $self->product_id, $changes->{value}->[0]) + ); + Bugzilla->memcached->clear({table => 'products', id => $self->product_id}); + } + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + + return $changes; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # The default milestone cannot be deleted. - if ($self->name eq $self->product->default_milestone) { - ThrowUserError('milestone_is_default', { milestone => $self }); - } + # The default milestone cannot be deleted. + if ($self->name eq $self->product->default_milestone) { + ThrowUserError('milestone_is_default', {milestone => $self}); + } + + if ($self->bug_count) { - if ($self->bug_count) { - # We don't want to delete bugs when deleting a milestone. - # Bugs concerned are reassigned to the default milestone. - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bug_id FROM bugs + # We don't want to delete bugs when deleting a milestone. + # Bugs concerned are reassigned to the default milestone. + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bug_id FROM bugs WHERE product_id = ? AND target_milestone = ?', - undef, ($self->product->id, $self->name)); - - my $timestamp = $dbh->selectrow_array('SELECT NOW()'); - - $dbh->do('UPDATE bugs SET target_milestone = ?, delta_ts = ? - WHERE ' . $dbh->sql_in('bug_id', $bug_ids), - undef, ($self->product->default_milestone, $timestamp)); - - require Bugzilla::Bug; - import Bugzilla::Bug qw(LogActivityEntry); - foreach my $bug_id (@$bug_ids) { - LogActivityEntry($bug_id, 'target_milestone', - $self->name, - $self->product->default_milestone, - Bugzilla->user->id, $timestamp); - } + undef, ($self->product->id, $self->name) + ); + + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + $dbh->do( + 'UPDATE bugs SET target_milestone = ?, delta_ts = ? + WHERE ' . $dbh->sql_in('bug_id', $bug_ids), undef, + ($self->product->default_milestone, $timestamp) + ); + + require Bugzilla::Bug; + import Bugzilla::Bug qw(LogActivityEntry); + foreach my $bug_id (@$bug_ids) { + LogActivityEntry($bug_id, 'target_milestone', $self->name, + $self->product->default_milestone, + Bugzilla->user->id, $timestamp); } - $self->SUPER::remove_from_db(); + } + $self->SUPER::remove_from_db(); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } ################################ @@ -166,78 +166,85 @@ sub remove_from_db { ################################ sub _check_value { - my ($invocant, $name, undef, $params) = @_; - my $product = blessed($invocant) ? $invocant->product : $params->{product}; - - $name = trim($name); - $name || ThrowUserError('milestone_blank_name'); - if (length($name) > MAX_MILESTONE_SIZE) { - ThrowUserError('milestone_name_too_long', {name => $name}); - } - - my $milestone = new Bugzilla::Milestone({product => $product, name => $name}); - if ($milestone && (!ref $invocant || $milestone->id != $invocant->id)) { - ThrowUserError('milestone_already_exists', { name => $milestone->name, - product => $product->name }); - } - return $name; + my ($invocant, $name, undef, $params) = @_; + my $product = blessed($invocant) ? $invocant->product : $params->{product}; + + $name = trim($name); + $name || ThrowUserError('milestone_blank_name'); + if (length($name) > MAX_MILESTONE_SIZE) { + ThrowUserError('milestone_name_too_long', {name => $name}); + } + + my $milestone = new Bugzilla::Milestone({product => $product, name => $name}); + if ($milestone && (!ref $invocant || $milestone->id != $invocant->id)) { + ThrowUserError('milestone_already_exists', + {name => $milestone->name, product => $product->name}); + } + return $name; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - - # Keep a copy in case detaint_signed() clears the sortkey - my $stored_sortkey = $sortkey; - - if (!detaint_signed($sortkey) || $sortkey < MIN_SMALLINT || $sortkey > MAX_SMALLINT) { - ThrowUserError('milestone_sortkey_invalid', {sortkey => $stored_sortkey}); - } - return $sortkey; + my ($invocant, $sortkey) = @_; + + # Keep a copy in case detaint_signed() clears the sortkey + my $stored_sortkey = $sortkey; + + if ( !detaint_signed($sortkey) + || $sortkey < MIN_SMALLINT + || $sortkey > MAX_SMALLINT) + { + ThrowUserError('milestone_sortkey_invalid', {sortkey => $stored_sortkey}); + } + return $sortkey; } 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); } ################################ # Methods ################################ -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_is_active { $_[0]->set('isactive', $_[1]); } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(q{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + q{ SELECT COUNT(*) FROM bugs - WHERE product_id = ? AND target_milestone = ?}, - undef, $self->product_id, $self->name) || 0; - } - return $self->{'bug_count'}; + WHERE product_id = ? AND target_milestone = ?}, undef, $self->product_id, + $self->name + ) || 0; + } + return $self->{'bug_count'}; } ################################ ##### Accessors ###### ################################ -sub name { return $_[0]->{'value'}; } +sub name { return $_[0]->{'value'}; } sub product_id { return $_[0]->{'product_id'}; } -sub sortkey { return $_[0]->{'sortkey'}; } -sub is_active { return $_[0]->{'isactive'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'isactive'}; } sub product { - my $self = shift; + my $self = shift; - require Bugzilla::Product; - $self->{'product'} ||= Bugzilla::Product->new({ id => $self->product_id, cache => 1 }); - return $self->{'product'}; + require Bugzilla::Product; + $self->{'product'} + ||= Bugzilla::Product->new({id => $self->product_id, cache => 1}); + return $self->{'product'}; } 1; diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm index eaafca219..11a6a5895 100644 --- a/Bugzilla/Object.pm +++ b/Bugzilla/Object.pm @@ -24,16 +24,17 @@ use constant NAME_FIELD => 'name'; use constant ID_FIELD => 'id'; use constant LIST_ORDER => NAME_FIELD; -use constant UPDATE_VALIDATORS => {}; -use constant NUMERIC_COLUMNS => (); -use constant DATE_COLUMNS => (); +use constant UPDATE_VALIDATORS => {}; +use constant NUMERIC_COLUMNS => (); +use constant DATE_COLUMNS => (); use constant VALIDATOR_DEPENDENCIES => {}; + # XXX At some point, this will be joined with FIELD_MAP. -use constant REQUIRED_FIELD_MAP => {}; +use constant REQUIRED_FIELD_MAP => {}; use constant EXTRA_REQUIRED_FIELDS => (); -use constant AUDIT_CREATES => 1; -use constant AUDIT_UPDATES => 1; -use constant AUDIT_REMOVES => 1; +use constant AUDIT_CREATES => 1; +use constant AUDIT_UPDATES => 1; +use constant AUDIT_REMOVES => 1; # When USE_MEMCACHED is true, the class is suitable for serialisation to # Memcached. See documentation in Bugzilla::Memcached for more information. @@ -50,54 +51,52 @@ use constant DYNAMIC_COLUMNS => 0; # This allows the JSON-RPC interface to return Bugzilla::Object instances # as though they were hashes. In the future, this may be modified to return # less information. -sub TO_JSON { return { %{ $_[0] } }; } +sub TO_JSON { return {%{$_[0]}}; } ############################### #### Initialization #### ############################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $param = shift; - - my $object = $class->_object_cache_get($param); - return $object if $object; - - my ($data, $set_memcached); - if (Bugzilla->memcached->enabled - && $class->USE_MEMCACHED - && ref($param) eq 'HASH' && $param->{cache}) - { - if (defined $param->{id}) { - $data = Bugzilla->memcached->get({ - table => $class->DB_TABLE, - id => $param->{id}, - }); - } - elsif (defined $param->{name}) { - $data = Bugzilla->memcached->get({ - table => $class->DB_TABLE, - name => $param->{name}, - }); - } - $set_memcached = $data ? 0 : 1; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $param = shift; + + my $object = $class->_object_cache_get($param); + return $object if $object; + + my ($data, $set_memcached); + if ( Bugzilla->memcached->enabled + && $class->USE_MEMCACHED + && ref($param) eq 'HASH' + && $param->{cache}) + { + if (defined $param->{id}) { + $data + = Bugzilla->memcached->get({table => $class->DB_TABLE, id => $param->{id},}); } - $data ||= $class->_load_from_db($param); - - if ($data && $set_memcached) { - Bugzilla->memcached->set({ - table => $class->DB_TABLE, - id => $data->{$class->ID_FIELD}, - name => $data->{$class->NAME_FIELD}, - data => $data, + elsif (defined $param->{name}) { + $data + = Bugzilla->memcached->get({table => $class->DB_TABLE, name => $param->{name}, }); } - - $object = $class->new_from_hash($data); - $class->_object_cache_set($param, $object); - - return $object; + $set_memcached = $data ? 0 : 1; + } + $data ||= $class->_load_from_db($param); + + if ($data && $set_memcached) { + Bugzilla->memcached->set({ + table => $class->DB_TABLE, + id => $data->{$class->ID_FIELD}, + name => $data->{$class->NAME_FIELD}, + data => $data, + }); + } + + $object = $class->new_from_hash($data); + $class->_object_cache_set($param, $object); + + return $object; } @@ -106,349 +105,346 @@ sub new { # in Bugzilla::DB::Pg appropriately, to add the right LOWER # index. You can see examples already there. sub _load_from_db { - my $class = shift; - my ($param) = @_; - my $dbh = Bugzilla->dbh; - my $columns = join(',', $class->_get_db_columns); - my $table = $class->DB_TABLE; - my $name_field = $class->NAME_FIELD; - my $id_field = $class->ID_FIELD; - - my $id = $param; - if (ref $param eq 'HASH') { - $id = $param->{id}; + my $class = shift; + my ($param) = @_; + my $dbh = Bugzilla->dbh; + my $columns = join(',', $class->_get_db_columns); + my $table = $class->DB_TABLE; + my $name_field = $class->NAME_FIELD; + my $id_field = $class->ID_FIELD; + + my $id = $param; + if (ref $param eq 'HASH') { + $id = $param->{id}; + } + + my $object_data; + if (defined $id) { + + # We special-case if somebody specifies an ID, so that we can + # validate it as numeric. + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::_load_from_db'}); + + # Too large integers make PostgreSQL crash. + return if $id > MAX_INT_32; + + $object_data = $dbh->selectrow_hashref( + qq{ + SELECT $columns FROM $table + WHERE $id_field = ?}, undef, $id + ); + } + else { + unless (defined $param->{name} + || (defined $param->{'condition'} && defined $param->{'values'})) + { + ThrowCodeError('bad_arg', {argument => 'param', function => $class . '::new'}); } - my $object_data; - if (defined $id) { - # We special-case if somebody specifies an ID, so that we can - # validate it as numeric. - detaint_natural($id) - || ThrowCodeError('param_must_be_numeric', - {function => $class . '::_load_from_db'}); - - # Too large integers make PostgreSQL crash. - return if $id > MAX_INT_32; - - $object_data = $dbh->selectrow_hashref(qq{ - SELECT $columns FROM $table - WHERE $id_field = ?}, undef, $id); - } else { - unless (defined $param->{name} || (defined $param->{'condition'} - && defined $param->{'values'})) + my ($condition, @values); + if (defined $param->{name}) { + $condition = $dbh->sql_istrcmp($name_field, '?'); + push(@values, $param->{name}); + } + elsif (defined $param->{'condition'} && defined $param->{'values'}) { + caller->isa('Bugzilla::Object') || ThrowCodeError( + 'protection_violation', { - ThrowCodeError('bad_arg', { argument => 'param', - function => $class . '::new' }); + caller => caller, + function => $class . '::new', + argument => 'condition/values' } - - my ($condition, @values); - if (defined $param->{name}) { - $condition = $dbh->sql_istrcmp($name_field, '?'); - push(@values, $param->{name}); - } - elsif (defined $param->{'condition'} && defined $param->{'values'}) { - caller->isa('Bugzilla::Object') - || ThrowCodeError('protection_violation', - { caller => caller, - function => $class . '::new', - argument => 'condition/values' }); - $condition = $param->{'condition'}; - push(@values, @{$param->{'values'}}); - } - - map { trick_taint($_) } @values; - $object_data = $dbh->selectrow_hashref( - "SELECT $columns FROM $table WHERE $condition", undef, @values); + ); + $condition = $param->{'condition'}; + push(@values, @{$param->{'values'}}); } - return $object_data; + + map { trick_taint($_) } @values; + $object_data + = $dbh->selectrow_hashref("SELECT $columns FROM $table WHERE $condition", + undef, @values); + } + return $object_data; } sub new_from_list { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($id_list) = @_; - my $id_field = $class->ID_FIELD; - - my @detainted_ids; - foreach my $id (@$id_list) { - detaint_natural($id) || - ThrowCodeError('param_must_be_numeric', - {function => $class . '::new_from_list'}); - # Too large integers make PostgreSQL crash. - next if $id > MAX_INT_32; - push(@detainted_ids, $id); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($id_list) = @_; + my $id_field = $class->ID_FIELD; + + my @detainted_ids; + foreach my $id (@$id_list) { + detaint_natural($id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::new_from_list'}); + + # Too large integers make PostgreSQL crash. + next if $id > MAX_INT_32; + push(@detainted_ids, $id); + } + + # We don't do $invocant->match because some classes have + # their own implementation of match which is not compatible + # with this one. However, match() still needs to have the right $invocant + # in order to do $class->DB_TABLE and so on. + my $list = match($invocant, {$id_field => \@detainted_ids}); + + # BMO: Populate the object cache with bug objects, which helps + # inline-history when viewing bugs with dependencies. + if ($class eq 'Bugzilla::Bug') { + foreach my $object (@$list) { + $class->_object_cache_set({id => $object->id, cache => 1}, $object); } + } - # We don't do $invocant->match because some classes have - # their own implementation of match which is not compatible - # with this one. However, match() still needs to have the right $invocant - # in order to do $class->DB_TABLE and so on. - my $list = match($invocant, { $id_field => \@detainted_ids }); - - # BMO: Populate the object cache with bug objects, which helps - # inline-history when viewing bugs with dependencies. - if ($class eq 'Bugzilla::Bug') { - foreach my $object (@$list) { - $class->_object_cache_set( - { id => $object->id, cache => 1 }, - $object - ); - } - } - - return $list; + return $list; } sub new_from_hash { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $object_data = shift || return; - $class->_serialisation_keys($object_data); - bless($object_data, $class); - $object_data->initialize(); - return $object_data; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $object_data = shift || return; + $class->_serialisation_keys($object_data); + bless($object_data, $class); + $object_data->initialize(); + return $object_data; } sub initialize { - # abstract + + # abstract } # Provides a mechanism for objects to be cached in the request_cahce sub object_cache_get { - my ($class, $id) = @_; - return $class->_object_cache_get( - { id => $id, cache => 1}, - $class - ); + my ($class, $id) = @_; + return $class->_object_cache_get({id => $id, cache => 1}, $class); } sub object_cache_set { - my $self = shift; - return $self->_object_cache_set( - { id => $self->id, cache => 1 }, - $self - ); + my $self = shift; + return $self->_object_cache_set({id => $self->id, cache => 1}, $self); } sub _object_cache_get { - my $class = shift; - my ($param) = @_; - my $cache_key = $class->object_cache_key($param) - || return; - return Bugzilla->request_cache->{$cache_key}; + my $class = shift; + my ($param) = @_; + my $cache_key = $class->object_cache_key($param) || return; + return Bugzilla->request_cache->{$cache_key}; } sub _object_cache_set { - my $class = shift; - my ($param, $object) = @_; - my $cache_key = $class->object_cache_key($param) - || return; - Bugzilla->request_cache->{$cache_key} = $object; + my $class = shift; + my ($param, $object) = @_; + my $cache_key = $class->object_cache_key($param) || return; + Bugzilla->request_cache->{$cache_key} = $object; } sub _object_cache_remove { - my $class = shift; - my ($param, $object) = @_; - $param->{cache} = 1; - my $cache_key = $class->object_cache_key($param) - || return; - delete Bugzilla->request_cache->{$cache_key}; + my $class = shift; + my ($param, $object) = @_; + $param->{cache} = 1; + my $cache_key = $class->object_cache_key($param) || return; + delete Bugzilla->request_cache->{$cache_key}; } sub object_cache_key { - my $class = shift; - my ($param) = @_; - if (ref($param) && $param->{cache} && ($param->{id} || $param->{name})) { - $class = blessed($class) if blessed($class); - return $class . ',' . ($param->{id} || $param->{name}); - } else { - return; - } + my $class = shift; + my ($param) = @_; + if (ref($param) && $param->{cache} && ($param->{id} || $param->{name})) { + $class = blessed($class) if blessed($class); + return $class . ',' . ($param->{id} || $param->{name}); + } + else { + return; + } } sub object_cache_clearall { - my $class = shift; - my $cache = Bugzilla->request_cache; - $class = blessed($class) if blessed($class); - my $prefix = $class . ','; - foreach my $key (grep { substr($_, 0, length($prefix)) eq $prefix } keys %$cache) { - delete $cache->{$key}; - } + my $class = shift; + my $cache = Bugzilla->request_cache; + $class = blessed($class) if blessed($class); + my $prefix = $class . ','; + foreach + my $key (grep { substr($_, 0, length($prefix)) eq $prefix } keys %$cache) + { + delete $cache->{$key}; + } } # To support serialisation, we need to capture the keys in an object's default # hashref. sub _serialisation_keys { - my ($class, $object) = @_; - my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {}; - $cache->{$class} = [ keys %$object ] if $object && !exists $cache->{$class}; - return @{ $cache->{$class} }; + my ($class, $object) = @_; + my $cache = Bugzilla->request_cache->{serialisation_keys} ||= {}; + $cache->{$class} = [keys %$object] if $object && !exists $cache->{$class}; + return @{$cache->{$class}}; } sub check { - my ($invocant, $param) = @_; - my $class = ref($invocant) || $invocant; - # If we were just passed a name, then just use the name. - if (!ref $param) { - $param = { name => $param }; - } - - # Don't allow empty names or ids. - my $check_param = exists $param->{id} ? 'id' : 'name'; - $param->{$check_param} = trim($param->{$check_param}); - # If somebody passes us "0", we want to throw an error like - # "there is no X with the name 0". This is true even for ids. So here, - # we only check if the parameter is undefined or empty. - if (!defined $param->{$check_param} or $param->{$check_param} eq '') { - ThrowUserError('object_not_specified', { class => $class }); + my ($invocant, $param) = @_; + my $class = ref($invocant) || $invocant; + + # If we were just passed a name, then just use the name. + if (!ref $param) { + $param = {name => $param}; + } + + # Don't allow empty names or ids. + my $check_param = exists $param->{id} ? 'id' : 'name'; + $param->{$check_param} = trim($param->{$check_param}); + + # If somebody passes us "0", we want to throw an error like + # "there is no X with the name 0". This is true even for ids. So here, + # we only check if the parameter is undefined or empty. + if (!defined $param->{$check_param} or $param->{$check_param} eq '') { + ThrowUserError('object_not_specified', {class => $class}); + } + + my $obj = $class->new($param); + if (!$obj) { + + # We don't want to override the normal template "user" object if + # "user" is one of the params. + delete $param->{user}; + if (my $error = delete $param->{_error}) { + ThrowUserError($error, {%$param, class => $class}); } - - my $obj = $class->new($param); - if (!$obj) { - # We don't want to override the normal template "user" object if - # "user" is one of the params. - delete $param->{user}; - if (my $error = delete $param->{_error}) { - ThrowUserError($error, { %$param, class => $class }); - } - else { - ThrowUserError('object_does_not_exist', { %$param, class => $class }); - } + else { + ThrowUserError('object_does_not_exist', {%$param, class => $class}); } - return $obj; + } + return $obj; } # Note: Future extensions to this could be: # * Add a MATCH_JOIN constant so that we can join against # certain other tables for the WHERE criteria. sub match { - my ($invocant, $criteria) = @_; - my $class = ref($invocant) || $invocant; - my $dbh = Bugzilla->dbh; - - return [$class->get_all] if !$criteria; - - my (@terms, @values, $postamble); - foreach my $field (keys %$criteria) { - my $value = $criteria->{$field}; - - # allow for LIMIT and OFFSET expressions via the criteria. - next if $field eq 'OFFSET'; - if ( $field eq 'LIMIT' ) { - next unless defined $value; - detaint_natural($value) - or ThrowCodeError('param_must_be_numeric', - { param => 'LIMIT', - function => "${class}::match" }); - my $offset; - if (defined $criteria->{OFFSET}) { - $offset = $criteria->{OFFSET}; - detaint_signed($offset) - or ThrowCodeError('param_must_be_numeric', - { param => 'OFFSET', - function => "${class}::match" }); - } - $postamble = $dbh->sql_limit($value, $offset); - next; - } - elsif ( $field eq 'WHERE' ) { - # the WHERE value is a hashref where the keys are - # "column_name operator ?" and values are the placeholder's - # value (either a scalar or an array of values). - foreach my $k (keys %$value) { - push(@terms, $k); - my @this_value = ref($value->{$k}) ? @{ $value->{$k} } - : ($value->{$k}); - push(@values, @this_value); - } - next; - } + my ($invocant, $criteria) = @_; + my $class = ref($invocant) || $invocant; + my $dbh = Bugzilla->dbh; + + return [$class->get_all] if !$criteria; + + my (@terms, @values, $postamble); + foreach my $field (keys %$criteria) { + my $value = $criteria->{$field}; + + # allow for LIMIT and OFFSET expressions via the criteria. + next if $field eq 'OFFSET'; + if ($field eq 'LIMIT') { + next unless defined $value; + detaint_natural($value) + or ThrowCodeError('param_must_be_numeric', + {param => 'LIMIT', function => "${class}::match"}); + my $offset; + if (defined $criteria->{OFFSET}) { + $offset = $criteria->{OFFSET}; + detaint_signed($offset) + or ThrowCodeError('param_must_be_numeric', + {param => 'OFFSET', function => "${class}::match"}); + } + $postamble = $dbh->sql_limit($value, $offset); + next; + } + elsif ($field eq 'WHERE') { + + # the WHERE value is a hashref where the keys are + # "column_name operator ?" and values are the placeholder's + # value (either a scalar or an array of values). + foreach my $k (keys %$value) { + push(@terms, $k); + my @this_value = ref($value->{$k}) ? @{$value->{$k}} : ($value->{$k}); + push(@values, @this_value); + } + next; + } - # It's always safe to use the field defined by classes as being - # their ID field. In particular, this means that new_from_list() - # is exempted from this check. - $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD; + # It's always safe to use the field defined by classes as being + # their ID field. In particular, this means that new_from_list() + # is exempted from this check. + $class->_check_field($field, 'match') unless $field eq $class->ID_FIELD; - if (ref $value eq 'ARRAY') { - # IN () is invalid SQL, and if we have an empty list - # to match against, we're just returning an empty - # array anyhow. - return [] if !scalar @$value; + if (ref $value eq 'ARRAY') { - my @qmarks = ("?") x @$value; - push(@terms, $dbh->sql_in($field, \@qmarks)); - push(@values, @$value); - } - elsif ($value eq NOT_NULL) { - push(@terms, "$field IS NOT NULL"); - } - elsif ($value eq IS_NULL) { - push(@terms, "$field IS NULL"); - } - else { - push(@terms, "$field = ?"); - push(@values, $value); - } + # 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(@terms, $dbh->sql_in($field, \@qmarks)); + push(@values, @$value); + } + elsif ($value eq NOT_NULL) { + push(@terms, "$field IS NOT NULL"); + } + elsif ($value eq IS_NULL) { + push(@terms, "$field IS NULL"); + } + else { + push(@terms, "$field = ?"); + push(@values, $value); } + } - my $where = join(' AND ', @terms) if scalar @terms; - return $class->_do_list_select($where, \@values, $postamble); + my $where = join(' AND ', @terms) if scalar @terms; + return $class->_do_list_select($where, \@values, $postamble); } sub _do_list_select { - my ($class, $where, $values, $postamble) = @_; - my $table = $class->DB_TABLE; - my $cols = join(',', $class->_get_db_columns); - my $order = $class->LIST_ORDER; - - # Unconditional requests for configuration data are cacheable. - my ($objects, $set_memcached, $memcached_key); - if (!defined $where - && Bugzilla->memcached->enabled - && $class->IS_CONFIG) - { - $memcached_key = "$class:get_all"; - $objects = Bugzilla->memcached->get_config({ key => $memcached_key }); - $set_memcached = $objects ? 0 : 1; - } - - if (!$objects) { - my $sql = "SELECT $cols FROM $table"; - if (defined $where) { - $sql .= " WHERE $where "; - } - $sql .= " ORDER BY $order"; - $sql .= " $postamble" if $postamble; - - my $dbh = Bugzilla->dbh; - # Sometimes the values are tainted, but we don't want to untaint them - # for the caller. So we copy the array. It's safe to untaint because - # they're only used in placeholders here. - my @untainted = @{ $values || [] }; - trick_taint($_) foreach @untainted; - $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted); - $class->_serialisation_keys($objects->[0]) if @$objects; + my ($class, $where, $values, $postamble) = @_; + my $table = $class->DB_TABLE; + my $cols = join(',', $class->_get_db_columns); + my $order = $class->LIST_ORDER; + + # Unconditional requests for configuration data are cacheable. + my ($objects, $set_memcached, $memcached_key); + if (!defined $where && Bugzilla->memcached->enabled && $class->IS_CONFIG) { + $memcached_key = "$class:get_all"; + $objects = Bugzilla->memcached->get_config({key => $memcached_key}); + $set_memcached = $objects ? 0 : 1; + } + + if (!$objects) { + my $sql = "SELECT $cols FROM $table"; + if (defined $where) { + $sql .= " WHERE $where "; } + $sql .= " ORDER BY $order"; + $sql .= " $postamble" if $postamble; - if ($objects && $set_memcached) { - Bugzilla->memcached->set_config({ - key => $memcached_key, - data => $objects - }); - } + my $dbh = Bugzilla->dbh; - foreach my $object (@$objects) { - $object = $class->new_from_hash($object); - } - return $objects; + # Sometimes the values are tainted, but we don't want to untaint them + # for the caller. So we copy the array. It's safe to untaint because + # they're only used in placeholders here. + my @untainted = @{$values || []}; + trick_taint($_) foreach @untainted; + $objects = $dbh->selectall_arrayref($sql, {Slice => {}}, @untainted); + $class->_serialisation_keys($objects->[0]) if @$objects; + } + + if ($objects && $set_memcached) { + Bugzilla->memcached->set_config({key => $memcached_key, data => $objects}); + } + + foreach my $object (@$objects) { + $object = $class->new_from_hash($object); + } + return $objects; } ############################### #### Accessors ###### ############################### -sub id { return $_[0]->{$_[0]->ID_FIELD}; } +sub id { return $_[0]->{$_[0]->ID_FIELD}; } sub name { return $_[0]->{$_[0]->NAME_FIELD}; } ############################### @@ -456,190 +452,200 @@ sub name { return $_[0]->{$_[0]->NAME_FIELD}; } ############################### sub set { - my ($self, $field, $value) = @_; - - # This method is protected. It's used to help implement set_ functions. - my $caller = caller; - $caller->isa('Bugzilla::Object') || $caller->isa('Bugzilla::Extension') - || ThrowCodeError('protection_violation', - { caller => caller, - superclass => __PACKAGE__, - function => 'Bugzilla::Object->set' }); - - Bugzilla::Hook::process('object_before_set', - { object => $self, field => $field, - value => $value }); - - my %validators = (%{$self->_get_validators}, %{$self->UPDATE_VALIDATORS}); - if (exists $validators{$field}) { - my $validator = $validators{$field}; - $value = $self->$validator($value, $field); - trick_taint($value) if (defined $value && !ref($value)); - - if ($self->can('_set_global_validator')) { - $self->_set_global_validator($value, $field); - } + my ($self, $field, $value) = @_; + + # This method is protected. It's used to help implement set_ functions. + my $caller = caller; + $caller->isa('Bugzilla::Object') + || $caller->isa('Bugzilla::Extension') + || ThrowCodeError( + 'protection_violation', + { + caller => caller, + superclass => __PACKAGE__, + function => 'Bugzilla::Object->set' + } + ); + + Bugzilla::Hook::process('object_before_set', + {object => $self, field => $field, value => $value}); + + my %validators = (%{$self->_get_validators}, %{$self->UPDATE_VALIDATORS}); + if (exists $validators{$field}) { + my $validator = $validators{$field}; + $value = $self->$validator($value, $field); + trick_taint($value) if (defined $value && !ref($value)); + + if ($self->can('_set_global_validator')) { + $self->_set_global_validator($value, $field); } + } - $self->{$field} = $value; + $self->{$field} = $value; - Bugzilla::Hook::process('object_end_of_set', - { object => $self, field => $field }); + Bugzilla::Hook::process('object_end_of_set', + {object => $self, field => $field}); } sub set_all { - my ($self, $params) = @_; - - # Don't let setters modify the values in $params for the caller. - my %field_values = %$params; - - my @sorted_names = $self->_sort_by_dep(keys %field_values); - - foreach my $key (@sorted_names) { - # It's possible for one set_ method to delete a key from $params - # for another set method, so if that's happened, we don't call the - # other set method. - next if !exists $field_values{$key}; - my $method = "set_$key"; - if (!$self->can($method)) { - my $class = ref($self) || $self; - ThrowCodeError("unknown_method", { method => "${class}::${method}" }); - } - $self->$method($field_values{$key}, \%field_values); + my ($self, $params) = @_; + + # Don't let setters modify the values in $params for the caller. + my %field_values = %$params; + + my @sorted_names = $self->_sort_by_dep(keys %field_values); + + foreach my $key (@sorted_names) { + + # It's possible for one set_ method to delete a key from $params + # for another set method, so if that's happened, we don't call the + # other set method. + next if !exists $field_values{$key}; + my $method = "set_$key"; + if (!$self->can($method)) { + my $class = ref($self) || $self; + ThrowCodeError("unknown_method", {method => "${class}::${method}"}); } - Bugzilla::Hook::process('object_end_of_set_all', - { object => $self, params => \%field_values }); + $self->$method($field_values{$key}, \%field_values); + } + Bugzilla::Hook::process('object_end_of_set_all', + {object => $self, params => \%field_values}); } sub update { - my $self = shift; - - my $dbh = Bugzilla->dbh; - my $table = $self->DB_TABLE; - my $id_field = $self->ID_FIELD; - - $dbh->bz_start_transaction(); - - my $old_self = $self->new($self->id); - - # BMO - allow altering values in a sane way - Bugzilla::Hook::process('object_start_of_update', - { object => $self, old_object => $old_self }); - - my @all_columns = $self->UPDATE_COLUMNS; - my @hook_columns; - Bugzilla::Hook::process('object_update_columns', - { object => $self, columns => \@hook_columns }); - push(@all_columns, @hook_columns); - - my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS; - my %date = map { $_ => 1 } $self->DATE_COLUMNS; - my (@update_columns, @values, %changes); - foreach my $column (@all_columns) { - my ($old, $new) = ($old_self->{$column}, $self->{$column}); - # This has to be written this way in order to allow us to set a field - # from undef or to undef, and avoid warnings about comparing an undef - # with the "eq" operator. - if (!defined $new || !defined $old) { - next if !defined $new && !defined $old; - } - elsif ( ($numeric{$column} && $old == $new) - || ($date{$column} && str2time($old) == str2time($new)) - || $old eq $new ) { - next; - } + my $self = shift; - trick_taint($new) if defined $new; - push(@values, $new); - push(@update_columns, $column); - # We don't use $new because we don't want to detaint this for - # the caller. - $changes{$column} = [$old, $self->{$column}]; - } + my $dbh = Bugzilla->dbh; + my $table = $self->DB_TABLE; + my $id_field = $self->ID_FIELD; - my $columns = join(', ', map {"$_ = ?"} @update_columns); + $dbh->bz_start_transaction(); - $dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", undef, - @values, $self->id) if @values; + my $old_self = $self->new($self->id); - Bugzilla::Hook::process('object_end_of_update', - { object => $self, old_object => $old_self, - changes => \%changes }); + # BMO - allow altering values in a sane way + Bugzilla::Hook::process('object_start_of_update', + {object => $self, old_object => $old_self}); - $self->audit_log(\%changes) if $self->AUDIT_UPDATES; + my @all_columns = $self->UPDATE_COLUMNS; + my @hook_columns; + Bugzilla::Hook::process('object_update_columns', + {object => $self, columns => \@hook_columns}); + push(@all_columns, @hook_columns); - $dbh->bz_commit_transaction(); - if ($self->USE_MEMCACHED && @values) { - Bugzilla->memcached->clear({ table => $table, id => $self->id }); - Bugzilla->memcached->clear_config() - if $self->IS_CONFIG; - } - $self->_object_cache_remove({ id => $self->id }); - $self->_object_cache_remove({ name => $self->name }) if $self->name; + my %numeric = map { $_ => 1 } $self->NUMERIC_COLUMNS; + my %date = map { $_ => 1 } $self->DATE_COLUMNS; + my (@update_columns, @values, %changes); + foreach my $column (@all_columns) { + my ($old, $new) = ($old_self->{$column}, $self->{$column}); - if (wantarray) { - return (\%changes, $old_self); + # This has to be written this way in order to allow us to set a field + # from undef or to undef, and avoid warnings about comparing an undef + # with the "eq" operator. + if (!defined $new || !defined $old) { + next if !defined $new && !defined $old; + } + elsif (($numeric{$column} && $old == $new) + || ($date{$column} && str2time($old) == str2time($new)) + || $old eq $new) + { + next; } - return \%changes; + trick_taint($new) if defined $new; + push(@values, $new); + push(@update_columns, $column); + + # We don't use $new because we don't want to detaint this for + # the caller. + $changes{$column} = [$old, $self->{$column}]; + } + + my $columns = join(', ', map {"$_ = ?"} @update_columns); + + $dbh->do("UPDATE $table SET $columns WHERE $id_field = ?", + undef, @values, $self->id) + if @values; + + Bugzilla::Hook::process('object_end_of_update', + {object => $self, old_object => $old_self, changes => \%changes}); + + $self->audit_log(\%changes) if $self->AUDIT_UPDATES; + + $dbh->bz_commit_transaction(); + if ($self->USE_MEMCACHED && @values) { + Bugzilla->memcached->clear({table => $table, id => $self->id}); + Bugzilla->memcached->clear_config() if $self->IS_CONFIG; + } + $self->_object_cache_remove({id => $self->id}); + $self->_object_cache_remove({name => $self->name}) if $self->name; + + if (wantarray) { + return (\%changes, $old_self); + } + + return \%changes; } sub remove_from_db { - my $self = shift; - Bugzilla::Hook::process('object_before_delete', { object => $self }); - my $table = $self->DB_TABLE; - my $id_field = $self->ID_FIELD; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - $self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES; - $dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id); - $dbh->bz_commit_transaction(); - if ($self->USE_MEMCACHED) { - Bugzilla->memcached->clear({ table => $table, id => $self->id }); - Bugzilla->memcached->clear_config() - if $self->IS_CONFIG; - } - $self->_object_cache_remove({ id => $self->id }); - $self->_object_cache_remove({ name => $self->name }) if $self->name; - undef $self; + my $self = shift; + Bugzilla::Hook::process('object_before_delete', {object => $self}); + my $table = $self->DB_TABLE; + my $id_field = $self->ID_FIELD; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + $self->audit_log(AUDIT_REMOVE) if $self->AUDIT_REMOVES; + $dbh->do("DELETE FROM $table WHERE $id_field = ?", undef, $self->id); + $dbh->bz_commit_transaction(); + + if ($self->USE_MEMCACHED) { + Bugzilla->memcached->clear({table => $table, id => $self->id}); + Bugzilla->memcached->clear_config() if $self->IS_CONFIG; + } + $self->_object_cache_remove({id => $self->id}); + $self->_object_cache_remove({name => $self->name}) if $self->name; + undef $self; } sub audit_log { - my ($self, $changes) = @_; - my $class = ref $self; - my $dbh = Bugzilla->dbh; - my $user_id = Bugzilla->user->id || undef; - my $sth = $dbh->prepare( - 'INSERT INTO audit_log (user_id, class, object_id, field, + my ($self, $changes) = @_; + my $class = ref $self; + my $dbh = Bugzilla->dbh; + my $user_id = Bugzilla->user->id || undef; + my $sth = $dbh->prepare( + 'INSERT INTO audit_log (user_id, class, object_id, field, removed, added, at_time) - VALUES (?,?,?,?,?,?,LOCALTIMESTAMP(0))'); - # During creation or removal, $changes is actually just a string - # indicating whether we're creating or removing the object. - if ($changes eq AUDIT_CREATE or $changes eq AUDIT_REMOVE) { - # We put the object's name in the "added" or "removed" field. - # We do this thing with NAME_FIELD because $self->name returns - # the wrong thing for Bugzilla::User. - my $name = $self->{$self->NAME_FIELD}; - my @added_removed = $changes eq AUDIT_CREATE ? (undef, $name) - : ($name, undef); - $sth->execute($user_id, $class, $self->id, $changes, @added_removed); - return; - } - - # During update, it's the actual %changes hash produced by update(). - foreach my $field (keys %$changes) { - # Skip private changes. - next if $field =~ /^_/; - my ($from, $to) = @{ $changes->{$field} }; - $sth->execute($user_id, $class, $self->id, $field, $from, $to); - } + VALUES (?,?,?,?,?,?,LOCALTIMESTAMP(0))' + ); + + # During creation or removal, $changes is actually just a string + # indicating whether we're creating or removing the object. + if ($changes eq AUDIT_CREATE or $changes eq AUDIT_REMOVE) { + + # We put the object's name in the "added" or "removed" field. + # We do this thing with NAME_FIELD because $self->name returns + # the wrong thing for Bugzilla::User. + my $name = $self->{$self->NAME_FIELD}; + my @added_removed = $changes eq AUDIT_CREATE ? (undef, $name) : ($name, undef); + $sth->execute($user_id, $class, $self->id, $changes, @added_removed); + return; + } + + # During update, it's the actual %changes hash produced by update(). + foreach my $field (keys %$changes) { + + # Skip private changes. + next if $field =~ /^_/; + my ($from, $to) = @{$changes->{$field}}; + $sth->execute($user_id, $class, $self->id, $field, $from, $to); + } } sub flatten_to_hash { - my $self = shift; - my $class = blessed($self); - my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys; - return \%hash; + my $self = shift; + my $class = blessed($self); + my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys; + return \%hash; } ############################### @@ -647,127 +653,125 @@ sub flatten_to_hash { ############################### sub any_exist { - my $class = shift; - my $table = $class->DB_TABLE; - my $dbh = Bugzilla->dbh; - my $any_exist = $dbh->selectrow_array( - "SELECT 1 FROM $table " . $dbh->sql_limit(1)); - return $any_exist ? 1 : 0; + my $class = shift; + my $table = $class->DB_TABLE; + my $dbh = Bugzilla->dbh; + my $any_exist + = $dbh->selectrow_array("SELECT 1 FROM $table " . $dbh->sql_limit(1)); + return $any_exist ? 1 : 0; } sub create { - my ($class, $params) = @_; - my $dbh = Bugzilla->dbh; + my ($class, $params) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - $class->check_required_create_fields($params); - my $field_values = $class->run_create_validators($params); - my $object = $class->insert_create_data($field_values); - $dbh->bz_commit_transaction(); + $dbh->bz_start_transaction(); + $class->check_required_create_fields($params); + my $field_values = $class->run_create_validators($params); + my $object = $class->insert_create_data($field_values); + $dbh->bz_commit_transaction(); - if (Bugzilla->memcached->enabled - && $class->USE_MEMCACHED - && $class->IS_CONFIG) - { - Bugzilla->memcached->clear_config(); - } + if (Bugzilla->memcached->enabled && $class->USE_MEMCACHED && $class->IS_CONFIG) + { + Bugzilla->memcached->clear_config(); + } - return $object; + return $object; } # Used to validate that a field name is in fact a valid column in the # current table before inserting it into SQL. sub _check_field { - my ($invocant, $field, $function) = @_; - my $class = ref($invocant) || $invocant; - if (!Bugzilla->dbh->bz_column_info($class->DB_TABLE, $field)) { - ThrowCodeError('param_invalid', { param => $field, - function => "${class}::$function" }); - } + my ($invocant, $field, $function) = @_; + my $class = ref($invocant) || $invocant; + if (!Bugzilla->dbh->bz_column_info($class->DB_TABLE, $field)) { + ThrowCodeError('param_invalid', + {param => $field, function => "${class}::$function"}); + } } sub check_required_create_fields { - my ($class, $params) = @_; + my ($class, $params) = @_; - # This hook happens here so that even subclasses that don't call - # SUPER::create are still affected by the hook. - Bugzilla::Hook::process('object_before_create', { class => $class, - params => $params }); + # This hook happens here so that even subclasses that don't call + # SUPER::create are still affected by the hook. + Bugzilla::Hook::process('object_before_create', + {class => $class, params => $params}); - my @check_fields = $class->_required_create_fields(); - foreach my $field (@check_fields) { - $params->{$field} = undef if !exists $params->{$field}; - } + my @check_fields = $class->_required_create_fields(); + foreach my $field (@check_fields) { + $params->{$field} = undef if !exists $params->{$field}; + } } sub run_create_validators { - my ($class, $params, $options) = @_; - - my $validators = $class->_get_validators; - my %field_values = %$params; + my ($class, $params, $options) = @_; - # Make a hash skiplist for easier searching later - my %skip_list = map { $_ => 1 } @{ $options->{skip} || [] }; + my $validators = $class->_get_validators; + my %field_values = %$params; - # Get the sorted field names - my @sorted_names = $class->_sort_by_dep(keys %field_values); + # Make a hash skiplist for easier searching later + my %skip_list = map { $_ => 1 } @{$options->{skip} || []}; - # Remove the skipped names - my @unskipped = grep { !$skip_list{$_} } @sorted_names; + # Get the sorted field names + my @sorted_names = $class->_sort_by_dep(keys %field_values); - foreach my $field (@unskipped) { - my $value; - if (exists $validators->{$field}) { - my $validator = $validators->{$field}; - $value = $class->$validator($field_values{$field}, $field, - \%field_values); - } - else { - $value = $field_values{$field}; - } + # Remove the skipped names + my @unskipped = grep { !$skip_list{$_} } @sorted_names; - # We want people to be able to explicitly set fields to NULL, - # and that means they can be set to undef. - trick_taint($value) if defined $value && !ref($value); - $field_values{$field} = $value; + foreach my $field (@unskipped) { + my $value; + if (exists $validators->{$field}) { + my $validator = $validators->{$field}; + $value = $class->$validator($field_values{$field}, $field, \%field_values); } - - Bugzilla::Hook::process('object_end_of_create_validators', - { class => $class, params => \%field_values }); - - return \%field_values; -} - -sub insert_create_data { - my ($class, $field_values) = @_; - my $dbh = Bugzilla->dbh; - - my (@field_names, @values); - while (my ($field, $value) = each %$field_values) { - $class->_check_field($field, 'create'); - push(@field_names, $field); - push(@values, $value); + else { + $value = $field_values{$field}; } - my $qmarks = '?,' x @field_names; - chop($qmarks); - my $table = $class->DB_TABLE; - $dbh->do("INSERT INTO $table (" . join(', ', @field_names) - . ") VALUES ($qmarks)", undef, @values); - my $id = $dbh->bz_last_key($table, $class->ID_FIELD); + # We want people to be able to explicitly set fields to NULL, + # and that means they can be set to undef. + trick_taint($value) if defined $value && !ref($value); + $field_values{$field} = $value; + } - my $object = $class->new($id); + Bugzilla::Hook::process('object_end_of_create_validators', + {class => $class, params => \%field_values}); - Bugzilla::Hook::process('object_end_of_create', { class => $class, - object => $object }); - $object->audit_log(AUDIT_CREATE) if $object->AUDIT_CREATES; + return \%field_values; +} - return $object; +sub insert_create_data { + my ($class, $field_values) = @_; + my $dbh = Bugzilla->dbh; + + my (@field_names, @values); + while (my ($field, $value) = each %$field_values) { + $class->_check_field($field, 'create'); + push(@field_names, $field); + push(@values, $value); + } + + my $qmarks = '?,' x @field_names; + chop($qmarks); + my $table = $class->DB_TABLE; + $dbh->do( + "INSERT INTO $table (" . join(', ', @field_names) . ") VALUES ($qmarks)", + undef, @values); + my $id = $dbh->bz_last_key($table, $class->ID_FIELD); + + my $object = $class->new($id); + + Bugzilla::Hook::process('object_end_of_create', + {class => $class, object => $object}); + $object->audit_log(AUDIT_CREATE) if $object->AUDIT_CREATES; + + return $object; } sub get_all { - my $class = shift; - return @{ $class->_do_list_select() }; + my $class = shift; + return @{$class->_do_list_select()}; } ############################### @@ -777,20 +781,19 @@ sub get_all { sub check_boolean { return $_[1] ? 1 : 0 } sub check_time { - my ($invocant, $value, $field, $params, $allow_negative) = @_; + my ($invocant, $value, $field, $params, $allow_negative) = @_; - # If we don't have a current value default to zero - my $current = blessed($invocant) ? $invocant->{$field} - : 0; - $current ||= 0; + # If we don't have a current value default to zero + my $current = blessed($invocant) ? $invocant->{$field} : 0; + $current ||= 0; - # Get the new value or zero if it isn't defined - $value = trim($value) || 0; + # Get the new value or zero if it isn't defined + $value = trim($value) || 0; - # Make sure the new value is well formed - _validate_time($value, $field, $allow_negative); + # Make sure the new value is well formed + _validate_time($value, $field, $allow_negative); - return $value; + return $value; } @@ -799,26 +802,25 @@ sub check_time { ################### sub _validate_time { - my ($time, $field, $allow_negative) = @_; - - # regexp verifies one or more digits, optionally followed by a period and - # zero or more digits, OR we have a period followed by one or more digits - # (allow negatives, though, so people can back out errors in time reporting) - if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) { - ThrowUserError("number_not_numeric", - {field => $field, num => "$time"}); - } - - # Callers can optionally allow negative times - if ( ($time < 0) && !$allow_negative ) { - ThrowUserError("number_too_small", - {field => $field, num => "$time", min_num => "0"}); - } - - if ($time > 99999.99) { - ThrowUserError("number_too_large", - {field => $field, num => "$time", max_num => "99999.99"}); - } + my ($time, $field, $allow_negative) = @_; + + # regexp verifies one or more digits, optionally followed by a period and + # zero or more digits, OR we have a period followed by one or more digits + # (allow negatives, though, so people can back out errors in time reporting) + if ($time !~ /^-?(?:\d+(?:\.\d*)?|\.\d+)$/) { + ThrowUserError("number_not_numeric", {field => $field, num => "$time"}); + } + + # Callers can optionally allow negative times + if (($time < 0) && !$allow_negative) { + ThrowUserError("number_too_small", + {field => $field, num => "$time", min_num => "0"}); + } + + if ($time > 99999.99) { + ThrowUserError("number_too_large", + {field => $field, num => "$time", max_num => "99999.99"}); + } } # Sorts fields according to VALIDATOR_DEPENDENCIES. This is not a @@ -826,54 +828,55 @@ sub _validate_time { # *have* to be in the list--it just has to be earlier than its dependent # if it *is* in the list. sub _sort_by_dep { - my ($invocant, @fields) = @_; - - my $dependencies = $invocant->VALIDATOR_DEPENDENCIES; - my ($has_deps, $no_deps) = part { $dependencies->{$_} ? 0 : 1 } @fields; - - # For fields with no dependencies, we sort them alphabetically, - # so that validation always happens in a consistent order. - # Fields with no dependencies come at the start of the list. - my @result = sort @{ $no_deps || [] }; - - # Fields with dependencies all go at the end of the list, and if - # they have dependencies on *each other*, then they have to be - # sorted properly. We go through $has_deps in sorted order to be - # sure that fields always validate in a consistent order. - foreach my $field (sort @{ $has_deps || [] }) { - if (!grep { $_ eq $field } @result) { - _insert_dep_field($field, $has_deps, $dependencies, \@result); - } + my ($invocant, @fields) = @_; + + my $dependencies = $invocant->VALIDATOR_DEPENDENCIES; + my ($has_deps, $no_deps) = part { $dependencies->{$_} ? 0 : 1 } @fields; + + # For fields with no dependencies, we sort them alphabetically, + # so that validation always happens in a consistent order. + # Fields with no dependencies come at the start of the list. + my @result = sort @{$no_deps || []}; + + # Fields with dependencies all go at the end of the list, and if + # they have dependencies on *each other*, then they have to be + # sorted properly. We go through $has_deps in sorted order to be + # sure that fields always validate in a consistent order. + foreach my $field (sort @{$has_deps || []}) { + if (!grep { $_ eq $field } @result) { + _insert_dep_field($field, $has_deps, $dependencies, \@result); } - return @result; + } + return @result; } sub _insert_dep_field { - my ($field, $insert_me, $dependencies, $result, $loop_tracking) = @_; + my ($field, $insert_me, $dependencies, $result, $loop_tracking) = @_; - if ($loop_tracking->{$field}) { - ThrowCodeError('object_dep_sort_loop', - { field => $field, - considered => [keys %$loop_tracking] }); - } - $loop_tracking->{$field} = 1; - - my $required_fields = $dependencies->{$field}; - # Imagine Field A requires field B... - foreach my $required_field (@$required_fields) { - # If our dependency is already satisfied, we're good. - next if grep { $_ eq $required_field } @$result; - - # If our dependency is not in the remaining fields to insert, - # then we're also OK. - next if !grep { $_ eq $required_field } @$insert_me; - - # So, at this point, we know that Field B is in $insert_me. - # So let's put the required field into the result. - _insert_dep_field($required_field, $insert_me, $dependencies, - $result, $loop_tracking); - } - push(@$result, $field); + if ($loop_tracking->{$field}) { + ThrowCodeError('object_dep_sort_loop', + {field => $field, considered => [keys %$loop_tracking]}); + } + $loop_tracking->{$field} = 1; + + my $required_fields = $dependencies->{$field}; + + # Imagine Field A requires field B... + foreach my $required_field (@$required_fields) { + + # If our dependency is already satisfied, we're good. + next if grep { $_ eq $required_field } @$result; + + # If our dependency is not in the remaining fields to insert, + # then we're also OK. + next if !grep { $_ eq $required_field } @$insert_me; + + # So, at this point, we know that Field B is in $insert_me. + # So let's put the required field into the result. + _insert_dep_field($required_field, $insert_me, $dependencies, $result, + $loop_tracking); + } + push(@$result, $field); } #################### @@ -886,67 +889,72 @@ sub _insert_dep_field { # page. sub _get_db_columns { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $cache = Bugzilla->request_cache; - my $cache_key = "object_${class}_db_columns"; - return @{ $cache->{$cache_key} } if $cache->{$cache_key}; - my @columns; - if ($class->DYNAMIC_COLUMNS) { - @columns = Bugzilla->dbh->bz_table_columns_real($class->DB_TABLE); - } - else { - # Currently you can only add new columns using object_columns, not - # remove or modify existing columns, because removing columns would - # almost certainly cause Bugzilla to function improperly. - my @add_columns; - Bugzilla::Hook::process('object_columns', - { class => $class, columns => \@add_columns }); - @columns = ($invocant->DB_COLUMNS, @add_columns); - } - $cache->{$cache_key} = \@columns; - return @{ $cache->{$cache_key} }; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $cache = Bugzilla->request_cache; + my $cache_key = "object_${class}_db_columns"; + return @{$cache->{$cache_key}} if $cache->{$cache_key}; + my @columns; + if ($class->DYNAMIC_COLUMNS) { + @columns = Bugzilla->dbh->bz_table_columns_real($class->DB_TABLE); + } + else { + # Currently you can only add new columns using object_columns, not + # remove or modify existing columns, because removing columns would + # almost certainly cause Bugzilla to function improperly. + my @add_columns; + Bugzilla::Hook::process('object_columns', + {class => $class, columns => \@add_columns}); + @columns = ($invocant->DB_COLUMNS, @add_columns); + } + $cache->{$cache_key} = \@columns; + return @{$cache->{$cache_key}}; } # This method is private and should only be called by Bugzilla::Object. sub _get_validators { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $cache = Bugzilla->request_cache; - my $cache_key = "object_${class}_validators"; - return $cache->{$cache_key} if $cache->{$cache_key}; - # We copy this into a hash so that the hook doesn't modify the constant. - # (That could be bad in mod_perl.) - my %validators = %{ $invocant->VALIDATORS }; - Bugzilla::Hook::process('object_validators', - { class => $class, validators => \%validators }); - $cache->{$cache_key} = \%validators; - return $cache->{$cache_key}; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $cache = Bugzilla->request_cache; + my $cache_key = "object_${class}_validators"; + return $cache->{$cache_key} if $cache->{$cache_key}; + + # We copy this into a hash so that the hook doesn't modify the constant. + # (That could be bad in mod_perl.) + my %validators = %{$invocant->VALIDATORS}; + Bugzilla::Hook::process('object_validators', + {class => $class, validators => \%validators}); + $cache->{$cache_key} = \%validators; + return $cache->{$cache_key}; } # These are all the fields that need to be checked, always, when # calling create(), because they have no DEFAULT and they are marked # NOT NULL. sub _required_create_fields { - my $class = shift; - my $dbh = Bugzilla->dbh; - my $table = $class->DB_TABLE; - - my @columns = $dbh->bz_table_columns($table); - my @required; - foreach my $column (@columns) { - my $def = $dbh->bz_column_info($table, $column); - if ($def->{NOTNULL} and !defined $def->{DEFAULT} - # SERIAL fields effectively have a DEFAULT, but they're not - # listed as having a DEFAULT in DB::Schema. - and $def->{TYPE} !~ /serial/i) - { - my $field = $class->REQUIRED_FIELD_MAP->{$column} || $column; - push(@required, $field); - } + my $class = shift; + my $dbh = Bugzilla->dbh; + my $table = $class->DB_TABLE; + + my @columns = $dbh->bz_table_columns($table); + my @required; + foreach my $column (@columns) { + my $def = $dbh->bz_column_info($table, $column); + if ( + $def->{NOTNULL} + and !defined $def->{DEFAULT} + + # SERIAL fields effectively have a DEFAULT, but they're not + # listed as having a DEFAULT in DB::Schema. + and $def->{TYPE} !~ /serial/i + ) + { + my $field = $class->REQUIRED_FIELD_MAP->{$column} || $column; + push(@required, $field); } - push(@required, $class->EXTRA_REQUIRED_FIELDS); - return @required; + } + push(@required, $class->EXTRA_REQUIRED_FIELDS); + return @required; } 1; diff --git a/Bugzilla/PSGI.pm b/Bugzilla/PSGI.pm index 46352b319..e95b5291b 100644 --- a/Bugzilla/PSGI.pm +++ b/Bugzilla/PSGI.pm @@ -16,27 +16,25 @@ use Bugzilla::Logging; our @EXPORT_OK = qw(compile_cgi); sub compile_cgi { - my ($script) = @_; - require CGI::Compile; - require CGI::Emulate::PSGI; + my ($script) = @_; + require CGI::Compile; + require CGI::Emulate::PSGI; - my $cgi = CGI::Compile->compile($script); - my $app = CGI::Emulate::PSGI->handler( - sub { - Bugzilla::init_page(); - $cgi->(); - } - ); - return sub { - my $env = shift; - if ($env->{'psgix.cleanup'}) { - push @{ $env->{'psgix.cleanup.handler'} }, \&Bugzilla::_cleanup; - } - my $res = $app->($env); - Bugzilla::_cleanup() if not $env->{'psgix.cleanup'}; - return $res; - }; + my $cgi = CGI::Compile->compile($script); + my $app = CGI::Emulate::PSGI->handler(sub { + Bugzilla::init_page(); + $cgi->(); + }); + return sub { + my $env = shift; + if ($env->{'psgix.cleanup'}) { + push @{$env->{'psgix.cleanup.handler'}}, \&Bugzilla::_cleanup; + } + my $res = $app->($env); + Bugzilla::_cleanup() if not $env->{'psgix.cleanup'}; + return $res; + }; } -1; \ No newline at end of file +1; diff --git a/Bugzilla/PatchReader/AddCVSContext.pm b/Bugzilla/PatchReader/AddCVSContext.pm index 4e7da5661..094ef6ed8 100644 --- a/Bugzilla/PatchReader/AddCVSContext.pm +++ b/Bugzilla/PatchReader/AddCVSContext.pm @@ -9,7 +9,8 @@ use Bugzilla::PatchReader::CVSClient; use Cwd; use File::Temp; -@Bugzilla::PatchReader::AddCVSContext::ISA = qw(Bugzilla::PatchReader::FilterPatch); +@Bugzilla::PatchReader::AddCVSContext::ISA + = qw(Bugzilla::PatchReader::FilterPatch); # XXX If you need to, get the entire patch worth of files and do a single # cvs update of all files as soon as you find a file where you need to do a @@ -31,7 +32,8 @@ sub my_rmtree { foreach my $file (glob("$dir/*")) { if (-d $file) { $this->my_rmtree($file); - } else { + } + else { trick_taint($file); unlink $file; } @@ -43,6 +45,7 @@ sub my_rmtree { sub end_patch { my $this = shift; if (exists($this->{TMPDIR})) { + # Set as variable to get rid of taint # One would like to use rmtree here, but that is not taint-safe. $this->my_rmtree($this->{TMPDIR}); @@ -52,10 +55,10 @@ sub end_patch { sub start_file { my $this = shift; my ($file) = @_; - $this->{HAS_CVS_CONTEXT} = !$file->{is_add} && !$file->{is_remove} && - $file->{old_revision}; - $this->{REVISION} = $file->{old_revision}; - $this->{FILENAME} = $file->{filename}; + $this->{HAS_CVS_CONTEXT} + = !$file->{is_add} && !$file->{is_remove} && $file->{old_revision}; + $this->{REVISION} = $file->{old_revision}; + $this->{FILENAME} = $file->{filename}; $this->{SECTION_END} = -1; $this->{TARGET}->start_file(@_) if $this->{TARGET}; } @@ -66,7 +69,7 @@ sub end_file { if ($this->{FILE}) { close $this->{FILE}; - unlink $this->{FILE}; # If it fails, it fails ... + unlink $this->{FILE}; # If it fails, it fails ... delete $this->{FILE}; } $this->{TARGET}->end_file(@_) if $this->{TARGET}; @@ -76,10 +79,12 @@ sub next_section { my $this = shift; my ($section) = @_; $this->{NEXT_PATCH_LINE} = $section->{old_start}; - $this->{NEXT_NEW_LINE} = $section->{new_start}; + $this->{NEXT_NEW_LINE} = $section->{new_start}; foreach my $line (@{$section->{lines}}) { + # If this is a line requiring context ... if ($line =~ /^[-\+]/) { + # Determine how much context is needed for both the previous section line # and this one: # - If there is no old line, start new section @@ -89,20 +94,23 @@ sub next_section { # space and therefore we end the old section and start the new one # - Else we add (old start context line through new line) context to # existing section - if (! exists($this->{SECTION})) { + if (!exists($this->{SECTION})) { $this->_start_section(); - } elsif ($this->{CONTEXT} eq "file") { + } + elsif ($this->{CONTEXT} eq "file") { $this->push_context_lines($this->{SECTION_END} + 1, - $this->{NEXT_PATCH_LINE} - 1); - } else { + $this->{NEXT_PATCH_LINE} - 1); + } + else { my $start_context = $this->{NEXT_PATCH_LINE} - $this->{CONTEXT}; $start_context = $start_context > 0 ? $start_context : 0; if (($this->{SECTION_END} + $this->{CONTEXT} + 1) < $start_context) { $this->flush_section(); $this->_start_section(); - } else { + } + else { $this->push_context_lines($this->{SECTION_END} + 1, - $this->{NEXT_PATCH_LINE} - 1); + $this->{NEXT_PATCH_LINE} - 1); } } push @{$this->{SECTION}{lines}}, $line; @@ -110,16 +118,19 @@ sub next_section { $this->{SECTION}{plus_lines}++; $this->{SECTION}{new_lines}++; $this->{NEXT_NEW_LINE}++; - } else { + } + else { $this->{SECTION_END}++; $this->{SECTION}{minus_lines}++; $this->{SECTION}{old_lines}++; $this->{NEXT_PATCH_LINE}++; } - } else { + } + else { $this->{NEXT_PATCH_LINE}++; $this->{NEXT_NEW_LINE}++; } + # If this is context, for now lose it (later we should try and determine if # we can just use it instead of pulling the file all the time) } @@ -130,7 +141,8 @@ sub determine_start { return 0 if $line < 0; if ($this->{CONTEXT} eq "file") { return 1; - } else { + } + else { my $start = $line - $this->{CONTEXT}; $start = $start > 0 ? $start : 1; return $start; @@ -146,23 +158,26 @@ sub _start_section { $this->{SECTION}{old_lines} = 0; $this->{SECTION}{new_lines} = 0; $this->{SECTION}{minus_lines} = 0; - $this->{SECTION}{plus_lines} = 0; - $this->{SECTION_END} = $this->{SECTION}{old_start} - 1; + $this->{SECTION}{plus_lines} = 0; + $this->{SECTION_END} = $this->{SECTION}{old_start} - 1; $this->push_context_lines($this->{SECTION}{old_start}, - $this->{NEXT_PATCH_LINE} - 1); + $this->{NEXT_PATCH_LINE} - 1); } sub flush_section { my $this = shift; if ($this->{SECTION}) { + # Add the necessary context to the end if ($this->{CONTEXT} eq "file") { $this->push_context_lines($this->{SECTION_END} + 1, "file"); - } else { + } + else { $this->push_context_lines($this->{SECTION_END} + 1, - $this->{SECTION_END} + $this->{CONTEXT}); + $this->{SECTION_END} + $this->{CONTEXT}); } + # Send the section and line notifications $this->{TARGET}->next_section($this->{SECTION}) if $this->{TARGET}; delete $this->{SECTION}; @@ -172,35 +187,41 @@ sub flush_section { sub push_context_lines { my $this = shift; + # Grab from start to end my ($start, $end) = @_; return if $end ne "file" && $start > $end; # If it's an added / removed file, don't do anything - return if ! $this->{HAS_CVS_CONTEXT}; + return if !$this->{HAS_CVS_CONTEXT}; # Get and open the file if necessary if (!$this->{FILE}) { my $olddir = getcwd(); - if (! exists($this->{TMPDIR})) { + if (!exists($this->{TMPDIR})) { $this->{TMPDIR} = File::Temp::tempdir(); - if (! -d $this->{TMPDIR}) { + if (!-d $this->{TMPDIR}) { die "Could not get temporary directory"; } } chdir($this->{TMPDIR}) or die "Could not cd $this->{TMPDIR}"; - if (Bugzilla::PatchReader::CVSClient::cvs_co_rev($this->{CVSROOT}, $this->{REVISION}, $this->{FILENAME})) { - die "Could not check out $this->{FILENAME} r$this->{REVISION} from $this->{CVSROOT}"; + if (Bugzilla::PatchReader::CVSClient::cvs_co_rev( + $this->{CVSROOT}, $this->{REVISION}, $this->{FILENAME} + )) + { + die + "Could not check out $this->{FILENAME} r$this->{REVISION} from $this->{CVSROOT}"; } open(my $fh, '<', $this->{FILENAME}) or die "Could not open $this->{FILENAME}"; - $this->{FILE} = $fh; + $this->{FILE} = $fh; $this->{NEXT_FILE_LINE} = 1; - trick_taint($olddir); # $olddir comes from getcwd() + trick_taint($olddir); # $olddir comes from getcwd() chdir($olddir) or die "Could not cd back to $olddir"; } # Read through the file to reach the line we need - die "File read too far!" if $this->{NEXT_FILE_LINE} && $this->{NEXT_FILE_LINE} > $start; + die "File read too far!" + if $this->{NEXT_FILE_LINE} && $this->{NEXT_FILE_LINE} > $start; my $fh = $this->{FILE}; while ($this->{NEXT_FILE_LINE} < $start) { my $dummy = <$fh>; diff --git a/Bugzilla/PatchReader/Base.pm b/Bugzilla/PatchReader/Base.pm index 26cb9a9a0..58b08fd25 100644 --- a/Bugzilla/PatchReader/Base.pm +++ b/Bugzilla/PatchReader/Base.pm @@ -17,7 +17,8 @@ sub sends_data_to { my $this = shift; if (defined($_[0])) { $this->{TARGET} = $_[0]; - } else { + } + else { return $this->{TARGET}; } } diff --git a/Bugzilla/PatchReader/CVSClient.pm b/Bugzilla/PatchReader/CVSClient.pm index 7a8875dc8..3f2a852f2 100644 --- a/Bugzilla/PatchReader/CVSClient.pm +++ b/Bugzilla/PatchReader/CVSClient.pm @@ -14,37 +14,36 @@ use strict; use warnings; sub parse_cvsroot { - my $cvsroot = $_[0]; - # Format: :method:[user[:password]@]server[:[port]]/path - if ($cvsroot =~ /^:([^:]*):(.*?)(\/.*)$/) { - my %retval; - $retval{protocol} = $1; - $retval{rootdir} = $3; - my $remote = $2; - if ($remote =~ /^(([^\@:]*)(:([^\@]*))?\@)?([^:]*)(:(.*))?$/) { - $retval{user} = $2; - $retval{password} = $4; - $retval{server} = $5; - $retval{port} = $7; - return %retval; - } + my $cvsroot = $_[0]; + + # Format: :method:[user[:password]@]server[:[port]]/path + if ($cvsroot =~ /^:([^:]*):(.*?)(\/.*)$/) { + my %retval; + $retval{protocol} = $1; + $retval{rootdir} = $3; + my $remote = $2; + if ($remote =~ /^(([^\@:]*)(:([^\@]*))?\@)?([^:]*)(:(.*))?$/) { + $retval{user} = $2; + $retval{password} = $4; + $retval{server} = $5; + $retval{port} = $7; + return %retval; } + } - return ( - rootdir => $cvsroot - ); + return (rootdir => $cvsroot); } sub cvs_co { - my ($cvsroot, @files) = @_; - my $cvs = $::cvsbin || "cvs"; - return system($cvs, "-Q", "-d$cvsroot", "co", @files); + my ($cvsroot, @files) = @_; + my $cvs = $::cvsbin || "cvs"; + return system($cvs, "-Q", "-d$cvsroot", "co", @files); } sub cvs_co_rev { - my ($cvsroot, $rev, @files) = @_; - my $cvs = $::cvsbin || "cvs"; - return system($cvs, "-Q", "-d$cvsroot", "co", "-r$rev", @files); + my ($cvsroot, $rev, @files) = @_; + my $cvs = $::cvsbin || "cvs"; + return system($cvs, "-Q", "-d$cvsroot", "co", "-r$rev", @files); } 1 diff --git a/Bugzilla/PatchReader/DiffPrinter/raw.pm b/Bugzilla/PatchReader/DiffPrinter/raw.pm index b7a0d8db2..81e01c283 100644 --- a/Bugzilla/PatchReader/DiffPrinter/raw.pm +++ b/Bugzilla/PatchReader/DiffPrinter/raw.pm @@ -29,7 +29,8 @@ sub start_file { my $fh = $this->{OUTFILE}; if ($file->{rcs_filename}) { print $fh "Index: $file->{filename}\n"; - print $fh "===================================================================\n"; + print $fh + "===================================================================\n"; print $fh "RCS file: $file->{rcs_filename}\n"; } my $old_file = $file->{is_add} ? "/dev/null" : $file->{filename}; @@ -53,7 +54,8 @@ sub next_section { return unless $section->{old_start} || $section->{new_start}; my $fh = $this->{OUTFILE}; - print $fh "@@ -$section->{old_start},$section->{old_lines} +$section->{new_start},$section->{new_lines} @@ $section->{func_info}\n"; + print $fh + "@@ -$section->{old_start},$section->{old_lines} +$section->{new_start},$section->{new_lines} @@ $section->{func_info}\n"; foreach my $line (@{$section->{lines}}) { $line =~ s/(\r?\n?)$/\n/; print $fh $line; diff --git a/Bugzilla/PatchReader/DiffPrinter/template.pm b/Bugzilla/PatchReader/DiffPrinter/template.pm index 4120f1d8a..54f3b4419 100644 --- a/Bugzilla/PatchReader/DiffPrinter/template.pm +++ b/Bugzilla/PatchReader/DiffPrinter/template.pm @@ -11,10 +11,10 @@ sub new { bless $this, $class; $this->{TEMPLATE_PROCESSOR} = $_[0]; - $this->{HEADER_TEMPLATE} = $_[1]; - $this->{FILE_TEMPLATE} = $_[2]; - $this->{FOOTER_TEMPLATE} = $_[3]; - $this->{ARGS} = $_[4] || {}; + $this->{HEADER_TEMPLATE} = $_[1]; + $this->{FILE_TEMPLATE} = $_[2]; + $this->{FOOTER_TEMPLATE} = $_[3]; + $this->{ARGS} = $_[4] || {}; $this->{ARGS}{file_count} = 0; return $this; @@ -23,20 +23,20 @@ sub new { sub start_patch { my $this = shift; $this->{TEMPLATE_PROCESSOR}->process($this->{HEADER_TEMPLATE}, $this->{ARGS}) - || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error()); + || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error()); } sub end_patch { my $this = shift; $this->{TEMPLATE_PROCESSOR}->process($this->{FOOTER_TEMPLATE}, $this->{ARGS}) - || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error()); + || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error()); } sub start_file { my $this = shift; $this->{ARGS}{file_count}++; - $this->{ARGS}{file} = shift; - $this->{ARGS}{file}{plus_lines} = 0; + $this->{ARGS}{file} = shift; + $this->{ARGS}{file}{plus_lines} = 0; $this->{ARGS}{file}{minus_lines} = 0; @{$this->{ARGS}{sections}} = (); } @@ -45,9 +45,11 @@ sub end_file { my $this = shift; my $file = $this->{ARGS}{file}; if ($file->{canonical} && $file->{old_revision} && $this->{ARGS}{bonsai_url}) { - $this->{ARGS}{bonsai_prefix} = "$this->{ARGS}{bonsai_url}/cvsblame.cgi?file=$file->{filename}&rev=$file->{old_revision}"; + $this->{ARGS}{bonsai_prefix} + = "$this->{ARGS}{bonsai_url}/cvsblame.cgi?file=$file->{filename}&rev=$file->{old_revision}"; } if ($file->{canonical} && $this->{ARGS}{lxr_url}) { + # Cut off the lxr root, if any my $filename = $file->{filename}; $filename = substr($filename, length($this->{ARGS}{lxr_root})); @@ -55,7 +57,7 @@ sub end_file { } $this->{TEMPLATE_PROCESSOR}->process($this->{FILE_TEMPLATE}, $this->{ARGS}) - || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error()); + || ::ThrowTemplateError($this->{TEMPLATE_PROCESSOR}->error()); @{$this->{ARGS}{sections}} = (); delete $this->{ARGS}{file}; } @@ -64,47 +66,46 @@ sub next_section { my $this = shift; my ($section) = @_; - $this->{ARGS}{file}{plus_lines} += $section->{plus_lines}; + $this->{ARGS}{file}{plus_lines} += $section->{plus_lines}; $this->{ARGS}{file}{minus_lines} += $section->{minus_lines}; # Get groups of lines and print them my $last_line_char = ''; - my $context_lines = []; - my $plus_lines = []; - my $minus_lines = []; + my $context_lines = []; + my $plus_lines = []; + my $minus_lines = []; foreach my $line (@{$section->{lines}}) { $line =~ s/\r?\n?$//; if ($line =~ /^ /) { if ($last_line_char ne ' ') { - push @{$section->{groups}}, {context => $context_lines, - plus => $plus_lines, - minus => $minus_lines}; + push @{$section->{groups}}, + {context => $context_lines, plus => $plus_lines, minus => $minus_lines}; $context_lines = []; - $plus_lines = []; - $minus_lines = []; + $plus_lines = []; + $minus_lines = []; } $last_line_char = ' '; push @{$context_lines}, substr($line, 1); - } elsif ($line =~ /^\+/) { + } + elsif ($line =~ /^\+/) { if ($last_line_char eq ' ' || $last_line_char eq '-' && @{$plus_lines}) { - push @{$section->{groups}}, {context => $context_lines, - plus => $plus_lines, - minus => $minus_lines}; - $context_lines = []; - $plus_lines = []; - $minus_lines = []; + push @{$section->{groups}}, + {context => $context_lines, plus => $plus_lines, minus => $minus_lines}; + $context_lines = []; + $plus_lines = []; + $minus_lines = []; $last_line_char = ''; } $last_line_char = '+'; push @{$plus_lines}, substr($line, 1); - } elsif ($line =~ /^-/) { + } + elsif ($line =~ /^-/) { if ($last_line_char eq '+' && @{$minus_lines}) { - push @{$section->{groups}}, {context => $context_lines, - plus => $plus_lines, - minus => $minus_lines}; - $context_lines = []; - $plus_lines = []; - $minus_lines = []; + push @{$section->{groups}}, + {context => $context_lines, plus => $plus_lines, minus => $minus_lines}; + $context_lines = []; + $plus_lines = []; + $minus_lines = []; $last_line_char = ''; } $last_line_char = '-'; @@ -112,9 +113,8 @@ sub next_section { } } - push @{$section->{groups}}, {context => $context_lines, - plus => $plus_lines, - minus => $minus_lines}; + push @{$section->{groups}}, + {context => $context_lines, plus => $plus_lines, minus => $minus_lines}; push @{$this->{ARGS}{sections}}, $section; } diff --git a/Bugzilla/PatchReader/FilterPatch.pm b/Bugzilla/PatchReader/FilterPatch.pm index 330f6329b..3891e4e57 100644 --- a/Bugzilla/PatchReader/FilterPatch.pm +++ b/Bugzilla/PatchReader/FilterPatch.pm @@ -11,7 +11,7 @@ use Bugzilla::PatchReader::Base; sub new { my $class = shift; $class = ref($class) || $class; - my $this = $class->SUPER::new(); + my $this = $class->SUPER::new(); bless $this, $class; return $this; diff --git a/Bugzilla/PatchReader/FixPatchRoot.pm b/Bugzilla/PatchReader/FixPatchRoot.pm index 1b0d250ad..a06682ba8 100644 --- a/Bugzilla/PatchReader/FixPatchRoot.pm +++ b/Bugzilla/PatchReader/FixPatchRoot.pm @@ -7,7 +7,8 @@ use warnings; use Bugzilla::PatchReader::FilterPatch; use Bugzilla::PatchReader::CVSClient; -@Bugzilla::PatchReader::FixPatchRoot::ISA = qw(Bugzilla::PatchReader::FilterPatch); +@Bugzilla::PatchReader::FixPatchRoot::ISA + = qw(Bugzilla::PatchReader::FilterPatch); sub new { my $class = shift; @@ -26,26 +27,29 @@ sub diff_root { my $this = shift; if (@_) { $this->{DIFF_ROOT} = $_[0]; - } else { + } + else { return $this->{DIFF_ROOT}; } } sub flush_delayed_commands { my $this = shift; - return if ! $this->{DELAYED_COMMANDS}; + return if !$this->{DELAYED_COMMANDS}; my $commands = $this->{DELAYED_COMMANDS}; delete $this->{DELAYED_COMMANDS}; $this->{FORCE_COMMANDS} = 1; foreach my $command_arr (@{$commands}) { my $command = $command_arr->[0]; - my $arg = $command_arr->[1]; + my $arg = $command_arr->[1]; if ($command eq "start_file") { $this->start_file($arg); - } elsif ($command eq "end_file") { + } + elsif ($command eq "end_file") { $this->end_file($arg); - } elsif ($command eq "section") { + } + elsif ($command eq "section") { $this->next_section($arg); } } @@ -60,10 +64,12 @@ sub end_patch { sub start_file { my $this = shift; my ($file) = @_; + # If the file is new, it will not have a filename that fits the repository # root and therefore needs to be fixed up to have the same root as everyone # else. At the same time we need to fix DIFF_ROOT too. if (exists($this->{DIFF_ROOT})) { + # XXX Return error if there are multiple roots in the patch by verifying # that the DIFF_ROOT is not different from the calculated diff root on this # filename @@ -71,31 +77,35 @@ sub start_file { $file->{filename} = $this->{DIFF_ROOT} . $file->{filename}; $file->{canonical} = 1; - } elsif ($file->{rcs_filename} && - substr($file->{rcs_filename}, 0, length($this->{REPOSITORY_ROOT})) eq - $this->{REPOSITORY_ROOT}) { + } + elsif ($file->{rcs_filename} + && substr($file->{rcs_filename}, 0, length($this->{REPOSITORY_ROOT})) eq + $this->{REPOSITORY_ROOT}) + { # Since we know the repository we can determine where the user was in the # repository when they did the diff by chopping off the repository root # from the rcs filename - $this->{DIFF_ROOT} = substr($file->{rcs_filename}, - length($this->{REPOSITORY_ROOT})); + $this->{DIFF_ROOT} + = substr($file->{rcs_filename}, length($this->{REPOSITORY_ROOT})); $this->{DIFF_ROOT} =~ s/,v$//; + # If the RCS file exists in the Attic then we need to correct for # this, stripping off the '/Attic' suffix in order to reduce the name # to just the CVS root. if ($this->{DIFF_ROOT} =~ m/Attic/) { $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0, -6); } + # XXX More error checking--that filename exists and that it is in fact # part of the rcs filename - $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0, - -length($file->{filename})); + $this->{DIFF_ROOT} = substr($this->{DIFF_ROOT}, 0, -length($file->{filename})); $this->flush_delayed_commands(); $file->{filename} = $this->{DIFF_ROOT} . $file->{filename}; $file->{canonical} = 1; - } else { + } + else { # DANGER Will Robinson. The first file in the patch is new. We will try # "delayed command mode" # @@ -104,7 +114,7 @@ sub start_file { # whatever the hell was in the patch) if (!$this->{FORCE_COMMANDS}) { - push @{$this->{DELAYED_COMMANDS}}, [ "start_file", { %{$file} } ]; + push @{$this->{DELAYED_COMMANDS}}, ["start_file", {%{$file}}]; return; } } @@ -114,8 +124,9 @@ sub start_file { sub end_file { my $this = shift; if (exists($this->{DELAYED_COMMANDS})) { - push @{$this->{DELAYED_COMMANDS}}, [ "end_file", { %{$_[0]} } ]; - } else { + push @{$this->{DELAYED_COMMANDS}}, ["end_file", {%{$_[0]}}]; + } + else { $this->{TARGET}->end_file(@_) if $this->{TARGET}; } } @@ -123,8 +134,9 @@ sub end_file { sub next_section { my $this = shift; if (exists($this->{DELAYED_COMMANDS})) { - push @{$this->{DELAYED_COMMANDS}}, [ "section", { %{$_[0]} } ]; - } else { + push @{$this->{DELAYED_COMMANDS}}, ["section", {%{$_[0]}}]; + } + else { $this->{TARGET}->next_section(@_) if $this->{TARGET}; } } diff --git a/Bugzilla/PatchReader/NarrowPatch.pm b/Bugzilla/PatchReader/NarrowPatch.pm index 1441e8366..2dd1a647f 100644 --- a/Bugzilla/PatchReader/NarrowPatch.pm +++ b/Bugzilla/PatchReader/NarrowPatch.pm @@ -6,7 +6,8 @@ use 5.10.1; use strict; use warnings; -@Bugzilla::PatchReader::NarrowPatch::ISA = qw(Bugzilla::PatchReader::FilterPatch); +@Bugzilla::PatchReader::NarrowPatch::ISA + = qw(Bugzilla::PatchReader::FilterPatch); sub new { my $class = shift; @@ -22,7 +23,9 @@ sub new { sub start_file { my $this = shift; my ($file) = @_; - if (grep { $_ eq substr($file->{filename}, 0, length($_)) } @{$this->{INCLUDE_FILES}}) { + if (grep { $_ eq substr($file->{filename}, 0, length($_)) } + @{$this->{INCLUDE_FILES}}) + { $this->{IS_INCLUDED} = 1; $this->{TARGET}->start_file(@_) if $this->{TARGET}; } diff --git a/Bugzilla/PatchReader/PatchInfoGrabber.pm b/Bugzilla/PatchReader/PatchInfoGrabber.pm index 6fb35fd16..96d20d0ba 100644 --- a/Bugzilla/PatchReader/PatchInfoGrabber.pm +++ b/Bugzilla/PatchReader/PatchInfoGrabber.pm @@ -6,7 +6,8 @@ use 5.10.1; use strict; use warnings; -@Bugzilla::PatchReader::PatchInfoGrabber::ISA = qw(Bugzilla::PatchReader::FilterPatch); +@Bugzilla::PatchReader::PatchInfoGrabber::ISA + = qw(Bugzilla::PatchReader::FilterPatch); sub new { my $class = shift; @@ -31,16 +32,18 @@ sub start_patch { sub start_file { my $this = shift; my ($file) = @_; - $this->{PATCH_INFO}{files}{$file->{filename}} = { %{$file} }; - $this->{FILE} = { %{$file} }; + $this->{PATCH_INFO}{files}{$file->{filename}} = {%{$file}}; + $this->{FILE} = {%{$file}}; $this->{TARGET}->start_file(@_) if $this->{TARGET}; } sub next_section { my $this = shift; my ($section) = @_; - $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{plus_lines} += $section->{plus_lines}; - $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{minus_lines} += $section->{minus_lines}; + $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{plus_lines} + += $section->{plus_lines}; + $this->{PATCH_INFO}{files}{$this->{FILE}{filename}}{minus_lines} + += $section->{minus_lines}; $this->{TARGET}->next_section(@_) if $this->{TARGET}; } diff --git a/Bugzilla/PatchReader/Raw.pm b/Bugzilla/PatchReader/Raw.pm index bb5a6cefd..06055c103 100644 --- a/Bugzilla/PatchReader/Raw.pm +++ b/Bugzilla/PatchReader/Raw.pm @@ -25,7 +25,7 @@ use Bugzilla::PatchReader::Base; sub new { my $class = shift; $class = ref($class) || $class; - my $this = $class->SUPER::new(); + my $this = $class->SUPER::new(); bless $this, $class; return $this; @@ -49,7 +49,8 @@ sub next_line { if ($1 eq "/dev/null") { $this->{FILE_STATE}{is_add} = 1; - } else { + } + else { $this->{FILE_STATE}{filename} = $1; } $this->{FILE_STATE}{old_date_str} = $2; @@ -57,7 +58,8 @@ sub next_line { $this->{IN_HEADER} = 1; - } elsif ($line =~ /^\+\+\+\s*([\S ]+)\s*\t([^\t\r\n]*)(\S*)/) { + } + elsif ($line =~ /^\+\+\+\s*([\S ]+)\s*\t([^\t\r\n]*)(\S*)/) { if ($1 eq "/dev/null") { $this->{FILE_STATE}{is_remove} = 1; } @@ -66,45 +68,57 @@ sub next_line { $this->{IN_HEADER} = 1; - } elsif ($line =~ /^RCS file: ([\S ]+)/) { + } + elsif ($line =~ /^RCS file: ([\S ]+)/) { $this->{FILE_STATE}{rcs_filename} = $1; $this->{IN_HEADER} = 1; - } elsif ($line =~ /^retrieving revision (\S+)/) { + } + elsif ($line =~ /^retrieving revision (\S+)/) { $this->{FILE_STATE}{old_revision} = $1; $this->{IN_HEADER} = 1; - } elsif ($line =~ /^Index:\s*([\S ]+)/) { + } + elsif ($line =~ /^Index:\s*([\S ]+)/) { $this->_maybe_end_file(); $this->{FILE_STATE}{filename} = $1; $this->{IN_HEADER} = 1; - } elsif ($line =~ /^diff\s*(-\S+\s*)*(\S+)\s*(\S*)/ && $3) { + } + elsif ($line =~ /^diff\s*(-\S+\s*)*(\S+)\s*(\S*)/ && $3) { + # Simple diff $this->_maybe_end_file(); $this->{FILE_STATE}{filename} = $2; $this->{IN_HEADER} = 1; - # section parsing - } elsif ($line =~ /^@@\s*-(\d+),?(\d*)\s*\+(\d+),?(\d*)\s*(?:@@\s*(.*))?/) { + # section parsing + } + elsif ($line =~ /^@@\s*-(\d+),?(\d*)\s*\+(\d+),?(\d*)\s*(?:@@\s*(.*))?/) { $this->{IN_HEADER} = 0; $this->_maybe_start_file(); $this->_maybe_end_section(); $2 = 0 if !defined($2); $4 = 0 if !defined($4); - $this->{SECTION_STATE} = { old_start => $1, old_lines => $2, - new_start => $3, new_lines => $4, - func_info => $5, - minus_lines => 0, plus_lines => 0, - }; + $this->{SECTION_STATE} = { + old_start => $1, + old_lines => $2, + new_start => $3, + new_lines => $4, + func_info => $5, + minus_lines => 0, + plus_lines => 0, + }; + + } + elsif ($line =~ /^(\d+),?(\d*)([acd])(\d+),?(\d*)/) { - } elsif ($line =~ /^(\d+),?(\d*)([acd])(\d+),?(\d*)/) { # Non-universal diff. Calculate as though it were universal. $this->{IN_HEADER} = 0; @@ -116,44 +130,56 @@ sub next_line { my $new_start; my $new_lines; if ($3 eq 'a') { + # 'a' has the old number one off from diff -u ("insert after this line" # vs. "insert at this line") $old_start = $1 + 1; $old_lines = 0; - } else { + } + else { $old_start = $1; $old_lines = $2 ? ($2 - $1 + 1) : 1; } if ($3 eq 'd') { + # 'd' has the new number one off from diff -u ("delete after this line" # vs. "delete at this line") $new_start = $4 + 1; $new_lines = 0; - } else { + } + else { $new_start = $4; $new_lines = $5 ? ($5 - $4 + 1) : 1; } - $this->{SECTION_STATE} = { old_start => $old_start, old_lines => $old_lines, - new_start => $new_start, new_lines => $new_lines, - minus_lines => 0, plus_lines => 0 - }; + $this->{SECTION_STATE} = { + old_start => $old_start, + old_lines => $old_lines, + new_start => $new_start, + new_lines => $new_lines, + minus_lines => 0, + plus_lines => 0 + }; } # line parsing (only when inside a section) return if $this->{IN_HEADER}; if ($line =~ /^ /) { push @{$this->{SECTION_STATE}{lines}}, $line; - } elsif ($line =~ /^-/) { + } + elsif ($line =~ /^-/) { $this->{SECTION_STATE}{minus_lines}++; push @{$this->{SECTION_STATE}{lines}}, $line; - } elsif ($line =~ /^\+/) { + } + elsif ($line =~ /^\+/) { $this->{SECTION_STATE}{plus_lines}++; push @{$this->{SECTION_STATE}{lines}}, $line; - } elsif ($line =~ /^< /) { + } + elsif ($line =~ /^< /) { $this->{SECTION_STATE}{minus_lines}++; push @{$this->{SECTION_STATE}{lines}}, "-" . substr($line, 2); - } elsif ($line =~ /^> /) { + } + elsif ($line =~ /^> /) { $this->{SECTION_STATE}{plus_lines}++; push @{$this->{SECTION_STATE}{lines}}, "+" . substr($line, 2); } @@ -179,14 +205,15 @@ sub end_lines { sub _init_state { my $this = shift; $this->{SECTION_STATE}{minus_lines} ||= 0; - $this->{SECTION_STATE}{plus_lines} ||= 0; + $this->{SECTION_STATE}{plus_lines} ||= 0; } sub _maybe_start_file { my $this = shift; $this->_init_state(); - if (exists($this->{FILE_STATE}) && !$this->{FILE_STARTED} || - $this->{FILE_NEVER_STARTED}) { + if (exists($this->{FILE_STATE}) && !$this->{FILE_STARTED} + || $this->{FILE_NEVER_STARTED}) + { $this->_start_file(); } } @@ -198,6 +225,7 @@ sub _maybe_end_file { $this->_maybe_end_section(); if (exists($this->{FILE_STATE})) { + # Handle empty patch sections (if the file has not been started and we're # already trying to end it, start it first!) if (!$this->{FILE_STARTED}) { @@ -216,7 +244,7 @@ sub _start_file { # Send start notification and set state if (!$this->{FILE_STATE}) { - $this->{FILE_STATE} = { filename => "file_not_specified_in_diff" }; + $this->{FILE_STATE} = {filename => "file_not_specified_in_diff"}; } # Send start notification and set state diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm index 3ac1692f0..a0ba5da27 100644 --- a/Bugzilla/Product.pm +++ b/Bugzilla/Product.pm @@ -40,32 +40,32 @@ use constant IS_CONFIG => 1; use constant DB_TABLE => 'products'; use constant DB_COLUMNS => qw( - id - name - classification_id - description - isactive - defaultmilestone - allows_unconfirmed + id + name + classification_id + description + isactive + defaultmilestone + allows_unconfirmed ); use constant UPDATE_COLUMNS => qw( - name - description - defaultmilestone - isactive - allows_unconfirmed + name + description + defaultmilestone + isactive + allows_unconfirmed ); use constant VALIDATORS => { - allows_unconfirmed => \&Bugzilla::Object::check_boolean, - classification => \&_check_classification, - name => \&_check_name, - description => \&_check_description, - version => \&_check_version, - defaultmilestone => \&_check_default_milestone, - isactive => \&Bugzilla::Object::check_boolean, - create_series => \&Bugzilla::Object::check_boolean + allows_unconfirmed => \&Bugzilla::Object::check_boolean, + classification => \&_check_classification, + name => \&_check_name, + description => \&_check_description, + version => \&_check_version, + defaultmilestone => \&_check_default_milestone, + isactive => \&Bugzilla::Object::check_boolean, + create_series => \&Bugzilla::Object::check_boolean }; ############################### @@ -73,263 +73,288 @@ use constant VALIDATORS => { ############################### 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(@_); + $class->check_required_create_fields(@_); - my $params = $class->run_create_validators(@_); - # Some fields do not exist in the DB as is. - if (defined $params->{classification}) { - $params->{classification_id} = delete $params->{classification}; - } - my $version = delete $params->{version}; - my $create_series = delete $params->{create_series}; + my $params = $class->run_create_validators(@_); - # Some fields can be NULLs - foreach my $field (qw( default_op_sys_id default_platform_id )) { - next unless exists $params->{$field} && defined $params->{$field}; - $params->{$field} = undef if $params->{$field} eq ''; - } + # Some fields do not exist in the DB as is. + if (defined $params->{classification}) { + $params->{classification_id} = delete $params->{classification}; + } + my $version = delete $params->{version}; + my $create_series = delete $params->{create_series}; + + # Some fields can be NULLs + foreach my $field (qw( default_op_sys_id default_platform_id )) { + next unless exists $params->{$field} && defined $params->{$field}; + $params->{$field} = undef if $params->{$field} eq ''; + } - my $product = $class->insert_create_data($params); - Bugzilla->user->clear_product_cache(); + my $product = $class->insert_create_data($params); + Bugzilla->user->clear_product_cache(); - # Add the new version and milestone into the DB as valid values. - Bugzilla::Version->create({ value => $version, product => $product }); - Bugzilla::Milestone->create({ value => $product->default_milestone, - product => $product }); + # Add the new version and milestone into the DB as valid values. + Bugzilla::Version->create({value => $version, product => $product}); + Bugzilla::Milestone->create( + {value => $product->default_milestone, product => $product}); - # Create groups and series for the new product, if requested. - $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'}; - $product->_create_series() if $create_series; + # Create groups and series for the new product, if requested. + $product->_create_bug_group() if Bugzilla->params->{'makeproductgroups'}; + $product->_create_series() if $create_series; - Bugzilla::Hook::process('product_end_of_create', { product => $product }); + Bugzilla::Hook::process('product_end_of_create', {product => $product}); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); - return $product; + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); + return $product; } # This is considerably faster than calling new_from_list three times # for each product in the list, particularly with hundreds or thousands # of products. sub preload { - my ($products, $preload_flagtypes, $flagtypes_params) = @_; - my %prods = map { $_->id => $_ } @$products; - my @prod_ids = keys %prods; - return unless @prod_ids; - - my $dbh = Bugzilla->dbh; - foreach my $field (qw(component version milestone)) { - my $classname = "Bugzilla::" . ucfirst($field); - my $objects = $classname->match({ product_id => \@prod_ids }); - - # Now populate the products with this set of objects. - foreach my $obj (@$objects) { - my $product_id = $obj->product_id; - $prods{$product_id}->{"${field}s"} ||= []; - push(@{$prods{$product_id}->{"${field}s"}}, $obj); - } - } - if ($preload_flagtypes) { - $_->flag_types($flagtypes_params) foreach @$products; + my ($products, $preload_flagtypes, $flagtypes_params) = @_; + my %prods = map { $_->id => $_ } @$products; + my @prod_ids = keys %prods; + return unless @prod_ids; + + my $dbh = Bugzilla->dbh; + foreach my $field (qw(component version milestone)) { + my $classname = "Bugzilla::" . ucfirst($field); + my $objects = $classname->match({product_id => \@prod_ids}); + + # Now populate the products with this set of objects. + foreach my $obj (@$objects) { + my $product_id = $obj->product_id; + $prods{$product_id}->{"${field}s"} ||= []; + push(@{$prods{$product_id}->{"${field}s"}}, $obj); } + } + if ($preload_flagtypes) { + $_->flag_types($flagtypes_params) foreach @$products; + } } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; - - # Don't update the DB if something goes wrong below -> transaction. - $dbh->bz_start_transaction(); - my ($changes, $old_self) = $self->SUPER::update(@_); - - # Also update group settings. - if ($self->{check_group_controls}) { - require Bugzilla::Bug; - import Bugzilla::Bug qw(LogActivityEntry); - - my $old_settings = $old_self->group_controls; - my $new_settings = $self->group_controls; - my $timestamp = $dbh->selectrow_array('SELECT NOW()'); - - foreach my $gid (keys %$new_settings) { - my $old_setting = $old_settings->{$gid} || {}; - my $new_setting = $new_settings->{$gid}; - # If all new settings are 0 for a given group, we delete the entry - # from group_control_map, so we have to track it here. - my $all_zero = 1; - my @fields; - my @values; - - foreach my $field ('entry', 'membercontrol', 'othercontrol', 'canedit', - 'editcomponents', 'editbugs', 'canconfirm') - { - my $old_value = $old_setting->{$field}; - my $new_value = $new_setting->{$field}; - $all_zero = 0 if $new_value; - next if (defined $old_value && $old_value == $new_value); - push(@fields, $field); - # The value has already been validated. - detaint_natural($new_value); - push(@values, $new_value); - } - # Is there anything to update? - next unless scalar @fields; - - if ($all_zero) { - $dbh->do('DELETE FROM group_control_map - WHERE product_id = ? AND group_id = ?', - undef, $self->id, $gid); - } - else { - if (exists $old_setting->{group}) { - # There is already an entry in the DB. - my $set_fields = join(', ', map {"$_ = ?"} @fields); - $dbh->do("UPDATE group_control_map SET $set_fields - WHERE product_id = ? AND group_id = ?", - undef, (@values, $self->id, $gid)); - } - else { - # No entry yet. - my $fields = join(', ', @fields); - # +2 because of the product and group IDs. - my $qmarks = join(',', ('?') x (scalar @fields + 2)); - $dbh->do("INSERT INTO group_control_map (product_id, group_id, $fields) - VALUES ($qmarks)", undef, ($self->id, $gid, @values)); - } - } - - # If the group is mandatory, restrict all bugs to it. - if ($new_setting->{membercontrol} == CONTROLMAPMANDATORY) { - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + my $self = shift; + my $dbh = Bugzilla->dbh; + + # Don't update the DB if something goes wrong below -> transaction. + $dbh->bz_start_transaction(); + my ($changes, $old_self) = $self->SUPER::update(@_); + + # Also update group settings. + if ($self->{check_group_controls}) { + require Bugzilla::Bug; + import Bugzilla::Bug qw(LogActivityEntry); + + my $old_settings = $old_self->group_controls; + my $new_settings = $self->group_controls; + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + foreach my $gid (keys %$new_settings) { + my $old_setting = $old_settings->{$gid} || {}; + my $new_setting = $new_settings->{$gid}; + + # If all new settings are 0 for a given group, we delete the entry + # from group_control_map, so we have to track it here. + my $all_zero = 1; + my @fields; + my @values; + + foreach my $field ( + 'entry', 'membercontrol', 'othercontrol', 'canedit', + 'editcomponents', 'editbugs', 'canconfirm' + ) + { + my $old_value = $old_setting->{$field}; + my $new_value = $new_setting->{$field}; + $all_zero = 0 if $new_value; + next if (defined $old_value && $old_value == $new_value); + push(@fields, $field); + + # The value has already been validated. + detaint_natural($new_value); + push(@values, $new_value); + } + + # Is there anything to update? + next unless scalar @fields; + + if ($all_zero) { + $dbh->do( + 'DELETE FROM group_control_map + WHERE product_id = ? AND group_id = ?', undef, $self->id, $gid + ); + } + else { + if (exists $old_setting->{group}) { + + # There is already an entry in the DB. + my $set_fields = join(', ', map {"$_ = ?"} @fields); + $dbh->do( + "UPDATE group_control_map SET $set_fields + WHERE product_id = ? AND group_id = ?", undef, + (@values, $self->id, $gid) + ); + } + else { + # No entry yet. + my $fields = join(', ', @fields); + + # +2 because of the product and group IDs. + my $qmarks = join(',', ('?') x (scalar @fields + 2)); + $dbh->do( + "INSERT INTO group_control_map (product_id, group_id, $fields) + VALUES ($qmarks)", undef, ($self->id, $gid, @values) + ); + } + } + + # If the group is mandatory, restrict all bugs to it. + if ($new_setting->{membercontrol} == CONTROLMAPMANDATORY) { + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs LEFT JOIN bug_group_map ON bug_group_map.bug_id = bugs.bug_id AND group_id = ? WHERE product_id = ? AND bug_group_map.bug_id IS NULL', - undef, $gid, $self->id); - - if (scalar @$bug_ids) { - 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); - # Add this change to the bug history. - LogActivityEntry($bug_id, 'bug_group', '', - $new_setting->{group}->name, - Bugzilla->user->id, $timestamp); - } - push(@{$changes->{'_group_controls'}->{'now_mandatory'}}, - {name => $new_setting->{group}->name, - bug_count => scalar @$bug_ids}); - } - } - # If the group can no longer be used to restrict bugs, remove them. - elsif ($new_setting->{membercontrol} == CONTROLMAPNA) { - my $bug_ids = - $dbh->selectcol_arrayref('SELECT bugs.bug_id + undef, $gid, $self->id + ); + + if (scalar @$bug_ids) { + 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); + + # Add this change to the bug history. + LogActivityEntry($bug_id, 'bug_group', '', $new_setting->{group}->name, + Bugzilla->user->id, $timestamp); + } + push( + @{$changes->{'_group_controls'}->{'now_mandatory'}}, + {name => $new_setting->{group}->name, bug_count => scalar @$bug_ids} + ); + } + } + + # If the group can no longer be used to restrict bugs, remove them. + elsif ($new_setting->{membercontrol} == CONTROLMAPNA) { + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT bugs.bug_id FROM bugs INNER JOIN bug_group_map ON bug_group_map.bug_id = bugs.bug_id WHERE product_id = ? AND group_id = ?', - undef, $self->id, $gid); - - if (scalar @$bug_ids) { - $dbh->do('DELETE FROM bug_group_map WHERE group_id = ? AND ' . - $dbh->sql_in('bug_id', $bug_ids), undef, $gid); - - # Add this change to the bug history. - foreach my $bug_id (@$bug_ids) { - LogActivityEntry($bug_id, 'bug_group', - $old_setting->{group}->name, '', - Bugzilla->user->id, $timestamp); - } - push(@{$changes->{'_group_controls'}->{'now_na'}}, - {name => $old_setting->{group}->name, - bug_count => scalar @$bug_ids}); - } - } + undef, $self->id, $gid + ); + + if (scalar @$bug_ids) { + $dbh->do( + 'DELETE FROM bug_group_map WHERE group_id = ? AND ' + . $dbh->sql_in('bug_id', $bug_ids), + undef, $gid + ); + + # Add this change to the bug history. + foreach my $bug_id (@$bug_ids) { + LogActivityEntry($bug_id, 'bug_group', $old_setting->{group}->name, + '', Bugzilla->user->id, $timestamp); + } + push( + @{$changes->{'_group_controls'}->{'now_na'}}, + {name => $old_setting->{group}->name, bug_count => scalar @$bug_ids} + ); } - - delete $self->{groups_available}; - delete $self->{groups_mandatory}; + } } - $dbh->bz_commit_transaction(); - # Changes have been committed. - delete $self->{check_group_controls}; - Bugzilla->user->clear_product_cache(); - Bugzilla->memcached->clear_config(); - return $changes; + delete $self->{groups_available}; + delete $self->{groups_mandatory}; + } + $dbh->bz_commit_transaction(); + + # Changes have been committed. + delete $self->{check_group_controls}; + Bugzilla->user->clear_product_cache(); + Bugzilla->memcached->clear_config(); + + return $changes; } sub remove_from_db { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - $self->_check_if_controller(); - - if ($self->bug_count) { - if (Bugzilla->params->{'allowbugdeletion'}) { - require Bugzilla::Bug; - foreach my $bug_id (@{$self->bug_ids}) { - # Note that we allow the user to delete bugs he can't see, - # which is okay, because he's deleting the whole Product. - my $bug = new Bugzilla::Bug($bug_id); - $bug->remove_from_db(); - } - } - else { - ThrowUserError('product_has_bugs', { nb => $self->bug_count }); - } + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + $self->_check_if_controller(); + + if ($self->bug_count) { + if (Bugzilla->params->{'allowbugdeletion'}) { + require Bugzilla::Bug; + foreach my $bug_id (@{$self->bug_ids}) { + + # Note that we allow the user to delete bugs he can't see, + # which is okay, because he's deleting the whole Product. + my $bug = new Bugzilla::Bug($bug_id); + $bug->remove_from_db(); + } + } + else { + ThrowUserError('product_has_bugs', {nb => $self->bug_count}); } + } - if ($params->{delete_series}) { - my $series_ids = - $dbh->selectcol_arrayref('SELECT series_id + if ($params->{delete_series}) { + my $series_ids = $dbh->selectcol_arrayref( + 'SELECT series_id FROM series INNER JOIN series_categories ON series_categories.id = series.category - WHERE series_categories.name = ?', - undef, $self->name); + WHERE series_categories.name = ?', undef, + $self->name + ); - if (scalar @$series_ids) { - $dbh->do('DELETE FROM series WHERE ' . $dbh->sql_in('series_id', $series_ids)); - } + if (scalar @$series_ids) { + $dbh->do('DELETE FROM series WHERE ' . $dbh->sql_in('series_id', $series_ids)); + } - # If no subcategory uses this product name, completely purge it. - my $in_use = - $dbh->selectrow_array('SELECT 1 + # If no subcategory uses this product name, completely purge it. + my $in_use = $dbh->selectrow_array( + 'SELECT 1 FROM series INNER JOIN series_categories ON series_categories.id = series.subcategory - WHERE series_categories.name = ? ' . - $dbh->sql_limit(1), - undef, $self->name); - if (!$in_use) { - $dbh->do('DELETE FROM series_categories WHERE name = ?', undef, $self->name); - } + WHERE series_categories.name = ? ' + . $dbh->sql_limit(1), undef, $self->name + ); + if (!$in_use) { + $dbh->do('DELETE FROM series_categories WHERE name = ?', undef, $self->name); } + } - $dbh->do("DELETE FROM products WHERE id = ?", undef, $self->id); + $dbh->do("DELETE FROM products WHERE id = ?", undef, $self->id); - $dbh->bz_commit_transaction(); - Bugzilla->memcached->clear_config(); + $dbh->bz_commit_transaction(); + Bugzilla->memcached->clear_config(); - # We have to delete these internal variables, else we get - # the old lists of products and classifications again. - delete $user->{selectable_products}; - delete $user->{selectable_classifications}; + # We have to delete these internal variables, else we get + # the old lists of products and classifications again. + delete $user->{selectable_products}; + delete $user->{selectable_classifications}; } @@ -338,91 +363,94 @@ sub remove_from_db { ############################### sub _check_classification { - my ($invocant, $classification_name) = @_; - - my $classification_id = 1; - if (Bugzilla->params->{'useclassification'}) { - my $classification = Bugzilla::Classification->check($classification_name); - $classification_id = $classification->id; - } - return $classification_id; + my ($invocant, $classification_name) = @_; + + my $classification_id = 1; + if (Bugzilla->params->{'useclassification'}) { + my $classification = Bugzilla::Classification->check($classification_name); + $classification_id = $classification->id; + } + return $classification_id; } sub _check_name { - my ($invocant, $name) = @_; + my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError('product_blank_name'); + $name = trim($name); + $name || ThrowUserError('product_blank_name'); - if (length($name) > MAX_PRODUCT_SIZE) { - ThrowUserError('product_name_too_long', {'name' => $name}); - } + if (length($name) > MAX_PRODUCT_SIZE) { + ThrowUserError('product_name_too_long', {'name' => $name}); + } - my $product = new Bugzilla::Product({name => $name}); - if ($product && (!ref $invocant || $product->id != $invocant->id)) { - # Check for exact case sensitive match: - if ($product->name eq $name) { - ThrowUserError('product_name_already_in_use', {'product' => $product->name}); - } - else { - ThrowUserError('product_name_diff_in_case', {'product' => $name, - 'existing_product' => $product->name}); - } + my $product = new Bugzilla::Product({name => $name}); + if ($product && (!ref $invocant || $product->id != $invocant->id)) { + + # Check for exact case sensitive match: + if ($product->name eq $name) { + ThrowUserError('product_name_already_in_use', {'product' => $product->name}); + } + else { + ThrowUserError('product_name_diff_in_case', + {'product' => $name, 'existing_product' => $product->name}); } - return $name; + } + return $name; } sub _check_description { - my ($invocant, $description) = @_; + my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowUserError('product_must_have_description'); - return $description; + $description = trim($description); + $description || ThrowUserError('product_must_have_description'); + return $description; } sub _check_version { - my ($invocant, $version) = @_; + my ($invocant, $version) = @_; - $version = trim($version); - $version || ThrowUserError('product_must_have_version'); - # We will check the version length when Bugzilla::Version->create will do it. - return $version; + $version = trim($version); + $version || ThrowUserError('product_must_have_version'); + + # We will check the version length when Bugzilla::Version->create will do it. + return $version; } sub _check_default_milestone { - my ($invocant, $milestone) = @_; + my ($invocant, $milestone) = @_; - # Do nothing if target milestones are not in use. - unless (Bugzilla->params->{'usetargetmilestone'}) { - return (ref $invocant) ? $invocant->default_milestone : '---'; - } + # Do nothing if target milestones are not in use. + unless (Bugzilla->params->{'usetargetmilestone'}) { + return (ref $invocant) ? $invocant->default_milestone : '---'; + } - $milestone = trim($milestone); + $milestone = trim($milestone); - if (ref $invocant) { - # The default milestone must be one of the existing milestones. - my $mil_obj = new Bugzilla::Milestone({name => $milestone, product => $invocant}); + if (ref $invocant) { - $mil_obj || ThrowUserError('product_must_define_defaultmilestone', - {product => $invocant->name, - milestone => $milestone}); - } - else { - $milestone ||= '---'; - } - return $milestone; + # The default milestone must be one of the existing milestones. + my $mil_obj + = new Bugzilla::Milestone({name => $milestone, product => $invocant}); + + $mil_obj || ThrowUserError('product_must_define_defaultmilestone', + {product => $invocant->name, milestone => $milestone}); + } + else { + $milestone ||= '---'; + } + return $milestone; } sub _check_milestone_url { - my ($invocant, $url) = @_; + my ($invocant, $url) = @_; - # Do nothing if target milestones are not in use. - unless (Bugzilla->params->{'usetargetmilestone'}) { - return (ref $invocant) ? $invocant->milestone_url : ''; - } + # Do nothing if target milestones are not in use. + unless (Bugzilla->params->{'usetargetmilestone'}) { + return (ref $invocant) ? $invocant->milestone_url : ''; + } - $url = trim($url || ''); - return $url; + $url = trim($url || ''); + return $url; } ##################################### @@ -437,394 +465,430 @@ use constant is_default => 0; ############################### sub _create_bug_group { - my $self = shift; - my $dbh = Bugzilla->dbh; - - my $group_name = $self->name; - while (new Bugzilla::Group({name => $group_name})) { - $group_name .= '_'; - } - my $group_description = get_text('bug_group_description', {product => $self}); - - my $group = Bugzilla::Group->create({name => $group_name, - description => $group_description, - isbuggroup => 1}); - - # Associate the new group and new product. - $dbh->do('INSERT INTO group_control_map + my $self = shift; + my $dbh = Bugzilla->dbh; + + my $group_name = $self->name; + while (new Bugzilla::Group({name => $group_name})) { + $group_name .= '_'; + } + my $group_description = get_text('bug_group_description', {product => $self}); + + my $group + = Bugzilla::Group->create({ + name => $group_name, description => $group_description, isbuggroup => 1 + }); + + # Associate the new group and new product. + $dbh->do( + 'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol) - VALUES (?, ?, ?, ?)', - undef, ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA)); + VALUES (?, ?, ?, ?)', undef, + ($group->id, $self->id, CONTROLMAPDEFAULT, CONTROLMAPNA) + ); } sub _create_series { - my $self = shift; - - my @series; - # We do every status, every resolution, and an "opened" one as well. - foreach my $bug_status (@{get_legal_field_values('bug_status')}) { - push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]); - } - - foreach my $resolution (@{get_legal_field_values('resolution')}) { - next if !$resolution; - push(@series, [$resolution, "resolution=" . url_quote($resolution)]); - } - - my @openedstatuses = BUG_STATE_OPEN; - my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses); - push(@series, [get_text('series_all_open'), $query]); - - foreach my $sdata (@series) { - my $series = new Bugzilla::Series(undef, $self->name, - get_text('series_subcategory'), - $sdata->[0], Bugzilla->user->id, 1, - $sdata->[1] . "&product=" . url_quote($self->name), 1); - $series->writeToDatabase(); - } + my $self = shift; + + my @series; + + # We do every status, every resolution, and an "opened" one as well. + foreach my $bug_status (@{get_legal_field_values('bug_status')}) { + push(@series, [$bug_status, "bug_status=" . url_quote($bug_status)]); + } + + foreach my $resolution (@{get_legal_field_values('resolution')}) { + next if !$resolution; + push(@series, [$resolution, "resolution=" . url_quote($resolution)]); + } + + my @openedstatuses = BUG_STATE_OPEN; + my $query = join("&", map { "bug_status=" . url_quote($_) } @openedstatuses); + push(@series, [get_text('series_all_open'), $query]); + + foreach my $sdata (@series) { + my $series + = new Bugzilla::Series(undef, $self->name, get_text('series_subcategory'), + $sdata->[0], Bugzilla->user->id, 1, + $sdata->[1] . "&product=" . url_quote($self->name), 1); + $series->writeToDatabase(); + } } -sub set_name { $_[0]->set('name', $_[1]); } -sub set_description { $_[0]->set('description', $_[1]); } -sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); } -sub set_is_active { $_[0]->set('isactive', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_description { $_[0]->set('description', $_[1]); } +sub set_default_milestone { $_[0]->set('defaultmilestone', $_[1]); } +sub set_is_active { $_[0]->set('isactive', $_[1]); } sub set_allows_unconfirmed { $_[0]->set('allows_unconfirmed', $_[1]); } sub set_group_controls { - my ($self, $group, $settings) = @_; - - $group->is_active_bug_group - || ThrowUserError('product_illegal_group', {group => $group}); - - scalar(keys %$settings) - || ThrowCodeError('product_empty_group_controls', {group => $group}); - - # We store current settings for this group. - my $gs = $self->group_controls->{$group->id}; - # If there is no entry for this group yet, create a default hash. - unless (defined $gs) { - $gs = { entry => 0, - membercontrol => CONTROLMAPNA, - othercontrol => CONTROLMAPNA, - canedit => 0, - editcomponents => 0, - editbugs => 0, - canconfirm => 0, - group => $group }; + my ($self, $group, $settings) = @_; + + $group->is_active_bug_group + || ThrowUserError('product_illegal_group', {group => $group}); + + scalar(keys %$settings) + || ThrowCodeError('product_empty_group_controls', {group => $group}); + + # We store current settings for this group. + my $gs = $self->group_controls->{$group->id}; + + # If there is no entry for this group yet, create a default hash. + unless (defined $gs) { + $gs = { + entry => 0, + membercontrol => CONTROLMAPNA, + othercontrol => CONTROLMAPNA, + canedit => 0, + editcomponents => 0, + editbugs => 0, + canconfirm => 0, + group => $group + }; + } + + # Both settings must be defined, or none of them can be updated. + if (defined $settings->{membercontrol} && defined $settings->{othercontrol}) { + + # Legality of control combination is a function of + # membercontrol\othercontrol + # NA SH DE MA + # NA + - - - + # SH + + + + + # DE + - + + + # MA - - - + + foreach my $field ('membercontrol', 'othercontrol') { + my ($is_legal) + = grep { $settings->{$field} == $_ } + (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY); + defined $is_legal || ThrowCodeError('product_illegal_group_control', + {field => $field, value => $settings->{$field}}); } - - # Both settings must be defined, or none of them can be updated. - if (defined $settings->{membercontrol} && defined $settings->{othercontrol}) { - # Legality of control combination is a function of - # membercontrol\othercontrol - # NA SH DE MA - # NA + - - - - # SH + + + + - # DE + - + + - # MA - - - + - foreach my $field ('membercontrol', 'othercontrol') { - my ($is_legal) = grep { $settings->{$field} == $_ } - (CONTROLMAPNA, CONTROLMAPSHOWN, CONTROLMAPDEFAULT, CONTROLMAPMANDATORY); - defined $is_legal || ThrowCodeError('product_illegal_group_control', - { field => $field, value => $settings->{$field} }); - } - unless ($settings->{membercontrol} == $settings->{othercontrol} - || $settings->{membercontrol} == CONTROLMAPSHOWN - || ($settings->{membercontrol} == CONTROLMAPDEFAULT - && $settings->{othercontrol} != CONTROLMAPSHOWN)) - { - ThrowUserError('illegal_group_control_combination', {groupname => $group->name}); - } - $gs->{membercontrol} = $settings->{membercontrol}; - $gs->{othercontrol} = $settings->{othercontrol}; - } - - foreach my $field ('entry', 'canedit', 'editcomponents', 'editbugs', 'canconfirm') { - next unless defined $settings->{$field}; - $gs->{$field} = $settings->{$field} ? 1 : 0; + unless ( + $settings->{membercontrol} == $settings->{othercontrol} + || $settings->{membercontrol} == CONTROLMAPSHOWN + || ( $settings->{membercontrol} == CONTROLMAPDEFAULT + && $settings->{othercontrol} != CONTROLMAPSHOWN) + ) + { + ThrowUserError('illegal_group_control_combination', + {groupname => $group->name}); } - $self->{group_controls}->{$group->id} = $gs; - $self->{check_group_controls} = 1; + $gs->{membercontrol} = $settings->{membercontrol}; + $gs->{othercontrol} = $settings->{othercontrol}; + } + + foreach + my $field ('entry', 'canedit', 'editcomponents', 'editbugs', 'canconfirm') + { + next unless defined $settings->{$field}; + $gs->{$field} = $settings->{$field} ? 1 : 0; + } + $self->{group_controls}->{$group->id} = $gs; + $self->{check_group_controls} = 1; } sub components { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{components}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{components}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM components WHERE product_id = ? - ORDER BY name}, undef, $self->id); + ORDER BY name}, undef, $self->id + ); - require Bugzilla::Component; - $self->{components} = Bugzilla::Component->new_from_list($ids); - } - return $self->{components}; + require Bugzilla::Component; + $self->{components} = Bugzilla::Component->new_from_list($ids); + } + return $self->{components}; } sub group_controls { - my ($self, $full_data) = @_; - my $dbh = Bugzilla->dbh; - - # By default, we don't return groups which are not listed in - # group_control_map. If $full_data is true, then we also - # return groups whose settings could be set for the product. - my $where_or_and = 'WHERE'; - my $and_or_where = 'AND'; - if ($full_data) { - $where_or_and = 'AND'; - $and_or_where = 'WHERE'; - } - - # If $full_data is true, we collect all the data in all cases, - # even if the cache is already populated. - # $full_data is never used except in the very special case where - # all configurable bug groups are displayed to administrators, - # so we don't care about collecting all the data again in this case. - if (!defined $self->{group_controls} || $full_data) { - # Include name to the list, to allow us sorting data more easily. - my $query = qq{SELECT id, name, entry, membercontrol, othercontrol, + my ($self, $full_data) = @_; + my $dbh = Bugzilla->dbh; + + # By default, we don't return groups which are not listed in + # group_control_map. If $full_data is true, then we also + # return groups whose settings could be set for the product. + my $where_or_and = 'WHERE'; + my $and_or_where = 'AND'; + if ($full_data) { + $where_or_and = 'AND'; + $and_or_where = 'WHERE'; + } + + # If $full_data is true, we collect all the data in all cases, + # even if the cache is already populated. + # $full_data is never used except in the very special case where + # all configurable bug groups are displayed to administrators, + # so we don't care about collecting all the data again in this case. + if (!defined $self->{group_controls} || $full_data) { + + # Include name to the list, to allow us sorting data more easily. + my $query = qq{SELECT id, name, entry, membercontrol, othercontrol, canedit, editcomponents, editbugs, canconfirm FROM groups LEFT JOIN group_control_map ON id = group_id $where_or_and product_id = ? $and_or_where isbuggroup = 1}; - $self->{group_controls} = - $dbh->selectall_hashref($query, 'id', undef, $self->id); - - # For each group ID listed above, create and store its group object. - my @gids = keys %{$self->{group_controls}}; - my $groups = Bugzilla::Group->new_from_list(\@gids); - $self->{group_controls}->{$_->id}->{group} = $_ foreach @$groups; - } - - # We never cache bug counts, for the same reason as above. - if ($full_data) { - my $counts = - $dbh->selectall_arrayref('SELECT group_id, COUNT(bugs.bug_id) AS bug_count + $self->{group_controls} + = $dbh->selectall_hashref($query, 'id', undef, $self->id); + + # For each group ID listed above, create and store its group object. + my @gids = keys %{$self->{group_controls}}; + my $groups = Bugzilla::Group->new_from_list(\@gids); + $self->{group_controls}->{$_->id}->{group} = $_ foreach @$groups; + } + + # We never cache bug counts, for the same reason as above. + if ($full_data) { + my $counts = $dbh->selectall_arrayref( + 'SELECT group_id, COUNT(bugs.bug_id) AS bug_count FROM bug_group_map INNER JOIN bugs ON bugs.bug_id = bug_group_map.bug_id - WHERE bugs.product_id = ? ' . - $dbh->sql_group_by('group_id'), - {'Slice' => {}}, $self->id); - foreach my $data (@$counts) { - $self->{group_controls}->{$data->{group_id}}->{bug_count} = $data->{bug_count}; - } + WHERE bugs.product_id = ? ' + . $dbh->sql_group_by('group_id'), {'Slice' => {}}, $self->id + ); + foreach my $data (@$counts) { + $self->{group_controls}->{$data->{group_id}}->{bug_count} = $data->{bug_count}; } - return $self->{group_controls}; + } + return $self->{group_controls}; } sub groups_available { - my ($self) = @_; - return $self->{groups_available} if defined $self->{groups_available}; - my $dbh = Bugzilla->dbh; - my $shown = CONTROLMAPSHOWN; - my $default = CONTROLMAPDEFAULT; - my %member_groups = @{ $dbh->selectcol_arrayref( - "SELECT group_id, membercontrol + my ($self) = @_; + return $self->{groups_available} if defined $self->{groups_available}; + my $dbh = Bugzilla->dbh; + my $shown = CONTROLMAPSHOWN; + my $default = CONTROLMAPDEFAULT; + my %member_groups = @{ + $dbh->selectcol_arrayref( + "SELECT group_id, membercontrol FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ? AND (membercontrol = $shown OR membercontrol = $default) - AND " . Bugzilla->user->groups_in_sql(), - {Columns=>[1,2]}, $self->id) }; - # We don't need to check the group membership here, because we only - # add these groups to the list below if the group isn't already listed - # for membercontrol. - my %other_groups = @{ $dbh->selectcol_arrayref( - "SELECT group_id, othercontrol + AND " . Bugzilla->user->groups_in_sql(), {Columns => [1, 2]}, + $self->id + ) + }; + + # We don't need to check the group membership here, because we only + # add these groups to the list below if the group isn't already listed + # for membercontrol. + my %other_groups = @{ + $dbh->selectcol_arrayref( + "SELECT group_id, othercontrol FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE isbuggroup = 1 AND isactive = 1 AND product_id = ? AND (othercontrol = $shown OR othercontrol = $default)", - {Columns=>[1,2]}, $self->id) }; - - # If the user is a member, then we use the membercontrol value. - # Otherwise, we use the othercontrol value. - my %all_groups = %member_groups; - foreach my $id (keys %other_groups) { - if (!defined $all_groups{$id}) { - $all_groups{$id} = $other_groups{$id}; - } + {Columns => [1, 2]}, $self->id + ) + }; + + # If the user is a member, then we use the membercontrol value. + # Otherwise, we use the othercontrol value. + my %all_groups = %member_groups; + foreach my $id (keys %other_groups) { + if (!defined $all_groups{$id}) { + $all_groups{$id} = $other_groups{$id}; } + } - my $available = Bugzilla::Group->new_from_list([keys %all_groups]); - foreach my $group (@$available) { - $group->{is_default} = 1 if $all_groups{$group->id} == $default; - } + my $available = Bugzilla::Group->new_from_list([keys %all_groups]); + foreach my $group (@$available) { + $group->{is_default} = 1 if $all_groups{$group->id} == $default; + } - $self->{groups_available} = $available; - return $self->{groups_available}; + $self->{groups_available} = $available; + return $self->{groups_available}; } sub groups_mandatory { - my ($self) = @_; - return $self->{groups_mandatory} if $self->{groups_mandatory}; - my $groups = Bugzilla->user->groups_as_string; - my $mandatory = CONTROLMAPMANDATORY; - # For membercontrol we don't check group_id IN, because if membercontrol - # is Mandatory, the group is Mandatory for everybody, regardless of their - # group membership. - my $ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT group_id + my ($self) = @_; + return $self->{groups_mandatory} if $self->{groups_mandatory}; + my $groups = Bugzilla->user->groups_as_string; + my $mandatory = CONTROLMAPMANDATORY; + + # For membercontrol we don't check group_id IN, because if membercontrol + # is Mandatory, the group is Mandatory for everybody, regardless of their + # group membership. + my $ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT group_id FROM group_control_map INNER JOIN groups ON group_control_map.group_id = groups.id WHERE product_id = ? AND isactive = 1 AND (membercontrol = $mandatory OR (othercontrol = $mandatory - AND group_id NOT IN ($groups)))", - undef, $self->id); - $self->{groups_mandatory} = Bugzilla::Group->new_from_list($ids); - return $self->{groups_mandatory}; + AND group_id NOT IN ($groups)))", undef, $self->id + ); + $self->{groups_mandatory} = Bugzilla::Group->new_from_list($ids); + return $self->{groups_mandatory}; } # We don't just check groups_valid, because we want to know specifically # if this group can be validly set by the currently-logged-in user. sub group_is_settable { - my ($self, $group) = @_; + my ($self, $group) = @_; - return 0 unless ($group->is_active && $group->is_bug_group); + return 0 unless ($group->is_active && $group->is_bug_group); - my $is_mandatory = grep { $group->id == $_->id } - @{ $self->groups_mandatory }; - my $is_available = grep { $group->id == $_->id } - @{ $self->groups_available }; - return ($is_mandatory or $is_available) ? 1 : 0; + my $is_mandatory = grep { $group->id == $_->id } @{$self->groups_mandatory}; + my $is_available = grep { $group->id == $_->id } @{$self->groups_available}; + return ($is_mandatory or $is_available) ? 1 : 0; } sub group_is_valid { - my ($self, $group) = @_; - return grep($_->id == $group->id, @{ $self->groups_valid }) ? 1 : 0; + my ($self, $group) = @_; + return grep($_->id == $group->id, @{$self->groups_valid}) ? 1 : 0; } sub groups_valid { - my ($self) = @_; - return $self->{groups_valid} if defined $self->{groups_valid}; + my ($self) = @_; + return $self->{groups_valid} if defined $self->{groups_valid}; - # Note that we don't check OtherControl below, because there is no - # valid NA/* combination. - my $ids = Bugzilla->dbh->selectcol_arrayref( - "SELECT DISTINCT group_id + # Note that we don't check OtherControl below, because there is no + # valid NA/* combination. + my $ids = Bugzilla->dbh->selectcol_arrayref( + "SELECT DISTINCT group_id FROM group_control_map AS gcm INNER JOIN groups ON gcm.group_id = groups.id WHERE product_id = ? AND isbuggroup = 1 - AND membercontrol != " . CONTROLMAPNA, undef, $self->id); - $self->{groups_valid} = Bugzilla::Group->new_from_list($ids); - return $self->{groups_valid}; + AND membercontrol != " . CONTROLMAPNA, undef, $self->id + ); + $self->{groups_valid} = Bugzilla::Group->new_from_list($ids); + return $self->{groups_valid}; } sub versions { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{versions}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{versions}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM versions - WHERE product_id = ?}, undef, $self->id); + WHERE product_id = ?}, undef, $self->id + ); - $self->{versions} = Bugzilla::Version->new_from_list($ids); - } - return $self->{versions}; + $self->{versions} = Bugzilla::Version->new_from_list($ids); + } + return $self->{versions}; } sub milestones { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{milestones}) { - my $ids = $dbh->selectcol_arrayref(q{ + if (!defined $self->{milestones}) { + my $ids = $dbh->selectcol_arrayref( + q{ SELECT id FROM milestones - WHERE product_id = ?}, undef, $self->id); + WHERE product_id = ?}, undef, $self->id + ); - $self->{milestones} = Bugzilla::Milestone->new_from_list($ids); - } - return $self->{milestones}; + $self->{milestones} = Bugzilla::Milestone->new_from_list($ids); + } + return $self->{milestones}; } sub bug_count { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!defined $self->{'bug_count'}) { - $self->{'bug_count'} = $dbh->selectrow_array(qq{ + if (!defined $self->{'bug_count'}) { + $self->{'bug_count'} = $dbh->selectrow_array( + qq{ SELECT COUNT(bug_id) FROM bugs - WHERE product_id = ?}, undef, $self->id); + WHERE product_id = ?}, undef, $self->id + ); - } - return $self->{'bug_count'}; + } + return $self->{'bug_count'}; } sub bug_ids { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!defined $self->{'bug_ids'}) { - $self->{'bug_ids'} = - $dbh->selectcol_arrayref(q{SELECT bug_id FROM bugs - WHERE product_id = ?}, - undef, $self->id); - } - return $self->{'bug_ids'}; + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!defined $self->{'bug_ids'}) { + $self->{'bug_ids'} = $dbh->selectcol_arrayref( + q{SELECT bug_id FROM bugs + WHERE product_id = ?}, undef, $self->id + ); + } + return $self->{'bug_ids'}; } sub user_has_access { - my ($self, $user) = @_; + my ($self, $user) = @_; - return Bugzilla->dbh->selectrow_array( - 'SELECT CASE WHEN group_id IS NULL THEN 1 ELSE 0 END + return Bugzilla->dbh->selectrow_array( + 'SELECT CASE WHEN group_id IS NULL THEN 1 ELSE 0 END FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id AND group_control_map.entry != 0 AND group_id NOT IN (' . $user->groups_as_string . ') - WHERE products.id = ? ' . Bugzilla->dbh->sql_limit(1), - undef, $self->id); + WHERE products.id = ? ' . Bugzilla->dbh->sql_limit(1), undef, $self->id + ); } sub flag_types { - my ($self, $params) = @_; - $params ||= {}; - - return $self->{'flag_types'} if defined $self->{'flag_types'}; - - # We cache flag types to avoid useless calls to get_clusions(). - my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {}; - $self->{flag_types} = {}; - my $prod_id = $self->id; - my $flagtypes = Bugzilla::FlagType::match({ product_id => $prod_id, %$params }); - - foreach my $type ('bug', 'attachment') { - my @flags = grep { $_->target_type eq $type } @$flagtypes; - $self->{flag_types}->{$type} = \@flags; - - # Also populate component flag types, while we are here. - foreach my $comp (@{$self->components}) { - $comp->{flag_types} ||= {}; - my $comp_id = $comp->id; - - foreach my $flag (@flags) { - my $flag_id = $flag->id; - $cache->{$flag_id} ||= $flag; - my $i = $cache->{$flag_id}->inclusions_as_hash; - my $e = $cache->{$flag_id}->exclusions_as_hash; - my $included = $i->{0}->{0} || $i->{0}->{$comp_id} - || $i->{$prod_id}->{0} || $i->{$prod_id}->{$comp_id}; - my $excluded = $e->{0}->{0} || $e->{0}->{$comp_id} - || $e->{$prod_id}->{0} || $e->{$prod_id}->{$comp_id}; - push(@{$comp->{flag_types}->{$type}}, $flag) if ($included && !$excluded); - } - } + my ($self, $params) = @_; + $params ||= {}; + + return $self->{'flag_types'} if defined $self->{'flag_types'}; + + # We cache flag types to avoid useless calls to get_clusions(). + my $cache = Bugzilla->request_cache->{flag_types_per_product} ||= {}; + $self->{flag_types} = {}; + my $prod_id = $self->id; + my $flagtypes = Bugzilla::FlagType::match({product_id => $prod_id, %$params}); + + foreach my $type ('bug', 'attachment') { + my @flags = grep { $_->target_type eq $type } @$flagtypes; + $self->{flag_types}->{$type} = \@flags; + + # Also populate component flag types, while we are here. + foreach my $comp (@{$self->components}) { + $comp->{flag_types} ||= {}; + my $comp_id = $comp->id; + + foreach my $flag (@flags) { + my $flag_id = $flag->id; + $cache->{$flag_id} ||= $flag; + my $i = $cache->{$flag_id}->inclusions_as_hash; + my $e = $cache->{$flag_id}->exclusions_as_hash; + my $included + = $i->{0}->{0} + || $i->{0}->{$comp_id} + || $i->{$prod_id}->{0} + || $i->{$prod_id}->{$comp_id}; + my $excluded + = $e->{0}->{0} + || $e->{0}->{$comp_id} + || $e->{$prod_id}->{0} + || $e->{$prod_id}->{$comp_id}; + push(@{$comp->{flag_types}->{$type}}, $flag) if ($included && !$excluded); + } } - return $self->{'flag_types'}; + } + return $self->{'flag_types'}; } sub classification { - my $self = shift; - $self->{'classification'} ||= - new Bugzilla::Classification({ id => $self->classification_id, cache => 1 }); - return $self->{'classification'}; + my $self = shift; + $self->{'classification'} ||= new Bugzilla::Classification( + {id => $self->classification_id, cache => 1}); + return $self->{'classification'}; } ############################### @@ -832,29 +896,29 @@ sub classification { ############################### sub allows_unconfirmed { return $_[0]->{'allows_unconfirmed'}; } -sub description { return $_[0]->{'description'}; } -sub is_active { return $_[0]->{'isactive'}; } -sub default_milestone { return $_[0]->{'defaultmilestone'}; } -sub classification_id { return $_[0]->{'classification_id'}; } +sub description { return $_[0]->{'description'}; } +sub is_active { return $_[0]->{'isactive'}; } +sub default_milestone { return $_[0]->{'defaultmilestone'}; } +sub classification_id { return $_[0]->{'classification_id'}; } ############################### #### Subroutines ###### ############################### sub check { - my ($class, $params) = @_; - $params = { name => $params } if !ref $params; - if (!$params->{allow_inaccessible}) { - $params->{_error} = 'product_access_denied'; - } - my $product = $class->SUPER::check($params); - - if (!$params->{allow_inaccessible} - && !Bugzilla->user->can_access_product($product)) - { - ThrowUserError('product_access_denied', $params); - } - return $product; + my ($class, $params) = @_; + $params = {name => $params} if !ref $params; + if (!$params->{allow_inaccessible}) { + $params->{_error} = 'product_access_denied'; + } + my $product = $class->SUPER::check($params); + + if ( !$params->{allow_inaccessible} + && !Bugzilla->user->can_access_product($product)) + { + ThrowUserError('product_access_denied', $params); + } + return $product; } 1; diff --git a/Bugzilla/Quantum.pm b/Bugzilla/Quantum.pm index 4fddb8da9..1638f8c9a 100644 --- a/Bugzilla/Quantum.pm +++ b/Bugzilla/Quantum.pm @@ -96,8 +96,7 @@ sub setup_routes { my $r = $self->routes; Bugzilla::Quantum::CGI->load_all($r); - Bugzilla::Quantum::CGI->load_one('bzapi_cgi', - 'extensions/BzAPI/bin/rest.cgi'); + Bugzilla::Quantum::CGI->load_one('bzapi_cgi', 'extensions/BzAPI/bin/rest.cgi'); $r->get('/home')->to('Home#index'); $r->any('/')->to('CGI#index_cgi'); @@ -118,8 +117,8 @@ sub setup_routes { $r->any('/bzapi/*PATH_INFO')->to('CGI#bzapi_cgi'); $r->static_file('/__lbheartbeat__'); - $r->static_file('/__version__' => - {file => 'version.json', content_type => 'application/json'}); + $r->static_file( + '/__version__' => {file => 'version.json', content_type => 'application/json'}); $r->static_file('/version.json', {content_type => 'application/json'}); $r->page('/review', 'splinter.html'); diff --git a/Bugzilla/Quantum/CGI.pm b/Bugzilla/Quantum/CGI.pm index 79fbcfde6..5a654111e 100644 --- a/Bugzilla/Quantum/CGI.pm +++ b/Bugzilla/Quantum/CGI.pm @@ -54,8 +54,7 @@ sub load_one { open STDIN, '<', $stdin->path or die "STDIN @{[$stdin->path]}: $!" if -s $stdin->path; - tie *STDOUT, 'Bugzilla::Quantum::Stdout', - controller => $c; ## no critic (tie) + tie *STDOUT, 'Bugzilla::Quantum::Stdout', controller => $c; ## no critic (tie) # the finally block calls cleanup. $c->stash->{cleanup_guard}->dismiss; @@ -129,19 +128,17 @@ sub _ENV { GATEWAY_INTERFACE => 'CGI/1.1', HTTPS => $req->is_secure ? 'on' : 'off', %env_headers, - QUERY_STRING => $cgi_query->to_string, - PATH_INFO => $path_info ? "/$path_info" : '', - REMOTE_ADDR => $tx->original_remote_address, - REMOTE_HOST => $tx->original_remote_address, - REMOTE_PORT => $tx->remote_port, - REMOTE_USER => $remote_user || '', - REQUEST_METHOD => $req->method, - SCRIPT_NAME => "$prefix$script_name", - SERVER_NAME => hostname, - SERVER_PORT => $tx->local_port, - SERVER_PROTOCOL => $req->is_secure - ? 'HTTPS' - : 'HTTP', # TODO: Version is missing + QUERY_STRING => $cgi_query->to_string, + PATH_INFO => $path_info ? "/$path_info" : '', + REMOTE_ADDR => $tx->original_remote_address, + REMOTE_HOST => $tx->original_remote_address, + REMOTE_PORT => $tx->remote_port, + REMOTE_USER => $remote_user || '', + REQUEST_METHOD => $req->method, + SCRIPT_NAME => "$prefix$script_name", + SERVER_NAME => hostname, + SERVER_PORT => $tx->local_port, + SERVER_PROTOCOL => $req->is_secure ? 'HTTPS' : 'HTTP', # TODO: Version is missing SERVER_SOFTWARE => __PACKAGE__, ); } diff --git a/Bugzilla/Quantum/Home.pm b/Bugzilla/Quantum/Home.pm index 48d5e47bd..6a3021f64 100644 --- a/Bugzilla/Quantum/Home.pm +++ b/Bugzilla/Quantum/Home.pm @@ -16,8 +16,7 @@ sub index { my ($c) = @_; $c->bugzilla->login(LOGIN_REQUIRED) or return; try { - ThrowUserError('invalid_username', {login => 'batman'}) - if $c->param('error'); + ThrowUserError('invalid_username', {login => 'batman'}) if $c->param('error'); $c->render(handler => 'bugzilla', template => 'index'); } catch { diff --git a/Bugzilla/Quantum/Plugin/BasicAuth.pm b/Bugzilla/Quantum/Plugin/BasicAuth.pm index e17273404..e0d4e8ecc 100644 --- a/Bugzilla/Quantum/Plugin/BasicAuth.pm +++ b/Bugzilla/Quantum/Plugin/BasicAuth.pm @@ -12,29 +12,29 @@ use Bugzilla::Logging; use Carp; sub register { - my ( $self, $app, $conf ) = @_; + my ($self, $app, $conf) = @_; - $app->renderer->add_helper( - basic_auth => sub { - my ( $c, $realm, $auth_user, $auth_pass ) = @_; - my $req = $c->req; - my ( $user, $password ) = $req->url->to_abs->userinfo =~ /^([^:]+):(.*)/; + $app->renderer->add_helper( + basic_auth => sub { + my ($c, $realm, $auth_user, $auth_pass) = @_; + my $req = $c->req; + my ($user, $password) = $req->url->to_abs->userinfo =~ /^([^:]+):(.*)/; - unless ( $realm && $auth_user && $auth_pass ) { - croak 'basic_auth() called with missing parameters.'; - } + unless ($realm && $auth_user && $auth_pass) { + croak 'basic_auth() called with missing parameters.'; + } - unless ( $user eq $auth_user && $password eq $auth_pass ) { - WARN('username and password do not match'); - $c->res->headers->www_authenticate("Basic realm=\"$realm\""); - $c->res->code(401); - $c->rendered; - return 0; - } + unless ($user eq $auth_user && $password eq $auth_pass) { + WARN('username and password do not match'); + $c->res->headers->www_authenticate("Basic realm=\"$realm\""); + $c->res->code(401); + $c->rendered; + return 0; + } - return 1; - } - ); + return 1; + } + ); } -1; \ No newline at end of file +1; diff --git a/Bugzilla/Quantum/Plugin/Glue.pm b/Bugzilla/Quantum/Plugin/Glue.pm index f04b9c025..de016356c 100644 --- a/Bugzilla/Quantum/Plugin/Glue.pm +++ b/Bugzilla/Quantum/Plugin/Glue.pm @@ -157,8 +157,7 @@ sub register { ); $app->log(MojoX::Log::Log4perl::Tiny->new( - logger => Log::Log4perl->get_logger(ref $app) - )); + logger => Log::Log4perl->get_logger(ref $app))); } 1; diff --git a/Bugzilla/Quantum/Plugin/Hostage.pm b/Bugzilla/Quantum/Plugin/Hostage.pm index 418b09a0c..df3e40ec1 100644 --- a/Bugzilla/Quantum/Plugin/Hostage.pm +++ b/Bugzilla/Quantum/Plugin/Hostage.pm @@ -3,78 +3,77 @@ use 5.10.1; use Mojo::Base 'Mojolicious::Plugin'; sub _attachment_root { - my ($base) = @_; - return undef unless $base; - return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} - ? $1 - : undef; + my ($base) = @_; + return undef unless $base; + return $base =~ m{^https?://(?:bug)?\%bugid\%\.([a-zA-Z\.-]+)} ? $1 : undef; } sub _attachment_host_regex { - my ($base) = @_; - return undef unless $base; - my $val = $base; - $val =~ s{^https?://}{}s; - $val =~ s{/$}{}s; - my $regex = quotemeta $val; - $regex =~ s/\\\%bugid\\\%/\\d+/g; - return qr/^$regex$/s; + my ($base) = @_; + return undef unless $base; + my $val = $base; + $val =~ s{^https?://}{}s; + $val =~ s{/$}{}s; + my $regex = quotemeta $val; + $regex =~ s/\\\%bugid\\\%/\\d+/g; + return qr/^$regex$/s; } sub register { - my ( $self, $app, $conf ) = @_; + my ($self, $app, $conf) = @_; - $app->hook(before_routes => \&_before_routes); + $app->hook(before_routes => \&_before_routes); } sub _before_routes { - my ( $c ) = @_; - state $urlbase = Bugzilla->localconfig->{urlbase}; - state $urlbase_uri = URI->new($urlbase); - state $urlbase_host = $urlbase_uri->host; - state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/; - state $attachment_base = Bugzilla->localconfig->{attachment_base}; - state $attachment_root = _attachment_root($attachment_base); - state $attachment_host_regex = _attachment_host_regex($attachment_base); + my ($c) = @_; + state $urlbase = Bugzilla->localconfig->{urlbase}; + state $urlbase_uri = URI->new($urlbase); + state $urlbase_host = $urlbase_uri->host; + state $urlbase_host_regex = qr/^bug(\d+)\.\Q$urlbase_host\E$/; + state $attachment_base = Bugzilla->localconfig->{attachment_base}; + state $attachment_root = _attachment_root($attachment_base); + state $attachment_host_regex = _attachment_host_regex($attachment_base); - my $stash = $c->stash; - my $req = $c->req; - my $url = $req->url->to_abs; + my $stash = $c->stash; + my $req = $c->req; + my $url = $req->url->to_abs; - return if $stash->{'mojo.static'}; + return if $stash->{'mojo.static'}; - my $hostname = $url->host; - return if $hostname eq $urlbase_host; + my $hostname = $url->host; + return if $hostname eq $urlbase_host; - my $path = $url->path; - return if $path eq '/__lbheartbeat__'; + my $path = $url->path; + return if $path eq '/__lbheartbeat__'; - if ($attachment_base && $hostname eq $attachment_root) { - $c->redirect_to($urlbase); - return; - } - elsif ($attachment_base && $hostname =~ $attachment_host_regex) { - if ($path =~ m{^/attachment\.cgi}s) { - return; - } else { - my $new_uri = $url->clone; - $new_uri->scheme($urlbase_uri->scheme); - $new_uri->host($urlbase_host); - $c->redirect_to($new_uri); - return; - } - } - elsif (my ($id) = $hostname =~ $urlbase_host_regex) { - my $new_uri = $urlbase_uri->clone; - $new_uri->path('/show_bug.cgi'); - $new_uri->query_form(id => $id); - $c->redirect_to($new_uri); - return; + if ($attachment_base && $hostname eq $attachment_root) { + $c->redirect_to($urlbase); + return; + } + elsif ($attachment_base && $hostname =~ $attachment_host_regex) { + if ($path =~ m{^/attachment\.cgi}s) { + return; } else { - $c->redirect_to($urlbase); - return; + my $new_uri = $url->clone; + $new_uri->scheme($urlbase_uri->scheme); + $new_uri->host($urlbase_host); + $c->redirect_to($new_uri); + return; } + } + elsif (my ($id) = $hostname =~ $urlbase_host_regex) { + my $new_uri = $urlbase_uri->clone; + $new_uri->path('/show_bug.cgi'); + $new_uri->query_form(id => $id); + $c->redirect_to($new_uri); + return; + } + else { + $c->redirect_to($urlbase); + return; + } } 1; diff --git a/Bugzilla/Quantum/SES.pm b/Bugzilla/Quantum/SES.pm index 03916075d..9d2149978 100644 --- a/Bugzilla/Quantum/SES.pm +++ b/Bugzilla/Quantum/SES.pm @@ -1,4 +1,5 @@ package Bugzilla::Quantum::SES; + # 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/. @@ -22,233 +23,228 @@ use Types::Standard qw( :all ); use Type::Utils; use Type::Params qw( compile ); -my $Invocant = class_type { class => __PACKAGE__ }; +my $Invocant = class_type {class => __PACKAGE__}; sub main { - my ($self) = @_; - try { - $self->_main; - } - catch { - FATAL("Error in SES Handler: ", $_); - $self->_respond( 400 => 'Bad Request' ); - }; + my ($self) = @_; + try { + $self->_main; + } + catch { + FATAL("Error in SES Handler: ", $_); + $self->_respond(400 => 'Bad Request'); + }; } sub _main { - my ($self) = @_; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $message = $self->_decode_json_wrapper( $self->req->body ) // return; - my $message_type = $self->req->headers->header('X-Amz-SNS-Message-Type') // '(missing)'; - - if ( $message_type eq 'SubscriptionConfirmation' ) { - $self->_confirm_subscription($message); + my ($self) = @_; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $message = $self->_decode_json_wrapper($self->req->body) // return; + my $message_type = $self->req->headers->header('X-Amz-SNS-Message-Type') + // '(missing)'; + + if ($message_type eq 'SubscriptionConfirmation') { + $self->_confirm_subscription($message); + } + + elsif ($message_type eq 'Notification') { + my $notification = $self->_decode_json_wrapper($message->{Message}) // return; + unless ( +# https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html + $self->_handle_notification($notification, 'eventType') + + # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html + || $self->_handle_notification($notification, 'notificationType') + ) + { + WARN('Failed to find notification type'); + $self->_respond(400 => 'Bad Request'); } + } - elsif ( $message_type eq 'Notification' ) { - my $notification = $self->_decode_json_wrapper( $message->{Message} ) // return; - unless ( - # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/event-publishing-retrieving-sns-contents.html - $self->_handle_notification( $notification, 'eventType' ) - - # https://docs.aws.amazon.com/ses/latest/DeveloperGuide/notification-contents.html - || $self->_handle_notification( $notification, 'notificationType' ) - ) - { - WARN('Failed to find notification type'); - $self->_respond( 400 => 'Bad Request' ); - } - } - - else { - WARN("Unsupported message-type: $message_type"); - $self->_respond( 200 => 'OK' ); - } + else { + WARN("Unsupported message-type: $message_type"); + $self->_respond(200 => 'OK'); + } } sub _confirm_subscription { - state $check = compile($Invocant, Dict[SubscribeURL => Str, slurpy Any]); - my ($self, $message) = $check->(@_); - - my $subscribe_url = $message->{SubscribeURL}; - if ( !$subscribe_url ) { - WARN('Bad SubscriptionConfirmation request: missing SubscribeURL'); - $self->_respond( 400 => 'Bad Request' ); - return; - } - - my $ua = ua(); - my $res = $ua->get( $message->{SubscribeURL} ); - if ( !$res->is_success ) { - WARN( 'Bad response from SubscribeURL: ' . $res->status_line ); - $self->_respond( 400 => 'Bad Request' ); - return; - } - - $self->_respond( 200 => 'OK' ); + state $check = compile($Invocant, Dict [SubscribeURL => Str, slurpy Any]); + my ($self, $message) = $check->(@_); + + my $subscribe_url = $message->{SubscribeURL}; + if (!$subscribe_url) { + WARN('Bad SubscriptionConfirmation request: missing SubscribeURL'); + $self->_respond(400 => 'Bad Request'); + return; + } + + my $ua = ua(); + my $res = $ua->get($message->{SubscribeURL}); + if (!$res->is_success) { + WARN('Bad response from SubscribeURL: ' . $res->status_line); + $self->_respond(400 => 'Bad Request'); + return; + } + + $self->_respond(200 => 'OK'); } my $NotificationType = Enum [qw( Bounce Complaint )]; my $TypeField = Enum [qw(eventType notificationType)]; -my $Notification = Dict [ - eventType => Optional [$NotificationType], - notificationType => Optional [$NotificationType], - slurpy Any, +my $Notification = Dict [ + eventType => Optional [$NotificationType], + notificationType => Optional [$NotificationType], + slurpy Any, ]; sub _handle_notification { - state $check = compile($Invocant, $Notification, $TypeField ); - my ( $self, $notification, $type_field ) = $check->(@_); - - if ( !exists $notification->{$type_field} ) { - return 0; - } - my $type = $notification->{$type_field}; - - if ( $type eq 'Bounce' ) { - $self->_process_bounce($notification); - } - elsif ( $type eq 'Complaint' ) { - $self->_process_complaint($notification); - } - else { - WARN("Unsupported notification-type: $type"); - $self->_respond( 200 => 'OK' ); - } - return 1; + state $check = compile($Invocant, $Notification, $TypeField); + my ($self, $notification, $type_field) = $check->(@_); + + if (!exists $notification->{$type_field}) { + return 0; + } + my $type = $notification->{$type_field}; + + if ($type eq 'Bounce') { + $self->_process_bounce($notification); + } + elsif ($type eq 'Complaint') { + $self->_process_complaint($notification); + } + else { + WARN("Unsupported notification-type: $type"); + $self->_respond(200 => 'OK'); + } + return 1; } -my $BouncedRecipients = ArrayRef[ - Dict[ - emailAddress => Str, - action => Str, - diagnosticCode => Str, - slurpy Any, - ], +my $BouncedRecipients = ArrayRef [ + Dict [emailAddress => Str, action => Str, diagnosticCode => Str, slurpy Any,], ]; my $BounceNotification = Dict [ - bounce => Dict [ - bouncedRecipients => $BouncedRecipients, - reportingMTA => Str, - bounceSubType => Str, - bounceType => Str, - slurpy Any, - ], + bounce => Dict [ + bouncedRecipients => $BouncedRecipients, + reportingMTA => Str, + bounceSubType => Str, + bounceType => Str, slurpy Any, + ], + slurpy Any, ]; sub _process_bounce { - state $check = compile($Invocant, $BounceNotification); - my ($self, $notification) = $check->(@_); - - # disable each account that is bouncing - foreach my $recipient ( @{ $notification->{bounce}->{bouncedRecipients} } ) { - my $address = $recipient->{emailAddress}; - my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', $recipient->{diagnosticCode} // 'unknown'; - - my $user = Bugzilla::User->new( { name => $address, cache => 1 } ); - if ($user) { - - # never auto-disable admin accounts - if ( $user->in_group('admin') ) { - Bugzilla->audit("ignoring bounce for admin <$address>: $reason"); - } - - else { - my $template = Bugzilla->template_inner(); - my $vars = { - mta => $notification->{bounce}->{reportingMTA} // 'unknown', - reason => $reason, - }; - my $disable_text; - $template->process( 'admin/users/bounce-disabled.txt.tmpl', $vars, \$disable_text ) - || die $template->error(); - - $user->set_disabledtext($disable_text); - $user->set_disable_mail(1); - $user->update(); - Bugzilla->audit( "bounce for <$address> disabled userid-" . $user->id . ": $reason" ); - } - } - - else { - Bugzilla->audit("bounce for <$address> has no user: $reason"); - } + state $check = compile($Invocant, $BounceNotification); + my ($self, $notification) = $check->(@_); + + # disable each account that is bouncing + foreach my $recipient (@{$notification->{bounce}->{bouncedRecipients}}) { + my $address = $recipient->{emailAddress}; + my $reason = sprintf '(%s) %s', $recipient->{action} // 'error', + $recipient->{diagnosticCode} // 'unknown'; + + my $user = Bugzilla::User->new({name => $address, cache => 1}); + if ($user) { + + # never auto-disable admin accounts + if ($user->in_group('admin')) { + Bugzilla->audit("ignoring bounce for admin <$address>: $reason"); + } + + else { + my $template = Bugzilla->template_inner(); + my $vars = { + mta => $notification->{bounce}->{reportingMTA} // 'unknown', + reason => $reason, + }; + my $disable_text; + $template->process('admin/users/bounce-disabled.txt.tmpl', + $vars, \$disable_text) + || die $template->error(); + + $user->set_disabledtext($disable_text); + $user->set_disable_mail(1); + $user->update(); + Bugzilla->audit( + "bounce for <$address> disabled userid-" . $user->id . ": $reason"); + } + } + + else { + Bugzilla->audit("bounce for <$address> has no user: $reason"); } + } - $self->_respond( 200 => 'OK' ); + $self->_respond(200 => 'OK'); } -my $ComplainedRecipients = ArrayRef[Dict[ emailAddress => Str, slurpy Any ]]; -my $ComplaintNotification = Dict[ - complaint => Dict [ - complainedRecipients => $ComplainedRecipients, - complaintFeedbackType => Str, - slurpy Any, - ], +my $ComplainedRecipients = ArrayRef [Dict [emailAddress => Str, slurpy Any]]; +my $ComplaintNotification = Dict [ + complaint => Dict [ + complainedRecipients => $ComplainedRecipients, + complaintFeedbackType => Str, slurpy Any, + ], + slurpy Any, ]; sub _process_complaint { - state $check = compile($Invocant, $ComplaintNotification); - my ($self, $notification) = $check->(@_); - my $template = Bugzilla->template_inner(); - my $json = JSON::MaybeXS->new( - pretty => 1, - utf8 => 1, - canonical => 1, - ); - - foreach my $recipient ( @{ $notification->{complaint}->{complainedRecipients} } ) { - my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown'; - my $address = $recipient->{emailAddress}; - Bugzilla->audit("complaint for <$address> for '$reason'"); - my $vars = { - email => $address, - user => Bugzilla::User->new( { name => $address, cache => 1 } ), - reason => $reason, - notification => $json->encode($notification), - }; - my $message; - $template->process( 'email/ses-complaint.txt.tmpl', $vars, \$message ) - || die $template->error(); - MessageToMTA($message); - } + state $check = compile($Invocant, $ComplaintNotification); + my ($self, $notification) = $check->(@_); + my $template = Bugzilla->template_inner(); + my $json = JSON::MaybeXS->new(pretty => 1, utf8 => 1, canonical => 1,); + + foreach my $recipient (@{$notification->{complaint}->{complainedRecipients}}) { + my $reason = $notification->{complaint}->{complaintFeedbackType} // 'unknown'; + my $address = $recipient->{emailAddress}; + Bugzilla->audit("complaint for <$address> for '$reason'"); + my $vars = { + email => $address, + user => Bugzilla::User->new({name => $address, cache => 1}), + reason => $reason, + notification => $json->encode($notification), + }; + my $message; + $template->process('email/ses-complaint.txt.tmpl', $vars, \$message) + || die $template->error(); + MessageToMTA($message); + } - $self->_respond( 200 => 'OK' ); + $self->_respond(200 => 'OK'); } sub _respond { - my ( $self, $code, $message ) = @_; - $self->render(text => "$message\n", status => $code); + my ($self, $code, $message) = @_; + $self->render(text => "$message\n", status => $code); } sub _decode_json_wrapper { - state $check = compile($Invocant, Str); - my ($self, $json) = $check->(@_); - my $result; - my $ok = try { - $result = decode_json($json); - } - catch { - WARN( 'Malformed JSON from ' . $self->tx->remote_address ); - $self->_respond( 400 => 'Bad Request' ); - return undef; - }; - return $ok ? $result : undef; + state $check = compile($Invocant, Str); + my ($self, $json) = $check->(@_); + my $result; + my $ok = try { + $result = decode_json($json); + } + catch { + WARN('Malformed JSON from ' . $self->tx->remote_address); + $self->_respond(400 => 'Bad Request'); + return undef; + }; + return $ok ? $result : undef; } sub ua { - my $ua = LWP::UserAgent->new(); - $ua->timeout(10); - $ua->protocols_allowed( [ 'http', 'https' ] ); - if ( my $proxy_url = Bugzilla->params->{'proxy_url'} ) { - $ua->proxy( [ 'http', 'https' ], $proxy_url ); - } - else { - $ua->env_proxy; - } - return $ua; + my $ua = LWP::UserAgent->new(); + $ua->timeout(10); + $ua->protocols_allowed(['http', 'https']); + if (my $proxy_url = Bugzilla->params->{'proxy_url'}) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy; + } + return $ua; } 1; diff --git a/Bugzilla/RNG.pm b/Bugzilla/RNG.pm index cc2dfc58c..b3b89a0ca 100644 --- a/Bugzilla/RNG.pm +++ b/Bugzilla/RNG.pm @@ -28,7 +28,7 @@ our @EXPORT_OK = qw(rand srand irand); use constant DIVIDE_BY => 2**32; # How many bytes of seed to read. -use constant SEED_SIZE => 16; # 128 bits. +use constant SEED_SIZE => 16; # 128 bits. ################# # Windows Stuff # @@ -43,6 +43,7 @@ use constant PROV_RSA_FULL => 1; # Flags for CryptGenRandom: # Don't ever display a UI to the user, just fail if one would be needed. use constant CRYPT_SILENT => 64; + # Don't require existing public/private keypairs. use constant CRYPT_VERIFYCONTEXT => 0xF0000000; @@ -60,40 +61,42 @@ END ################# sub rand (;$) { - my ($limit) = @_; - my $int = irand(); - return _to_float($int, $limit); + my ($limit) = @_; + my $int = irand(); + return _to_float($int, $limit); } sub irand (;$) { - my ($limit) = @_; - Bugzilla::RNG::srand() if !defined $RNG; - my $int = $RNG->irand(); - if (defined $limit) { - # We can't just use the mod operator because it will bias - # our output. Search for "modulo bias" on the Internet for - # details. This is slower than mod(), but does not have a bias, - # as demonstrated by Math::Random::Secure's uniform.t test. - return int(_to_float($int, $limit)); - } - return $int; + my ($limit) = @_; + Bugzilla::RNG::srand() if !defined $RNG; + my $int = $RNG->irand(); + if (defined $limit) { + + # We can't just use the mod operator because it will bias + # our output. Search for "modulo bias" on the Internet for + # details. This is slower than mod(), but does not have a bias, + # as demonstrated by Math::Random::Secure's uniform.t test. + return int(_to_float($int, $limit)); + } + return $int; } sub srand (;$) { - my ($value) = @_; - # Remove any RNG that might already have been made. - $RNG = undef; - my %args; - if (defined $value) { - $args{seed} = $value; - } - $RNG = _create_rng(\%args); + my ($value) = @_; + + # Remove any RNG that might already have been made. + $RNG = undef; + my %args; + if (defined $value) { + $args{seed} = $value; + } + $RNG = _create_rng(\%args); } sub _to_float { - my ($integer, $limit) = @_; - $limit ||= 1; - return ($integer / DIVIDE_BY) * $limit; + my ($integer, $limit) = @_; + $limit ||= 1; + return ($integer / DIVIDE_BY) * $limit; } ########################## @@ -101,123 +104,123 @@ sub _to_float { ########################## sub _create_rng { - my ($params) = @_; + my ($params) = @_; - if (!defined $params->{seed}) { - $params->{seed} = _get_seed(); - } + if (!defined $params->{seed}) { + $params->{seed} = _get_seed(); + } - _check_seed($params->{seed}); + _check_seed($params->{seed}); - my @seed_ints = unpack('L*', $params->{seed}); + my @seed_ints = unpack('L*', $params->{seed}); - my $rng = Math::Random::ISAAC->new(@seed_ints); + my $rng = Math::Random::ISAAC->new(@seed_ints); - # It's faster to skip the frontend interface of Math::Random::ISAAC - # and just use the backend directly. However, in case the internal - # code of Math::Random::ISAAC changes at some point, we do make sure - # that the {backend} element actually exists first. - return $rng->{backend} ? $rng->{backend} : $rng; + # It's faster to skip the frontend interface of Math::Random::ISAAC + # and just use the backend directly. However, in case the internal + # code of Math::Random::ISAAC changes at some point, we do make sure + # that the {backend} element actually exists first. + return $rng->{backend} ? $rng->{backend} : $rng; } sub _check_seed { - my ($seed) = @_; - if (length($seed) < 8) { - warn "Your seed is less than 8 bytes (64 bits). It could be" - . " easy to crack"; - } - # If it looks like we were seeded with a 32-bit integer, warn the - # user that they are making a dangerous, easily-crackable mistake. - elsif (length($seed) <= 10 and $seed =~ /^\d+$/) { - warn "RNG seeded with a 32-bit integer, this is easy to crack"; - } + my ($seed) = @_; + if (length($seed) < 8) { + warn "Your seed is less than 8 bytes (64 bits). It could be" . " easy to crack"; + } + + # If it looks like we were seeded with a 32-bit integer, warn the + # user that they are making a dangerous, easily-crackable mistake. + elsif (length($seed) <= 10 and $seed =~ /^\d+$/) { + warn "RNG seeded with a 32-bit integer, this is easy to crack"; + } } sub _get_seed { - return _windows_seed() if ON_WINDOWS; + return _windows_seed() if ON_WINDOWS; - if (-r '/dev/urandom') { - return _read_seed_from('/dev/urandom'); - } + if (-r '/dev/urandom') { + return _read_seed_from('/dev/urandom'); + } - return _read_seed_from('/dev/random'); + return _read_seed_from('/dev/random'); } sub _read_seed_from { - my ($from) = @_; - - my $fh = IO::File->new($from, "r") or die "$from: $!"; - my $buffer; - $fh->read($buffer, SEED_SIZE); - if (length($buffer) < SEED_SIZE) { - die "Could not read enough seed bytes from $from, got only " - . length($buffer); - } - $fh->close; - return $buffer; + my ($from) = @_; + + my $fh = IO::File->new($from, "r") or die "$from: $!"; + my $buffer; + $fh->read($buffer, SEED_SIZE); + if (length($buffer) < SEED_SIZE) { + die "Could not read enough seed bytes from $from, got only " . length($buffer); + } + $fh->close; + return $buffer; } sub _windows_seed { - my ($major, $minor) = (Win32::GetOSVersion())[1,2]; - if ($major < 5) { - die "Bugzilla does not support versions of Windows before" - . " Windows 2000"; - } - # This means Windows 2000. - if ($major == 5 and $minor == 0) { - return _win2k_seed(); - } - - my $rtlgenrand = Win32::API->new('advapi32', RTLGENRANDOM_PROTO); - if (!defined $rtlgenrand) { - die "Could not import RtlGenRand: $^E"; - } - my $buffer = chr(0) x SEED_SIZE; - my $result = $rtlgenrand->Call($buffer, SEED_SIZE); - if (!$result) { - die "RtlGenRand failed: $^E"; - } - return $buffer; + my ($major, $minor) = (Win32::GetOSVersion())[1, 2]; + if ($major < 5) { + die "Bugzilla does not support versions of Windows before" . " Windows 2000"; + } + + # This means Windows 2000. + if ($major == 5 and $minor == 0) { + return _win2k_seed(); + } + + my $rtlgenrand = Win32::API->new('advapi32', RTLGENRANDOM_PROTO); + if (!defined $rtlgenrand) { + die "Could not import RtlGenRand: $^E"; + } + my $buffer = chr(0) x SEED_SIZE; + my $result = $rtlgenrand->Call($buffer, SEED_SIZE); + if (!$result) { + die "RtlGenRand failed: $^E"; + } + return $buffer; } sub _win2k_seed { - my $crypt_acquire = Win32::API->new( - "advapi32", 'CryptAcquireContext', 'PPPNN', 'I'); - if (!defined $crypt_acquire) { - die "Could not import CryptAcquireContext: $^E"; - } - - my $crypt_release = Win32::API->new( - "advapi32", 'CryptReleaseContext', 'NN', 'I'); - if (!defined $crypt_release) { - die "Could not import CryptReleaseContext: $^E"; - } - - my $crypt_gen_random = Win32::API->new( - "advapi32", 'CryptGenRandom', 'NNP', 'I'); - if (!defined $crypt_gen_random) { - die "Could not import CryptGenRandom: $^E"; - } - - my $context = chr(0) x Win32::API::Type->sizeof('PULONG'); - my $acquire_result = $crypt_acquire->Call( - $context, 0, 0, PROV_RSA_FULL, CRYPT_SILENT | CRYPT_VERIFYCONTEXT); - if (!defined $acquire_result) { - die "CryptAcquireContext failed: $^E"; - } - - my $pack_type = Win32::API::Type::packing('PULONG'); - $context = unpack($pack_type, $context); - - my $buffer = chr(0) x SEED_SIZE; - my $rand_result = $crypt_gen_random->Call($context, SEED_SIZE, $buffer); - my $rand_error = $^E; - # We don't check this if it fails, we don't care. - $crypt_release->Call($context, 0); - if (!defined $rand_result) { - die "CryptGenRandom failed: $rand_error"; - } - return $buffer; + my $crypt_acquire + = Win32::API->new("advapi32", 'CryptAcquireContext', 'PPPNN', 'I'); + if (!defined $crypt_acquire) { + die "Could not import CryptAcquireContext: $^E"; + } + + my $crypt_release + = Win32::API->new("advapi32", 'CryptReleaseContext', 'NN', 'I'); + if (!defined $crypt_release) { + die "Could not import CryptReleaseContext: $^E"; + } + + my $crypt_gen_random + = Win32::API->new("advapi32", 'CryptGenRandom', 'NNP', 'I'); + if (!defined $crypt_gen_random) { + die "Could not import CryptGenRandom: $^E"; + } + + my $context = chr(0) x Win32::API::Type->sizeof('PULONG'); + my $acquire_result = $crypt_acquire->Call($context, 0, 0, PROV_RSA_FULL, + CRYPT_SILENT | CRYPT_VERIFYCONTEXT); + if (!defined $acquire_result) { + die "CryptAcquireContext failed: $^E"; + } + + my $pack_type = Win32::API::Type::packing('PULONG'); + $context = unpack($pack_type, $context); + + my $buffer = chr(0) x SEED_SIZE; + my $rand_result = $crypt_gen_random->Call($context, SEED_SIZE, $buffer); + my $rand_error = $^E; + + # We don't check this if it fails, we don't care. + $crypt_release->Call($context, 0); + if (!defined $rand_result) { + die "CryptGenRandom failed: $rand_error"; + } + return $buffer; } 1; diff --git a/Bugzilla/Report/SecurityRisk.pm b/Bugzilla/Report/SecurityRisk.pm index 5eb98fd7f..53a8e3224 100644 --- a/Bugzilla/Report/SecurityRisk.pm +++ b/Bugzilla/Report/SecurityRisk.pm @@ -21,101 +21,72 @@ use POSIX qw(ceil); use Type::Utils; use Types::Standard qw(Num Int Bool Str HashRef ArrayRef CodeRef Map Dict Enum); -my $DateTime = class_type { class => 'DateTime' }; +my $DateTime = class_type {class => 'DateTime'}; -has 'start_date' => ( - is => 'ro', - required => 1, - isa => $DateTime, -); +has 'start_date' => (is => 'ro', required => 1, isa => $DateTime,); -has 'end_date' => ( - is => 'ro', - required => 1, - isa => $DateTime, -); +has 'end_date' => (is => 'ro', required => 1, isa => $DateTime,); -has 'products' => ( - is => 'ro', - required => 1, - isa => ArrayRef [Str], -); +has 'products' => (is => 'ro', required => 1, isa => ArrayRef [Str],); -has 'sec_keywords' => ( - is => 'ro', - required => 1, - isa => ArrayRef [Str], -); +has 'sec_keywords' => (is => 'ro', required => 1, isa => ArrayRef [Str],); -has 'initial_bug_ids' => ( - is => 'lazy', - isa => ArrayRef [Int], -); +has 'initial_bug_ids' => (is => 'lazy', isa => ArrayRef [Int],); has 'initial_bugs' => ( - is => 'lazy', - isa => HashRef [ - Dict [ - id => Int, - product => Str, - sec_level => Str, - is_open => Bool, - created_at => $DateTime, - ], + is => 'lazy', + isa => HashRef [ + Dict [ + id => Int, + product => Str, + sec_level => Str, + is_open => Bool, + created_at => $DateTime, ], + ], ); -has 'check_open_state' => ( - is => 'ro', - isa => CodeRef, - default => sub { return \&is_open_state; }, -); +has 'check_open_state' => + (is => 'ro', isa => CodeRef, default => sub { return \&is_open_state; },); has 'events' => ( - is => 'lazy', - isa => ArrayRef [ - Dict [ - bug_id => Int, - bug_when => $DateTime, - field_name => Enum [qw(bug_status keywords)], - removed => Str, - added => Str, - ], + is => 'lazy', + isa => ArrayRef [ + Dict [ + bug_id => Int, + bug_when => $DateTime, + field_name => Enum [qw(bug_status keywords)], + removed => Str, + added => Str, ], + ], ); has 'results' => ( - is => 'lazy', - isa => ArrayRef [ - Dict [ - date => $DateTime, - bugs_by_product => HashRef [ - Dict [ - open => ArrayRef [Int], - closed => ArrayRef [Int], - median_age_open => Num - ] - ], - bugs_by_sec_keyword => HashRef [ - Dict [ - open => ArrayRef [Int], - closed => ArrayRef [Int], - median_age_open => Num - ] - ], - ], + is => 'lazy', + isa => ArrayRef [ + Dict [ + date => $DateTime, + bugs_by_product => HashRef [ + Dict [open => ArrayRef [Int], closed => ArrayRef [Int], median_age_open => Num] + ], + bugs_by_sec_keyword => HashRef [ + Dict [open => ArrayRef [Int], closed => ArrayRef [Int], median_age_open => Num] + ], ], + ], ); sub _build_initial_bug_ids { - # TODO: Handle changes in product (e.g. gravyarding) by searching the events table - # for changes to the 'product' field where one of $self->products is found in - # the 'removed' field, add the related bug id to the list of initial bugs. - my ($self) = @_; - my $dbh = Bugzilla->dbh; - my $products = join ', ', map { $dbh->quote($_) } @{ $self->products }; - my $sec_keywords = join ', ', map { $dbh->quote($_) } @{ $self->sec_keywords }; - my $query = qq{ + +# TODO: Handle changes in product (e.g. gravyarding) by searching the events table +# for changes to the 'product' field where one of $self->products is found in +# the 'removed' field, add the related bug id to the list of initial bugs. + my ($self) = @_; + my $dbh = Bugzilla->dbh; + my $products = join ', ', map { $dbh->quote($_) } @{$self->products}; + my $sec_keywords = join ', ', map { $dbh->quote($_) } @{$self->sec_keywords}; + my $query = qq{ SELECT bug_id FROM @@ -128,39 +99,40 @@ sub _build_initial_bug_ids { keyword.name IN ($sec_keywords) AND product.name IN ($products) }; - return Bugzilla->dbh->selectcol_arrayref($query); + return Bugzilla->dbh->selectcol_arrayref($query); } sub _build_initial_bugs { - my ($self) = @_; - my $bugs = {}; - my $bugs_list = Bugzilla::Bug->new_from_list( $self->initial_bug_ids ); - for my $bug (@$bugs_list) { - $bugs->{ $bug->id } = { - id => $bug->id, - product => $bug->product, - sec_level => ( - # Select the first keyword matching one of the target keywords - # (of which there _should_ only be one found anyway). - first { - my $x = $_; - grep { lc($_) eq lc( $x->name ) } @{ $self->sec_keywords } - } - @{ $bug->keyword_objects } - )->name, - is_open => $self->check_open_state->( $bug->status->name ), - created_at => datetime_from( $bug->creation_ts ), - }; - } - return $bugs; + my ($self) = @_; + my $bugs = {}; + my $bugs_list = Bugzilla::Bug->new_from_list($self->initial_bug_ids); + for my $bug (@$bugs_list) { + $bugs->{$bug->id} = { + id => $bug->id, + product => $bug->product, + sec_level => ( + + # Select the first keyword matching one of the target keywords + # (of which there _should_ only be one found anyway). + first { + my $x = $_; + grep { lc($_) eq lc($x->name) } @{$self->sec_keywords} + } + @{$bug->keyword_objects} + )->name, + is_open => $self->check_open_state->($bug->status->name), + created_at => datetime_from($bug->creation_ts), + }; + } + return $bugs; } sub _build_events { - my ($self) = @_; - return [] if !(@{$self->initial_bug_ids}); - my $bug_ids = join ', ', @{ $self->initial_bug_ids }; - my $start_date = $self->start_date->ymd('-'); - my $query = qq{ + my ($self) = @_; + return [] if !(@{$self->initial_bug_ids}); + my $bug_ids = join ', ', @{$self->initial_bug_ids}; + my $start_date = $self->start_date->ymd('-'); + my $query = qq{ SELECT bug_id, bug_when, @@ -177,138 +149,153 @@ sub _build_events { AND bug_when >= '$start_date 00:00:00' GROUP BY bug_id , bug_when , field.name }; - my $result = Bugzilla->dbh->selectall_hashref( $query, 'bug_id' ); - my @events = values %$result; - foreach my $event (@events) { - $event->{bug_when} = datetime_from( $event->{bug_when} ); - } + my $result = Bugzilla->dbh->selectall_hashref($query, 'bug_id'); + my @events = values %$result; + foreach my $event (@events) { + $event->{bug_when} = datetime_from($event->{bug_when}); + } - # We sort by reverse chronological order instead of ORDER BY - # since values %hash doesn't guareentee any order. - @events = sort { $b->{bug_when} cmp $a->{bug_when} } @events; - return \@events; + # We sort by reverse chronological order instead of ORDER BY + # since values %hash doesn't guareentee any order. + @events = sort { $b->{bug_when} cmp $a->{bug_when} } @events; + return \@events; } sub _build_results { - my ($self) = @_; - my $e = 0; - my $bugs = $self->initial_bugs; - my @results = (); - - # We must generate a report for each week in the target time interval, regardless of - # whether anything changed. The for loop here ensures that we do so. - for ( my $report_date = $self->end_date; $report_date >= $self->start_date; $report_date->subtract( weeks => 1 ) ) { - # We rewind events while there are still events existing which occured after the start - # of the report week. The bugs will reflect a snapshot of how they were at the start of the week. - # $self->events is ordered reverse chronologically, so the end of the array is the earliest event. - while ( $e < @{ $self->events } - && ( @{ $self->events }[$e] )->{bug_when} > $report_date ) - { - my $event = @{ $self->events }[$e]; - my $bug = $bugs->{ $event->{bug_id} }; - - # Undo bug status changes - if ( $event->{field_name} eq 'bug_status' ) { - $bug->{is_open} = $self->check_open_state->( $event->{removed} ); - } - - # Undo keyword changes - if ( $event->{field_name} eq 'keywords' ) { - my $bug_sec_level = $bug->{sec_level}; - if ( $event->{added} =~ /\b\Q$bug_sec_level\E\b/ ) { - # If the currently set sec level was added in this event, remove it. - $bug->{sec_level} = undef; - } - if ( $event->{removed} ) { - # If a target sec keyword was removed, add the first one back. - my $removed_sec = first { $event->{removed} =~ /\b\Q$_\E\b/ } @{ $self->sec_keywords }; - $bug->{sec_level} = $removed_sec if ($removed_sec); - } - } - - $e++; + my ($self) = @_; + my $e = 0; + my $bugs = $self->initial_bugs; + my @results = (); + +# We must generate a report for each week in the target time interval, regardless of +# whether anything changed. The for loop here ensures that we do so. + for ( + my $report_date = $self->end_date; + $report_date >= $self->start_date; + $report_date->subtract(weeks => 1) + ) + { +# We rewind events while there are still events existing which occured after the start +# of the report week. The bugs will reflect a snapshot of how they were at the start of the week. +# $self->events is ordered reverse chronologically, so the end of the array is the earliest event. + while ($e < @{$self->events} + && (@{$self->events}[$e])->{bug_when} > $report_date) + { + my $event = @{$self->events}[$e]; + my $bug = $bugs->{$event->{bug_id}}; + + # Undo bug status changes + if ($event->{field_name} eq 'bug_status') { + $bug->{is_open} = $self->check_open_state->($event->{removed}); + } + + # Undo keyword changes + if ($event->{field_name} eq 'keywords') { + my $bug_sec_level = $bug->{sec_level}; + if ($event->{added} =~ /\b\Q$bug_sec_level\E\b/) { + + # If the currently set sec level was added in this event, remove it. + $bug->{sec_level} = undef; } + if ($event->{removed}) { - # Remove uncreated bugs - foreach my $bug_key ( keys %$bugs ) { - if ( $bugs->{$bug_key}->{created_at} > $report_date ) { - delete $bugs->{$bug_key}; - } + # If a target sec keyword was removed, add the first one back. + my $removed_sec + = first { $event->{removed} =~ /\b\Q$_\E\b/ } @{$self->sec_keywords}; + $bug->{sec_level} = $removed_sec if ($removed_sec); } + } - # Report! - my $date_snapshot = $report_date->clone(); - my @bugs_snapshot = values %$bugs; - my $result = { - date => $date_snapshot, - bugs_by_product => $self->_bugs_by_product( $date_snapshot, @bugs_snapshot ), - bugs_by_sec_keyword => $self->_bugs_by_sec_keyword( $date_snapshot, @bugs_snapshot ), - }; - push @results, $result; + $e++; } - return [reverse @results]; + # Remove uncreated bugs + foreach my $bug_key (keys %$bugs) { + if ($bugs->{$bug_key}->{created_at} > $report_date) { + delete $bugs->{$bug_key}; + } + } + + # Report! + my $date_snapshot = $report_date->clone(); + my @bugs_snapshot = values %$bugs; + my $result = { + date => $date_snapshot, + bugs_by_product => $self->_bugs_by_product($date_snapshot, @bugs_snapshot), + bugs_by_sec_keyword => + $self->_bugs_by_sec_keyword($date_snapshot, @bugs_snapshot), + }; + push @results, $result; + } + + return [reverse @results]; } sub _bugs_by_product { - my ( $self, $report_date, @bugs ) = @_; - my $result = {}; - my $groups = {}; - foreach my $product ( @{ $self->products } ) { - $groups->{$product} = []; - } - foreach my $bug (@bugs) { - # We skip over bugs with no sec level which can happen during event rewinding. - if ( $bug->{sec_level} ) { - push @{ $groups->{ $bug->{product} } }, $bug; - } - } - foreach my $product ( @{ $self->products } ) { - my @open = map { $_->{id} } grep { ( $_->{is_open} ) } @{ $groups->{$product} }; - my @closed = map { $_->{id} } grep { !( $_->{is_open} ) } @{ $groups->{$product} }; - my @ages = map { $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400; } - grep { ( $_->{is_open} ) } @{ $groups->{$product} }; - $result->{$product} = { - open => \@open, - closed => \@closed, - median_age_open => @ages ? _median(@ages) : 0, - }; + my ($self, $report_date, @bugs) = @_; + my $result = {}; + my $groups = {}; + foreach my $product (@{$self->products}) { + $groups->{$product} = []; + } + foreach my $bug (@bugs) { + + # We skip over bugs with no sec level which can happen during event rewinding. + if ($bug->{sec_level}) { + push @{$groups->{$bug->{product}}}, $bug; } + } + foreach my $product (@{$self->products}) { + my @open = map { $_->{id} } grep { ($_->{is_open}) } @{$groups->{$product}}; + my @closed = map { $_->{id} } grep { !($_->{is_open}) } @{$groups->{$product}}; + my @ages = map { + $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400; + } grep { ($_->{is_open}) } @{$groups->{$product}}; + $result->{$product} = { + open => \@open, + closed => \@closed, + median_age_open => @ages ? _median(@ages) : 0, + }; + } - return $result; + return $result; } sub _bugs_by_sec_keyword { - my ( $self, $report_date, @bugs ) = @_; - my $result = {}; - my $groups = {}; - foreach my $sec_keyword ( @{ $self->sec_keywords } ) { - $groups->{$sec_keyword} = []; - } - foreach my $bug (@bugs) { - # We skip over bugs with no sec level which can happen during event rewinding. - if ( $bug->{sec_level} ) { - push @{ $groups->{ $bug->{sec_level} } }, $bug; - } - } - foreach my $sec_keyword ( @{ $self->sec_keywords } ) { - my @open = map { $_->{id} } grep { ( $_->{is_open} ) } @{ $groups->{$sec_keyword} }; - my @closed = map { $_->{id} } grep { !( $_->{is_open} ) } @{ $groups->{$sec_keyword} }; - my @ages = map { $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400 } - grep { ( $_->{is_open} ) } @{ $groups->{$sec_keyword} }; - $result->{$sec_keyword} = { - open => \@open, - closed => \@closed, - median_age_open => @ages ? _median(@ages) : 0, - }; + my ($self, $report_date, @bugs) = @_; + my $result = {}; + my $groups = {}; + foreach my $sec_keyword (@{$self->sec_keywords}) { + $groups->{$sec_keyword} = []; + } + foreach my $bug (@bugs) { + + # We skip over bugs with no sec level which can happen during event rewinding. + if ($bug->{sec_level}) { + push @{$groups->{$bug->{sec_level}}}, $bug; } + } + foreach my $sec_keyword (@{$self->sec_keywords}) { + my @open = map { $_->{id} } grep { ($_->{is_open}) } @{$groups->{$sec_keyword}}; + my @closed + = map { $_->{id} } grep { !($_->{is_open}) } @{$groups->{$sec_keyword}}; + my @ages = map { + $_->{created_at}->subtract_datetime_absolute($report_date)->seconds / 86_400 + } grep { ($_->{is_open}) } @{$groups->{$sec_keyword}}; + $result->{$sec_keyword} = { + open => \@open, + closed => \@closed, + median_age_open => @ages ? _median(@ages) : 0, + }; + } - return $result; + return $result; } sub _median { - # From tlm @ https://www.perlmonks.org/?node_id=474564. Jul 14, 2005 - return sum( ( sort { $a <=> $b } @_ )[ int( $#_ / 2 ), ceil( $#_ / 2 ) ] ) / 2; + + # From tlm @ https://www.perlmonks.org/?node_id=474564. Jul 14, 2005 + return sum((sort { $a <=> $b } @_)[int($#_ / 2), ceil($#_ / 2)]) / 2; } 1; diff --git a/Bugzilla/S3.pm b/Bugzilla/S3.pm index 26d77562f..ceb1451fa 100644 --- a/Bugzilla/S3.pm +++ b/Bugzilla/S3.pm @@ -28,7 +28,7 @@ use XML::Simple; use base qw(Class::Accessor::Fast); __PACKAGE__->mk_accessors( - qw(aws_access_key_id aws_secret_access_key secure ua err errstr timeout retry host) + qw(aws_access_key_id aws_secret_access_key secure ua err errstr timeout retry host) ); our $VERSION = '0.45bmo'; @@ -37,148 +37,149 @@ my $METADATA_PREFIX = 'x-amz-meta-'; my $KEEP_ALIVE_CACHESIZE = 10; sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - - die "No aws_access_key_id" unless $self->aws_access_key_id; - die "No aws_secret_access_key" unless $self->aws_secret_access_key; - - $self->secure(1) if not defined $self->secure; - $self->timeout(30) if not defined $self->timeout; - $self->host('s3.amazonaws.com') if not defined $self->host; - - my $ua; - if ($self->retry) { - require LWP::UserAgent::Determined; - $ua = LWP::UserAgent::Determined->new( - keep_alive => $KEEP_ALIVE_CACHESIZE, - requests_redirectable => [qw(GET HEAD DELETE PUT)], - ); - $ua->timing('1,2,4,8,16,32'); - } - else { - $ua = LWP::UserAgent->new( - keep_alive => $KEEP_ALIVE_CACHESIZE, - requests_redirectable => [qw(GET HEAD DELETE PUT)], - ); - } - - $ua->timeout($self->timeout); - if (my $proxy = Bugzilla->params->{proxy_url}) { - $ua->proxy([ 'https', 'http' ], $proxy); - } - $self->ua($ua); - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + + die "No aws_access_key_id" unless $self->aws_access_key_id; + die "No aws_secret_access_key" unless $self->aws_secret_access_key; + + $self->secure(1) if not defined $self->secure; + $self->timeout(30) if not defined $self->timeout; + $self->host('s3.amazonaws.com') if not defined $self->host; + + my $ua; + if ($self->retry) { + require LWP::UserAgent::Determined; + $ua = LWP::UserAgent::Determined->new( + keep_alive => $KEEP_ALIVE_CACHESIZE, + requests_redirectable => [qw(GET HEAD DELETE PUT)], + ); + $ua->timing('1,2,4,8,16,32'); + } + else { + $ua = LWP::UserAgent->new( + keep_alive => $KEEP_ALIVE_CACHESIZE, + requests_redirectable => [qw(GET HEAD DELETE PUT)], + ); + } + + $ua->timeout($self->timeout); + if (my $proxy = Bugzilla->params->{proxy_url}) { + $ua->proxy(['https', 'http'], $proxy); + } + $self->ua($ua); + return $self; } sub bucket { - my ($self, $bucketname) = @_; - return Bugzilla::S3::Bucket->new({bucket => $bucketname, account => $self}); + my ($self, $bucketname) = @_; + return Bugzilla::S3::Bucket->new({bucket => $bucketname, account => $self}); } sub _validate_acl_short { - my ($self, $policy_name) = @_; - - if (!grep({$policy_name eq $_} - qw(private public-read public-read-write authenticated-read))) - { - croak "$policy_name is not a supported canned access policy"; - } + my ($self, $policy_name) = @_; + + if ( + !grep({ $policy_name eq $_ } + qw(private public-read public-read-write authenticated-read))) + { + croak "$policy_name is not a supported canned access policy"; + } } # EU buckets must be accessed via their DNS name. This routine figures out if # a given bucket name can be safely used as a DNS name. sub _is_dns_bucket { - my $bucketname = $_[0]; + my $bucketname = $_[0]; - if (length $bucketname > 63) { - return 0; - } - if (length $bucketname < 3) { - return; - } - return 0 unless $bucketname =~ m{^[a-z0-9][a-z0-9.-]+$}; - my @components = split /\./, $bucketname; - for my $c (@components) { - return 0 if $c =~ m{^-}; - return 0 if $c =~ m{-$}; - return 0 if $c eq ''; - } - return 1; + if (length $bucketname > 63) { + return 0; + } + if (length $bucketname < 3) { + return; + } + return 0 unless $bucketname =~ m{^[a-z0-9][a-z0-9.-]+$}; + my @components = split /\./, $bucketname; + for my $c (@components) { + return 0 if $c =~ m{^-}; + return 0 if $c =~ m{-$}; + return 0 if $c eq ''; + } + return 1; } # make the HTTP::Request object sub _make_request { - my ($self, $method, $path, $headers, $data, $metadata) = @_; - croak 'must specify method' unless $method; - croak 'must specify path' unless defined $path; - $headers ||= {}; - $data = '' if not defined $data; - $metadata ||= {}; - my $http_headers = $self->_merge_meta($headers, $metadata); - - $self->_add_auth_header($http_headers, $method, $path) - unless exists $headers->{Authorization}; - my $protocol = $self->secure ? 'https' : 'http'; - my $host = $self->host; - my $url = "$protocol://$host/$path"; - if ($path =~ m{^([^/?]+)(.*)} && _is_dns_bucket($1)) { - $url = "$protocol://$1.$host$2"; - } - - my $request = HTTP::Request->new($method, $url, $http_headers); - $request->content($data); - - # my $req_as = $request->as_string; - # $req_as =~ s/[^\n\r\x20-\x7f]/?/g; - # $req_as = substr( $req_as, 0, 1024 ) . "\n\n"; - # warn $req_as; - - return $request; + my ($self, $method, $path, $headers, $data, $metadata) = @_; + croak 'must specify method' unless $method; + croak 'must specify path' unless defined $path; + $headers ||= {}; + $data = '' if not defined $data; + $metadata ||= {}; + my $http_headers = $self->_merge_meta($headers, $metadata); + + $self->_add_auth_header($http_headers, $method, $path) + unless exists $headers->{Authorization}; + my $protocol = $self->secure ? 'https' : 'http'; + my $host = $self->host; + my $url = "$protocol://$host/$path"; + if ($path =~ m{^([^/?]+)(.*)} && _is_dns_bucket($1)) { + $url = "$protocol://$1.$host$2"; + } + + my $request = HTTP::Request->new($method, $url, $http_headers); + $request->content($data); + + # my $req_as = $request->as_string; + # $req_as =~ s/[^\n\r\x20-\x7f]/?/g; + # $req_as = substr( $req_as, 0, 1024 ) . "\n\n"; + # warn $req_as; + + return $request; } # $self->_send_request($HTTP::Request) # $self->_send_request(@params_to_make_request) sub _send_request { - my $self = shift; - my $request; - if (@_ == 1) { - $request = shift; - } - else { - $request = $self->_make_request(@_); - } + my $self = shift; + my $request; + if (@_ == 1) { + $request = shift; + } + else { + $request = $self->_make_request(@_); + } - my $response = $self->_do_http($request); - my $content = $response->content; + my $response = $self->_do_http($request); + my $content = $response->content; - return $content unless $response->content_type eq 'application/xml'; - return unless $content; - return $self->_xpc_of_content($content); + return $content unless $response->content_type eq 'application/xml'; + return unless $content; + return $self->_xpc_of_content($content); } # centralize all HTTP work, for debugging sub _do_http { - my ($self, $request, $filename) = @_; + my ($self, $request, $filename) = @_; - # convenient time to reset any error conditions - $self->err(undef); - $self->errstr(undef); - return $self->ua->request($request, $filename); + # convenient time to reset any error conditions + $self->err(undef); + $self->errstr(undef); + return $self->ua->request($request, $filename); } sub _send_request_expect_nothing { - my $self = shift; - my $request = $self->_make_request(@_); + my $self = shift; + my $request = $self->_make_request(@_); - my $response = $self->_do_http($request); - my $content = $response->content; + my $response = $self->_do_http($request); + my $content = $response->content; - return 1 if $response->code =~ /^2\d\d$/; + return 1 if $response->code =~ /^2\d\d$/; - # anything else is a failure, and we save the parsed result - $self->_remember_errors($response->content); - return 0; + # anything else is a failure, and we save the parsed result + $self->_remember_errors($response->content); + return 0; } # Send a HEAD request first, to find out if we'll be hit with a 307 redirect. @@ -189,185 +190,187 @@ sub _send_request_expect_nothing { # first time we used it. Thus, we need to probe first to find out what's going on, # before we start sending any actual data. sub _send_request_expect_nothing_probed { - my $self = shift; - my ($method, $path, $conf, $value) = @_; - my $request = $self->_make_request('HEAD', $path); - my $override_uri = undef; + my $self = shift; + my ($method, $path, $conf, $value) = @_; + my $request = $self->_make_request('HEAD', $path); + my $override_uri = undef; - my $old_redirectable = $self->ua->requests_redirectable; - $self->ua->requests_redirectable([]); + my $old_redirectable = $self->ua->requests_redirectable; + $self->ua->requests_redirectable([]); - my $response = $self->_do_http($request); + my $response = $self->_do_http($request); - if ($response->code =~ /^3/ && defined $response->header('Location')) { - $override_uri = $response->header('Location'); - } - $request = $self->_make_request(@_); - $request->uri($override_uri) if defined $override_uri; + if ($response->code =~ /^3/ && defined $response->header('Location')) { + $override_uri = $response->header('Location'); + } + $request = $self->_make_request(@_); + $request->uri($override_uri) if defined $override_uri; - $response = $self->_do_http($request); - $self->ua->requests_redirectable($old_redirectable); + $response = $self->_do_http($request); + $self->ua->requests_redirectable($old_redirectable); - my $content = $response->content; + my $content = $response->content; - return 1 if $response->code =~ /^2\d\d$/; + return 1 if $response->code =~ /^2\d\d$/; - # anything else is a failure, and we save the parsed result - $self->_remember_errors($response->content); - return 0; + # anything else is a failure, and we save the parsed result + $self->_remember_errors($response->content); + return 0; } sub _check_response { - my ($self, $response) = @_; - return 1 if $response->code =~ /^2\d\d$/; - $self->err("network_error"); - $self->errstr($response->status_line); - $self->_remember_errors($response->content); - return undef; + my ($self, $response) = @_; + return 1 if $response->code =~ /^2\d\d$/; + $self->err("network_error"); + $self->errstr($response->status_line); + $self->_remember_errors($response->content); + return undef; } sub _croak_if_response_error { - my ($self, $response) = @_; - unless ($response->code =~ /^2\d\d$/) { - $self->err("network_error"); - $self->errstr($response->status_line); - croak "Bugzilla::S3: Amazon responded with " - . $response->status_line . "\n"; - } + my ($self, $response) = @_; + unless ($response->code =~ /^2\d\d$/) { + $self->err("network_error"); + $self->errstr($response->status_line); + croak "Bugzilla::S3: Amazon responded with " . $response->status_line . "\n"; + } } sub _xpc_of_content { - return XMLin($_[1], 'KeepRoot' => 1, 'SuppressEmpty' => '', 'ForceArray' => ['Contents']); + return XMLin( + $_[1], + 'KeepRoot' => 1, + 'SuppressEmpty' => '', + 'ForceArray' => ['Contents'] + ); } # returns 1 if errors were found sub _remember_errors { - my ($self, $src) = @_; + my ($self, $src) = @_; - unless (ref $src || $src =~ m/^[[:space:]]*err($code); - $self->errstr($src); - return 1; - } + unless (ref $src || $src =~ m/^[[:space:]]*err($code); + $self->errstr($src); + return 1; + } - my $r = ref $src ? $src : $self->_xpc_of_content($src); + my $r = ref $src ? $src : $self->_xpc_of_content($src); - if ($r->{Error}) { - $self->err($r->{Error}{Code}); - $self->errstr($r->{Error}{Message}); - return 1; - } - return 0; + if ($r->{Error}) { + $self->err($r->{Error}{Code}); + $self->errstr($r->{Error}{Message}); + return 1; + } + return 0; } sub _add_auth_header { - my ($self, $headers, $method, $path) = @_; - my $aws_access_key_id = $self->aws_access_key_id; - my $aws_secret_access_key = $self->aws_secret_access_key; - - if (not $headers->header('Date')) { - $headers->header(Date => time2str(time)); - } - my $canonical_string = $self->_canonical_string($method, $path, $headers); - my $encoded_canonical = - $self->_encode($aws_secret_access_key, $canonical_string); - $headers->header( - Authorization => "AWS $aws_access_key_id:$encoded_canonical"); + my ($self, $headers, $method, $path) = @_; + my $aws_access_key_id = $self->aws_access_key_id; + my $aws_secret_access_key = $self->aws_secret_access_key; + + if (not $headers->header('Date')) { + $headers->header(Date => time2str(time)); + } + my $canonical_string = $self->_canonical_string($method, $path, $headers); + my $encoded_canonical + = $self->_encode($aws_secret_access_key, $canonical_string); + $headers->header(Authorization => "AWS $aws_access_key_id:$encoded_canonical"); } # generates an HTTP::Headers objects given one hash that represents http # headers to set and another hash that represents an object's metadata. sub _merge_meta { - my ($self, $headers, $metadata) = @_; - $headers ||= {}; - $metadata ||= {}; - - my $http_header = HTTP::Headers->new; - while (my ($k, $v) = each %$headers) { - $http_header->header($k => $v); - } - while (my ($k, $v) = each %$metadata) { - $http_header->header("$METADATA_PREFIX$k" => $v); - } - - return $http_header; + my ($self, $headers, $metadata) = @_; + $headers ||= {}; + $metadata ||= {}; + + my $http_header = HTTP::Headers->new; + while (my ($k, $v) = each %$headers) { + $http_header->header($k => $v); + } + while (my ($k, $v) = each %$metadata) { + $http_header->header("$METADATA_PREFIX$k" => $v); + } + + return $http_header; } # generate a canonical string for the given parameters. expires is optional and is # only used by query string authentication. sub _canonical_string { - my ($self, $method, $path, $headers, $expires) = @_; - my %interesting_headers = (); - while (my ($key, $value) = each %$headers) { - my $lk = lc $key; - if ( $lk eq 'content-md5' - or $lk eq 'content-type' - or $lk eq 'date' - or $lk =~ /^$AMAZON_HEADER_PREFIX/) - { - $interesting_headers{$lk} = trim($value); - } + my ($self, $method, $path, $headers, $expires) = @_; + my %interesting_headers = (); + while (my ($key, $value) = each %$headers) { + my $lk = lc $key; + if ( $lk eq 'content-md5' + or $lk eq 'content-type' + or $lk eq 'date' + or $lk =~ /^$AMAZON_HEADER_PREFIX/) + { + $interesting_headers{$lk} = trim($value); } + } - # these keys get empty strings if they don't exist - $interesting_headers{'content-type'} ||= ''; - $interesting_headers{'content-md5'} ||= ''; - - # just in case someone used this. it's not necessary in this lib. - $interesting_headers{'date'} = '' - if $interesting_headers{'x-amz-date'}; - - # if you're using expires for query string auth, then it trumps date - # (and x-amz-date) - $interesting_headers{'date'} = $expires if $expires; - - my $buf = "$method\n"; - foreach my $key (sort keys %interesting_headers) { - if ($key =~ /^$AMAZON_HEADER_PREFIX/) { - $buf .= "$key:$interesting_headers{$key}\n"; - } - else { - $buf .= "$interesting_headers{$key}\n"; - } - } + # these keys get empty strings if they don't exist + $interesting_headers{'content-type'} ||= ''; + $interesting_headers{'content-md5'} ||= ''; - # don't include anything after the first ? in the resource... - $path =~ /^([^?]*)/; - $buf .= "/$1"; + # just in case someone used this. it's not necessary in this lib. + $interesting_headers{'date'} = '' if $interesting_headers{'x-amz-date'}; - # ...unless there is an acl or torrent parameter - if ($path =~ /[&?]acl($|=|&)/) { - $buf .= '?acl'; - } - elsif ($path =~ /[&?]torrent($|=|&)/) { - $buf .= '?torrent'; + # if you're using expires for query string auth, then it trumps date + # (and x-amz-date) + $interesting_headers{'date'} = $expires if $expires; + + my $buf = "$method\n"; + foreach my $key (sort keys %interesting_headers) { + if ($key =~ /^$AMAZON_HEADER_PREFIX/) { + $buf .= "$key:$interesting_headers{$key}\n"; } - elsif ($path =~ /[&?]location($|=|&)/) { - $buf .= '?location'; + else { + $buf .= "$interesting_headers{$key}\n"; } - - return $buf; + } + + # don't include anything after the first ? in the resource... + $path =~ /^([^?]*)/; + $buf .= "/$1"; + + # ...unless there is an acl or torrent parameter + if ($path =~ /[&?]acl($|=|&)/) { + $buf .= '?acl'; + } + elsif ($path =~ /[&?]torrent($|=|&)/) { + $buf .= '?torrent'; + } + elsif ($path =~ /[&?]location($|=|&)/) { + $buf .= '?location'; + } + + return $buf; } # finds the hmac-sha1 hash of the canonical string and the aws secret access key and then # base64 encodes the result (optionally urlencoding after that). sub _encode { - my ($self, $aws_secret_access_key, $str, $urlencode) = @_; - my $hmac = Digest::HMAC_SHA1->new($aws_secret_access_key); - $hmac->add($str); - my $b64 = encode_base64($hmac->digest, ''); - if ($urlencode) { - return $self->_urlencode($b64); - } - else { - return $b64; - } + my ($self, $aws_secret_access_key, $str, $urlencode) = @_; + my $hmac = Digest::HMAC_SHA1->new($aws_secret_access_key); + $hmac->add($str); + my $b64 = encode_base64($hmac->digest, ''); + if ($urlencode) { + return $self->_urlencode($b64); + } + else { + return $b64; + } } sub _urlencode { - my ($self, $unencoded) = @_; - return uri_escape_utf8($unencoded, '^A-Za-z0-9_-'); + my ($self, $unencoded) = @_; + return uri_escape_utf8($unencoded, '^A-Za-z0-9_-'); } 1; diff --git a/Bugzilla/S3/Bucket.pm b/Bugzilla/S3/Bucket.pm index a53ab5c51..8e6731ce5 100644 --- a/Bugzilla/S3/Bucket.pm +++ b/Bugzilla/S3/Bucket.pm @@ -14,170 +14,166 @@ use base qw(Class::Accessor::Fast); __PACKAGE__->mk_accessors(qw(bucket creation_date account)); sub new { - my $class = shift; - my $self = $class->SUPER::new(@_); - croak "no bucket" unless $self->bucket; - croak "no account" unless $self->account; - return $self; + my $class = shift; + my $self = $class->SUPER::new(@_); + croak "no bucket" unless $self->bucket; + croak "no account" unless $self->account; + return $self; } sub _uri { - my ($self, $key) = @_; - return ($key) - ? $self->bucket . "/" . $self->account->_urlencode($key) - : $self->bucket . "/"; + my ($self, $key) = @_; + return ($key) + ? $self->bucket . "/" . $self->account->_urlencode($key) + : $self->bucket . "/"; } # returns bool sub add_key { - my ($self, $key, $value, $conf) = @_; - croak 'must specify key' unless $key && length $key; - - if ($conf->{acl_short}) { - $self->account->_validate_acl_short($conf->{acl_short}); - $conf->{'x-amz-acl'} = $conf->{acl_short}; - delete $conf->{acl_short}; - } - - if (ref($value) eq 'SCALAR') { - $conf->{'Content-Length'} ||= -s $$value; - $value = _content_sub($$value); - } - else { - $conf->{'Content-Length'} ||= length $value; - } - - # If we're pushing to a bucket that's under DNS flux, we might get a 307 - # Since LWP doesn't support actually waiting for a 100 Continue response, - # we'll just send a HEAD first to see what's going on - - if (ref($value)) { - return $self->account->_send_request_expect_nothing_probed('PUT', - $self->_uri($key), $conf, $value); - } - else { - return $self->account->_send_request_expect_nothing('PUT', - $self->_uri($key), $conf, $value); - } + my ($self, $key, $value, $conf) = @_; + croak 'must specify key' unless $key && length $key; + + if ($conf->{acl_short}) { + $self->account->_validate_acl_short($conf->{acl_short}); + $conf->{'x-amz-acl'} = $conf->{acl_short}; + delete $conf->{acl_short}; + } + + if (ref($value) eq 'SCALAR') { + $conf->{'Content-Length'} ||= -s $$value; + $value = _content_sub($$value); + } + else { + $conf->{'Content-Length'} ||= length $value; + } + + # If we're pushing to a bucket that's under DNS flux, we might get a 307 + # Since LWP doesn't support actually waiting for a 100 Continue response, + # we'll just send a HEAD first to see what's going on + + if (ref($value)) { + return $self->account->_send_request_expect_nothing_probed('PUT', + $self->_uri($key), $conf, $value); + } + else { + return $self->account->_send_request_expect_nothing('PUT', $self->_uri($key), + $conf, $value); + } } sub add_key_filename { - my ($self, $key, $value, $conf) = @_; - return $self->add_key($key, \$value, $conf); + my ($self, $key, $value, $conf) = @_; + return $self->add_key($key, \$value, $conf); } sub head_key { - my ($self, $key) = @_; - return $self->get_key($key, "HEAD"); + my ($self, $key) = @_; + return $self->get_key($key, "HEAD"); } sub get_key { - my ($self, $key, $method, $filename) = @_; - $method ||= "GET"; - $filename = $$filename if ref $filename; - my $acct = $self->account; - - my $request = $acct->_make_request($method, $self->_uri($key), {}); - my $response = $acct->_do_http($request, $filename); - - if ($response->code == 404) { - $acct->err(404); - $acct->errstr('The requested key was not found'); - return undef; - } - - return undef unless $acct->_check_response($response); - - my $etag = $response->header('ETag'); - if ($etag) { - $etag =~ s/^"//; - $etag =~ s/"$//; - } - - my $return = { - content_length => $response->content_length || 0, - content_type => $response->content_type, - etag => $etag, - value => $response->content, - }; - - foreach my $header ($response->headers->header_field_names) { - next unless $header =~ /x-amz-meta-/i; - $return->{lc $header} = $response->header($header); - } - - return $return; + my ($self, $key, $method, $filename) = @_; + $method ||= "GET"; + $filename = $$filename if ref $filename; + my $acct = $self->account; + + my $request = $acct->_make_request($method, $self->_uri($key), {}); + my $response = $acct->_do_http($request, $filename); + + if ($response->code == 404) { + $acct->err(404); + $acct->errstr('The requested key was not found'); + return undef; + } + + return undef unless $acct->_check_response($response); + + my $etag = $response->header('ETag'); + if ($etag) { + $etag =~ s/^"//; + $etag =~ s/"$//; + } + + my $return = { + content_length => $response->content_length || 0, + content_type => $response->content_type, + etag => $etag, + value => $response->content, + }; + + foreach my $header ($response->headers->header_field_names) { + next unless $header =~ /x-amz-meta-/i; + $return->{lc $header} = $response->header($header); + } + + return $return; } sub get_key_filename { - my ($self, $key, $method, $filename) = @_; - $filename = $key unless defined $filename; - return $self->get_key($key, $method, \$filename); + my ($self, $key, $method, $filename) = @_; + $filename = $key unless defined $filename; + return $self->get_key($key, $method, \$filename); } # returns bool sub delete_key { - my ($self, $key) = @_; - croak 'must specify key' unless $key && length $key; - return $self->account->_send_request_expect_nothing('DELETE', - $self->_uri($key), {}); + my ($self, $key) = @_; + croak 'must specify key' unless $key && length $key; + return $self->account->_send_request_expect_nothing('DELETE', + $self->_uri($key), {}); } sub get_acl { - my ($self, $key) = @_; - my $acct = $self->account; + my ($self, $key) = @_; + my $acct = $self->account; - my $request = $acct->_make_request('GET', $self->_uri($key) . '?acl', {}); - my $response = $acct->_do_http($request); + my $request = $acct->_make_request('GET', $self->_uri($key) . '?acl', {}); + my $response = $acct->_do_http($request); - if ($response->code == 404) { - return undef; - } + if ($response->code == 404) { + return undef; + } - return undef unless $acct->_check_response($response); + return undef unless $acct->_check_response($response); - return $response->content; + return $response->content; } sub set_acl { - my ($self, $conf) = @_; - $conf ||= {}; + my ($self, $conf) = @_; + $conf ||= {}; - unless ($conf->{acl_xml} || $conf->{acl_short}) { - croak "need either acl_xml or acl_short"; - } + unless ($conf->{acl_xml} || $conf->{acl_short}) { + croak "need either acl_xml or acl_short"; + } - if ($conf->{acl_xml} && $conf->{acl_short}) { - croak "cannot provide both acl_xml and acl_short"; - } + if ($conf->{acl_xml} && $conf->{acl_short}) { + croak "cannot provide both acl_xml and acl_short"; + } - my $path = $self->_uri($conf->{key}) . '?acl'; + my $path = $self->_uri($conf->{key}) . '?acl'; - my $hash_ref = - ($conf->{acl_short}) - ? {'x-amz-acl' => $conf->{acl_short}} - : {}; + my $hash_ref = ($conf->{acl_short}) ? {'x-amz-acl' => $conf->{acl_short}} : {}; - my $xml = $conf->{acl_xml} || ''; + my $xml = $conf->{acl_xml} || ''; - return $self->account->_send_request_expect_nothing('PUT', $path, - $hash_ref, $xml); + return $self->account->_send_request_expect_nothing('PUT', $path, $hash_ref, + $xml); } sub get_location_constraint { - my ($self) = @_; + my ($self) = @_; - my $xpc = - $self->account->_send_request('GET', $self->bucket . '/?location'); - return undef unless $xpc && !$self->account->_remember_errors($xpc); + my $xpc = $self->account->_send_request('GET', $self->bucket . '/?location'); + return undef unless $xpc && !$self->account->_remember_errors($xpc); - my $lc = $xpc->{content}; - if (defined $lc && $lc eq '') { - $lc = undef; - } - return $lc; + my $lc = $xpc->{content}; + if (defined $lc && $lc eq '') { + $lc = undef; + } + return $lc; } # proxy up the err requests @@ -187,42 +183,37 @@ sub err { $_[0]->account->err } sub errstr { $_[0]->account->errstr } sub _content_sub { - my $filename = shift; - my $stat = stat($filename); - my $remaining = $stat->size; - my $blksize = $stat->blksize || 4096; - - croak "$filename not a readable file with fixed size" - unless -r $filename - and $remaining; - - my $fh = IO::File->new($filename, 'r') - or croak "Could not open $filename: $!"; - $fh->binmode; - - return sub { - my $buffer; - - # upon retries the file is closed and we must reopen it - unless ($fh->opened) { - $fh = IO::File->new($filename, 'r') - or croak "Could not open $filename: $!"; - $fh->binmode; - $remaining = $stat->size; - } - - unless (my $read = $fh->read($buffer, $blksize)) { - croak - "Error while reading upload content $filename ($remaining remaining) $!" - if $! and $remaining; - $fh->close # otherwise, we found EOF - or croak "close of upload content $filename failed: $!"; - $buffer - ||= ''; # LWP expects an empty string on finish, read returns 0 - } - $remaining -= length($buffer); - return $buffer; - }; + my $filename = shift; + my $stat = stat($filename); + my $remaining = $stat->size; + my $blksize = $stat->blksize || 4096; + + croak "$filename not a readable file with fixed size" + unless -r $filename and $remaining; + + my $fh = IO::File->new($filename, 'r') or croak "Could not open $filename: $!"; + $fh->binmode; + + return sub { + my $buffer; + + # upon retries the file is closed and we must reopen it + unless ($fh->opened) { + $fh = IO::File->new($filename, 'r') or croak "Could not open $filename: $!"; + $fh->binmode; + $remaining = $stat->size; + } + + unless (my $read = $fh->read($buffer, $blksize)) { + croak "Error while reading upload content $filename ($remaining remaining) $!" + if $! and $remaining; + $fh->close # otherwise, we found EOF + or croak "close of upload content $filename failed: $!"; + $buffer ||= ''; # LWP expects an empty string on finish, read returns 0 + } + $remaining -= length($buffer); + return $buffer; + }; } 1; diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm index e15c60f7f..34e103a33 100644 --- a/Bugzilla/Search.pm +++ b/Bugzilla/Search.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Exporter); @Bugzilla::Search::EXPORT = qw( - IsValidQueryType - split_order_term + IsValidQueryType + split_order_term ); use Bugzilla::Error; @@ -109,9 +109,7 @@ use Time::HiRes qw(gettimeofday tv_interval); ############# # BMO - product aliases for searching -use constant PRODUCT_ALIASES => { - 'Boot2Gecko' => 'Firefox OS', -}; +use constant PRODUCT_ALIASES => {'Boot2Gecko' => 'Firefox OS',}; # When doing searches, NULL datetimes are treated as this date. use constant EMPTY_DATETIME => '1970-01-01 00:00:00'; @@ -140,320 +138,281 @@ use constant NUMBER_REGEX => qr/ # If you specify a search type in the boolean charts, this describes # which operator maps to which internal function here. use constant OPERATORS => { - equals => \&_simple_operator, - notequals => \&_simple_operator, - casesubstring => \&_casesubstring, - substring => \&_substring, - substr => \&_substring, - notsubstring => \&_notsubstring, - regexp => \&_regexp, - notregexp => \&_notregexp, - lessthan => \&_simple_operator, - lessthaneq => \&_simple_operator, - matches => sub { ThrowUserError("search_content_without_matches"); }, - notmatches => sub { ThrowUserError("search_content_without_matches"); }, - greaterthan => \&_simple_operator, - greaterthaneq => \&_simple_operator, - anyexact => \&_anyexact, - anywordssubstr => \&_anywordsubstr, - allwordssubstr => \&_allwordssubstr, - nowordssubstr => \&_nowordssubstr, - anywords => \&_anywords, - allwords => \&_allwords, - nowords => \&_nowords, - changedbefore => \&_changedbefore_changedafter, - changedafter => \&_changedbefore_changedafter, - changedfrom => \&_changedfrom_changedto, - changedto => \&_changedfrom_changedto, - changedby => \&_changedby, - isempty => \&_isempty, - isnotempty => \&_isnotempty, + equals => \&_simple_operator, + notequals => \&_simple_operator, + casesubstring => \&_casesubstring, + substring => \&_substring, + substr => \&_substring, + notsubstring => \&_notsubstring, + regexp => \&_regexp, + notregexp => \&_notregexp, + lessthan => \&_simple_operator, + lessthaneq => \&_simple_operator, + matches => sub { ThrowUserError("search_content_without_matches"); }, + notmatches => sub { ThrowUserError("search_content_without_matches"); }, + greaterthan => \&_simple_operator, + greaterthaneq => \&_simple_operator, + anyexact => \&_anyexact, + anywordssubstr => \&_anywordsubstr, + allwordssubstr => \&_allwordssubstr, + nowordssubstr => \&_nowordssubstr, + anywords => \&_anywords, + allwords => \&_allwords, + nowords => \&_nowords, + changedbefore => \&_changedbefore_changedafter, + changedafter => \&_changedbefore_changedafter, + changedfrom => \&_changedfrom_changedto, + changedto => \&_changedfrom_changedto, + changedby => \&_changedby, + isempty => \&_isempty, + isnotempty => \&_isnotempty, }; # Some operators are really just standard SQL operators, and are # all implemented by the _simple_operator function, which uses this # constant. use constant SIMPLE_OPERATORS => { - equals => '=', - notequals => '!=', - greaterthan => '>', - greaterthaneq => '>=', - lessthan => '<', - lessthaneq => "<=", + equals => '=', + notequals => '!=', + greaterthan => '>', + greaterthaneq => '>=', + lessthan => '<', + lessthaneq => "<=", }; # Most operators just reverse by removing or adding "not" from/to them. # However, some operators reverse in a different way, so those are listed # here. use constant OPERATOR_REVERSE => { - nowords => 'anywords', - nowordssubstr => 'anywordssubstr', - anywords => 'nowords', - anywordssubstr => 'nowordssubstr', - lessthan => 'greaterthaneq', - lessthaneq => 'greaterthan', - greaterthan => 'lessthaneq', - greaterthaneq => 'lessthan', - isempty => 'isnotempty', - isnotempty => 'isempty', - # The following don't currently have reversals: - # casesubstring, anyexact, allwords, allwordssubstr + nowords => 'anywords', + nowordssubstr => 'anywordssubstr', + anywords => 'nowords', + anywordssubstr => 'nowordssubstr', + lessthan => 'greaterthaneq', + lessthaneq => 'greaterthan', + greaterthan => 'lessthaneq', + greaterthaneq => 'lessthan', + isempty => 'isnotempty', + isnotempty => 'isempty', + + # The following don't currently have reversals: + # casesubstring, anyexact, allwords, allwordssubstr }; # For these operators, even if a field is numeric (is_numeric returns true), # we won't treat the input like a number. use constant NON_NUMERIC_OPERATORS => qw( - changedafter - changedbefore - changedfrom - changedto - regexp - notregexp + changedafter + changedbefore + changedfrom + changedto + regexp + notregexp ); # These operators ignore the entered value use constant NO_VALUE_OPERATORS => qw( - isempty - isnotempty + isempty + isnotempty ); use constant MULTI_SELECT_OVERRIDE => { - notequals => \&_multiselect_negative, - notregexp => \&_multiselect_negative, - notsubstring => \&_multiselect_negative, - nowords => \&_multiselect_negative, - nowordssubstr => \&_multiselect_negative, - - allwords => \&_multiselect_multiple, - allwordssubstr => \&_multiselect_multiple, - anyexact => \&_multiselect_multiple, - anywords => \&_multiselect_multiple, - anywordssubstr => \&_multiselect_multiple, - - _non_changed => \&_multiselect_nonchanged, + notequals => \&_multiselect_negative, + notregexp => \&_multiselect_negative, + notsubstring => \&_multiselect_negative, + nowords => \&_multiselect_negative, + nowordssubstr => \&_multiselect_negative, + + allwords => \&_multiselect_multiple, + allwordssubstr => \&_multiselect_multiple, + anyexact => \&_multiselect_multiple, + anywords => \&_multiselect_multiple, + anywordssubstr => \&_multiselect_multiple, + + _non_changed => \&_multiselect_nonchanged, }; use constant OPERATOR_FIELD_OVERRIDE => { - # User fields - 'attachments.submitter' => { - _non_changed => \&_user_nonchanged, - }, - assigned_to => { - _non_changed => \&_user_nonchanged, - }, - cc => { - _non_changed => \&_user_nonchanged, - }, - commenter => { - _non_changed => \&_user_nonchanged, - }, - reporter => { - _non_changed => \&_user_nonchanged, - }, - 'requestees.login_name' => { - _non_changed => \&_user_nonchanged, - }, - 'setters.login_name' => { - _non_changed => \&_user_nonchanged, - }, - qa_contact => { - _non_changed => \&_user_nonchanged, - }, - - # General Bug Fields - alias => { _non_changed => \&_nullable }, - # We check all attachment fields against this. - attachments => MULTI_SELECT_OVERRIDE, - assignee_last_login => { - _default => \&_assignee_last_login, - }, - blocked => MULTI_SELECT_OVERRIDE, - bug_file_loc => { _non_changed => \&_nullable }, - bug_group => MULTI_SELECT_OVERRIDE, - classification => { - _non_changed => \&_classification_nonchanged, - }, - component => { - _non_changed => \&_component_nonchanged, - }, - content => { - matches => \&_content_matches, - notmatches => \&_content_matches, - _default => sub { ThrowUserError("search_content_without_matches"); }, - }, - days_elapsed => { - _default => \&_days_elapsed, - }, - dependson => MULTI_SELECT_OVERRIDE, - keywords => MULTI_SELECT_OVERRIDE, - 'flagtypes.name' => { - _non_changed => \&_flagtypes_nonchanged, - }, - longdesc => { - changedby => \&_long_desc_changedby, - changedbefore => \&_long_desc_changedbefore_after, - changedafter => \&_long_desc_changedbefore_after, - _non_changed => \&_long_desc_nonchanged, - }, - 'longdescs.count' => { - changedby => \&_long_desc_changedby, - changedbefore => \&_long_desc_changedbefore_after, - changedafter => \&_long_desc_changedbefore_after, - changedfrom => \&_invalid_combination, - changedto => \&_invalid_combination, - _default => \&_long_descs_count, - }, - 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE, - owner_idle_time => { - greaterthan => \&_owner_idle_time_greater_less, - greaterthaneq => \&_owner_idle_time_greater_less, - lessthan => \&_owner_idle_time_greater_less, - lessthaneq => \&_owner_idle_time_greater_less, - _default => \&_invalid_combination, - }, - product => { - _non_changed => \&_product_nonchanged, - }, - tag => MULTI_SELECT_OVERRIDE, - comment_tag => MULTI_SELECT_OVERRIDE, - # Timetracking Fields - deadline => { _non_changed => \&_deadline }, - percentage_complete => { - _non_changed => \&_percentage_complete, - }, - work_time => { - changedby => \&_work_time_changedby, - changedbefore => \&_work_time_changedbefore_after, - changedafter => \&_work_time_changedbefore_after, - _default => \&_work_time, - }, - last_visit_ts => { - _non_changed => \&_last_visit_ts, - _default => \&_invalid_operator, - }, - bug_interest_ts => { - _non_changed => \&_bug_interest_ts, - _default => \&_invalid_operator, - }, - triage_owner => { - _non_changed => \&_triage_owner_nonchanged, - }, - # Custom Fields - FIELD_TYPE_FREETEXT, { _non_changed => \&_nullable }, - FIELD_TYPE_BUG_ID, { _non_changed => \&_nullable_int }, - FIELD_TYPE_DATETIME, { _non_changed => \&_nullable_datetime }, - FIELD_TYPE_DATE, { _non_changed => \&_nullable_date }, - FIELD_TYPE_TEXTAREA, { _non_changed => \&_nullable }, - FIELD_TYPE_MULTI_SELECT, MULTI_SELECT_OVERRIDE, - FIELD_TYPE_BUG_URLS, MULTI_SELECT_OVERRIDE, + # User fields + 'attachments.submitter' => {_non_changed => \&_user_nonchanged,}, + assigned_to => {_non_changed => \&_user_nonchanged,}, + cc => {_non_changed => \&_user_nonchanged,}, + commenter => {_non_changed => \&_user_nonchanged,}, + reporter => {_non_changed => \&_user_nonchanged,}, + 'requestees.login_name' => {_non_changed => \&_user_nonchanged,}, + 'setters.login_name' => {_non_changed => \&_user_nonchanged,}, + qa_contact => {_non_changed => \&_user_nonchanged,}, + + # General Bug Fields + alias => {_non_changed => \&_nullable}, + + # We check all attachment fields against this. + attachments => MULTI_SELECT_OVERRIDE, + assignee_last_login => {_default => \&_assignee_last_login,}, + blocked => MULTI_SELECT_OVERRIDE, + bug_file_loc => {_non_changed => \&_nullable}, + bug_group => MULTI_SELECT_OVERRIDE, + classification => {_non_changed => \&_classification_nonchanged,}, + component => {_non_changed => \&_component_nonchanged,}, + content => { + matches => \&_content_matches, + notmatches => \&_content_matches, + _default => sub { ThrowUserError("search_content_without_matches"); }, + }, + days_elapsed => {_default => \&_days_elapsed,}, + dependson => MULTI_SELECT_OVERRIDE, + keywords => MULTI_SELECT_OVERRIDE, + 'flagtypes.name' => {_non_changed => \&_flagtypes_nonchanged,}, + longdesc => { + changedby => \&_long_desc_changedby, + changedbefore => \&_long_desc_changedbefore_after, + changedafter => \&_long_desc_changedbefore_after, + _non_changed => \&_long_desc_nonchanged, + }, + 'longdescs.count' => { + changedby => \&_long_desc_changedby, + changedbefore => \&_long_desc_changedbefore_after, + changedafter => \&_long_desc_changedbefore_after, + changedfrom => \&_invalid_combination, + changedto => \&_invalid_combination, + _default => \&_long_descs_count, + }, + 'longdescs.isprivate' => MULTI_SELECT_OVERRIDE, + owner_idle_time => { + greaterthan => \&_owner_idle_time_greater_less, + greaterthaneq => \&_owner_idle_time_greater_less, + lessthan => \&_owner_idle_time_greater_less, + lessthaneq => \&_owner_idle_time_greater_less, + _default => \&_invalid_combination, + }, + product => {_non_changed => \&_product_nonchanged,}, + tag => MULTI_SELECT_OVERRIDE, + comment_tag => MULTI_SELECT_OVERRIDE, + + # Timetracking Fields + deadline => {_non_changed => \&_deadline}, + percentage_complete => {_non_changed => \&_percentage_complete,}, + work_time => { + changedby => \&_work_time_changedby, + changedbefore => \&_work_time_changedbefore_after, + changedafter => \&_work_time_changedbefore_after, + _default => \&_work_time, + }, + last_visit_ts => + {_non_changed => \&_last_visit_ts, _default => \&_invalid_operator,}, + bug_interest_ts => + {_non_changed => \&_bug_interest_ts, _default => \&_invalid_operator,}, + triage_owner => {_non_changed => \&_triage_owner_nonchanged,}, + + # Custom Fields + FIELD_TYPE_FREETEXT, + {_non_changed => \&_nullable}, + FIELD_TYPE_BUG_ID, + {_non_changed => \&_nullable_int}, + FIELD_TYPE_DATETIME, + {_non_changed => \&_nullable_datetime}, + FIELD_TYPE_DATE, + {_non_changed => \&_nullable_date}, + FIELD_TYPE_TEXTAREA, + {_non_changed => \&_nullable}, + FIELD_TYPE_MULTI_SELECT, + MULTI_SELECT_OVERRIDE, + FIELD_TYPE_BUG_URLS, + MULTI_SELECT_OVERRIDE, }; # These are fields where special action is taken depending on the # *value* passed in to the chart, sometimes. # This is a sub because custom fields are dynamic sub SPECIAL_PARSING { - my $map = { - # Pronoun Fields (Ones that can accept %user%, etc.) - assigned_to => \&_contact_pronoun, - 'attachments.submitter' => \&_contact_pronoun, - cc => \&_cc_pronoun, - commenter => \&_commenter_pronoun, - qa_contact => \&_contact_pronoun, - reporter => \&_contact_pronoun, - - # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format. - creation_ts => \&_datetime_translate, - deadline => \&_date_translate, - delta_ts => \&_datetime_translate, - - # last_visit field that accept both a 1d, 1w, 1m, 1y format and the - # %last_changed% pronoun. - last_visit_ts => \&_last_visit_datetime, - bug_interest_ts => \&_last_visit_datetime, - - # BMO - Add ability to use pronoun for bug mentors field - bug_mentor => \&_commenter_pronoun, - - # BMO - add ability to use pronoun for triage owners - triage_owner => \&_triage_owner_pronoun, - }; - foreach my $field (Bugzilla->active_custom_fields) { - if ($field->type == FIELD_TYPE_DATETIME) { - $map->{$field->name} = \&_datetime_translate; - } elsif ($field->type == FIELD_TYPE_DATE) { - $map->{$field->name} = \&_date_translate; - } + my $map = { + + # Pronoun Fields (Ones that can accept %user%, etc.) + assigned_to => \&_contact_pronoun, + 'attachments.submitter' => \&_contact_pronoun, + cc => \&_cc_pronoun, + commenter => \&_commenter_pronoun, + qa_contact => \&_contact_pronoun, + reporter => \&_contact_pronoun, + + # Date Fields that accept the 1d, 1w, 1m, 1y, etc. format. + creation_ts => \&_datetime_translate, + deadline => \&_date_translate, + delta_ts => \&_datetime_translate, + + # last_visit field that accept both a 1d, 1w, 1m, 1y format and the + # %last_changed% pronoun. + last_visit_ts => \&_last_visit_datetime, + bug_interest_ts => \&_last_visit_datetime, + + # BMO - Add ability to use pronoun for bug mentors field + bug_mentor => \&_commenter_pronoun, + + # BMO - add ability to use pronoun for triage owners + triage_owner => \&_triage_owner_pronoun, + }; + foreach my $field (Bugzilla->active_custom_fields) { + if ($field->type == FIELD_TYPE_DATETIME) { + $map->{$field->name} = \&_datetime_translate; } - return $map; -}; + elsif ($field->type == FIELD_TYPE_DATE) { + $map->{$field->name} = \&_date_translate; + } + } + return $map; +} # Information about fields that represent "users", used by _user_nonchanged. # There are other user fields than the ones listed here, but those use # defaults in _user_nonchanged. use constant USER_FIELDS => { - 'attachments.submitter' => { - field => 'submitter_id', - join => { table => 'attachments' }, - isprivate => 1, - }, - cc => { - field => 'who', - join => { table => 'cc' }, - }, - commenter => { - field => 'who', - join => { table => 'longdescs', join => 'INNER' }, - isprivate => 1, - }, - qa_contact => { - nullable => 1, - }, - 'requestees.login_name' => { - nullable => 1, - field => 'requestee_id', - join => { table => 'flags' }, - }, - 'setters.login_name' => { - field => 'setter_id', - join => { table => 'flags' }, - }, - # BMO - Ability to search for bugs with specific mentors - 'bug_mentor' => { - field => 'user_id', - join => { table => 'bug_mentors' }, - } + 'attachments.submitter' => + {field => 'submitter_id', join => {table => 'attachments'}, isprivate => 1,}, + cc => {field => 'who', join => {table => 'cc'},}, + commenter => { + field => 'who', + join => {table => 'longdescs', join => 'INNER'}, + isprivate => 1, + }, + qa_contact => {nullable => 1,}, + 'requestees.login_name' => + {nullable => 1, field => 'requestee_id', join => {table => 'flags'},}, + 'setters.login_name' => {field => 'setter_id', join => {table => 'flags'},}, + + # BMO - Ability to search for bugs with specific mentors + 'bug_mentor' => {field => 'user_id', join => {table => 'bug_mentors'},} }; # Backwards compatibility for times that we changed the names of fields # or URL parameters. use constant FIELD_MAP => { - bugidtype => 'bug_id_type', - changedin => 'days_elapsed', - long_desc => 'longdesc', + bugidtype => 'bug_id_type', + changedin => 'days_elapsed', + long_desc => 'longdesc', }; # Some fields are not sorted on themselves, but on other fields. # We need to have a list of these fields and what they map to. use constant SPECIAL_ORDER => { - 'target_milestone' => { - order => ['map_target_milestone.sortkey','map_target_milestone.value'], - join => { - table => 'milestones', - from => 'target_milestone', - to => 'value', - extra => ['bugs.product_id = map_target_milestone.product_id'], - join => 'INNER', - } - }, + 'target_milestone' => { + order => ['map_target_milestone.sortkey', 'map_target_milestone.value'], + join => { + table => 'milestones', + from => 'target_milestone', + to => 'value', + extra => ['bugs.product_id = map_target_milestone.product_id'], + join => 'INNER', + } + }, }; # Certain columns require other columns to come before them # in _select_columns, and should be put there if they're not there. use constant COLUMN_DEPENDS => { - classification => ['product'], - percentage_complete => ['actual_time', 'remaining_time'], - triage_owner => ['component'], + classification => ['product'], + percentage_complete => ['actual_time', 'remaining_time'], + triage_owner => ['component'], }; # This describes tables that must be joined when you want to display @@ -461,112 +420,87 @@ use constant COLUMN_DEPENDS => { # DB::Schema to figure out what needs to be joined, but for some # fields it needs a little help. sub COLUMN_JOINS { - my $user = Bugzilla->user; - - my $joins = { - actual_time => { - table => '(SELECT bug_id, SUM(work_time) AS total' - . ' FROM longdescs GROUP BY bug_id)', - join => 'INNER', - }, - assigned_to => { - from => 'assigned_to', - to => 'userid', - table => 'profiles', - join => 'INNER', - }, - assignee_last_login => { - as => 'assignee', - from => 'assigned_to', - to => 'userid', - table => 'profiles', - join => 'INNER', - }, - reporter => { - from => 'reporter', - to => 'userid', - table => 'profiles', - join => 'INNER', - }, - qa_contact => { - from => 'qa_contact', - to => 'userid', - table => 'profiles', - }, - component => { - from => 'component_id', - to => 'id', - table => 'components', - join => 'INNER', - }, - product => { - from => 'product_id', - to => 'id', - table => 'products', - join => 'INNER', - }, - classification => { - table => 'classifications', - from => 'map_product.classification_id', - to => 'id', - join => 'INNER', - }, - 'flagtypes.name' => { - as => 'map_flags', - table => 'flags', - extra => ['map_flags.attach_id IS NULL'], - then_to => { - as => 'map_flagtypes', - table => 'flagtypes', - from => 'map_flags.type_id', - to => 'id', - }, - }, - 'triage_owner' => { - table => 'profiles', - as => 'map_triage_owner', - from => 'map_component.triage_owner_id', - to => 'userid', - join => 'LEFT', - }, - keywords => { - table => 'keywords', - then_to => { - as => 'map_keyworddefs', - table => 'keyworddefs', - from => 'map_keywords.keywordid', - to => 'id', - }, - }, - blocked => { - table => 'dependencies', - to => 'dependson', - }, - dependson => { - table => 'dependencies', - to => 'blocked', - }, - 'longdescs.count' => { - table => 'longdescs', - join => 'INNER', - }, - last_visit_ts => { - as => 'bug_user_last_visit', - table => 'bug_user_last_visit', - extra => ['bug_user_last_visit.user_id = ' . $user->id], - from => 'bug_id', - to => 'bug_id', - }, - bug_interest_ts => { - as => 'bug_interest', - table => 'bug_interest', - extra => ['bug_interest.user_id = ' . $user->id], - from => 'bug_id', - to => 'bug_id', - }, - }; - return $joins; -}; + my $user = Bugzilla->user; + + my $joins = { + actual_time => { + table => '(SELECT bug_id, SUM(work_time) AS total' + . ' FROM longdescs GROUP BY bug_id)', + join => 'INNER', + }, + assigned_to => { + from => 'assigned_to', + to => 'userid', + table => 'profiles', + join => 'INNER', + }, + assignee_last_login => { + as => 'assignee', + from => 'assigned_to', + to => 'userid', + table => 'profiles', + join => 'INNER', + }, + reporter => + {from => 'reporter', to => 'userid', table => 'profiles', join => 'INNER',}, + qa_contact => {from => 'qa_contact', to => 'userid', table => 'profiles',}, + component => + {from => 'component_id', to => 'id', table => 'components', join => 'INNER',}, + product => + {from => 'product_id', to => 'id', table => 'products', join => 'INNER',}, + classification => { + table => 'classifications', + from => 'map_product.classification_id', + to => 'id', + join => 'INNER', + }, + 'flagtypes.name' => { + as => 'map_flags', + table => 'flags', + extra => ['map_flags.attach_id IS NULL'], + then_to => { + as => 'map_flagtypes', + table => 'flagtypes', + from => 'map_flags.type_id', + to => 'id', + }, + }, + 'triage_owner' => { + table => 'profiles', + as => 'map_triage_owner', + from => 'map_component.triage_owner_id', + to => 'userid', + join => 'LEFT', + }, + keywords => { + table => 'keywords', + then_to => { + as => 'map_keyworddefs', + table => 'keyworddefs', + from => 'map_keywords.keywordid', + to => 'id', + }, + }, + blocked => {table => 'dependencies', to => 'dependson',}, + dependson => {table => 'dependencies', to => 'blocked',}, + 'longdescs.count' => {table => 'longdescs', join => 'INNER',}, + last_visit_ts => { + as => 'bug_user_last_visit', + table => 'bug_user_last_visit', + extra => ['bug_user_last_visit.user_id = ' . $user->id], + from => 'bug_id', + to => 'bug_id', + }, + bug_interest_ts => { + as => 'bug_interest', + table => 'bug_interest', + extra => ['bug_interest.user_id = ' . $user->id], + from => 'bug_id', + to => 'bug_id', + }, + }; + return $joins; +} # This constant defines the columns that can be selected in a query # and/or displayed in a bug list. Column records include the following @@ -589,161 +523,160 @@ sub COLUMN_JOINS { # and we don't want it to happen at compile time, so we have it as a # subroutine. sub COLUMNS { - my $invocant = shift; - my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - my $dbh = Bugzilla->dbh; - my $cache = Bugzilla->request_cache; - - if (defined $cache->{search_columns}->{$user->id}) { - return $cache->{search_columns}->{$user->id}; - } + my $invocant = shift; + my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; + my $dbh = Bugzilla->dbh; + my $cache = Bugzilla->request_cache; - # These are columns that don't exist in fielddefs, but are valid buglist - # columns. (Also see near the bottom of this function for the definition - # of short_short_desc.) - my %columns = ( - relevance => { title => 'Relevance' }, - assigned_to_realname => { title => 'Assignee' }, - reporter_realname => { title => 'Reporter' }, - qa_contact_realname => { title => 'QA Contact' }, - ); - - # Next we define columns that have special SQL instead of just something - # like "bugs.bug_id". - my $total_time = "(map_actual_time.total + bugs.remaining_time)"; - my %special_sql = ( - deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'), - actual_time => 'map_actual_time.total', - - # "FLOOR" is in there to turn this into an integer, making searches - # totally predictable. Otherwise you get floating-point numbers that - # are rather hard to search reliably if you're asking for exact - # numbers. - percentage_complete => - "(CASE WHEN $total_time = 0" - . " THEN 0" - . " ELSE FLOOR(100 * (map_actual_time.total / $total_time))" - . " END)", - - 'flagtypes.name' => $dbh->sql_group_concat('DISTINCT ' - . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status')), - - 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), - - blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'), - dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'), - - 'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)', - last_visit_ts => 'bug_user_last_visit.last_visit_ts', - bug_interest_ts => 'bug_interest.modification_time', - assignee_last_login => 'assignee.last_seen_date', - ); - - if ($user->id) { - $special_sql{triage_owner} = 'map_triage_owner.login_name'; + if (defined $cache->{search_columns}->{$user->id}) { + return $cache->{search_columns}->{$user->id}; + } + + # These are columns that don't exist in fielddefs, but are valid buglist + # columns. (Also see near the bottom of this function for the definition + # of short_short_desc.) + my %columns = ( + relevance => {title => 'Relevance'}, + assigned_to_realname => {title => 'Assignee'}, + reporter_realname => {title => 'Reporter'}, + qa_contact_realname => {title => 'QA Contact'}, + ); + + # Next we define columns that have special SQL instead of just something + # like "bugs.bug_id". + my $total_time = "(map_actual_time.total + bugs.remaining_time)"; + my %special_sql = ( + deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'), + actual_time => 'map_actual_time.total', + + # "FLOOR" is in there to turn this into an integer, making searches + # totally predictable. Otherwise you get floating-point numbers that + # are rather hard to search reliably if you're asking for exact + # numbers. + percentage_complete => "(CASE WHEN $total_time = 0" + . " THEN 0" + . " ELSE FLOOR(100 * (map_actual_time.total / $total_time))" . " END)", + + 'flagtypes.name' => $dbh->sql_group_concat( + 'DISTINCT ' . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status') + ), + + 'keywords' => $dbh->sql_group_concat('DISTINCT map_keyworddefs.name'), + + blocked => $dbh->sql_group_concat('DISTINCT map_blocked.blocked'), + dependson => $dbh->sql_group_concat('DISTINCT map_dependson.dependson'), + + 'longdescs.count' => 'COUNT(DISTINCT map_longdescs_count.comment_id)', + last_visit_ts => 'bug_user_last_visit.last_visit_ts', + bug_interest_ts => 'bug_interest.modification_time', + assignee_last_login => 'assignee.last_seen_date', + ); + + if ($user->id) { + $special_sql{triage_owner} = 'map_triage_owner.login_name'; + } + else { + $special_sql{triage_owner} = 'map_triage_owner.realname'; + } + + # Backward-compatibility for old field names. Goes new_name => old_name. + # These are here and not in _translate_old_column because the rest of the + # code actually still uses the old names, while the fielddefs table uses + # the new names (which is not the case for the fields handled by + # _translate_old_column). + my %old_names = ( + creation_ts => 'opendate', + delta_ts => 'changeddate', + work_time => 'actual_time', + ); + + # Fields that are email addresses + my @email_fields = qw(assigned_to reporter qa_contact); + + # Other fields that are stored in the bugs table as an id, but + # should be displayed using their name. + my @id_fields = qw(product component classification); + + foreach my $col (@email_fields) { + my $sql = "map_${col}.login_name"; + if (!$user->id) { + $sql = $dbh->sql_string_until($sql, $dbh->quote('@')); + } + $special_sql{$col} = $sql; + $columns{"${col}_realname"}->{name} = "map_${col}.realname"; + } + + foreach my $col (@id_fields) { + $special_sql{$col} = "map_${col}.name"; + } + + # Do the actual column-getting from fielddefs, now. + my @fields = @{Bugzilla->fields({obsolete => 0, buglist => 1})}; + foreach my $field (@fields) { + my $id = $field->name; + $id = $old_names{$id} if exists $old_names{$id}; + my $sql; + if (exists $special_sql{$id}) { + $sql = $special_sql{$id}; + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + $sql = $dbh->sql_group_concat('DISTINCT map_' . $field->name . '.value'); } else { - $special_sql{triage_owner} = 'map_triage_owner.realname'; + $sql = 'bugs.' . $field->name; } + $columns{$id} = {name => $sql, title => $field->description}; + } - # Backward-compatibility for old field names. Goes new_name => old_name. - # These are here and not in _translate_old_column because the rest of the - # code actually still uses the old names, while the fielddefs table uses - # the new names (which is not the case for the fields handled by - # _translate_old_column). - my %old_names = ( - creation_ts => 'opendate', - delta_ts => 'changeddate', - work_time => 'actual_time', - ); - - # Fields that are email addresses - my @email_fields = qw(assigned_to reporter qa_contact); - # Other fields that are stored in the bugs table as an id, but - # should be displayed using their name. - my @id_fields = qw(product component classification); - - foreach my $col (@email_fields) { - my $sql = "map_${col}.login_name"; - if (!$user->id) { - $sql = $dbh->sql_string_until($sql, $dbh->quote('@')); - } - $special_sql{$col} = $sql; - $columns{"${col}_realname"}->{name} = "map_${col}.realname"; - } + # The short_short_desc column is identical to short_desc + $columns{'short_short_desc'} = $columns{'short_desc'}; - foreach my $col (@id_fields) { - $special_sql{$col} = "map_${col}.name"; - } + Bugzilla::Hook::process('buglist_columns', {columns => \%columns}); - # Do the actual column-getting from fielddefs, now. - my @fields = @{ Bugzilla->fields({ obsolete => 0, buglist => 1 }) }; - foreach my $field (@fields) { - my $id = $field->name; - $id = $old_names{$id} if exists $old_names{$id}; - my $sql; - if (exists $special_sql{$id}) { - $sql = $special_sql{$id}; - } - elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - $sql = $dbh->sql_group_concat( - 'DISTINCT map_' . $field->name . '.value'); - } - else { - $sql = 'bugs.' . $field->name; - } - $columns{$id} = { name => $sql, title => $field->description }; - } + $cache->{search_columns}->{$user->id} = \%columns; + return $cache->{search_columns}->{$user->id}; +} - # The short_short_desc column is identical to short_desc - $columns{'short_short_desc'} = $columns{'short_desc'}; +sub REPORT_COLUMNS { + my $invocant = shift; + my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - Bugzilla::Hook::process('buglist_columns', { columns => \%columns }); + my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS); - $cache->{search_columns}->{$user->id} = \%columns; - return $cache->{search_columns}->{$user->id}; -} + # There's no reason to support reporting on unique fields. + # Also, some other fields don't make very good reporting axises, + # or simply don't work with the current reporting system. + my @no_report_columns = qw(bug_id alias short_short_desc opendate changeddate + flagtypes.name keywords relevance); -sub REPORT_COLUMNS { - my $invocant = shift; - my $user = blessed($invocant) ? $invocant->_user : Bugzilla->user; - - my $columns = dclone(blessed($invocant) ? $invocant->COLUMNS : COLUMNS); - # There's no reason to support reporting on unique fields. - # Also, some other fields don't make very good reporting axises, - # or simply don't work with the current reporting system. - my @no_report_columns = - qw(bug_id alias short_short_desc opendate changeddate - flagtypes.name keywords relevance); - - # Multi-select fields are not currently supported. - my @multi_selects = @{Bugzilla->fields( - { obsolete => 0, type => FIELD_TYPE_MULTI_SELECT })}; - push(@no_report_columns, map { $_->name } @multi_selects); - - # If you're not a time-tracker, you can't use time-tracking - # columns. - if (!$user->is_timetracker) { - push(@no_report_columns, TIMETRACKING_FIELDS); - } + # Multi-select fields are not currently supported. + my @multi_selects + = @{Bugzilla->fields({obsolete => 0, type => FIELD_TYPE_MULTI_SELECT})}; + push(@no_report_columns, map { $_->name } @multi_selects); - foreach my $name (@no_report_columns) { - delete $columns->{$name}; - } - return $columns; + # If you're not a time-tracker, you can't use time-tracking + # columns. + if (!$user->is_timetracker) { + push(@no_report_columns, TIMETRACKING_FIELDS); + } + + foreach my $name (@no_report_columns) { + delete $columns->{$name}; + } + return $columns; } # These are fields that never go into the GROUP BY on any DB. bug_id # is here because it *always* goes into the GROUP BY as the first item, # so it should be skipped when determining extra GROUP BY columns. use constant GROUP_BY_SKIP => qw( - blocked - bug_id - dependson - flagtypes.name - keywords - longdescs.count - percentage_complete + blocked + bug_id + dependson + flagtypes.name + keywords + longdescs.count + percentage_complete ); ############### @@ -752,27 +685,27 @@ use constant GROUP_BY_SKIP => qw( # Note that the params argument may be modified by Bugzilla::Search sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; + my $invocant = shift; + my $class = ref($invocant) || $invocant; - my $self = { @_ }; - bless($self, $class); - $self->{'user'} ||= Bugzilla->user; + my $self = {@_}; + bless($self, $class); + $self->{'user'} ||= Bugzilla->user; - # There are certain behaviors of the CGI "Vars" hash that we don't want. - # In particular, if you put a single-value arrayref into it, later you - # get back out a string, which breaks anyexact charts (because they - # need arrays even for individual items, or we will re-trigger bug 67036). - # - # We can't just untie the hash--that would give us a hash with no values. - # We have to manually copy the hash into a new one, and we have to always - # do it, because there's no way to know if we were passed a tied hash - # or not. - my $params_in = $self->_params; - my %params = map { $_ => $params_in->{$_} } keys %$params_in; - $self->{params} = \%params; + # There are certain behaviors of the CGI "Vars" hash that we don't want. + # In particular, if you put a single-value arrayref into it, later you + # get back out a string, which breaks anyexact charts (because they + # need arrays even for individual items, or we will re-trigger bug 67036). + # + # We can't just untie the hash--that would give us a hash with no values. + # We have to manually copy the hash into a new one, and we have to always + # do it, because there's no way to know if we were passed a tied hash + # or not. + my $params_in = $self->_params; + my %params = map { $_ => $params_in->{$_} } keys %$params_in; + $self->{params} = \%params; - return $self; + return $self; } @@ -781,234 +714,246 @@ sub new { #################### sub data { - my $self = shift; - return $self->{data} if $self->{data}; - my $dbh = Bugzilla->dbh; - - Bugzilla->log_user_request(undef, undef, "search") if Bugzilla->user->id; - # If all fields belong to the 'bugs' table, there is no need to split - # the original query into two pieces. Else we override the 'fields' - # argument to first get bug IDs based on the search criteria defined - # by the caller, and the desired fields are collected in the 2nd query. - my @orig_fields = $self->_input_columns; - my $all_in_bugs_table = 1; - foreach my $field (@orig_fields) { - next if $self->COLUMNS->{$field}->{name} =~ /^bugs\.\w+$/; - $self->{fields} = ['bug_id']; - $all_in_bugs_table = 0; - last; + my $self = shift; + return $self->{data} if $self->{data}; + my $dbh = Bugzilla->dbh; + + Bugzilla->log_user_request(undef, undef, "search") if Bugzilla->user->id; + + # If all fields belong to the 'bugs' table, there is no need to split + # the original query into two pieces. Else we override the 'fields' + # argument to first get bug IDs based on the search criteria defined + # by the caller, and the desired fields are collected in the 2nd query. + my @orig_fields = $self->_input_columns; + my $all_in_bugs_table = 1; + foreach my $field (@orig_fields) { + next if $self->COLUMNS->{$field}->{name} =~ /^bugs\.\w+$/; + $self->{fields} = ['bug_id']; + $all_in_bugs_table = 0; + last; + } + + # BMO - to avoid massive amounts of joins, if we're selecting a lot of + # tracking flags, replace them with placeholders. the values will be + # retrieved later and injected into the result. + if (Bugzilla->has_extension('TrackingFlags')) { + my %tf_map + = map { $_ => 1 } Bugzilla::Extension::TrackingFlags::Flag->get_all_names(); + my @tf_selected = grep { exists $tf_map{$_} } @orig_fields; + + # mysql has a limit of 61 joins, and we want to avoid massive amounts of joins + # 30 ensures we won't hit the limit, nor generate too many joins + if (scalar @tf_selected > 30) { + foreach my $column (@tf_selected) { + $self->COLUMNS->{$column}->{name} = "'---'"; + } + $self->{tracking_flags} = \@tf_selected; } - - # BMO - to avoid massive amounts of joins, if we're selecting a lot of - # tracking flags, replace them with placeholders. the values will be - # retrieved later and injected into the result. - if (Bugzilla->has_extension('TrackingFlags')) { - my %tf_map = map { $_ => 1 } Bugzilla::Extension::TrackingFlags::Flag->get_all_names(); - my @tf_selected = grep { exists $tf_map{$_} } @orig_fields; - # mysql has a limit of 61 joins, and we want to avoid massive amounts of joins - # 30 ensures we won't hit the limit, nor generate too many joins - if (scalar @tf_selected > 30) { - foreach my $column (@tf_selected) { - $self->COLUMNS->{$column}->{name} = "'---'"; - } - $self->{tracking_flags} = \@tf_selected; - } - else { - $self->{tracking_flags} = []; - } + else { + $self->{tracking_flags} = []; } + } - my $start_time = [gettimeofday()]; - my $sql = $self->_sql; - # Do we just want bug IDs to pass to the 2nd query or all the data immediately? - my $func = $all_in_bugs_table ? 'selectall_arrayref' : 'selectcol_arrayref'; - my $bug_ids = $dbh->$func($sql); - my @extra_data = ({sql => $sql, time => tv_interval($start_time)}); - # Restore the original 'fields' argument, just in case. - $self->{fields} = \@orig_fields unless $all_in_bugs_table; - - # BMO if the caller only wants the count, that's all we need to return - return $bug_ids->[0]->[0] if $self->_params->{count_only}; - - # If there are no bugs found, or all fields are in the 'bugs' table, - # there is no need for another query. - if (!scalar @$bug_ids || $all_in_bugs_table) { - $self->{data} = $bug_ids; - return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; - } + my $start_time = [gettimeofday()]; + my $sql = $self->_sql; - # Make sure the bug_id will be returned. If not, append it to the list. - my $pos = firstidx { $_ eq 'bug_id' } @orig_fields; - if ($pos < 0) { - push(@orig_fields, 'bug_id'); - $pos = $#orig_fields; - } + # Do we just want bug IDs to pass to the 2nd query or all the data immediately? + my $func = $all_in_bugs_table ? 'selectall_arrayref' : 'selectcol_arrayref'; + my $bug_ids = $dbh->$func($sql); + my @extra_data = ({sql => $sql, time => tv_interval($start_time)}); - # Now create a query with the buglist above as the single criteria - # and the fields that the caller wants. No need to redo security checks; - # the list has already been validated above. - my $search = $self->new('fields' => \@orig_fields, - 'params' => {bug_id => $bug_ids, bug_id_type => 'anyexact'}, - 'sharer' => $self->_sharer_id, - 'user' => $self->_user, - 'allow_unlimited' => 1, - '_no_security_check' => 1); + # Restore the original 'fields' argument, just in case. + $self->{fields} = \@orig_fields unless $all_in_bugs_table; - $start_time = [gettimeofday()]; - $sql = $search->_sql; - my $unsorted_data = $dbh->selectall_arrayref($sql); - push(@extra_data, {sql => $sql, time => tv_interval($start_time)}); - # Let's sort the data. We didn't do it in the query itself because - # we already know in which order to sort bugs thanks to the first query, - # and this avoids additional table joins in the SQL query. - my %data = map { $_->[$pos] => $_ } @$unsorted_data; - $self->{data} = [map { $data{$_} } @$bug_ids]; - - # BMO - get tracking flags values, and insert into result - if (Bugzilla->has_extension('TrackingFlags') && @{ $self->{tracking_flags} }) { - # read values - my $values; - $sql = " + # BMO if the caller only wants the count, that's all we need to return + return $bug_ids->[0]->[0] if $self->_params->{count_only}; + + # If there are no bugs found, or all fields are in the 'bugs' table, + # there is no need for another query. + if (!scalar @$bug_ids || $all_in_bugs_table) { + $self->{data} = $bug_ids; + return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; + } + + # Make sure the bug_id will be returned. If not, append it to the list. + my $pos = firstidx { $_ eq 'bug_id' } @orig_fields; + if ($pos < 0) { + push(@orig_fields, 'bug_id'); + $pos = $#orig_fields; + } + + # Now create a query with the buglist above as the single criteria + # and the fields that the caller wants. No need to redo security checks; + # the list has already been validated above. + my $search = $self->new( + 'fields' => \@orig_fields, + 'params' => {bug_id => $bug_ids, bug_id_type => 'anyexact'}, + 'sharer' => $self->_sharer_id, + 'user' => $self->_user, + 'allow_unlimited' => 1, + '_no_security_check' => 1 + ); + + $start_time = [gettimeofday()]; + $sql = $search->_sql; + my $unsorted_data = $dbh->selectall_arrayref($sql); + push(@extra_data, {sql => $sql, time => tv_interval($start_time)}); + + # Let's sort the data. We didn't do it in the query itself because + # we already know in which order to sort bugs thanks to the first query, + # and this avoids additional table joins in the SQL query. + my %data = map { $_->[$pos] => $_ } @$unsorted_data; + $self->{data} = [map { $data{$_} } @$bug_ids]; + + # BMO - get tracking flags values, and insert into result + if (Bugzilla->has_extension('TrackingFlags') && @{$self->{tracking_flags}}) { + + # read values + my $values; + $sql = " SELECT bugs.bug_id, tracking_flags.name, tracking_flags_bugs.value FROM bugs LEFT JOIN tracking_flags_bugs ON tracking_flags_bugs.bug_id = bugs.bug_id LEFT JOIN tracking_flags ON tracking_flags.id = tracking_flags_bugs.tracking_flag_id WHERE " . $dbh->sql_in('bugs.bug_id', $bug_ids); - $start_time = [gettimeofday()]; - my $rows = $dbh->selectall_arrayref($sql); - push(@extra_data, {sql => $sql, time => tv_interval($start_time)}); - foreach my $row (@$rows) { - $values->{$row->[0]}{$row->[1]} = $row->[2] if defined($row->[2]); - } + $start_time = [gettimeofday()]; + my $rows = $dbh->selectall_arrayref($sql); + push(@extra_data, {sql => $sql, time => tv_interval($start_time)}); + foreach my $row (@$rows) { + $values->{$row->[0]}{$row->[1]} = $row->[2] if defined($row->[2]); + } - # find the columns of the tracking flags - my %tf_pos; - for (my $i = 0; $i <= $#orig_fields; $i++) { - if (grep { $_ eq $orig_fields[$i] } @{ $self->{tracking_flags} }) { - $tf_pos{$orig_fields[$i]} = $i; - } - } + # find the columns of the tracking flags + my %tf_pos; + for (my $i = 0; $i <= $#orig_fields; $i++) { + if (grep { $_ eq $orig_fields[$i] } @{$self->{tracking_flags}}) { + $tf_pos{$orig_fields[$i]} = $i; + } + } - # replace the placeholder value with the field's value - foreach my $row (@{ $self->{data} }) { - my $bug_id = $row->[$pos]; - next unless exists $values->{$bug_id}; - foreach my $field (keys %{ $values->{$bug_id} }) { - if (exists $tf_pos{$field}) { - $row->[$tf_pos{$field}] = $values->{$bug_id}{$field}; - } - } + # replace the placeholder value with the field's value + foreach my $row (@{$self->{data}}) { + my $bug_id = $row->[$pos]; + next unless exists $values->{$bug_id}; + foreach my $field (keys %{$values->{$bug_id}}) { + if (exists $tf_pos{$field}) { + $row->[$tf_pos{$field}] = $values->{$bug_id}{$field}; } + } } + } - return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; + return wantarray ? ($self->{data}, \@extra_data) : $self->{data}; } sub _sql { - my ($self) = @_; - return $self->{sql} if $self->{sql}; - my $dbh = Bugzilla->dbh; - - my ($joins, $clause) = $self->_charts_to_conditions(); - - if (!$clause->as_string - && !Bugzilla->params->{'search_allow_no_criteria'} - && !$self->{allow_unlimited}) - { - ThrowUserError('buglist_parameters_required'); - } - - my $select = join(', ', $self->_sql_select); - my $from = $self->_sql_from($joins); - my $where = $self->_sql_where($clause); - my $group_by = $dbh->sql_group_by($self->_sql_group_by); - my $order_by = $self->_sql_order_by - ? "\nORDER BY " . join(', ', $self->_sql_order_by) : ''; - my $limit = $self->_sql_limit; - $limit = "\n$limit" if $limit; - - # BMO allow fetching just the number of matching bugs - if ($self->_params->{count_only}) { - $select = 'COUNT(*) AS count'; - $group_by = ''; - $order_by = ''; - $limit = ''; - } - - my $query = <{sql} if $self->{sql}; + my $dbh = Bugzilla->dbh; + + my ($joins, $clause) = $self->_charts_to_conditions(); + + if ( !$clause->as_string + && !Bugzilla->params->{'search_allow_no_criteria'} + && !$self->{allow_unlimited}) + { + ThrowUserError('buglist_parameters_required'); + } + + my $select = join(', ', $self->_sql_select); + my $from = $self->_sql_from($joins); + my $where = $self->_sql_where($clause); + my $group_by = $dbh->sql_group_by($self->_sql_group_by); + my $order_by + = $self->_sql_order_by + ? "\nORDER BY " . join(', ', $self->_sql_order_by) + : ''; + my $limit = $self->_sql_limit; + $limit = "\n$limit" if $limit; + + # BMO allow fetching just the number of matching bugs + if ($self->_params->{count_only}) { + $select = 'COUNT(*) AS count'; + $group_by = ''; + $order_by = ''; + $limit = ''; + } + + my $query = <{sql} = $query; - return $self->{sql}; + $self->{sql} = $query; + return $self->{sql}; } sub search_description { - my ($self, $params) = @_; - my $desc = $self->{'search_description'} ||= []; - if ($params) { - - # BMO - product aliases - # display the new product name on the search results name to avoid a - # disparity between the search summary and the results. - if ($params->{field} eq 'product') { - my $aliased; - my @values = split(/,/, $params->{value}); - foreach my $value (@values) { - if (exists PRODUCT_ALIASES->{lc($value)}) { - $value = PRODUCT_ALIASES->{lc($value)}; - $aliased = 1; - } - } - if ($aliased) { - $params->{value} = join(',', @values); - } - } + my ($self, $params) = @_; + my $desc = $self->{'search_description'} ||= []; + if ($params) { - push(@$desc, $params); - } - # Make sure that the description has actually been generated if - # people are asking for the whole thing. - else { - $self->_sql; + # BMO - product aliases + # display the new product name on the search results name to avoid a + # disparity between the search summary and the results. + if ($params->{field} eq 'product') { + my $aliased; + my @values = split(/,/, $params->{value}); + foreach my $value (@values) { + if (exists PRODUCT_ALIASES->{lc($value)}) { + $value = PRODUCT_ALIASES->{lc($value)}; + $aliased = 1; + } + } + if ($aliased) { + $params->{value} = join(',', @values); + } } - return $self->{'search_description'}; + + push(@$desc, $params); + } + + # Make sure that the description has actually been generated if + # people are asking for the whole thing. + else { + $self->_sql; + } + return $self->{'search_description'}; } sub boolean_charts_to_custom_search { - my ($self, $cgi_buffer) = @_; - my @as_params = $self->_boolean_charts->as_params; - - # We need to start our new ids after the last custom search "f" id. - # We can just pick the last id in the array because they are sorted - # numerically. - my $last_id = ($self->_field_ids)[-1]; - my $count = defined($last_id) ? $last_id + 1 : 0; - foreach my $param_set (@as_params) { - foreach my $name (keys %$param_set) { - my $value = $param_set->{$name}; - next if !defined $value; - $cgi_buffer->param($name . $count, $value); - } - $count++; + my ($self, $cgi_buffer) = @_; + my @as_params = $self->_boolean_charts->as_params; + + # We need to start our new ids after the last custom search "f" id. + # We can just pick the last id in the array because they are sorted + # numerically. + my $last_id = ($self->_field_ids)[-1]; + my $count = defined($last_id) ? $last_id + 1 : 0; + foreach my $param_set (@as_params) { + foreach my $name (keys %$param_set) { + my $value = $param_set->{$name}; + next if !defined $value; + $cgi_buffer->param($name . $count, $value); } + $count++; + } } sub invalid_order_columns { - my ($self) = @_; - my @invalid_columns; - foreach my $order ($self->_input_order) { - next if defined $self->_validate_order_column($order); - push(@invalid_columns, $order); - } - return \@invalid_columns; + my ($self) = @_; + my @invalid_columns; + foreach my $order ($self->_input_order) { + next if defined $self->_validate_order_column($order); + push(@invalid_columns, $order); + } + return \@invalid_columns; } sub order { - my ($self) = @_; - return $self->_valid_order; + my ($self) = @_; + return $self->_valid_order; } ###################### @@ -1017,38 +962,39 @@ sub order { # Fields that are legal for boolean charts of any kind. sub _chart_fields { - my ($self) = @_; - return $self->{chart_fields} //= search_fields({ user => $self->_user }); + my ($self) = @_; + return $self->{chart_fields} //= search_fields({user => $self->_user}); } # There are various places in Search.pm that we need to know the list of # valid multi-select fields--or really, fields that are stored like # multi-selects, which includes BUG_URLS fields. sub _multi_select_fields { - my ($self) = @_; - $self->{multi_select_fields} ||= Bugzilla->fields({ - by_name => 1, - type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS]}); - return $self->{multi_select_fields}; + my ($self) = @_; + $self->{multi_select_fields} + ||= Bugzilla->fields({ + by_name => 1, type => [FIELD_TYPE_MULTI_SELECT, FIELD_TYPE_BUG_URLS] + }); + return $self->{multi_select_fields}; } # $self->{params} contains values that could be undef, could be a string, # or could be an arrayref. Sometimes we want that value as an array, # always. sub _param_array { - my ($self, $name) = @_; - my $value = $self->_params->{$name}; - if (!defined $value) { - return (); - } - if (ref($value) eq 'ARRAY') { - return @$value; - } - return ($value); -} - -sub _params { $_[0]->{params} } -sub _user { return $_[0]->{user} } + my ($self, $name) = @_; + my $value = $self->_params->{$name}; + if (!defined $value) { + return (); + } + if (ref($value) eq 'ARRAY') { + return @$value; + } + return ($value); +} + +sub _params { $_[0]->{params} } +sub _user { return $_[0]->{user} } sub _sharer_id { $_[0]->{sharer} } ############################## @@ -1057,75 +1003,78 @@ sub _sharer_id { $_[0]->{sharer} } # These are the fields the user has chosen to display on the buglist, # exactly as they were passed to new(). -sub _input_columns { @{ $_[0]->{'fields'} || [] } } +sub _input_columns { @{$_[0]->{'fields'} || []} } # These are columns that are also going to be in the SELECT for one reason # or another, but weren't actually requested by the caller. sub _extra_columns { - my ($self) = @_; - # Everything that's going to be in the ORDER BY must also be - # in the SELECT. - push(@{ $self->{extra_columns} }, $self->_valid_order_columns); - return @{ $self->{extra_columns} }; + my ($self) = @_; + + # Everything that's going to be in the ORDER BY must also be + # in the SELECT. + push(@{$self->{extra_columns}}, $self->_valid_order_columns); + return @{$self->{extra_columns}}; } # For search functions to modify extra_columns. It doesn't matter if # people push the same column onto this array multiple times, because # _select_columns will call "uniq" on its final result. sub _add_extra_column { - my ($self, $column) = @_; - push(@{ $self->{extra_columns} }, $column); + my ($self, $column) = @_; + push(@{$self->{extra_columns}}, $column); } # These are the columns that we're going to be actually SELECTing. sub _display_columns { - my ($self) = @_; - return @{ $self->{display_columns} } if $self->{display_columns}; - - # Do not alter the list from _input_columns at all, even if there are - # duplicated columns. Those are passed by the caller, and the caller - # expects to get them back in the exact same order. - my @columns = $self->_input_columns; - - # Only add columns which are not already listed. - my %list = map { $_ => 1 } @columns; - foreach my $column ($self->_extra_columns) { - push(@columns, $column) unless $list{$column}++; - } - $self->{display_columns} = \@columns; - return @{ $self->{display_columns} }; + my ($self) = @_; + return @{$self->{display_columns}} if $self->{display_columns}; + + # Do not alter the list from _input_columns at all, even if there are + # duplicated columns. Those are passed by the caller, and the caller + # expects to get them back in the exact same order. + my @columns = $self->_input_columns; + + # Only add columns which are not already listed. + my %list = map { $_ => 1 } @columns; + foreach my $column ($self->_extra_columns) { + push(@columns, $column) unless $list{$column}++; + } + $self->{display_columns} = \@columns; + return @{$self->{display_columns}}; } # These are the columns that are involved in the query. sub _select_columns { - my ($self) = @_; - return @{ $self->{select_columns} } if $self->{select_columns}; + my ($self) = @_; + return @{$self->{select_columns}} if $self->{select_columns}; - my @select_columns; - foreach my $column ($self->_display_columns) { - if (my $add_first = COLUMN_DEPENDS->{$column}) { - push(@select_columns, @$add_first); - } - push(@select_columns, $column); + my @select_columns; + foreach my $column ($self->_display_columns) { + if (my $add_first = COLUMN_DEPENDS->{$column}) { + push(@select_columns, @$add_first); } - # Remove duplicated columns. - $self->{select_columns} = [uniq @select_columns]; - return @{ $self->{select_columns} }; + push(@select_columns, $column); + } + + # Remove duplicated columns. + $self->{select_columns} = [uniq @select_columns]; + return @{$self->{select_columns}}; } # This takes _display_columns and translates it into the actual SQL that # will go into the SELECT clause. sub _sql_select { - my ($self) = @_; - my @sql_fields; - foreach my $column ($self->_display_columns) { - my $alias = $column; - # Aliases cannot contain dots in them. We convert them to underscores. - $alias =~ s/\./_/g; - my $sql = $self->COLUMNS->{$column}->{name} . " AS $alias"; - push(@sql_fields, $sql); - } - return @sql_fields; + my ($self) = @_; + my @sql_fields; + foreach my $column ($self->_display_columns) { + my $alias = $column; + + # Aliases cannot contain dots in them. We convert them to underscores. + $alias =~ s/\./_/g; + my $sql = $self->COLUMNS->{$column}->{name} . " AS $alias"; + push(@sql_fields, $sql); + } + return @sql_fields; } ################################ @@ -1134,85 +1083,83 @@ sub _sql_select { # The "order" that was requested by the consumer, exactly as it was # requested. -sub _input_order { @{ $_[0]->{'order'} || [] } } +sub _input_order { @{$_[0]->{'order'} || []} } + # Requested order with invalid values removed and old names translated sub _valid_order { - my ($self) = @_; - return map { ($self->_validate_order_column($_)) } $self->_input_order; + my ($self) = @_; + return map { ($self->_validate_order_column($_)) } $self->_input_order; } + # The valid order with just the column names, and no ASC or DESC. sub _valid_order_columns { - my ($self) = @_; - return map { (split_order_term($_))[0] } $self->_valid_order; + my ($self) = @_; + return map { (split_order_term($_))[0] } $self->_valid_order; } sub _validate_order_column { - my ($self, $order_item) = @_; + my ($self, $order_item) = @_; - # Translate old column names - my ($field, $direction) = split_order_term($order_item); - $field = $self->_translate_old_column($field); + # Translate old column names + my ($field, $direction) = split_order_term($order_item); + $field = $self->_translate_old_column($field); - # Only accept valid columns - return if (!exists $self->COLUMNS->{$field}); + # Only accept valid columns + return if (!exists $self->COLUMNS->{$field}); - # Relevance column can be used only with one or more fulltext searches - return if ($field eq 'relevance' && !$self->COLUMNS->{$field}->{name}); + # Relevance column can be used only with one or more fulltext searches + return if ($field eq 'relevance' && !$self->COLUMNS->{$field}->{name}); - $direction = " $direction" if $direction; - return "$field$direction"; + $direction = " $direction" if $direction; + return "$field$direction"; } # A hashref that describes all the special stuff that has to be done # for various fields if they go into the ORDER BY clause. sub _special_order { - my ($self) = @_; - return $self->{special_order} if $self->{special_order}; - - my %special_order = %{ SPECIAL_ORDER() }; - my $select_fields = Bugzilla->fields({ type => FIELD_TYPE_SINGLE_SELECT }); - foreach my $field (@$select_fields) { - next if $field->is_abnormal; - my $name = $field->name; - $special_order{$name} = { - order => ["map_$name.sortkey", "map_$name.value"], - join => { - table => $name, - from => "bugs.$name", - to => "value", - join => 'INNER', - } - }; - } - $self->{special_order} = \%special_order; - return $self->{special_order}; + my ($self) = @_; + return $self->{special_order} if $self->{special_order}; + + my %special_order = %{SPECIAL_ORDER()}; + my $select_fields = Bugzilla->fields({type => FIELD_TYPE_SINGLE_SELECT}); + foreach my $field (@$select_fields) { + next if $field->is_abnormal; + my $name = $field->name; + $special_order{$name} = { + order => ["map_$name.sortkey", "map_$name.value"], + join => {table => $name, from => "bugs.$name", to => "value", join => 'INNER',} + }; + } + $self->{special_order} = \%special_order; + return $self->{special_order}; } sub _sql_order_by { - my ($self) = @_; - if (!$self->{sql_order_by}) { - my @order_by = map { $self->_translate_order_by_column($_) } - $self->_valid_order; - $self->{sql_order_by} = \@order_by; - } - return @{ $self->{sql_order_by} }; + my ($self) = @_; + if (!$self->{sql_order_by}) { + my @order_by + = map { $self->_translate_order_by_column($_) } $self->_valid_order; + $self->{sql_order_by} = \@order_by; + } + return @{$self->{sql_order_by}}; } sub _translate_order_by_column { - my ($self, $order_by_item) = @_; - - my ($field, $direction) = split_order_term($order_by_item); - - $direction = '' if lc($direction) eq 'asc'; - my $special_order = $self->_special_order->{$field}->{order}; - # Standard fields have underscores in their SELECT alias instead - # of a period (because aliases can't have periods). - $field =~ s/\./_/g; - my @items = $special_order ? @$special_order : $field; - if (lc($direction) eq 'desc') { - @items = map { "$_ DESC" } @items; - } - return @items; + my ($self, $order_by_item) = @_; + + my ($field, $direction) = split_order_term($order_by_item); + + $direction = '' if lc($direction) eq 'asc'; + my $special_order = $self->_special_order->{$field}->{order}; + + # Standard fields have underscores in their SELECT alias instead + # of a period (because aliases can't have periods). + $field =~ s/\./_/g; + my @items = $special_order ? @$special_order : $field; + if (lc($direction) eq 'desc') { + @items = map {"$_ DESC"} @items; + } + return @items; } ############################# @@ -1220,32 +1167,30 @@ sub _translate_order_by_column { ############################# sub _sql_limit { - my ($self) = @_; - my $limit = $self->_params->{limit}; - my $offset = $self->_params->{offset}; - - my $max_results = Bugzilla->params->{'max_search_results'}; - if (!$self->{allow_unlimited} && (!$limit || $limit > $max_results)) { - $limit = $max_results; - } - - if (defined($offset) && !$limit) { - $limit = INT_MAX; - } - if (defined $limit) { - detaint_natural($limit) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::Search::new', - param => 'limit' }); - if (defined $offset) { - detaint_natural($offset) - || ThrowCodeError('param_must_be_numeric', - { function => 'Bugzilla::Search::new', - param => 'offset' }); - } - return Bugzilla->dbh->sql_limit($limit, $offset); - } - return ''; + my ($self) = @_; + my $limit = $self->_params->{limit}; + my $offset = $self->_params->{offset}; + + my $max_results = Bugzilla->params->{'max_search_results'}; + if (!$self->{allow_unlimited} && (!$limit || $limit > $max_results)) { + $limit = $max_results; + } + + if (defined($offset) && !$limit) { + $limit = INT_MAX; + } + if (defined $limit) { + detaint_natural($limit) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::Search::new', param => 'limit'}); + if (defined $offset) { + detaint_natural($offset) + || ThrowCodeError('param_must_be_numeric', + {function => 'Bugzilla::Search::new', param => 'offset'}); + } + return Bugzilla->dbh->sql_limit($limit, $offset); + } + return ''; } ############################ @@ -1253,173 +1198,172 @@ sub _sql_limit { ############################ sub _column_join { - my ($self, $field) = @_; - # The _realname fields require the same join as the username fields. - $field =~ s/_realname$//; - my $column_joins = $self->_get_column_joins(); - my $join_info = $column_joins->{$field}; - if ($join_info) { - # Don't allow callers to modify the constant. - $join_info = dclone($join_info); - } - else { - if ($self->_multi_select_fields->{$field}) { - $join_info = { table => "bug_$field" }; - } - } - if ($join_info and !$join_info->{as}) { - $join_info = dclone($join_info); - $join_info->{as} = "map_$field"; + my ($self, $field) = @_; + + # The _realname fields require the same join as the username fields. + $field =~ s/_realname$//; + my $column_joins = $self->_get_column_joins(); + my $join_info = $column_joins->{$field}; + if ($join_info) { + + # Don't allow callers to modify the constant. + $join_info = dclone($join_info); + } + else { + if ($self->_multi_select_fields->{$field}) { + $join_info = {table => "bug_$field"}; } - return $join_info ? $join_info : (); + } + if ($join_info and !$join_info->{as}) { + $join_info = dclone($join_info); + $join_info->{as} = "map_$field"; + } + return $join_info ? $join_info : (); } # Sometimes we join the same table more than once. In this case, we # want to AND all the various critiera that were used in both joins. sub _combine_joins { - my ($self, $joins) = @_; - my @result; - while(my $join = shift @$joins) { - my $name = $join->{as}; - my ($others_like_me, $the_rest) = part { $_->{as} eq $name ? 0 : 1 } - @$joins; - if ($others_like_me) { - my $from = $join->{from}; - my $to = $join->{to}; - # Sanity check to make sure that we have the same from and to - # for all the same-named joins. - if ($from) { - all { $_->{from} eq $from } @$others_like_me - or die "Not all same-named joins have identical 'from': " - . Dumper($join, $others_like_me); - } - if ($to) { - all { $_->{to} eq $to } @$others_like_me - or die "Not all same-named joins have identical 'to': " - . Dumper($join, $others_like_me); - } - - # We don't need to call uniq here--translate_join will do that - # for us. - my @conditions = map { @{ $_->{extra} || [] } } - ($join, @$others_like_me); - $join->{extra} = \@conditions; - $joins = $the_rest; - } - push(@result, $join); - } - - return @result; + my ($self, $joins) = @_; + my @result; + while (my $join = shift @$joins) { + my $name = $join->{as}; + my ($others_like_me, $the_rest) = part { $_->{as} eq $name ? 0 : 1 } + @$joins; + if ($others_like_me) { + my $from = $join->{from}; + my $to = $join->{to}; + + # Sanity check to make sure that we have the same from and to + # for all the same-named joins. + if ($from) { + all { $_->{from} eq $from } @$others_like_me + or die "Not all same-named joins have identical 'from': " + . Dumper($join, $others_like_me); + } + if ($to) { + all { $_->{to} eq $to } @$others_like_me + or die "Not all same-named joins have identical 'to': " + . Dumper($join, $others_like_me); + } + + # We don't need to call uniq here--translate_join will do that + # for us. + my @conditions = map { @{$_->{extra} || []} } ($join, @$others_like_me); + $join->{extra} = \@conditions; + $joins = $the_rest; + } + push(@result, $join); + } + + return @result; } # Takes all the "then_to" items and just puts them as the next item in # the array. Right now this only does one level of "then_to", but we # could re-write this to handle then_to recursively if we need more levels. sub _extract_then_to { - my ($self, $joins) = @_; - my @result; - foreach my $join (@$joins) { - push(@result, $join); - if (my $then_to = $join->{then_to}) { - push(@result, $then_to); - } + my ($self, $joins) = @_; + my @result; + foreach my $join (@$joins) { + push(@result, $join); + if (my $then_to = $join->{then_to}) { + push(@result, $then_to); } - return @result; + } + return @result; } # JOIN statements for the SELECT and ORDER BY columns. This should not be # called until the moment it is needed, because _select_columns might be # modified by the charts. sub _select_order_joins { - my ($self) = @_; - my @joins; - foreach my $field ($self->_select_columns) { - my @column_join = $self->_column_join($field); - push(@joins, @column_join); - } - foreach my $field ($self->_valid_order_columns) { - my $join_info = $self->_special_order->{$field}->{join}; - if ($join_info) { - # Don't let callers modify SPECIAL_ORDER. - $join_info = dclone($join_info); - if (!$join_info->{as}) { - $join_info->{as} = "map_$field"; - } - push(@joins, $join_info); - } + my ($self) = @_; + my @joins; + foreach my $field ($self->_select_columns) { + my @column_join = $self->_column_join($field); + push(@joins, @column_join); + } + foreach my $field ($self->_valid_order_columns) { + my $join_info = $self->_special_order->{$field}->{join}; + if ($join_info) { + + # Don't let callers modify SPECIAL_ORDER. + $join_info = dclone($join_info); + if (!$join_info->{as}) { + $join_info->{as} = "map_$field"; + } + push(@joins, $join_info); } - return @joins; + } + return @joins; } # These are the joins that are *always* in the FROM clause. sub _standard_joins { - my ($self) = @_; - my $user = $self->_user; - my @joins; - return () if $self->{_no_security_check}; - - my $security_join = { - table => 'bug_group_map', - as => 'security_map', + my ($self) = @_; + my $user = $self->_user; + my @joins; + return () if $self->{_no_security_check}; + + my $security_join = {table => 'bug_group_map', as => 'security_map',}; + push(@joins, $security_join); + + if ($user->id) { + $security_join->{extra} + = ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"]; + + my $security_cc_join = { + table => 'cc', + as => 'security_cc', + extra => ['security_cc.who = ' . $user->id], }; - push(@joins, $security_join); + push(@joins, $security_cc_join); + } - if ($user->id) { - $security_join->{extra} = - ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"]; - - my $security_cc_join = { - table => 'cc', - as => 'security_cc', - extra => ['security_cc.who = ' . $user->id], - }; - push(@joins, $security_cc_join); - } - - return @joins; + return @joins; } sub _sql_from { - my ($self, $joins_input) = @_; - my @joins = ($self->_standard_joins, $self->_select_order_joins, - @$joins_input); - @joins = $self->_extract_then_to(\@joins); - @joins = $self->_combine_joins(\@joins); - my @join_sql = map { $self->_translate_join($_) } @joins; - return "bugs\n" . join("\n", @join_sql); + my ($self, $joins_input) = @_; + my @joins = ($self->_standard_joins, $self->_select_order_joins, @$joins_input); + @joins = $self->_extract_then_to(\@joins); + @joins = $self->_combine_joins(\@joins); + my @join_sql = map { $self->_translate_join($_) } @joins; + return "bugs\n" . join("\n", @join_sql); } # This takes a join data structure and turns it into actual JOIN SQL. sub _translate_join { - my ($self, $join_info) = @_; - - die "join with no table: " . Dumper($join_info) if !$join_info->{table}; - die "join with no 'as': " . Dumper($join_info) if !$join_info->{as}; - - my $from_table = $join_info->{bugs_table} || "bugs"; - my $from = $join_info->{from} || "bug_id"; - if ($from =~ /^(\w+)\.(\w+)$/) { - ($from_table, $from) = ($1, $2); - } - my $table = $join_info->{table}; - my $name = $join_info->{as}; - my $to = $join_info->{to} || "bug_id"; - my $join = $join_info->{join} || 'LEFT'; - my @extra = @{ $join_info->{extra} || [] }; - $name =~ s/\./_/g; - - # If a term contains ORs, we need to put parens around the condition. - # This is a pretty weak test, but it's actually OK to put parens - # around too many things. - @extra = map { $_ =~ /\bOR\b/i ? "($_)" : $_ } @extra; - my $extra_condition = join(' AND ', uniq @extra); - if ($extra_condition) { - $extra_condition = " AND $extra_condition"; - } - - my @join_sql = "$join JOIN $table AS $name" - . " ON $from_table.$from = $name.$to$extra_condition"; - return @join_sql; + my ($self, $join_info) = @_; + + die "join with no table: " . Dumper($join_info) if !$join_info->{table}; + die "join with no 'as': " . Dumper($join_info) if !$join_info->{as}; + + my $from_table = $join_info->{bugs_table} || "bugs"; + my $from = $join_info->{from} || "bug_id"; + if ($from =~ /^(\w+)\.(\w+)$/) { + ($from_table, $from) = ($1, $2); + } + my $table = $join_info->{table}; + my $name = $join_info->{as}; + my $to = $join_info->{to} || "bug_id"; + my $join = $join_info->{join} || 'LEFT'; + my @extra = @{$join_info->{extra} || []}; + $name =~ s/\./_/g; + + # If a term contains ORs, we need to put parens around the condition. + # This is a pretty weak test, but it's actually OK to put parens + # around too many things. + @extra = map { $_ =~ /\bOR\b/i ? "($_)" : $_ } @extra; + my $extra_condition = join(' AND ', uniq @extra); + if ($extra_condition) { + $extra_condition = " AND $extra_condition"; + } + + my @join_sql = "$join JOIN $table AS $name" + . " ON $from_table.$from = $name.$to$extra_condition"; + return @join_sql; } ############################# @@ -1432,47 +1376,50 @@ sub _translate_join { # The terms that are always in the WHERE clause. These implement bug # group security. sub _standard_where { - my ($self) = @_; - return ('1=1') if $self->{_no_security_check}; - # If replication lags badly between the shadow db and the main DB, - # it's possible for bugs to show up in searches before their group - # controls are properly set. To prevent this, when initially creating - # bugs we set their creation_ts to NULL, and don't give them a creation_ts - # until their group controls are set. So if a bug has a NULL creation_ts, - # it shouldn't show up in searches at all. - my @where = ('bugs.creation_ts IS NOT NULL'); - - my $security_term = 'security_map.group_id IS NULL'; - - my $user = $self->_user; - if ($user->id) { - my $userid = $user->id; - # This indentation makes the resulting SQL more readable. - $security_term .= <{_no_security_check}; + + # If replication lags badly between the shadow db and the main DB, + # it's possible for bugs to show up in searches before their group + # controls are properly set. To prevent this, when initially creating + # bugs we set their creation_ts to NULL, and don't give them a creation_ts + # until their group controls are set. So if a bug has a NULL creation_ts, + # it shouldn't show up in searches at all. + my @where = ('bugs.creation_ts IS NOT NULL'); + + my $security_term = 'security_map.group_id IS NULL'; + + my $user = $self->_user; + if ($user->id) { + my $userid = $user->id; + + # This indentation makes the resulting SQL more readable. + $security_term .= <params->{'useqacontact'}) { - $security_term.= " OR bugs.qa_contact = $userid"; - } - $security_term = "($security_term)"; + if (Bugzilla->params->{'useqacontact'}) { + $security_term .= " OR bugs.qa_contact = $userid"; } + $security_term = "($security_term)"; + } - push(@where, $security_term); + push(@where, $security_term); - return @where; + return @where; } sub _sql_where { - my ($self, $main_clause) = @_; - # The newline and this particular spacing makes the resulting - # SQL a bit more readable for debugging. - my $where = join("\n AND ", $self->_standard_where); - my $clause_sql = $main_clause->as_string; - $where .= "\n AND " . $clause_sql if $clause_sql; - return $where; + my ($self, $main_clause) = @_; + + # The newline and this particular spacing makes the resulting + # SQL a bit more readable for debugging. + my $where = join("\n AND ", $self->_standard_where); + my $clause_sql = $main_clause->as_string; + $where .= "\n AND " . $clause_sql if $clause_sql; + return $where; } ################################ @@ -1482,40 +1429,40 @@ sub _sql_where { # And these are the fields that we have to do GROUP BY for in DBs # that are more strict about putting everything into GROUP BY. sub _sql_group_by { - my ($self) = @_; - - # Strict DBs require every element from the SELECT to be in the GROUP BY, - # unless that element is being used in an aggregate function. - my @extra_group_by; - foreach my $column ($self->_select_columns) { - next if $self->_skip_group_by->{$column}; - my $sql = $self->COLUMNS->{$column}->{name}; - push(@extra_group_by, $sql); - } + my ($self) = @_; - # And all items from ORDER BY must be in the GROUP BY. The above loop - # doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER. - foreach my $column ($self->_valid_order_columns) { - my $special_order = $self->_special_order->{$column}->{order}; - next if !$special_order; - push(@extra_group_by, @$special_order); - } + # Strict DBs require every element from the SELECT to be in the GROUP BY, + # unless that element is being used in an aggregate function. + my @extra_group_by; + foreach my $column ($self->_select_columns) { + next if $self->_skip_group_by->{$column}; + my $sql = $self->COLUMNS->{$column}->{name}; + push(@extra_group_by, $sql); + } + + # And all items from ORDER BY must be in the GROUP BY. The above loop + # doesn't catch items that were put into the ORDER BY from SPECIAL_ORDER. + foreach my $column ($self->_valid_order_columns) { + my $special_order = $self->_special_order->{$column}->{order}; + next if !$special_order; + push(@extra_group_by, @$special_order); + } - @extra_group_by = uniq @extra_group_by; + @extra_group_by = uniq @extra_group_by; - # bug_id is the only field we actually group by. - return ('bugs.bug_id', join(',', @extra_group_by)); + # bug_id is the only field we actually group by. + return ('bugs.bug_id', join(',', @extra_group_by)); } # A helper for _sql_group_by. sub _skip_group_by { - my ($self) = @_; - return $self->{skip_group_by} if $self->{skip_group_by}; - my @skip_list = GROUP_BY_SKIP; - push(@skip_list, keys %{ $self->_multi_select_fields }); - my %skip_hash = map { $_ => 1 } @skip_list; - $self->{skip_group_by} = \%skip_hash; - return $self->{skip_group_by}; + my ($self) = @_; + return $self->{skip_group_by} if $self->{skip_group_by}; + my @skip_list = GROUP_BY_SKIP; + push(@skip_list, keys %{$self->_multi_select_fields}); + my %skip_hash = map { $_ => 1 } @skip_list; + $self->{skip_group_by} = \%skip_hash; + return $self->{skip_group_by}; } ############################################## @@ -1524,244 +1471,254 @@ sub _skip_group_by { # Backwards compatibility for old field names. sub _convert_old_params { - my ($self) = @_; - my $params = $self->_params; + my ($self) = @_; + my $params = $self->_params; - # bugidtype has different values in modern Search.pm. - if (defined $params->{'bugidtype'}) { - my $value = $params->{'bugidtype'}; - $params->{'bugidtype'} = $value eq 'exclude' ? 'nowords' : 'anyexact'; - } + # bugidtype has different values in modern Search.pm. + if (defined $params->{'bugidtype'}) { + my $value = $params->{'bugidtype'}; + $params->{'bugidtype'} = $value eq 'exclude' ? 'nowords' : 'anyexact'; + } - foreach my $old_name (keys %{ FIELD_MAP() }) { - if (defined $params->{$old_name}) { - my $new_name = FIELD_MAP->{$old_name}; - $params->{$new_name} = delete $params->{$old_name}; - } + foreach my $old_name (keys %{FIELD_MAP()}) { + if (defined $params->{$old_name}) { + my $new_name = FIELD_MAP->{$old_name}; + $params->{$new_name} = delete $params->{$old_name}; } + } } # This parses all the standard search parameters except for the boolean # charts. sub _special_charts { - my ($self) = @_; - $self->_convert_old_params(); - $self->_special_parse_bug_status(); - $self->_special_parse_resolution(); - my $clause = new Bugzilla::Search::Clause(); - $clause->add( $self->_parse_basic_fields() ); - $clause->add( $self->_special_parse_email() ); - $clause->add( $self->_special_parse_chfield() ); - $clause->add( $self->_special_parse_deadline() ); - return $clause; + my ($self) = @_; + $self->_convert_old_params(); + $self->_special_parse_bug_status(); + $self->_special_parse_resolution(); + my $clause = new Bugzilla::Search::Clause(); + $clause->add($self->_parse_basic_fields()); + $clause->add($self->_special_parse_email()); + $clause->add($self->_special_parse_chfield()); + $clause->add($self->_special_parse_deadline()); + return $clause; } sub _parse_basic_fields { - my ($self) = @_; - my $params = $self->_params; - my $chart_fields = $self->_chart_fields; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $field_name (keys %$chart_fields) { - # CGI params shouldn't have periods in them, so we only accept - # period-separated fields with underscores where the periods go. - my $param_name = $field_name; - $param_name =~ s/\./_/g; - my @values = $self->_param_array($param_name); - next if !@values; - my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact'; - my $operator = $params->{"${param_name}_type"} || $default_op; - # Fields that are displayed as multi-selects are passed as arrays, - # so that they can properly search values that contain commas. - # However, other fields are sent as strings, so that they are properly - # split on commas if required. - my $field = $chart_fields->{$field_name}; - my $pass_value; - if ($field->is_select or $field->name eq 'version' - or $field->name eq 'target_milestone') - { - $pass_value = \@values; - } - else { - $pass_value = join(',', @values); - } - $clause->add($field_name, $operator, $pass_value); + my ($self) = @_; + my $params = $self->_params; + my $chart_fields = $self->_chart_fields; + + my $clause = new Bugzilla::Search::Clause(); + foreach my $field_name (keys %$chart_fields) { + + # CGI params shouldn't have periods in them, so we only accept + # period-separated fields with underscores where the periods go. + my $param_name = $field_name; + $param_name =~ s/\./_/g; + my @values = $self->_param_array($param_name); + next if !@values; + my $default_op = $param_name eq 'content' ? 'matches' : 'anyexact'; + my $operator = $params->{"${param_name}_type"} || $default_op; + + # Fields that are displayed as multi-selects are passed as arrays, + # so that they can properly search values that contain commas. + # However, other fields are sent as strings, so that they are properly + # split on commas if required. + my $field = $chart_fields->{$field_name}; + my $pass_value; + if ( $field->is_select + or $field->name eq 'version' + or $field->name eq 'target_milestone') + { + $pass_value = \@values; + } + else { + $pass_value = join(',', @values); } - return $clause; + $clause->add($field_name, $operator, $pass_value); + } + return $clause; } sub _special_parse_bug_status { - my ($self) = @_; - my $params = $self->_params; - return if !defined $params->{'bug_status'}; - # We want to allow the bug_status_type parameter to work normally, - # meaning that this special code should only be activated if we are - # doing the normal "anyexact" search on bug_status. - return if (defined $params->{'bug_status_type'} - and $params->{'bug_status_type'} ne 'anyexact'); - - my @bug_status = $self->_param_array('bug_status'); - # Also include inactive bug statuses, as you can query them. - my $legal_statuses = $self->_chart_fields->{'bug_status'}->legal_values; - - # If the status contains __open__ or __closed__, translate those - # into their equivalent lists of open and closed statuses. - if (grep { $_ eq '__open__' } @bug_status) { - my @open = grep { $_->is_open } @$legal_statuses; - @open = map { $_->name } @open; - push(@bug_status, @open); - } - if (grep { $_ eq '__closed__' } @bug_status) { - my @closed = grep { not $_->is_open } @$legal_statuses; - @closed = map { $_->name } @closed; - push(@bug_status, @closed); - } - - @bug_status = uniq @bug_status; - my $all = grep { $_ eq "__all__" } @bug_status; - # This will also handle removing __open__ and __closed__ for us - # (__all__ too, which is why we check for it above, first). - @bug_status = _valid_values(\@bug_status, $legal_statuses); - - # If the user has selected every status, change to selecting none. - # This is functionally equivalent, but quite a lot faster. - if ($all or scalar(@bug_status) == scalar(@$legal_statuses)) { - delete $params->{'bug_status'}; - } - else { - $params->{'bug_status'} = \@bug_status; - } + my ($self) = @_; + my $params = $self->_params; + return if !defined $params->{'bug_status'}; + + # We want to allow the bug_status_type parameter to work normally, + # meaning that this special code should only be activated if we are + # doing the normal "anyexact" search on bug_status. + return + if (defined $params->{'bug_status_type'} + and $params->{'bug_status_type'} ne 'anyexact'); + + my @bug_status = $self->_param_array('bug_status'); + + # Also include inactive bug statuses, as you can query them. + my $legal_statuses = $self->_chart_fields->{'bug_status'}->legal_values; + + # If the status contains __open__ or __closed__, translate those + # into their equivalent lists of open and closed statuses. + if (grep { $_ eq '__open__' } @bug_status) { + my @open = grep { $_->is_open } @$legal_statuses; + @open = map { $_->name } @open; + push(@bug_status, @open); + } + if (grep { $_ eq '__closed__' } @bug_status) { + my @closed = grep { not $_->is_open } @$legal_statuses; + @closed = map { $_->name } @closed; + push(@bug_status, @closed); + } + + @bug_status = uniq @bug_status; + my $all = grep { $_ eq "__all__" } @bug_status; + + # This will also handle removing __open__ and __closed__ for us + # (__all__ too, which is why we check for it above, first). + @bug_status = _valid_values(\@bug_status, $legal_statuses); + + # If the user has selected every status, change to selecting none. + # This is functionally equivalent, but quite a lot faster. + if ($all or scalar(@bug_status) == scalar(@$legal_statuses)) { + delete $params->{'bug_status'}; + } + else { + $params->{'bug_status'} = \@bug_status; + } } sub _special_parse_chfield { - my ($self) = @_; - my $params = $self->_params; - - my $date_from = trim(lc($params->{'chfieldfrom'} || '')); - my $date_to = trim(lc($params->{'chfieldto'} || '')); - $date_from = '' if $date_from eq 'now'; - $date_to = '' if $date_to eq 'now'; - my @fields = $self->_param_array('chfield'); - my $value_to = $params->{'chfieldvalue'}; - $value_to = '' if !defined $value_to; - - @fields = map { $_ eq '[Bug creation]' ? 'creation_ts' : $_ } @fields; - - return undef unless ($date_from ne '' || $date_to ne '' || $value_to ne ''); - - my $clause = new Bugzilla::Search::Clause(); - - # It is always safe and useful to push delta_ts into the charts - # if there is a "from" date specified. It doesn't conflict with - # searching [Bug creation], because a bug's delta_ts is set to - # its creation_ts when it is created. So this just gives the - # database an additional index to possibly choose, on a table that - # is smaller than bugs_activity. - if ($date_from ne '') { - $clause->add('delta_ts', 'greaterthaneq', $date_from); - } - # It's not normally safe to do it for "to" dates, though--"chfieldto" means - # "a field that changed before this date", and delta_ts could be either - # later or earlier than that, if we're searching for the time that a field - # changed. However, chfieldto all by itself, without any chfieldvalue or - # chfield, means "just search delta_ts", and so we still want that to - # work. - if ($date_to ne '' and !@fields and $value_to eq '') { - $clause->add('delta_ts', 'lessthaneq', $date_to); - } - - # chfieldto is supposed to be a relative date or a date of the form - # YYYY-MM-DD, i.e. without the time appended to it. We append the - # time ourselves so that the end date is correctly taken into account. - $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/; - - my $join_clause = new Bugzilla::Search::Clause('OR'); - - foreach my $field (@fields) { - my $sub_clause = new Bugzilla::Search::ClauseGroup(); - $sub_clause->add(condition($field, 'changedto', $value_to)) if $value_to ne ''; - $sub_clause->add(condition($field, 'changedafter', $date_from)) if $date_from ne ''; - $sub_clause->add(condition($field, 'changedbefore', $date_to)) if $date_to ne ''; - $join_clause->add($sub_clause); - } - $clause->add($join_clause); - - return $clause; + my ($self) = @_; + my $params = $self->_params; + + my $date_from = trim(lc($params->{'chfieldfrom'} || '')); + my $date_to = trim(lc($params->{'chfieldto'} || '')); + $date_from = '' if $date_from eq 'now'; + $date_to = '' if $date_to eq 'now'; + my @fields = $self->_param_array('chfield'); + my $value_to = $params->{'chfieldvalue'}; + $value_to = '' if !defined $value_to; + + @fields = map { $_ eq '[Bug creation]' ? 'creation_ts' : $_ } @fields; + + return undef unless ($date_from ne '' || $date_to ne '' || $value_to ne ''); + + my $clause = new Bugzilla::Search::Clause(); + + # It is always safe and useful to push delta_ts into the charts + # if there is a "from" date specified. It doesn't conflict with + # searching [Bug creation], because a bug's delta_ts is set to + # its creation_ts when it is created. So this just gives the + # database an additional index to possibly choose, on a table that + # is smaller than bugs_activity. + if ($date_from ne '') { + $clause->add('delta_ts', 'greaterthaneq', $date_from); + } + + # It's not normally safe to do it for "to" dates, though--"chfieldto" means + # "a field that changed before this date", and delta_ts could be either + # later or earlier than that, if we're searching for the time that a field + # changed. However, chfieldto all by itself, without any chfieldvalue or + # chfield, means "just search delta_ts", and so we still want that to + # work. + if ($date_to ne '' and !@fields and $value_to eq '') { + $clause->add('delta_ts', 'lessthaneq', $date_to); + } + + # chfieldto is supposed to be a relative date or a date of the form + # YYYY-MM-DD, i.e. without the time appended to it. We append the + # time ourselves so that the end date is correctly taken into account. + $date_to .= ' 23:59:59' if $date_to =~ /^\d{4}-\d{1,2}-\d{1,2}$/; + + my $join_clause = new Bugzilla::Search::Clause('OR'); + + foreach my $field (@fields) { + my $sub_clause = new Bugzilla::Search::ClauseGroup(); + $sub_clause->add(condition($field, 'changedto', $value_to)) if $value_to ne ''; + $sub_clause->add(condition($field, 'changedafter', $date_from)) + if $date_from ne ''; + $sub_clause->add(condition($field, 'changedbefore', $date_to)) + if $date_to ne ''; + $join_clause->add($sub_clause); + } + $clause->add($join_clause); + + return $clause; } sub _special_parse_deadline { - my ($self) = @_; - return if !$self->_user->is_timetracker; - my $params = $self->_params; + my ($self) = @_; + return if !$self->_user->is_timetracker; + my $params = $self->_params; - my $clause = new Bugzilla::Search::Clause(); - if (my $from = $params->{'deadlinefrom'}) { - $clause->add('deadline', 'greaterthaneq', $from); - } - if (my $to = $params->{'deadlineto'}) { - $clause->add('deadline', 'lessthaneq', $to); - } + my $clause = new Bugzilla::Search::Clause(); + if (my $from = $params->{'deadlinefrom'}) { + $clause->add('deadline', 'greaterthaneq', $from); + } + if (my $to = $params->{'deadlineto'}) { + $clause->add('deadline', 'lessthaneq', $to); + } - return $clause; + return $clause; } sub _special_parse_email { - my ($self) = @_; - my $params = $self->_params; - - my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $param (@email_params) { - $param =~ /(\d+)$/; - my $id = $1; - my $email = trim($params->{"email$id"}); - next if !$email; - my $type = $params->{"emailtype$id"} || 'anyexact'; - $type = "anyexact" if $type eq "exact"; - - my $or_clause = new Bugzilla::Search::Clause('OR'); - foreach my $field (qw(assigned_to reporter cc qa_contact bug_mentor)) { - if ($params->{"email$field$id"}) { - $or_clause->add($field, $type, $email); - } - } - if ($params->{"emaillongdesc$id"}) { - $or_clause->add("commenter", $type, $email); - } + my ($self) = @_; + my $params = $self->_params; + + my @email_params = grep { $_ =~ /^email\d+$/ } keys %$params; - $clause->add($or_clause); + my $clause = new Bugzilla::Search::Clause(); + foreach my $param (@email_params) { + $param =~ /(\d+)$/; + my $id = $1; + my $email = trim($params->{"email$id"}); + next if !$email; + my $type = $params->{"emailtype$id"} || 'anyexact'; + $type = "anyexact" if $type eq "exact"; + + my $or_clause = new Bugzilla::Search::Clause('OR'); + foreach my $field (qw(assigned_to reporter cc qa_contact bug_mentor)) { + if ($params->{"email$field$id"}) { + $or_clause->add($field, $type, $email); + } + } + if ($params->{"emaillongdesc$id"}) { + $or_clause->add("commenter", $type, $email); } - return $clause; + $clause->add($or_clause); + } + + return $clause; } sub _special_parse_resolution { - my ($self) = @_; - my $params = $self->_params; - return if !defined $params->{'resolution'}; - - my @resolution = $self->_param_array('resolution'); - my $legal_resolutions = $self->_chart_fields->{resolution}->legal_values; - @resolution = _valid_values(\@resolution, $legal_resolutions, '---'); - if (scalar(@resolution) == scalar(@$legal_resolutions)) { - delete $params->{'resolution'}; - } + my ($self) = @_; + my $params = $self->_params; + return if !defined $params->{'resolution'}; + + my @resolution = $self->_param_array('resolution'); + my $legal_resolutions = $self->_chart_fields->{resolution}->legal_values; + @resolution = _valid_values(\@resolution, $legal_resolutions, '---'); + if (scalar(@resolution) == scalar(@$legal_resolutions)) { + delete $params->{'resolution'}; + } } sub _valid_values { - my ($input, $valid, $extra_value) = @_; - my @result; - foreach my $item (@$input) { - $item = trim($item); - if (defined $extra_value and $item eq $extra_value) { - push(@result, $item); - } - elsif (grep { $_->name eq $item } @$valid) { - push(@result, $item); - } + my ($input, $valid, $extra_value) = @_; + my @result; + foreach my $item (@$input) { + $item = trim($item); + if (defined $extra_value and $item eq $extra_value) { + push(@result, $item); + } + elsif (grep { $_->name eq $item } @$valid) { + push(@result, $item); } - return @result; + } + return @result; } ###################################### @@ -1769,213 +1726,220 @@ sub _valid_values { ###################################### sub _charts_to_conditions { - my ($self) = @_; - - my $clause = $self->_charts; - my @joins; - $clause->walk_conditions(sub { - my ($clause, $condition) = @_; - return if !$condition->translated; - push(@joins, @{ $condition->translated->{joins} }); - }); - return (\@joins, $clause); + my ($self) = @_; + + my $clause = $self->_charts; + my @joins; + $clause->walk_conditions(sub { + my ($clause, $condition) = @_; + return if !$condition->translated; + push(@joins, @{$condition->translated->{joins}}); + }); + return (\@joins, $clause); } sub _charts { - my ($self) = @_; + my ($self) = @_; - my $clause = $self->_params_to_data_structure(); - my $chart_id = 0; - $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) }); - return $clause; + my $clause = $self->_params_to_data_structure(); + my $chart_id = 0; + $clause->walk_conditions(sub { $self->_handle_chart($chart_id++, @_) }); + return $clause; } sub _params_to_data_structure { - my ($self) = @_; + my ($self) = @_; - # First we get the "special" charts, representing all the normal - # fields on the search page. This may modify _params, so it needs to - # happen first. - my $clause = $self->_special_charts; + # First we get the "special" charts, representing all the normal + # fields on the search page. This may modify _params, so it needs to + # happen first. + my $clause = $self->_special_charts; - # Then we process the old Boolean Charts input format. - $clause->add( $self->_boolean_charts ); + # Then we process the old Boolean Charts input format. + $clause->add($self->_boolean_charts); - # And then process the modern "custom search" format. - $clause->add( $self->_custom_search ); + # And then process the modern "custom search" format. + $clause->add($self->_custom_search); - return $clause; + return $clause; } sub _boolean_charts { - my ($self) = @_; - - my $params = $self->_params; - my @param_list = keys %$params; - - my @all_field_params = grep { /^field-?\d+/ } @param_list; - my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params; - @chart_ids = sort { $a <=> $b } uniq @chart_ids; - - my $clause = new Bugzilla::Search::Clause(); - foreach my $chart_id (@chart_ids) { - my @all_and = grep { /^field$chart_id-\d+/ } @param_list; - my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and; - @and_ids = sort { $a <=> $b } uniq @and_ids; - - my $and_clause = new Bugzilla::Search::Clause(); - foreach my $and_id (@and_ids) { - my @all_or = grep { /^field$chart_id-$and_id-\d+/ } @param_list; - my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or; - @or_ids = sort { $a <=> $b } uniq @or_ids; - - my $or_clause = new Bugzilla::Search::Clause('OR'); - foreach my $or_id (@or_ids) { - my $identifier = "$chart_id-$and_id-$or_id"; - my $field = $params->{"field$identifier"}; - my $operator = $params->{"type$identifier"}; - my $value = $params->{"value$identifier"}; - # no-value operators ignore the value, however a value needs to be set - $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; - $or_clause->add($field, $operator, $value); - } - $and_clause->add($or_clause); - $and_clause->negate(1) if $params->{"negate$chart_id"}; - } - $clause->add($and_clause); + my ($self) = @_; + + my $params = $self->_params; + my @param_list = keys %$params; + + my @all_field_params = grep {/^field-?\d+/} @param_list; + my @chart_ids = map { /^field(-?\d+)/; $1 } @all_field_params; + @chart_ids = sort { $a <=> $b } uniq @chart_ids; + + my $clause = new Bugzilla::Search::Clause(); + foreach my $chart_id (@chart_ids) { + my @all_and = grep {/^field$chart_id-\d+/} @param_list; + my @and_ids = map { /^field$chart_id-(\d+)/; $1 } @all_and; + @and_ids = sort { $a <=> $b } uniq @and_ids; + + my $and_clause = new Bugzilla::Search::Clause(); + foreach my $and_id (@and_ids) { + my @all_or = grep {/^field$chart_id-$and_id-\d+/} @param_list; + my @or_ids = map { /^field$chart_id-$and_id-(\d+)/; $1 } @all_or; + @or_ids = sort { $a <=> $b } uniq @or_ids; + + my $or_clause = new Bugzilla::Search::Clause('OR'); + foreach my $or_id (@or_ids) { + my $identifier = "$chart_id-$and_id-$or_id"; + my $field = $params->{"field$identifier"}; + my $operator = $params->{"type$identifier"}; + my $value = $params->{"value$identifier"}; + + # no-value operators ignore the value, however a value needs to be set + $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; + $or_clause->add($field, $operator, $value); + } + $and_clause->add($or_clause); + $and_clause->negate(1) if $params->{"negate$chart_id"}; } + $clause->add($and_clause); + } - return $clause; + return $clause; } sub _custom_search { - my ($self) = @_; - my $params = $self->_params; - - my $joiner = $params->{j_top} || ''; - my $current_clause = $joiner eq 'AND_G' + my ($self) = @_; + my $params = $self->_params; + + my $joiner = $params->{j_top} || ''; + my $current_clause + = $joiner eq 'AND_G' + ? new Bugzilla::Search::ClauseGroup() + : new Bugzilla::Search::Clause($joiner); + my @clause_stack; + foreach my $id ($self->_field_ids) { + my $field = $params->{"f$id"}; + if ($field eq 'OP') { + my $joiner = $params->{"j$id"} || ''; + my $new_clause + = $joiner eq 'AND_G' ? new Bugzilla::Search::ClauseGroup() : new Bugzilla::Search::Clause($joiner); - my @clause_stack; - foreach my $id ($self->_field_ids) { - my $field = $params->{"f$id"}; - if ($field eq 'OP') { - my $joiner = $params->{"j$id"} || ''; - my $new_clause = $joiner eq 'AND_G' - ? new Bugzilla::Search::ClauseGroup() - : new Bugzilla::Search::Clause($joiner); - $new_clause->negate($params->{"n$id"}); - $current_clause->add($new_clause); - push(@clause_stack, $current_clause); - $current_clause = $new_clause; - next; - } - if ($field eq 'CP') { - $current_clause = pop @clause_stack; - ThrowCodeError('search_cp_without_op', { id => $id }) - if !$current_clause; - next; - } - - my $operator = $params->{"o$id"}; - my $value = $params->{"v$id"}; - # no-value operators ignore the value, however a value needs to be set - $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; - my $condition = condition($field, $operator, $value); - $condition->negate($params->{"n$id"}); - $current_clause->add($condition); + $new_clause->negate($params->{"n$id"}); + $current_clause->add($new_clause); + push(@clause_stack, $current_clause); + $current_clause = $new_clause; + next; } + if ($field eq 'CP') { + $current_clause = pop @clause_stack; + ThrowCodeError('search_cp_without_op', {id => $id}) if !$current_clause; + next; + } + + my $operator = $params->{"o$id"}; + my $value = $params->{"v$id"}; - # We allow people to specify more OPs than CPs, so at the end of the - # loop our top clause may be still in the stack instead of being - # $current_clause. - return $clause_stack[0] || $current_clause; + # no-value operators ignore the value, however a value needs to be set + $value = ' ' if $operator && grep { $_ eq $operator } NO_VALUE_OPERATORS; + my $condition = condition($field, $operator, $value); + $condition->negate($params->{"n$id"}); + $current_clause->add($condition); + } + + # We allow people to specify more OPs than CPs, so at the end of the + # loop our top clause may be still in the stack instead of being + # $current_clause. + return $clause_stack[0] || $current_clause; } sub _field_ids { - my ($self) = @_; - my $params = $self->_params; - my @param_list = keys %$params; + my ($self) = @_; + my $params = $self->_params; + my @param_list = keys %$params; - my @field_params = grep { /^f\d+$/ } @param_list; - my @field_ids = map { /(\d+)/; $1 } @field_params; - @field_ids = sort { $a <=> $b } @field_ids; - return @field_ids; + my @field_params = grep {/^f\d+$/} @param_list; + my @field_ids = map { /(\d+)/; $1 } @field_params; + @field_ids = sort { $a <=> $b } @field_ids; + return @field_ids; } sub _handle_chart { - my ($self, $chart_id, $clause, $condition) = @_; - my $dbh = Bugzilla->dbh; - my $params = $self->_params; - my ($field, $operator, $value) = $condition->fov; - return if (!defined $field or !defined $operator or !defined $value); - $field = FIELD_MAP->{$field} || $field; - - my $string_value; - if (ref $value eq 'ARRAY') { - # Trim input and ignore blank values. - @$value = map { trim($_) } @$value; - @$value = grep { defined $_ and $_ ne '' } @$value; - return if !@$value; - $string_value = join(',', @$value); - } - else { - return if $value eq ''; - $string_value = $value; - } - - $self->_chart_fields->{$field} - or ThrowCodeError("invalid_field_name", { field => $field }); - trick_taint($field); - - # This is the field as you'd reference it in a SQL statement. - my $full_field = $field =~ /\./ ? $field : "bugs.$field"; - - # "value" and "quoted" are for search functions that always operate - # on a scalar string and never care if they were passed multiple - # parameters. If the user does pass multiple parameters, they will - # become a space-separated string for those search functions. - # - # all_values is for search functions that do operate - # on multiple values, like anyexact. - - my %search_args = ( - chart_id => $chart_id, - sequence => $chart_id, - field => $field, - full_field => $full_field, - operator => $operator, - value => $string_value, - all_values => $value, - joins => [], - bugs_table => 'bugs', - table_suffix => '', - condition => $condition, - ); - $clause->update_search_args(\%search_args); - - $search_args{quoted} = $self->_quote_unless_numeric(\%search_args); - # This should add a "term" selement to %search_args. - $self->do_search_function(\%search_args); - - # If term is left empty, then this means the criteria - # has no effect and can be ignored. - return unless $search_args{term}; - - # All the things here that don't get pulled out of - # %search_args are their original values before - # do_search_function modified them. - $self->search_description({ - field => $field, type => $operator, - value => $string_value, term => $search_args{term}, - }); - - foreach my $join (@{ $search_args{joins} }) { - $join->{bugs_table} = $search_args{bugs_table}; - $join->{table_suffix} = $search_args{table_suffix}; - } - - $condition->translated(\%search_args); + my ($self, $chart_id, $clause, $condition) = @_; + my $dbh = Bugzilla->dbh; + my $params = $self->_params; + my ($field, $operator, $value) = $condition->fov; + return if (!defined $field or !defined $operator or !defined $value); + $field = FIELD_MAP->{$field} || $field; + + my $string_value; + if (ref $value eq 'ARRAY') { + + # Trim input and ignore blank values. + @$value = map { trim($_) } @$value; + @$value = grep { defined $_ and $_ ne '' } @$value; + return if !@$value; + $string_value = join(',', @$value); + } + else { + return if $value eq ''; + $string_value = $value; + } + + $self->_chart_fields->{$field} + or ThrowCodeError("invalid_field_name", {field => $field}); + trick_taint($field); + + # This is the field as you'd reference it in a SQL statement. + my $full_field = $field =~ /\./ ? $field : "bugs.$field"; + + # "value" and "quoted" are for search functions that always operate + # on a scalar string and never care if they were passed multiple + # parameters. If the user does pass multiple parameters, they will + # become a space-separated string for those search functions. + # + # all_values is for search functions that do operate + # on multiple values, like anyexact. + + my %search_args = ( + chart_id => $chart_id, + sequence => $chart_id, + field => $field, + full_field => $full_field, + operator => $operator, + value => $string_value, + all_values => $value, + joins => [], + bugs_table => 'bugs', + table_suffix => '', + condition => $condition, + ); + $clause->update_search_args(\%search_args); + + $search_args{quoted} = $self->_quote_unless_numeric(\%search_args); + + # This should add a "term" selement to %search_args. + $self->do_search_function(\%search_args); + + # If term is left empty, then this means the criteria + # has no effect and can be ignored. + return unless $search_args{term}; + + # All the things here that don't get pulled out of + # %search_args are their original values before + # do_search_function modified them. + $self->search_description({ + field => $field, + type => $operator, + value => $string_value, + term => $search_args{term}, + }); + + foreach my $join (@{$search_args{joins}}) { + $join->{bugs_table} = $search_args{bugs_table}; + $join->{table_suffix} = $search_args{table_suffix}; + } + + $condition->translated(\%search_args); } ################################## @@ -1985,122 +1949,128 @@ sub _handle_chart { # This takes information about the current boolean chart and translates # it into SQL, using the constants at the top of this file. sub do_search_function { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; - - if (my $parse_func = SPECIAL_PARSING->{$field}) { - $self->$parse_func($args); - # Some parsing functions set $term, though most do not. - # For the ones that set $term, we don't need to do any further - # parsing. - return if $args->{term}; - } + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; - my $operator_field_override = $self->_get_operator_field_override(); - my $override = $operator_field_override->{$field}; - # Attachment fields get special handling, if they don't have a specific - # individual override. - if (!$override and $field =~ /^attachments\./) { - $override = $operator_field_override->{attachments}; - } - # If there's still no override, check for an override on the field's type. - if (!$override) { - my $field_obj = $self->_chart_fields->{$field}; - $override = $operator_field_override->{$field_obj->type}; - } + if (my $parse_func = SPECIAL_PARSING->{$field}) { + $self->$parse_func($args); - if ($override) { - my $search_func = $self->_pick_override_function($override, $operator); - $self->$search_func($args) if $search_func; - } + # Some parsing functions set $term, though most do not. + # For the ones that set $term, we don't need to do any further + # parsing. + return if $args->{term}; + } - # Some search functions set $term, and some don't. For the ones that - # don't (or for fields that don't have overrides) we now call the - # direct operator function from OPERATORS. - if (!defined $args->{term}) { - $self->_do_operator_function($args); - } + my $operator_field_override = $self->_get_operator_field_override(); + my $override = $operator_field_override->{$field}; - if (!defined $args->{term}) { - # This field and this type don't work together. Generally, - # this should never be reached, because it should be handled - # explicitly by OPERATOR_FIELD_OVERRIDE. - ThrowUserError("search_field_operator_invalid", - { field => $field, operator => $operator }); - } + # Attachment fields get special handling, if they don't have a specific + # individual override. + if (!$override and $field =~ /^attachments\./) { + $override = $operator_field_override->{attachments}; + } + + # If there's still no override, check for an override on the field's type. + if (!$override) { + my $field_obj = $self->_chart_fields->{$field}; + $override = $operator_field_override->{$field_obj->type}; + } + + if ($override) { + my $search_func = $self->_pick_override_function($override, $operator); + $self->$search_func($args) if $search_func; + } + + # Some search functions set $term, and some don't. For the ones that + # don't (or for fields that don't have overrides) we now call the + # direct operator function from OPERATORS. + if (!defined $args->{term}) { + $self->_do_operator_function($args); + } + + if (!defined $args->{term}) { + + # This field and this type don't work together. Generally, + # this should never be reached, because it should be handled + # explicitly by OPERATOR_FIELD_OVERRIDE. + ThrowUserError("search_field_operator_invalid", + {field => $field, operator => $operator}); + } } # A helper for various search functions that need to run operator # functions directly. sub _do_operator_function { - my ($self, $func_args) = @_; - my $operator = $func_args->{operator}; - my $operator_func = OPERATORS->{$operator} - || ThrowCodeError("search_field_operator_unsupported", - { operator => $operator }); - $self->$operator_func($func_args); + my ($self, $func_args) = @_; + my $operator = $func_args->{operator}; + my $operator_func + = OPERATORS->{$operator} + || ThrowCodeError("search_field_operator_unsupported", + {operator => $operator}); + $self->$operator_func($func_args); } sub _reverse_operator { - my ($self, $operator) = @_; - my $reverse = OPERATOR_REVERSE->{$operator}; - return $reverse if $reverse; - if ($operator =~ s/^not//) { - return $operator; - } - return "not$operator"; + my ($self, $operator) = @_; + my $reverse = OPERATOR_REVERSE->{$operator}; + return $reverse if $reverse; + if ($operator =~ s/^not//) { + return $operator; + } + return "not$operator"; } sub _pick_override_function { - my ($self, $override, $operator) = @_; - my $search_func = $override->{$operator}; - - if (!$search_func) { - # If we don't find an override for one specific operator, - # then there are some special override types: - # _non_changed: For any operator that doesn't have the word - # "changed" in it - # _default: Overrides all operators that aren't explicitly specified. - if ($override->{_non_changed} and $operator !~ /changed/) { - $search_func = $override->{_non_changed}; - } - elsif ($override->{_default}) { - $search_func = $override->{_default}; - } + my ($self, $override, $operator) = @_; + my $search_func = $override->{$operator}; + + if (!$search_func) { + + # If we don't find an override for one specific operator, + # then there are some special override types: + # _non_changed: For any operator that doesn't have the word + # "changed" in it + # _default: Overrides all operators that aren't explicitly specified. + if ($override->{_non_changed} and $operator !~ /changed/) { + $search_func = $override->{_non_changed}; + } + elsif ($override->{_default}) { + $search_func = $override->{_default}; } + } - return $search_func; + return $search_func; } sub _get_operator_field_override { - my $self = shift; - my $cache = Bugzilla->request_cache; + my $self = shift; + my $cache = Bugzilla->request_cache; - return $cache->{operator_field_override} - if defined $cache->{operator_field_override}; + return $cache->{operator_field_override} + if defined $cache->{operator_field_override}; - my %operator_field_override = %{ OPERATOR_FIELD_OVERRIDE() }; - Bugzilla::Hook::process('search_operator_field_override', - { search => $self, - operators => \%operator_field_override }); + my %operator_field_override = %{OPERATOR_FIELD_OVERRIDE()}; + Bugzilla::Hook::process('search_operator_field_override', + {search => $self, operators => \%operator_field_override}); - $cache->{operator_field_override} = \%operator_field_override; - return $cache->{operator_field_override}; + $cache->{operator_field_override} = \%operator_field_override; + return $cache->{operator_field_override}; } sub _get_column_joins { - my $self = shift; - my $cache = Bugzilla->request_cache; + my $self = shift; + my $cache = Bugzilla->request_cache; - return $cache->{column_joins} if defined $cache->{column_joins}; + return $cache->{column_joins} if defined $cache->{column_joins}; - my %column_joins = %{ COLUMN_JOINS() }; - # BMO - add search object to hook - Bugzilla::Hook::process('buglist_column_joins', - { column_joins => \%column_joins, search => $self }); + my %column_joins = %{COLUMN_JOINS()}; - $cache->{column_joins} = \%column_joins; - return $cache->{column_joins}; + # BMO - add search object to hook + Bugzilla::Hook::process('buglist_column_joins', + {column_joins => \%column_joins, search => $self}); + + $cache->{column_joins} = \%column_joins; + return $cache->{column_joins}; } ########################### @@ -2112,33 +2082,34 @@ sub _get_column_joins { # is just a performance optimization, but on SQLite it actually changes # the behavior of some searches. sub _quote_unless_numeric { - my ($self, $args, $value) = @_; - if (!defined $value) { - $value = $args->{value}; - } - my ($field, $operator) = @$args{qw(field operator)}; - - my $numeric_operator = !grep { $_ eq $operator } NON_NUMERIC_OPERATORS; - my $numeric_field = $self->_chart_fields->{$field}->is_numeric; - my $numeric_value = ($value =~ NUMBER_REGEX) ? 1 : 0; - my $is_numeric = $numeric_operator && $numeric_field && $numeric_value; - if ($is_numeric) { - my $quoted = $value; - trick_taint($quoted); - return $quoted; - } - return Bugzilla->dbh->quote($value); + my ($self, $args, $value) = @_; + if (!defined $value) { + $value = $args->{value}; + } + my ($field, $operator) = @$args{qw(field operator)}; + + my $numeric_operator = !grep { $_ eq $operator } NON_NUMERIC_OPERATORS; + my $numeric_field = $self->_chart_fields->{$field}->is_numeric; + my $numeric_value = ($value =~ NUMBER_REGEX) ? 1 : 0; + my $is_numeric = $numeric_operator && $numeric_field && $numeric_value; + if ($is_numeric) { + my $quoted = $value; + trick_taint($quoted); + return $quoted; + } + return Bugzilla->dbh->quote($value); } sub build_subselect { - my ($outer, $inner, $table, $cond, $negate) = @_; - # Execute subselects immediately to avoid dependent subqueries, which are - # large performance hits on MySql - my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond"; - my $dbh = Bugzilla->dbh; - my $list = $dbh->selectcol_arrayref($q); - return $negate ? "1=1" : "1=2" unless @$list; - return $dbh->sql_in($outer, $list, $negate); + my ($outer, $inner, $table, $cond, $negate) = @_; + + # Execute subselects immediately to avoid dependent subqueries, which are + # large performance hits on MySql + my $q = "SELECT DISTINCT $inner FROM $table WHERE $cond"; + my $dbh = Bugzilla->dbh; + my $list = $dbh->selectcol_arrayref($q); + return $negate ? "1=1" : "1=2" unless @$list; + return $dbh->sql_in($outer, $list, $negate); } # Used by anyexact to get the list of input values. This allows us to @@ -2146,70 +2117,71 @@ sub build_subselect { # still accept string values for the boolean charts (and split them on # commas). sub _all_values { - my ($self, $args, $split_on) = @_; - $split_on ||= qr/[\s,]+/; - my $dbh = Bugzilla->dbh; - my $all_values = $args->{all_values}; - - my @array; - if (ref $all_values eq 'ARRAY') { - @array = @$all_values; - } - else { - @array = split($split_on, $all_values); - @array = map { trim($_) } @array; - @array = grep { defined $_ and $_ ne '' } @array; - } + my ($self, $args, $split_on) = @_; + $split_on ||= qr/[\s,]+/; + my $dbh = Bugzilla->dbh; + my $all_values = $args->{all_values}; - if ($args->{field} eq 'resolution') { - @array = map { $_ eq '---' ? '' : $_ } @array; - } + my @array; + if (ref $all_values eq 'ARRAY') { + @array = @$all_values; + } + else { + @array = split($split_on, $all_values); + @array = map { trim($_) } @array; + @array = grep { defined $_ and $_ ne '' } @array; + } + + if ($args->{field} eq 'resolution') { + @array = map { $_ eq '---' ? '' : $_ } @array; + } - return @array; + return @array; } # Support for "any/all/nowordssubstr" comparison type ("words as substrings") sub _substring_terms { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; - # We don't have to (or want to) use _all_values, because we'd just - # split each term on spaces and commas anyway. - my @words = split(/[\s,]+/, $args->{value}); - @words = grep { defined $_ and $_ ne '' } @words; - @words = map { $dbh->quote($_) } @words; - my @terms = map { $dbh->sql_iposition($_, $args->{full_field}) . " > 0" } - @words; - return @terms; + # We don't have to (or want to) use _all_values, because we'd just + # split each term on spaces and commas anyway. + my @words = split(/[\s,]+/, $args->{value}); + @words = grep { defined $_ and $_ ne '' } @words; + @words = map { $dbh->quote($_) } @words; + my @terms + = map { $dbh->sql_iposition($_, $args->{full_field}) . " > 0" } @words; + return @terms; } sub _word_terms { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - my @values = split(/[\s,]+/, $args->{value}); - @values = grep { defined $_ and $_ ne '' } @values; - my @substring_terms = $self->_substring_terms($args); - - my @terms; - my $start = $dbh->WORD_START; - my $end = $dbh->WORD_END; - foreach my $word (@values) { - my $regex = $start . quotemeta($word) . $end; - my $quoted = $dbh->quote($regex); - # We don't have to check the regexp, because we escaped it, so we're - # sure it's valid. - my $regex_term = $dbh->sql_regexp($args->{full_field}, $quoted, - 'no check'); - # Regular expressions are slow--substring searches are faster. - # If we're searching for a word, we're also certain that the - # substring will appear in the value. So we limit first by - # substring and then by a regex that will match just words. - my $substring_term = shift @substring_terms; - push(@terms, "$substring_term AND $regex_term"); - } + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + my @values = split(/[\s,]+/, $args->{value}); + @values = grep { defined $_ and $_ ne '' } @values; + my @substring_terms = $self->_substring_terms($args); + + my @terms; + my $start = $dbh->WORD_START; + my $end = $dbh->WORD_END; + foreach my $word (@values) { + my $regex = $start . quotemeta($word) . $end; + my $quoted = $dbh->quote($regex); - return @terms; + # We don't have to check the regexp, because we escaped it, so we're + # sure it's valid. + my $regex_term = $dbh->sql_regexp($args->{full_field}, $quoted, 'no check'); + + # Regular expressions are slow--substring searches are faster. + # If we're searching for a word, we're also certain that the + # substring will appear in the value. So we limit first by + # substring and then by a regex that will match just words. + my $substring_term = shift @substring_terms; + push(@terms, "$substring_term AND $regex_term"); + } + + return @terms; } ##################################### @@ -2217,106 +2189,115 @@ sub _word_terms { ##################################### sub _timestamp_translate { - my ($self, $ignore_time, $args) = @_; - my $value = $args->{value}; - my $dbh = Bugzilla->dbh; + my ($self, $ignore_time, $args) = @_; + my $value = $args->{value}; + my $dbh = Bugzilla->dbh; - return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i; + return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i; - $value = SqlifyDate($value); - # By default, the time is appended to the date, which we don't always want. - if ($ignore_time) { - ($value) = split(/\s/, $value); - } - $args->{value} = $value; - $args->{quoted} = $dbh->quote($value); + $value = SqlifyDate($value); + + # By default, the time is appended to the date, which we don't always want. + if ($ignore_time) { + ($value) = split(/\s/, $value); + } + $args->{value} = $value; + $args->{quoted} = $dbh->quote($value); } sub _datetime_translate { - return shift->_timestamp_translate(0, @_); + return shift->_timestamp_translate(0, @_); } sub _last_visit_datetime { - my ($self, $args) = @_; - my $value = $args->{value}; - - $self->_datetime_translate($args); - if ($value eq $args->{value}) { - # Failed to translate a datetime. let's try the pronoun expando. - if ($value eq '%last_changed%') { - $self->_add_extra_column('changeddate'); - $args->{value} = $args->{quoted} = 'bugs.delta_ts'; - } + my ($self, $args) = @_; + my $value = $args->{value}; + + $self->_datetime_translate($args); + if ($value eq $args->{value}) { + + # Failed to translate a datetime. let's try the pronoun expando. + if ($value eq '%last_changed%') { + $self->_add_extra_column('changeddate'); + $args->{value} = $args->{quoted} = 'bugs.delta_ts'; } + } } sub _date_translate { - return shift->_timestamp_translate(1, @_); + return shift->_timestamp_translate(1, @_); } sub SqlifyDate { - my ($str) = @_; - my $fmt = "%Y-%m-%d %H:%M:%S"; - $str = "" if (!defined $str || lc($str) eq 'now'); - if ($str eq "") { - my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time()); - return sprintf("%4d-%02d-%02d 00:00:00", $year+1900, $month+1, $mday); - } - - if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date - my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time); - my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); - if ($sign && $sign eq '+') { $amount = -$amount; } - $startof = 1 if $amount == 0; - if ($unit eq 'w') { # convert weeks to days - $amount = 7*$amount; - $amount += $wday if $startof; - $unit = 'd'; - } - if ($unit eq 'd') { - if ($startof) { - $fmt = "%Y-%m-%d 00:00:00"; - $date -= $sec + 60*$min + 3600*$hour; - } - $date -= 24*3600*$amount; - return time2str($fmt, $date); - } - elsif ($unit eq 'y') { - if ($startof) { - return sprintf("%4d-01-01 00:00:00", $year+1900-$amount); - } - else { - return sprintf("%4d-%02d-%02d %02d:%02d:%02d", - $year+1900-$amount, $month+1, $mday, $hour, $min, $sec); - } - } - elsif ($unit eq 'm') { - $month -= $amount; - while ($month<0) { $year--; $month += 12; } - if ($startof) { - return sprintf("%4d-%02d-01 00:00:00", $year+1900, $month+1); - } - else { - return sprintf("%4d-%02d-%02d %02d:%02d:%02d", - $year+1900, $month+1, $mday, $hour, $min, $sec); - } - } - elsif ($unit eq 'h') { - # Special case for 'beginning of an hour' - if ($startof) { - $fmt = "%Y-%m-%d %H:00:00"; - } - $date -= 3600*$amount; - return time2str($fmt, $date); - } - return undef; # should not happen due to regexp at top - } - my $date = str2time($str); - if (!defined($date)) { - ThrowUserError("illegal_date", { date => $str }); - } - return time2str($fmt, $date); + my ($str) = @_; + my $fmt = "%Y-%m-%d %H:%M:%S"; + $str = "" if (!defined $str || lc($str) eq 'now'); + if ($str eq "") { + my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime(time()); + return sprintf("%4d-%02d-%02d 00:00:00", $year + 1900, $month + 1, $mday); + } + + if ($str =~ /^(-|\+)?(\d+)([hdwmy])(s?)$/i) { # relative date + my ($sign, $amount, $unit, $startof, $date) = ($1, $2, lc $3, lc $4, time); + my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); + if ($sign && $sign eq '+') { $amount = -$amount; } + $startof = 1 if $amount == 0; + if ($unit eq 'w') { # convert weeks to days + $amount = 7 * $amount; + $amount += $wday if $startof; + $unit = 'd'; + } + if ($unit eq 'd') { + if ($startof) { + $fmt = "%Y-%m-%d 00:00:00"; + $date -= $sec + 60 * $min + 3600 * $hour; + } + $date -= 24 * 3600 * $amount; + return time2str($fmt, $date); + } + elsif ($unit eq 'y') { + if ($startof) { + return sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount); + } + else { + return sprintf( + "%4d-%02d-%02d %02d:%02d:%02d", + $year + 1900 - $amount, + $month + 1, $mday, $hour, $min, $sec + ); + } + } + elsif ($unit eq 'm') { + $month -= $amount; + while ($month < 0) { $year--; $month += 12; } + if ($startof) { + return sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1); + } + else { + return sprintf( + "%4d-%02d-%02d %02d:%02d:%02d", + $year + 1900, + $month + 1, $mday, $hour, $min, $sec + ); + } + } + elsif ($unit eq 'h') { + + # Special case for 'beginning of an hour' + if ($startof) { + $fmt = "%Y-%m-%d %H:00:00"; + } + $date -= 3600 * $amount; + return time2str($fmt, $date); + } + return undef; # should not happen due to regexp at top + } + my $date = str2time($str); + if (!defined($date)) { + ThrowUserError("illegal_date", {date => $str}); + } + return time2str($fmt, $date); } ###################################### @@ -2324,163 +2305,170 @@ sub SqlifyDate { ###################################### sub pronoun { - my ($noun, $user) = (@_); - if ($noun eq "%user%") { - if ($user->id) { - return $user->id; - } else { - ThrowUserError('login_required_for_pronoun'); - } - } - if ($noun eq "%reporter%") { - return "bugs.reporter"; - } - if ($noun eq "%assignee%") { - return "bugs.assigned_to"; + my ($noun, $user) = (@_); + if ($noun eq "%user%") { + if ($user->id) { + return $user->id; } - if ($noun eq "%qacontact%") { - return "COALESCE(bugs.qa_contact,0)"; + else { + ThrowUserError('login_required_for_pronoun'); } - return 0; + } + if ($noun eq "%reporter%") { + return "bugs.reporter"; + } + if ($noun eq "%assignee%") { + return "bugs.assigned_to"; + } + if ($noun eq "%qacontact%") { + return "COALESCE(bugs.qa_contact,0)"; + } + return 0; } sub _contact_pronoun { - my ($self, $args) = @_; - my $value = $args->{value}; - my $user = $self->_user; + my ($self, $args) = @_; + my $value = $args->{value}; + my $user = $self->_user; - if ($value =~ /^\%group\.[^%]+%$/) { - $self->_contact_exact_group($args); - } - elsif ($value =~ /^(%\w+%)$/) { - $args->{value} = pronoun($1, $user); - $args->{quoted} = $args->{value}; - $args->{value_is_id} = 1; - } + if ($value =~ /^\%group\.[^%]+%$/) { + $self->_contact_exact_group($args); + } + elsif ($value =~ /^(%\w+%)$/) { + $args->{value} = pronoun($1, $user); + $args->{quoted} = $args->{value}; + $args->{value_is_id} = 1; + } } sub _contact_exact_group { - my ($self, $args) = @_; - my ($value, $operator, $field, $chart_id, $joins) = - @$args{qw(value operator field chart_id joins)}; - my $dbh = Bugzilla->dbh; - my $user = $self->_user; - - # We already know $value will match this regexp, else we wouldn't be here. - $value =~ /\%group\.([^%]+)%/; - my $group_name = $1; - my $group = Bugzilla::Group->check({ name => $group_name, _error => 'invalid_group_name' }); - # Pass $group_name instead of $group->name to the error message - # to not leak the existence of the group. - $user->in_group($group) - || ThrowUserError('invalid_group_name', { name => $group_name }); - # Now that we know the user belongs to this group, it's safe - # to disclose more information. - $group->check_members_are_visible(); - - my $group_ids = Bugzilla::Group->flatten_group_membership($group->id); - my $table = "user_group_map_$chart_id"; - my $join = { - table => 'user_group_map', - as => $table, - from => $field, - to => 'user_id', - extra => [$dbh->sql_in("$table.group_id", $group_ids), - "$table.isbless = 0"], - }; - push(@$joins, $join); - if ($operator =~ /^not/) { - $args->{term} = "$table.group_id IS NULL"; - } - else { - $args->{term} = "$table.group_id IS NOT NULL"; - } + my ($self, $args) = @_; + my ($value, $operator, $field, $chart_id, $joins) + = @$args{qw(value operator field chart_id joins)}; + my $dbh = Bugzilla->dbh; + my $user = $self->_user; + + # We already know $value will match this regexp, else we wouldn't be here. + $value =~ /\%group\.([^%]+)%/; + my $group_name = $1; + my $group = Bugzilla::Group->check( + {name => $group_name, _error => 'invalid_group_name'}); + + # Pass $group_name instead of $group->name to the error message + # to not leak the existence of the group. + $user->in_group($group) + || ThrowUserError('invalid_group_name', {name => $group_name}); + + # Now that we know the user belongs to this group, it's safe + # to disclose more information. + $group->check_members_are_visible(); + + my $group_ids = Bugzilla::Group->flatten_group_membership($group->id); + my $table = "user_group_map_$chart_id"; + my $join = { + table => 'user_group_map', + as => $table, + from => $field, + to => 'user_id', + extra => [$dbh->sql_in("$table.group_id", $group_ids), "$table.isbless = 0"], + }; + push(@$joins, $join); + if ($operator =~ /^not/) { + $args->{term} = "$table.group_id IS NULL"; + } + else { + $args->{term} = "$table.group_id IS NOT NULL"; + } } sub _cc_pronoun { - my ($self, $args) = @_; - my ($full_field, $value) = @$args{qw(full_field value)}; - my $user = $self->_user; + my ($self, $args) = @_; + my ($full_field, $value) = @$args{qw(full_field value)}; + my $user = $self->_user; - if ($value =~ /\%group/) { - return $self->_cc_exact_group($args); - } - elsif ($value =~ /^(%\w+%)$/) { - $args->{value} = pronoun($1, $user); - $args->{quoted} = $args->{value}; - $args->{value_is_id} = 1; - } + if ($value =~ /\%group/) { + return $self->_cc_exact_group($args); + } + elsif ($value =~ /^(%\w+%)$/) { + $args->{value} = pronoun($1, $user); + $args->{quoted} = $args->{value}; + $args->{value_is_id} = 1; + } } sub _cc_exact_group { - my ($self, $args) = @_; - my ($chart_id, $sequence, $joins, $operator, $value) = - @$args{qw(chart_id sequence joins operator value)}; - my $user = $self->_user; - my $dbh = Bugzilla->dbh; - - $value =~ m/%group\.([^%]+)%/; - my $group = Bugzilla::Group->check({ name => $1, _error => 'invalid_group_name' }); - $group->check_members_are_visible(); - $user->in_group($group) - || ThrowUserError('invalid_group_name', {name => $group->name}); - - my $all_groups = Bugzilla::Group->flatten_group_membership($group->id); - - # This is for the email1, email2, email3 fields from query.cgi. - if ($chart_id eq "") { - $chart_id = "CC$$sequence"; - $args->{sequence}++; - } - - my $cc_table = "cc_$chart_id"; - push(@$joins, { table => 'cc', as => $cc_table }); - my $group_table = "user_group_map_$chart_id"; - my $group_join = { - table => 'user_group_map', - as => $group_table, - from => "$cc_table.who", - to => 'user_id', - extra => [$dbh->sql_in("$group_table.group_id", $all_groups), - "$group_table.isbless = 0"], - }; - push(@$joins, $group_join); - - if ($operator =~ /^not/) { - $args->{term} = "$group_table.group_id IS NULL"; - } - else { - $args->{term} = "$group_table.group_id IS NOT NULL"; - } + my ($self, $args) = @_; + my ($chart_id, $sequence, $joins, $operator, $value) + = @$args{qw(chart_id sequence joins operator value)}; + my $user = $self->_user; + my $dbh = Bugzilla->dbh; + + $value =~ m/%group\.([^%]+)%/; + my $group + = Bugzilla::Group->check({name => $1, _error => 'invalid_group_name'}); + $group->check_members_are_visible(); + $user->in_group($group) + || ThrowUserError('invalid_group_name', {name => $group->name}); + + my $all_groups = Bugzilla::Group->flatten_group_membership($group->id); + + # This is for the email1, email2, email3 fields from query.cgi. + if ($chart_id eq "") { + $chart_id = "CC$$sequence"; + $args->{sequence}++; + } + + my $cc_table = "cc_$chart_id"; + push(@$joins, {table => 'cc', as => $cc_table}); + my $group_table = "user_group_map_$chart_id"; + my $group_join = { + table => 'user_group_map', + as => $group_table, + from => "$cc_table.who", + to => 'user_id', + extra => [ + $dbh->sql_in("$group_table.group_id", $all_groups), + "$group_table.isbless = 0" + ], + }; + push(@$joins, $group_join); + + if ($operator =~ /^not/) { + $args->{term} = "$group_table.group_id IS NULL"; + } + else { + $args->{term} = "$group_table.group_id IS NOT NULL"; + } } # XXX This should probably be merged with cc_pronoun. sub _commenter_pronoun { - my ($self, $args) = @_; - my $value = $args->{value}; - my $user = $self->_user; - - if ($value =~ /^(%\w+%)$/) { - $args->{value} = pronoun($1, $user); - $args->{quoted} = $args->{value}; - $args->{value_is_id} = 1; - } + my ($self, $args) = @_; + my $value = $args->{value}; + my $user = $self->_user; + + if ($value =~ /^(%\w+%)$/) { + $args->{value} = pronoun($1, $user); + $args->{quoted} = $args->{value}; + $args->{value_is_id} = 1; + } } # XXX only works with %user% currently sub _triage_owner_pronoun { - my ($self, $args) = @_; - my $value = $args->{value}; - my $user = $self->_user; - if ($value eq "%user%") { - if ($user->id) { - $args->{value} = $user->id; - $args->{quoted} = $args->{value}; - $args->{value_is_id} = 1; - } else { - ThrowUserError('login_required_for_pronoun'); - } + my ($self, $args) = @_; + my $value = $args->{value}; + my $user = $self->_user; + if ($value eq "%user%") { + if ($user->id) { + $args->{value} = $user->id; + $args->{quoted} = $args->{value}; + $args->{value_is_id} = 1; + } + else { + ThrowUserError('login_required_for_pronoun'); } + } } ##################################################################### @@ -2488,571 +2476,584 @@ sub _triage_owner_pronoun { ##################################################################### sub _invalid_combination { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; - ThrowUserError('search_field_operator_invalid', - { field => $field, operator => $operator }); + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); } # For all the "user" fields--assigned_to, reporter, qa_contact, # cc, commenter, requestee, etc. sub _user_nonchanged { - my ($self, $args) = @_; - my ($field, $operator, $chart_id, $sequence, $joins) = - @$args{qw(field operator chart_id sequence joins)}; - - my $is_in_other_table; - if (my $join = USER_FIELDS->{$field}->{join}) { - $is_in_other_table = 1; - my $as = "${field}_$chart_id"; - # Needed for setters.login_name and requestees.login_name. - # Otherwise when we try to join "profiles" below, we'd get - # something like "setters.login_name.login_name" in the "from". - $as =~ s/\./_/g; - # This helps implement the email1, email2, etc. parameters. - if ($chart_id =~ /default/) { - $as .= "_$sequence"; - } - my $isprivate = USER_FIELDS->{$field}->{isprivate}; - my $extra = ($isprivate and !$self->_user->is_insider) - ? ["$as.isprivate = 0"] : []; - # We want to copy $join so as not to modify USER_FIELDS. - push(@$joins, { %$join, as => $as, extra => $extra }); - my $search_field = USER_FIELDS->{$field}->{field}; - $args->{full_field} = "$as.$search_field"; + my ($self, $args) = @_; + my ($field, $operator, $chart_id, $sequence, $joins) + = @$args{qw(field operator chart_id sequence joins)}; + + my $is_in_other_table; + if (my $join = USER_FIELDS->{$field}->{join}) { + $is_in_other_table = 1; + my $as = "${field}_$chart_id"; + + # Needed for setters.login_name and requestees.login_name. + # Otherwise when we try to join "profiles" below, we'd get + # something like "setters.login_name.login_name" in the "from". + $as =~ s/\./_/g; + + # This helps implement the email1, email2, etc. parameters. + if ($chart_id =~ /default/) { + $as .= "_$sequence"; + } + my $isprivate = USER_FIELDS->{$field}->{isprivate}; + my $extra + = ($isprivate and !$self->_user->is_insider) ? ["$as.isprivate = 0"] : []; + + # We want to copy $join so as not to modify USER_FIELDS. + push(@$joins, {%$join, as => $as, extra => $extra}); + my $search_field = USER_FIELDS->{$field}->{field}; + $args->{full_field} = "$as.$search_field"; + } + + my $is_nullable = USER_FIELDS->{$field}->{nullable}; + my $null_alternate = "''"; + + # When using a pronoun, we use the userid, and we don't have to + # join the profiles table. + if ($args->{value_is_id}) { + $null_alternate = 0; + } + else { + my $as = "name_${field}_$chart_id"; + + # For fields with periods in their name. + $as =~ s/\./_/; + my $join = { + table => 'profiles', + as => $as, + from => $args->{full_field}, + to => 'userid', + join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef, + }; + push(@$joins, $join); + $args->{full_field} = "$as.login_name"; + } + + # We COALESCE fields that can be NULL, to make "not"-style operators + # continue to work properly. For example, "qa_contact is not equal to bob" + # should also show bugs where the qa_contact is NULL. With COALESCE, + # it does. + if ($is_nullable) { + $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)"; + } + + # For fields whose values are stored in other tables, negation (NOT) + # only works properly if we put the condition into the JOIN instead + # of the WHERE. + if ($is_in_other_table) { + + # Using the last join works properly whether we're searching based + # on userid or login_name. + my $last_join = $joins->[-1]; + + # For negative operators, the system we're using here + # only works properly if we reverse the operator and check IS NULL + # in the WHERE. + my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0; + if ($is_negative) { + $args->{operator} = $self->_reverse_operator($operator); } - - my $is_nullable = USER_FIELDS->{$field}->{nullable}; - my $null_alternate = "''"; - # When using a pronoun, we use the userid, and we don't have to - # join the profiles table. - if ($args->{value_is_id}) { - $null_alternate = 0; + $self->_do_operator_function($args); + push(@{$last_join->{extra}}, $args->{term}); + + # For login_name searches, we only want a single join. + # So we create a subselect table out of our two joins. This makes + # negation (NOT) work properly for values that are in other + # tables. + if ($last_join->{table} eq 'profiles') { + pop @$joins; + $last_join->{join} = 'INNER'; + my ($join_sql) = $self->_translate_join($last_join); + my $first_join = $joins->[-1]; + my $as = $first_join->{as}; + my $table = $first_join->{table}; + my $columns = "bug_id"; + $columns .= ",isprivate" if @{$first_join->{extra}}; + my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql"; + $first_join->{table} = "($new_table)"; + + # We always want to LEFT JOIN the generated table. + delete $first_join->{join}; + + # To support OR charts, we need multiple tables. + my $new_as = $first_join->{as} . "_$sequence"; + $_ =~ s/\Q$as\E/$new_as/ foreach @{$first_join->{extra}}; + $first_join->{as} = $new_as; + $last_join = $first_join; + } + + # If we're joining the first table (we're using a pronoun and + # searching by user id) then we need to check $other_table->{field}. + my $check_field = $last_join->{as} . '.bug_id'; + if ($is_negative) { + $args->{term} = "$check_field IS NULL"; } else { - my $as = "name_${field}_$chart_id"; - # For fields with periods in their name. - $as =~ s/\./_/; - my $join = { - table => 'profiles', - as => $as, - from => $args->{full_field}, - to => 'userid', - join => (!$is_in_other_table and !$is_nullable) ? 'INNER' : undef, - }; - push(@$joins, $join); - $args->{full_field} = "$as.login_name"; - } - - # We COALESCE fields that can be NULL, to make "not"-style operators - # continue to work properly. For example, "qa_contact is not equal to bob" - # should also show bugs where the qa_contact is NULL. With COALESCE, - # it does. - if ($is_nullable) { - $args->{full_field} = "COALESCE($args->{full_field}, $null_alternate)"; - } - - # For fields whose values are stored in other tables, negation (NOT) - # only works properly if we put the condition into the JOIN instead - # of the WHERE. - if ($is_in_other_table) { - # Using the last join works properly whether we're searching based - # on userid or login_name. - my $last_join = $joins->[-1]; - - # For negative operators, the system we're using here - # only works properly if we reverse the operator and check IS NULL - # in the WHERE. - my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0; - if ($is_negative) { - $args->{operator} = $self->_reverse_operator($operator); - } - $self->_do_operator_function($args); - push(@{ $last_join->{extra} }, $args->{term}); - - # For login_name searches, we only want a single join. - # So we create a subselect table out of our two joins. This makes - # negation (NOT) work properly for values that are in other - # tables. - if ($last_join->{table} eq 'profiles') { - pop @$joins; - $last_join->{join} = 'INNER'; - my ($join_sql) = $self->_translate_join($last_join); - my $first_join = $joins->[-1]; - my $as = $first_join->{as}; - my $table = $first_join->{table}; - my $columns = "bug_id"; - $columns .= ",isprivate" if @{ $first_join->{extra} }; - my $new_table = "SELECT DISTINCT $columns FROM $table AS $as $join_sql"; - $first_join->{table} = "($new_table)"; - # We always want to LEFT JOIN the generated table. - delete $first_join->{join}; - # To support OR charts, we need multiple tables. - my $new_as = $first_join->{as} . "_$sequence"; - $_ =~ s/\Q$as\E/$new_as/ foreach @{ $first_join->{extra} }; - $first_join->{as} = $new_as; - $last_join = $first_join; - } - - # If we're joining the first table (we're using a pronoun and - # searching by user id) then we need to check $other_table->{field}. - my $check_field = $last_join->{as} . '.bug_id'; - if ($is_negative) { - $args->{term} = "$check_field IS NULL"; - } - else { - $args->{term} = "$check_field IS NOT NULL"; - } + $args->{term} = "$check_field IS NOT NULL"; } + } } # XXX This duplicates having Commenter as a search field. sub _long_desc_changedby { - my ($self, $args) = @_; - my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)}; + my ($self, $args) = @_; + my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)}; - my $table = "longdescs_$chart_id"; - push(@$joins, { table => 'longdescs', as => $table }); - my $user_id = login_to_id($value, THROW_ERROR); - $args->{term} = "$table.who = $user_id"; + my $table = "longdescs_$chart_id"; + push(@$joins, {table => 'longdescs', as => $table}); + my $user_id = login_to_id($value, THROW_ERROR); + $args->{term} = "$table.who = $user_id"; } sub _long_desc_changedbefore_after { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins) = - @$args{qw(chart_id operator value joins)}; - my $dbh = Bugzilla->dbh; - - my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; - my $table = "longdescs_$chart_id"; - my $sql_date = $dbh->quote(SqlifyDate($value)); - my $join = { - table => 'longdescs', - as => $table, - extra => ["$table.bug_when $sql_operator $sql_date"], - }; - push(@$joins, $join); - $args->{term} = "$table.bug_when IS NOT NULL"; - - # If the user is not part of the insiders group, they cannot see - # private comments - if (!$self->_user->is_insider) { - $args->{term} .= " AND $table.isprivate = 0"; - } + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins) + = @$args{qw(chart_id operator value joins)}; + my $dbh = Bugzilla->dbh; + + my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; + my $table = "longdescs_$chart_id"; + my $sql_date = $dbh->quote(SqlifyDate($value)); + my $join = { + table => 'longdescs', + as => $table, + extra => ["$table.bug_when $sql_operator $sql_date"], + }; + push(@$joins, $join); + $args->{term} = "$table.bug_when IS NOT NULL"; + + # If the user is not part of the insiders group, they cannot see + # private comments + if (!$self->_user->is_insider) { + $args->{term} .= " AND $table.isprivate = 0"; + } } sub _long_desc_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins, $bugs_table) = - @$args{qw(chart_id operator value joins bugs_table)}; - - if ($operator =~ /^is(not)?empty$/) { - $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); - return; - } - my $dbh = Bugzilla->dbh; - - my $table = "longdescs_$chart_id"; - my $join_args = { - chart_id => $chart_id, - sequence => $chart_id, - field => 'longdesc', - full_field => "$table.thetext", - operator => $operator, - value => $value, - all_values => $value, - quoted => $dbh->quote($value), - joins => [], - bugs_table => $bugs_table, - }; - $self->_do_operator_function($join_args); - - # If the user is not part of the insiders group, they cannot see - # private comments - if (!$self->_user->is_insider) { - $join_args->{term} .= ($join_args->{term} ? " AND " : "") - . "$table.isprivate = 0"; - } - - my $join = { - table => 'longdescs', - as => $table, - extra => [ $join_args->{term} ], - }; - push(@$joins, $join); - - $args->{term} = "$table.comment_id IS NOT NULL"; + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins, $bugs_table) + = @$args{qw(chart_id operator value joins bugs_table)}; + + if ($operator =~ /^is(not)?empty$/) { + $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); + return; + } + my $dbh = Bugzilla->dbh; + + my $table = "longdescs_$chart_id"; + my $join_args = { + chart_id => $chart_id, + sequence => $chart_id, + field => 'longdesc', + full_field => "$table.thetext", + operator => $operator, + value => $value, + all_values => $value, + quoted => $dbh->quote($value), + joins => [], + bugs_table => $bugs_table, + }; + $self->_do_operator_function($join_args); + + # If the user is not part of the insiders group, they cannot see + # private comments + if (!$self->_user->is_insider) { + $join_args->{term} + .= ($join_args->{term} ? " AND " : "") . "$table.isprivate = 0"; + } + + my $join = {table => 'longdescs', as => $table, extra => [$join_args->{term}],}; + push(@$joins, $join); + + $args->{term} = "$table.comment_id IS NOT NULL"; } sub _content_matches { - my ($self, $args) = @_; - my ($chart_id, $joins, $fields, $operator, $value) = - @$args{qw(chart_id joins fields operator value)}; - my $dbh = Bugzilla->dbh; - - # "content" is an alias for columns containing text for which we - # can search a full-text index and retrieve results by relevance, - # currently just bug comments (and summaries to some degree). - # There's only one way to search a full-text index, so we only - # accept the "matches" operator, which is specific to full-text - # index searches. - - # Add the fulltext table to the query so we can search on it. - my $table = "bugs_fulltext_$chart_id"; - my $comments_col = "comments"; - $comments_col = "comments_noprivate" unless $self->_user->is_insider; - push(@$joins, { table => 'bugs_fulltext', as => $table }); - - # Create search terms to add to the SELECT and WHERE clauses. - my ($term1, $rterm1) = - $dbh->sql_fulltext_search("$table.$comments_col", $value); - my ($term2, $rterm2) = - $dbh->sql_fulltext_search("$table.short_desc", $value); - $rterm1 = $term1 if !$rterm1; - $rterm2 = $term2 if !$rterm2; - - # The term to use in the WHERE clause. - my $term = "$term1 OR $term2"; - if ($operator =~ /not/i) { - $term = "NOT($term)"; - } - $args->{term} = $term; - - # In order to sort by relevance (in case the user requests it), - # we SELECT the relevance value so we can add it to the ORDER BY - # clause. Every time a new fulltext chart isadded, this adds more - # terms to the relevance sql. - # - # We build the relevance SQL by modifying the COLUMNS list directly, - # which is kind of a hack but works. - my $current = $self->COLUMNS->{'relevance'}->{name}; - $current = $current ? "$current + " : ''; - # For NOT searches, we just add 0 to the relevance. - my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)"; - $self->COLUMNS->{'relevance'}->{name} = $select_term; + my ($self, $args) = @_; + my ($chart_id, $joins, $fields, $operator, $value) + = @$args{qw(chart_id joins fields operator value)}; + my $dbh = Bugzilla->dbh; + + # "content" is an alias for columns containing text for which we + # can search a full-text index and retrieve results by relevance, + # currently just bug comments (and summaries to some degree). + # There's only one way to search a full-text index, so we only + # accept the "matches" operator, which is specific to full-text + # index searches. + + # Add the fulltext table to the query so we can search on it. + my $table = "bugs_fulltext_$chart_id"; + my $comments_col = "comments"; + $comments_col = "comments_noprivate" unless $self->_user->is_insider; + push(@$joins, {table => 'bugs_fulltext', as => $table}); + + # Create search terms to add to the SELECT and WHERE clauses. + my ($term1, $rterm1) + = $dbh->sql_fulltext_search("$table.$comments_col", $value); + my ($term2, $rterm2) = $dbh->sql_fulltext_search("$table.short_desc", $value); + $rterm1 = $term1 if !$rterm1; + $rterm2 = $term2 if !$rterm2; + + # The term to use in the WHERE clause. + my $term = "$term1 OR $term2"; + if ($operator =~ /not/i) { + $term = "NOT($term)"; + } + $args->{term} = $term; + + # In order to sort by relevance (in case the user requests it), + # we SELECT the relevance value so we can add it to the ORDER BY + # clause. Every time a new fulltext chart isadded, this adds more + # terms to the relevance sql. + # + # We build the relevance SQL by modifying the COLUMNS list directly, + # which is kind of a hack but works. + my $current = $self->COLUMNS->{'relevance'}->{name}; + $current = $current ? "$current + " : ''; + + # For NOT searches, we just add 0 to the relevance. + my $select_term = $operator =~ /not/ ? 0 : "($current$rterm1 + $rterm2)"; + $self->COLUMNS->{'relevance'}->{name} = $select_term; } sub _long_descs_count { - my ($self, $args) = @_; - my ($chart_id, $joins) = @$args{qw(chart_id joins)}; - my $table = "longdescs_count_$chart_id"; - my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0"; - my $join = { - table => "(SELECT bug_id, COUNT(*) AS num" - . " FROM longdescs $extra GROUP BY bug_id)", - as => $table, - }; - push(@$joins, $join); - $args->{full_field} = "${table}.num"; + my ($self, $args) = @_; + my ($chart_id, $joins) = @$args{qw(chart_id joins)}; + my $table = "longdescs_count_$chart_id"; + my $extra = $self->_user->is_insider ? "" : "WHERE isprivate = 0"; + my $join = { + table => "(SELECT bug_id, COUNT(*) AS num" + . " FROM longdescs $extra GROUP BY bug_id)", + as => $table, + }; + push(@$joins, $join); + $args->{full_field} = "${table}.num"; } sub _work_time_changedby { - my ($self, $args) = @_; - my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)}; + my ($self, $args) = @_; + my ($chart_id, $joins, $value) = @$args{qw(chart_id joins value)}; - my $table = "longdescs_$chart_id"; - push(@$joins, { table => 'longdescs', as => $table }); - my $user_id = login_to_id($value, THROW_ERROR); - $args->{term} = "$table.who = $user_id AND $table.work_time != 0"; + my $table = "longdescs_$chart_id"; + push(@$joins, {table => 'longdescs', as => $table}); + my $user_id = login_to_id($value, THROW_ERROR); + $args->{term} = "$table.who = $user_id AND $table.work_time != 0"; } sub _work_time_changedbefore_after { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins) = - @$args{qw(chart_id operator value joins)}; - my $dbh = Bugzilla->dbh; - - my $table = "longdescs_$chart_id"; - my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; - my $sql_date = $dbh->quote(SqlifyDate($value)); - my $join = { - table => 'longdescs', - as => $table, - extra => ["$table.work_time != 0", - "$table.bug_when $sql_operator $sql_date"], - }; - push(@$joins, $join); + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins) + = @$args{qw(chart_id operator value joins)}; + my $dbh = Bugzilla->dbh; - $args->{term} = "$table.bug_when IS NOT NULL"; + my $table = "longdescs_$chart_id"; + my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; + my $sql_date = $dbh->quote(SqlifyDate($value)); + my $join = { + table => 'longdescs', + as => $table, + extra => ["$table.work_time != 0", "$table.bug_when $sql_operator $sql_date"], + }; + push(@$joins, $join); + + $args->{term} = "$table.bug_when IS NOT NULL"; } sub _work_time { - my ($self, $args) = @_; - $self->_add_extra_column('actual_time'); - $args->{full_field} = $self->COLUMNS->{actual_time}->{name}; + my ($self, $args) = @_; + $self->_add_extra_column('actual_time'); + $args->{full_field} = $self->COLUMNS->{actual_time}->{name}; } sub _percentage_complete { - my ($self, $args) = @_; + my ($self, $args) = @_; - $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name}; + $args->{full_field} = $self->COLUMNS->{percentage_complete}->{name}; - # We need actual_time in _select_columns, otherwise we can't use - # it in the expression for searching percentage_complete. - $self->_add_extra_column('actual_time'); + # We need actual_time in _select_columns, otherwise we can't use + # it in the expression for searching percentage_complete. + $self->_add_extra_column('actual_time'); } sub _last_visit_ts { - my ($self, $args) = @_; + my ($self, $args) = @_; - $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name}; - $self->_add_extra_column('last_visit_ts'); + $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name}; + $self->_add_extra_column('last_visit_ts'); } sub _bug_interest_ts { - my ($self, $args) = @_; + my ($self, $args) = @_; - $args->{full_field} = $self->COLUMNS->{bug_interest_ts}->{name}; - $self->_add_extra_column('bug_interest_ts'); + $args->{full_field} = $self->COLUMNS->{bug_interest_ts}->{name}; + $self->_add_extra_column('bug_interest_ts'); } sub _invalid_operator { - my ($self, $args) = @_; + my ($self, $args) = @_; - ThrowUserError('search_field_operator_invalid', - { field => $args->{field}, - operator => $args->{operator} }); + ThrowUserError('search_field_operator_invalid', + {field => $args->{field}, operator => $args->{operator}}); } sub _days_elapsed { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; - $args->{full_field} = "(" . $dbh->sql_to_days('NOW()') . " - " . - $dbh->sql_to_days('bugs.delta_ts') . ")"; + $args->{full_field} + = "(" + . $dbh->sql_to_days('NOW()') . " - " + . $dbh->sql_to_days('bugs.delta_ts') . ")"; } sub _assignee_last_login { - my ($self, $args) = @_; - - push @{ $args->{joins} }, { - as => 'assignee', - table => 'profiles', - from => 'assigned_to', - to => 'userid', - join => 'INNER', + my ($self, $args) = @_; + + push @{$args->{joins}}, + { + as => 'assignee', + table => 'profiles', + from => 'assigned_to', + to => 'userid', + join => 'INNER', }; - # coalesce to 1998 to make it easy to search for users who haven't logged - # in since we added last_seen_date - $args->{full_field} = "COALESCE(assignee.last_seen_date, '1998-01-01')"; + + # coalesce to 1998 to make it easy to search for users who haven't logged + # in since we added last_seen_date + $args->{full_field} = "COALESCE(assignee.last_seen_date, '1998-01-01')"; } sub _component_nonchanged { - my ($self, $args) = @_; + my ($self, $args) = @_; - $args->{full_field} = "components.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("bugs.component_id", - "components.id", "components", $args->{term}); + $args->{full_field} = "components.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} + = build_subselect("bugs.component_id", "components.id", "components", + $args->{term}); } sub _product_nonchanged { - my ($self, $args) = @_; - - # BMO - product aliases - # swap out old product names for new ones - if (ref($args->{all_values})) { - my $aliased; - foreach my $value (@{ $args->{all_values} }) { - if (exists PRODUCT_ALIASES->{lc($value)}) { - $value = PRODUCT_ALIASES->{lc($value)}; - $aliased = 1; - } - } - if ($aliased) { - $args->{value} = join(',', @{ $args->{all_values} }); - $args->{quoted} = Bugzilla->dbh->quote($args->{value}); - } - } - elsif (exists PRODUCT_ALIASES->{lc($args->{value})}) { - $args->{value} = PRODUCT_ALIASES->{lc($args->{value})}; - $args->{all_values} = $args->{value}; - $args->{quoted} = Bugzilla->dbh->quote($args->{value}); - } - - # Generate the restriction condition - $args->{full_field} = "products.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("bugs.product_id", - "products.id", "products", $term); + my ($self, $args) = @_; + + # BMO - product aliases + # swap out old product names for new ones + if (ref($args->{all_values})) { + my $aliased; + foreach my $value (@{$args->{all_values}}) { + if (exists PRODUCT_ALIASES->{lc($value)}) { + $value = PRODUCT_ALIASES->{lc($value)}; + $aliased = 1; + } + } + if ($aliased) { + $args->{value} = join(',', @{$args->{all_values}}); + $args->{quoted} = Bugzilla->dbh->quote($args->{value}); + } + } + elsif (exists PRODUCT_ALIASES->{lc($args->{value})}) { + $args->{value} = PRODUCT_ALIASES->{lc($args->{value})}; + $args->{all_values} = $args->{value}; + $args->{quoted} = Bugzilla->dbh->quote($args->{value}); + } + + # Generate the restriction condition + $args->{full_field} = "products.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} + = build_subselect("bugs.product_id", "products.id", "products", $term); } sub _classification_nonchanged { - my ($self, $args) = @_; - my $joins = $args->{joins}; + my ($self, $args) = @_; + my $joins = $args->{joins}; - # This joins the right tables for us. - $self->_add_extra_column('product'); + # This joins the right tables for us. + $self->_add_extra_column('product'); - # Generate the restriction condition - $args->{full_field} = "classifications.name"; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect("map_product.classification_id", - "classifications.id", "classifications", $term); + # Generate the restriction condition + $args->{full_field} = "classifications.name"; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} = build_subselect("map_product.classification_id", + "classifications.id", "classifications", $term); } sub _triage_owner_nonchanged { - my ($self, $args) = @_; - $self->_add_extra_column('triage_owner'); - $args->{full_field} = $args->{value_is_id} ? 'profiles.userid' : 'profiles.login_name'; - $self->_do_operator_function($args); - my $term = $args->{term}; - $args->{term} = build_subselect('bugs.component_id', 'components.id', - 'profiles JOIN components ON components.triage_owner_id = profiles.userid', $term); + my ($self, $args) = @_; + $self->_add_extra_column('triage_owner'); + $args->{full_field} + = $args->{value_is_id} ? 'profiles.userid' : 'profiles.login_name'; + $self->_do_operator_function($args); + my $term = $args->{term}; + $args->{term} + = build_subselect('bugs.component_id', 'components.id', + 'profiles JOIN components ON components.triage_owner_id = profiles.userid', + $term); } sub _nullable { - my ($self, $args) = @_; - my $field = $args->{full_field}; - $args->{full_field} = "COALESCE($field, '')"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + $args->{full_field} = "COALESCE($field, '')"; } sub _nullable_int { - my ($self, $args) = @_; - my $field = $args->{full_field}; - $args->{full_field} = "COALESCE($field, 0)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + $args->{full_field} = "COALESCE($field, 0)"; } sub _nullable_datetime { - my ($self, $args) = @_; - my $field = $args->{full_field}; - my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME); - $args->{full_field} = "COALESCE($field, $empty)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + my $empty = Bugzilla->dbh->quote(EMPTY_DATETIME); + $args->{full_field} = "COALESCE($field, $empty)"; } sub _nullable_date { - my ($self, $args) = @_; - my $field = $args->{full_field}; - my $empty = Bugzilla->dbh->quote(EMPTY_DATE); - $args->{full_field} = "COALESCE($field, $empty)"; + my ($self, $args) = @_; + my $field = $args->{full_field}; + my $empty = Bugzilla->dbh->quote(EMPTY_DATE); + $args->{full_field} = "COALESCE($field, $empty)"; } sub _deadline { - my ($self, $args) = @_; - my $field = $args->{full_field}; - # This makes "equals" searches work on all DBs (even on MySQL, which - # has a bug: http://bugs.mysql.com/bug.php?id=60324). - $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d'); - $self->_nullable_datetime($args); + my ($self, $args) = @_; + my $field = $args->{full_field}; + + # This makes "equals" searches work on all DBs (even on MySQL, which + # has a bug: http://bugs.mysql.com/bug.php?id=60324). + $args->{full_field} = Bugzilla->dbh->sql_date_format($field, '%Y-%m-%d'); + $self->_nullable_datetime($args); } sub _owner_idle_time_greater_less { - my ($self, $args) = @_; - my ($chart_id, $joins, $value, $operator) = - @$args{qw(chart_id joins value operator)}; - my $dbh = Bugzilla->dbh; - - my $table = "idle_$chart_id"; - my $quoted = $dbh->quote(SqlifyDate($value)); - - my $ld_table = "comment_$table"; - my $act_table = "activity_$table"; - my $comments_join = { - table => 'longdescs', - as => $ld_table, - from => 'assigned_to', - to => 'who', - extra => ["$ld_table.bug_when > $quoted"], - }; - my $activity_join = { - table => 'bugs_activity', - as => $act_table, - from => 'assigned_to', - to => 'who', - extra => ["$act_table.bug_when > $quoted"] - }; - - push(@$joins, $comments_join, $activity_join); - - if ($operator =~ /greater/) { - $args->{term} = - "$ld_table.who IS NULL AND $act_table.who IS NULL"; - } else { - $args->{term} = - "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)"; - } + my ($self, $args) = @_; + my ($chart_id, $joins, $value, $operator) + = @$args{qw(chart_id joins value operator)}; + my $dbh = Bugzilla->dbh; + + my $table = "idle_$chart_id"; + my $quoted = $dbh->quote(SqlifyDate($value)); + + my $ld_table = "comment_$table"; + my $act_table = "activity_$table"; + my $comments_join = { + table => 'longdescs', + as => $ld_table, + from => 'assigned_to', + to => 'who', + extra => ["$ld_table.bug_when > $quoted"], + }; + my $activity_join = { + table => 'bugs_activity', + as => $act_table, + from => 'assigned_to', + to => 'who', + extra => ["$act_table.bug_when > $quoted"] + }; + + push(@$joins, $comments_join, $activity_join); + + if ($operator =~ /greater/) { + $args->{term} = "$ld_table.who IS NULL AND $act_table.who IS NULL"; + } + else { + $args->{term} = "($ld_table.who IS NOT NULL OR $act_table.who IS NOT NULL)"; + } } sub _multiselect_negative { - my ($self, $args) = @_; - my ($field, $operator) = @$args{qw(field operator)}; + my ($self, $args) = @_; + my ($field, $operator) = @$args{qw(field operator)}; - $args->{operator} = $self->_reverse_operator($operator); - $args->{term} = $self->_multiselect_term($args, 1); + $args->{operator} = $self->_reverse_operator($operator); + $args->{term} = $self->_multiselect_term($args, 1); } sub _multiselect_multiple { - my ($self, $args) = @_; - my ($chart_id, $field, $operator, $value) - = @$args{qw(chart_id field operator value)}; - my $dbh = Bugzilla->dbh; - - # We want things like "cf_multi_select=two+words" to still be - # considered a search for two separate words, unless we're using - # anyexact. (_all_values would consider that to be one "word" with a - # space in it, because it's not in the Boolean Charts). - my @words = $operator eq 'anyexact' ? $self->_all_values($args) - : split(/[\s,]+/, $value); - - my @terms; - foreach my $word (@words) { - next if $word eq ''; - $args->{value} = $word; - $args->{quoted} = $dbh->quote($word); - push(@terms, $self->_multiselect_term($args)); - } - - # The spacing in the joins helps make the resulting SQL more readable. - if ($operator =~ /^any/) { - $args->{term} = join("\n OR ", @terms); - } - else { - $args->{term} = join("\n AND ", @terms); - } + my ($self, $args) = @_; + my ($chart_id, $field, $operator, $value) + = @$args{qw(chart_id field operator value)}; + my $dbh = Bugzilla->dbh; + + # We want things like "cf_multi_select=two+words" to still be + # considered a search for two separate words, unless we're using + # anyexact. (_all_values would consider that to be one "word" with a + # space in it, because it's not in the Boolean Charts). + my @words + = $operator eq 'anyexact' + ? $self->_all_values($args) + : split(/[\s,]+/, $value); + + my @terms; + foreach my $word (@words) { + next if $word eq ''; + $args->{value} = $word; + $args->{quoted} = $dbh->quote($word); + push(@terms, $self->_multiselect_term($args)); + } + + # The spacing in the joins helps make the resulting SQL more readable. + if ($operator =~ /^any/) { + $args->{term} = join("\n OR ", @terms); + } + else { + $args->{term} = join("\n AND ", @terms); + } } sub _flagtypes_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) = - @$args{qw(chart_id operator value joins bugs_table condition)}; - - if ($operator =~ /^is(not)?empty$/) { - $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); - return; - } - - my $dbh = Bugzilla->dbh; - - # For 'not' operators, we need to negate the whole term. - # If you search for "Flags" (does not contain) "approval+" we actually want - # to return *bugs* that don't contain an approval+ flag. Without rewriting - # the negation we'll search for *flags* which don't contain approval+. - if ($operator =~ s/^not//) { - $args->{operator} = $operator; - $condition->operator($operator); - $condition->negate(1); - } - - my $subselect_args = { - chart_id => $chart_id, - sequence => $chart_id, - field => 'flagtypes.name', - full_field => $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"), - operator => $operator, - value => $value, - all_values => $value, - quoted => $dbh->quote($value), - joins => [], - bugs_table => "bugs_$chart_id", - }; - $self->_do_operator_function($subselect_args); - my $subselect_term = $subselect_args->{term}; - - # don't call build_subselect as this must run as a true sub-select - $args->{term} = "EXISTS ( + my ($self, $args) = @_; + my ($chart_id, $operator, $value, $joins, $bugs_table, $condition) + = @$args{qw(chart_id operator value joins bugs_table condition)}; + + if ($operator =~ /^is(not)?empty$/) { + $args->{term} = $self->_multiselect_isempty($args, $operator eq 'isnotempty'); + return; + } + + my $dbh = Bugzilla->dbh; + + # For 'not' operators, we need to negate the whole term. + # If you search for "Flags" (does not contain) "approval+" we actually want + # to return *bugs* that don't contain an approval+ flag. Without rewriting + # the negation we'll search for *flags* which don't contain approval+. + if ($operator =~ s/^not//) { + $args->{operator} = $operator; + $condition->operator($operator); + $condition->negate(1); + } + + my $subselect_args = { + chart_id => $chart_id, + sequence => $chart_id, + field => 'flagtypes.name', + full_field => + $dbh->sql_string_concat("flagtypes_$chart_id.name", "flags_$chart_id.status"), + operator => $operator, + value => $value, + all_values => $value, + quoted => $dbh->quote($value), + joins => [], + bugs_table => "bugs_$chart_id", + }; + $self->_do_operator_function($subselect_args); + my $subselect_term = $subselect_args->{term}; + + # don't call build_subselect as this must run as a true sub-select + $args->{term} = "EXISTS ( SELECT 1 FROM $bugs_table bugs_$chart_id LEFT JOIN attachments AS attachments_$chart_id @@ -3069,182 +3070,192 @@ sub _flagtypes_nonchanged { } sub _multiselect_nonchanged { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator) = - @$args{qw(chart_id joins field operator)}; - $args->{term} = $self->_multiselect_term($args) + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator) + = @$args{qw(chart_id joins field operator)}; + $args->{term} = $self->_multiselect_term($args); } sub _multiselect_table { - my ($self, $args) = @_; - my ($field, $chart_id) = @$args{qw(field chart_id)}; - my $dbh = Bugzilla->dbh; - - if ($field eq 'keywords') { - $args->{full_field} = 'keyworddefs.name'; - return "keywords INNER JOIN keyworddefs". - " ON keywords.keywordid = keyworddefs.id"; - } - elsif ($field eq 'tag') { - $args->{full_field} = 'tag.name'; - return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = " - . ($self->_sharer_id || $self->_user->id); - } - elsif ($field eq 'bug_group') { - $args->{full_field} = 'groups.name'; - return "bug_group_map INNER JOIN groups + my ($self, $args) = @_; + my ($field, $chart_id) = @$args{qw(field chart_id)}; + my $dbh = Bugzilla->dbh; + + if ($field eq 'keywords') { + $args->{full_field} = 'keyworddefs.name'; + return "keywords INNER JOIN keyworddefs" + . " ON keywords.keywordid = keyworddefs.id"; + } + elsif ($field eq 'tag') { + $args->{full_field} = 'tag.name'; + return "bug_tag INNER JOIN tag ON bug_tag.tag_id = tag.id AND user_id = " + . ($self->_sharer_id || $self->_user->id); + } + elsif ($field eq 'bug_group') { + $args->{full_field} = 'groups.name'; + return "bug_group_map INNER JOIN groups ON bug_group_map.group_id = groups.id"; - } - elsif ($field eq 'blocked' or $field eq 'dependson') { - my $select = $field eq 'blocked' ? 'dependson' : 'blocked'; - $args->{_select_field} = $select; - $args->{full_field} = $field; - return "dependencies"; - } - elsif ($field eq 'longdesc') { - $args->{_extra_where} = " AND isprivate = 0" - if !$self->_user->is_insider; - $args->{full_field} = 'thetext'; - return "longdescs"; - } - elsif ($field eq 'longdescs.isprivate') { - ThrowUserError('auth_failure', { action => 'search', - object => 'bug_fields', - field => 'longdescs.isprivate' }) - if !$self->_user->is_insider; - $args->{full_field} = 'isprivate'; - return "longdescs"; - } - elsif ($field =~ /^attachments/) { - $args->{_extra_where} = " AND isprivate = 0" - if !$self->_user->is_insider; - $field =~ /^attachments\.(.+)$/; - $args->{full_field} = $1; - return "attachments"; - } - elsif ($field eq 'flagtypes.name') { - $args->{full_field} = $dbh->sql_string_concat("flagtypes.name", - "flags.status"); - return "flags INNER JOIN flagtypes ON flags.type_id = flagtypes.id"; - } - elsif ($field eq 'comment_tag') { - $args->{_extra_where} = " AND longdescs.isprivate = 0" - if !$self->_user->is_insider; - $args->{full_field} = 'longdescs_tags.tag'; - return "longdescs INNER JOIN longdescs_tags". - " ON longdescs.comment_id = longdescs_tags.comment_id"; - } - my $table = "bug_$field"; - $args->{full_field} = "bug_$field.value"; - return $table; + } + elsif ($field eq 'blocked' or $field eq 'dependson') { + my $select = $field eq 'blocked' ? 'dependson' : 'blocked'; + $args->{_select_field} = $select; + $args->{full_field} = $field; + return "dependencies"; + } + elsif ($field eq 'longdesc') { + $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider; + $args->{full_field} = 'thetext'; + return "longdescs"; + } + elsif ($field eq 'longdescs.isprivate') { + ThrowUserError('auth_failure', + {action => 'search', object => 'bug_fields', field => 'longdescs.isprivate'}) + if !$self->_user->is_insider; + $args->{full_field} = 'isprivate'; + return "longdescs"; + } + elsif ($field =~ /^attachments/) { + $args->{_extra_where} = " AND isprivate = 0" if !$self->_user->is_insider; + $field =~ /^attachments\.(.+)$/; + $args->{full_field} = $1; + return "attachments"; + } + elsif ($field eq 'flagtypes.name') { + $args->{full_field} = $dbh->sql_string_concat("flagtypes.name", "flags.status"); + return "flags INNER JOIN flagtypes ON flags.type_id = flagtypes.id"; + } + elsif ($field eq 'comment_tag') { + $args->{_extra_where} = " AND longdescs.isprivate = 0" + if !$self->_user->is_insider; + $args->{full_field} = 'longdescs_tags.tag'; + return "longdescs INNER JOIN longdescs_tags" + . " ON longdescs.comment_id = longdescs_tags.comment_id"; + } + my $table = "bug_$field"; + $args->{full_field} = "bug_$field.value"; + return $table; } sub _multiselect_term { - my ($self, $args, $not) = @_; - my ($operator) = $args->{operator}; - # 'empty' operators require special handling - return $self->_multiselect_isempty($args, $not) - if $operator =~ /^is(not)?empty$/; - my $table = $self->_multiselect_table($args); - $self->_do_operator_function($args); - my $term = $args->{term}; - $term .= $args->{_extra_where} || ''; - my $select = $args->{_select_field} || 'bug_id'; - return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, $not); + my ($self, $args, $not) = @_; + my ($operator) = $args->{operator}; + + # 'empty' operators require special handling + return $self->_multiselect_isempty($args, $not) + if $operator =~ /^is(not)?empty$/; + my $table = $self->_multiselect_table($args); + $self->_do_operator_function($args); + my $term = $args->{term}; + $term .= $args->{_extra_where} || ''; + my $select = $args->{_select_field} || 'bug_id'; + return build_subselect("$args->{bugs_table}.bug_id", $select, $table, $term, + $not); } # We can't use the normal operator_functions to build isempty queries which # join to different tables. sub _multiselect_isempty { - my ($self, $args, $not) = @_; - my ($field, $operator, $joins, $chart_id) = @$args{qw(field operator joins chart_id)}; - my $dbh = Bugzilla->dbh; - $operator = $self->_reverse_operator($operator) if $not; - $not = $operator eq 'isnotempty' ? 'NOT' : ''; - - if ($field eq 'keywords') { - push @$joins, { - table => 'keywords', - as => "keywords_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "keywords_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'bug_group') { - push @$joins, { - table => 'bug_group_map', - as => "bug_group_map_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "bug_group_map_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'flagtypes.name') { - push @$joins, { - table => 'flags', - as => "flags_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - return "flags_$chart_id.bug_id IS $not NULL"; - } - elsif ($field eq 'blocked' or $field eq 'dependson') { - my $to = $field eq 'blocked' ? 'dependson' : 'blocked'; - push @$joins, { - table => 'dependencies', - as => "dependencies_$chart_id", - from => 'bug_id', - to => $to, - }; - return "dependencies_$chart_id.$to IS $not NULL"; - } - elsif ($field eq 'longdesc') { - my @extra = ( "longdescs_$chart_id.type != " . CMT_HAS_DUPE ); - push @extra, "longdescs_$chart_id.isprivate = 0" - unless $self->_user->is_insider; - push @$joins, { - table => 'longdescs', - as => "longdescs_$chart_id", - from => 'bug_id', - to => 'bug_id', - extra => \@extra, - }; - return $not - ? "longdescs_$chart_id.thetext != ''" - : "longdescs_$chart_id.thetext = ''"; - } - elsif ($field eq 'longdescs.isprivate') { - ThrowUserError('search_field_operator_invalid', { field => $field, - operator => $operator }); - } - elsif ($field =~ /^attachments\.(.+)/) { - my $sub_field = $1; - if ($sub_field eq 'description' || $sub_field eq 'filename' || $sub_field eq 'mimetype') { - # can't be null/empty - return $not ? '1=1' : '1=2'; - } else { - # all other fields which get here are boolean - ThrowUserError('search_field_operator_invalid', { field => $field, - operator => $operator }); - } - } - elsif ($field eq 'tag') { - push @$joins, { - table => 'bug_tag', - as => "bug_tag_$chart_id", - from => 'bug_id', - to => 'bug_id', - }; - push @$joins, { - table => 'tag', - as => "tag_$chart_id", - from => "bug_tag_$chart_id.tag_id", - to => 'id', - extra => [ "tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id) ], - }; - return "tag_$chart_id.id IS $not NULL"; + my ($self, $args, $not) = @_; + my ($field, $operator, $joins, $chart_id) + = @$args{qw(field operator joins chart_id)}; + my $dbh = Bugzilla->dbh; + $operator = $self->_reverse_operator($operator) if $not; + $not = $operator eq 'isnotempty' ? 'NOT' : ''; + + if ($field eq 'keywords') { + push @$joins, + { + table => 'keywords', + as => "keywords_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "keywords_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'bug_group') { + push @$joins, + { + table => 'bug_group_map', + as => "bug_group_map_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "bug_group_map_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'flagtypes.name') { + push @$joins, + { + table => 'flags', + as => "flags_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + return "flags_$chart_id.bug_id IS $not NULL"; + } + elsif ($field eq 'blocked' or $field eq 'dependson') { + my $to = $field eq 'blocked' ? 'dependson' : 'blocked'; + push @$joins, + { + table => 'dependencies', + as => "dependencies_$chart_id", + from => 'bug_id', + to => $to, + }; + return "dependencies_$chart_id.$to IS $not NULL"; + } + elsif ($field eq 'longdesc') { + my @extra = ("longdescs_$chart_id.type != " . CMT_HAS_DUPE); + push @extra, "longdescs_$chart_id.isprivate = 0" + unless $self->_user->is_insider; + push @$joins, + { + table => 'longdescs', + as => "longdescs_$chart_id", + from => 'bug_id', + to => 'bug_id', + extra => \@extra, + }; + return $not + ? "longdescs_$chart_id.thetext != ''" + : "longdescs_$chart_id.thetext = ''"; + } + elsif ($field eq 'longdescs.isprivate') { + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); + } + elsif ($field =~ /^attachments\.(.+)/) { + my $sub_field = $1; + if ( $sub_field eq 'description' + || $sub_field eq 'filename' + || $sub_field eq 'mimetype') + { + # can't be null/empty + return $not ? '1=1' : '1=2'; } + else { + # all other fields which get here are boolean + ThrowUserError('search_field_operator_invalid', + {field => $field, operator => $operator}); + } + } + elsif ($field eq 'tag') { + push @$joins, + { + table => 'bug_tag', + as => "bug_tag_$chart_id", + from => 'bug_id', + to => 'bug_id', + }; + push @$joins, + { + table => 'tag', + as => "tag_$chart_id", + from => "bug_tag_$chart_id.tag_id", + to => 'id', + extra => ["tag_$chart_id.user_id = " . ($self->_sharer_id || $self->_user->id)], + }; + return "tag_$chart_id.id IS $not NULL"; + } } ############################### @@ -3252,236 +3263,238 @@ sub _multiselect_isempty { ############################### sub _simple_operator { - my ($self, $args) = @_; - my ($full_field, $quoted, $operator) = - @$args{qw(full_field quoted operator)}; - my $sql_operator = SIMPLE_OPERATORS->{$operator}; - $args->{term} = "$full_field $sql_operator $quoted"; + my ($self, $args) = @_; + my ($full_field, $quoted, $operator) = @$args{qw(full_field quoted operator)}; + my $sql_operator = SIMPLE_OPERATORS->{$operator}; + $args->{term} = "$full_field $sql_operator $quoted"; } sub _casesubstring { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_position($quoted, $full_field) . " > 0"; + $args->{term} = $dbh->sql_position($quoted, $full_field) . " > 0"; } sub _substring { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; - # XXX This should probably be changed to just use LIKE - $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " > 0"; + # XXX This should probably be changed to just use LIKE + $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " > 0"; } sub _notsubstring { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; - # XXX This should probably be changed to just use NOT LIKE - $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " = 0"; + # XXX This should probably be changed to just use NOT LIKE + $args->{term} = $dbh->sql_iposition($quoted, $full_field) . " = 0"; } sub _regexp { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_regexp($full_field, $quoted); + $args->{term} = $dbh->sql_regexp($full_field, $quoted); } sub _notregexp { - my ($self, $args) = @_; - my ($full_field, $quoted) = @$args{qw(full_field quoted)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($full_field, $quoted) = @$args{qw(full_field quoted)}; + my $dbh = Bugzilla->dbh; - $args->{term} = $dbh->sql_not_regexp($full_field, $quoted); + $args->{term} = $dbh->sql_not_regexp($full_field, $quoted); } sub _anyexact { - my ($self, $args) = @_; - my ($field, $full_field) = @$args{qw(field full_field)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($field, $full_field) = @$args{qw(field full_field)}; + my $dbh = Bugzilla->dbh; - my @list = $self->_all_values($args, ','); - @list = map { $self->_quote_unless_numeric($args, $_) } @list; + my @list = $self->_all_values($args, ','); + @list = map { $self->_quote_unless_numeric($args, $_) } @list; - if (@list) { - $args->{term} = $dbh->sql_in($full_field, \@list); - } - else { - $args->{term} = ''; - } + if (@list) { + $args->{term} = $dbh->sql_in($full_field, \@list); + } + else { + $args->{term} = ''; + } } sub _anywordsubstr { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_substring_terms($args); - $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; + my @terms = $self->_substring_terms($args); + $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; } sub _allwordssubstr { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_substring_terms($args); - $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; + my @terms = $self->_substring_terms($args); + $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; } sub _nowordssubstr { - my ($self, $args) = @_; - $self->_anywordsubstr($args); - my $term = $args->{term}; - $args->{term} = "NOT($term)"; + my ($self, $args) = @_; + $self->_anywordsubstr($args); + my $term = $args->{term}; + $args->{term} = "NOT($term)"; } sub _anywords { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my @terms = $self->_word_terms($args); - my @terms = $self->_word_terms($args); - # Because _word_terms uses AND, we need to parenthesize its terms - # if there are more than one. - @terms = map("($_)", @terms) if scalar(@terms) > 1; - $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; + # Because _word_terms uses AND, we need to parenthesize its terms + # if there are more than one. + @terms = map("($_)", @terms) if scalar(@terms) > 1; + $args->{term} = @terms ? '(' . join("\n\tOR ", @terms) . ')' : ''; } sub _allwords { - my ($self, $args) = @_; + my ($self, $args) = @_; - my @terms = $self->_word_terms($args); - $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; + my @terms = $self->_word_terms($args); + $args->{term} = @terms ? '(' . join("\n\tAND ", @terms) . ')' : ''; } sub _nowords { - my ($self, $args) = @_; - $self->_anywords($args); - my $term = $args->{term}; - $args->{term} = "NOT($term)"; + my ($self, $args) = @_; + $self->_anywords($args); + my $term = $args->{term}; + $args->{term} = "NOT($term)"; } sub _changedbefore_changedafter { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $value) = - @$args{qw(chart_id joins field operator value)}; - my $dbh = Bugzilla->dbh; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - - # Asking when creation_ts changed is just asking when the bug was created. - if ($field_object->name eq 'creation_ts') { - $args->{operator} = - $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq'; - return $self->_do_operator_function($args); - } + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $value) + = @$args{qw(chart_id joins field operator value)}; + my $dbh = Bugzilla->dbh; - my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; - my $field_id = $field_object->id; - # Charts on changed* fields need to be field-specific. Otherwise, - # OR chart rows make no sense if they contain multiple fields. - my $table = "act_${field_id}_$chart_id"; + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); - my $sql_date = $dbh->quote(SqlifyDate($value)); - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.bug_when $sql_operator $sql_date"], - }; + # Asking when creation_ts changed is just asking when the bug was created. + if ($field_object->name eq 'creation_ts') { + $args->{operator} + = $operator eq 'changedbefore' ? 'lessthaneq' : 'greaterthaneq'; + return $self->_do_operator_function($args); + } - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my $sql_operator = ($operator =~ /before/) ? '<=' : '>='; + my $field_id = $field_object->id; + + # Charts on changed* fields need to be field-specific. Otherwise, + # OR chart rows make no sense if they contain multiple fields. + my $table = "act_${field_id}_$chart_id"; + + my $sql_date = $dbh->quote(SqlifyDate($value)); + my $join = { + table => 'bugs_activity', + as => $table, + extra => + ["$table.fieldid = $field_id", "$table.bug_when $sql_operator $sql_date"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changedfrom_changedto { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $quoted) = - @$args{qw(chart_id joins field operator quoted)}; - - my $column = ($operator =~ /from/) ? 'removed' : 'added'; - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - my $table = "act_${field_id}_$chart_id"; - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.$column = $quoted"], - }; - - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $quoted) + = @$args{qw(chart_id joins field operator quoted)}; + + my $column = ($operator =~ /from/) ? 'removed' : 'added'; + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + my $table = "act_${field_id}_$chart_id"; + my $join = { + table => 'bugs_activity', + as => $table, + extra => ["$table.fieldid = $field_id", "$table.$column = $quoted"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changedby { - my ($self, $args) = @_; - my ($chart_id, $joins, $field, $operator, $value) = - @$args{qw(chart_id joins field operator value)}; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - my $table = "act_${field_id}_$chart_id"; - my $user_id = login_to_id($value, THROW_ERROR); - my $join = { - table => 'bugs_activity', - as => $table, - extra => ["$table.fieldid = $field_id", - "$table.who = $user_id"], - }; - - $args->{term} = "$table.bug_when IS NOT NULL"; - $self->_changed_security_check($args, $join); - push(@$joins, $join); + my ($self, $args) = @_; + my ($chart_id, $joins, $field, $operator, $value) + = @$args{qw(chart_id joins field operator value)}; + + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + my $table = "act_${field_id}_$chart_id"; + my $user_id = login_to_id($value, THROW_ERROR); + my $join = { + table => 'bugs_activity', + as => $table, + extra => ["$table.fieldid = $field_id", "$table.who = $user_id"], + }; + + $args->{term} = "$table.bug_when IS NOT NULL"; + $self->_changed_security_check($args, $join); + push(@$joins, $join); } sub _changed_security_check { - my ($self, $args, $join) = @_; - my ($chart_id, $field) = @$args{qw(chart_id field)}; - - my $field_object = $self->_chart_fields->{$field} - || ThrowCodeError("invalid_field_name", { field => $field }); - my $field_id = $field_object->id; - - # If the user is not part of the insiders group, they cannot see - # changes to attachments (including attachment flags) that are private - if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) { - $join->{then_to} = { - as => "attach_${field_id}_$chart_id", - table => 'attachments', - from => "act_${field_id}_$chart_id.attach_id", - to => 'attach_id', - }; - - $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0"; - } + my ($self, $args, $join) = @_; + my ($chart_id, $field) = @$args{qw(chart_id field)}; + + my $field_object = $self->_chart_fields->{$field} + || ThrowCodeError("invalid_field_name", {field => $field}); + my $field_id = $field_object->id; + + # If the user is not part of the insiders group, they cannot see + # changes to attachments (including attachment flags) that are private + if ($field =~ /^(?:flagtypes\.name$|attach)/ and !$self->_user->is_insider) { + $join->{then_to} = { + as => "attach_${field_id}_$chart_id", + table => 'attachments', + from => "act_${field_id}_$chart_id.attach_id", + to => 'attach_id', + }; + + $args->{term} .= " AND COALESCE(attach_${field_id}_$chart_id.isprivate, 0) = 0"; + } } sub _isempty { - my ($self, $args) = @_; - my $full_field = $args->{full_field}; - $args->{term} = "$full_field IS NULL OR $full_field = " . $self->_empty_value($args->{field}); + my ($self, $args) = @_; + my $full_field = $args->{full_field}; + $args->{term} = "$full_field IS NULL OR $full_field = " + . $self->_empty_value($args->{field}); } sub _isnotempty { - my ($self, $args) = @_; - my $full_field = $args->{full_field}; - $args->{term} = "$full_field IS NOT NULL AND $full_field != " . $self->_empty_value($args->{field}); + my ($self, $args) = @_; + my $full_field = $args->{full_field}; + $args->{term} = "$full_field IS NOT NULL AND $full_field != " + . $self->_empty_value($args->{field}); } sub _empty_value { - my ($self, $field) = @_; - my $field_obj = $self->_chart_fields->{$field}; - return "0" if $field_obj->type == FIELD_TYPE_BUG_ID; - return Bugzilla->dbh->quote(EMPTY_DATETIME) if $field_obj->type == FIELD_TYPE_DATETIME; - return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE; - return "''"; + my ($self, $field) = @_; + my $field_obj = $self->_chart_fields->{$field}; + return "0" if $field_obj->type == FIELD_TYPE_BUG_ID; + return Bugzilla->dbh->quote(EMPTY_DATETIME) + if $field_obj->type == FIELD_TYPE_DATETIME; + return Bugzilla->dbh->quote(EMPTY_DATE) if $field_obj->type == FIELD_TYPE_DATE; + return "''"; } ###################### @@ -3489,75 +3502,77 @@ sub _empty_value { ###################### # Validate that the query type is one we can deal with -sub IsValidQueryType -{ - my ($queryType) = @_; - # BMO: Added google and instant - if (grep { $_ eq $queryType } qw(specific advanced google instant)) { - return 1; - } - return 0; +sub IsValidQueryType { + my ($queryType) = @_; + + # BMO: Added google and instant + if (grep { $_ eq $queryType } qw(specific advanced google instant)) { + return 1; + } + return 0; } # Splits out "asc|desc" from a sort order item. sub split_order_term { - my $fragment = shift; - $fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i; - my ($column_name, $direction) = (lc($1), uc($2 || '')); - return wantarray ? ($column_name, $direction) : $column_name; + my $fragment = shift; + $fragment =~ /^(.+?)(?:\s+(ASC|DESC))?$/i; + my ($column_name, $direction) = (lc($1), uc($2 || '')); + return wantarray ? ($column_name, $direction) : $column_name; } # Used to translate old SQL fragments from buglist.cgi's "order" argument # into our modern field IDs. sub _translate_old_column { - my ($self, $column) = @_; - # All old SQL fragments have a period in them somewhere. - return $column if $column !~ /\./; + my ($self, $column) = @_; - if ($column =~ /\bAS\s+(\w+)$/i) { - return $1; - } - # product, component, classification, assigned_to, qa_contact, reporter - elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) { - return $1; - } + # All old SQL fragments have a period in them somewhere. + return $column if $column !~ /\./; - # If it doesn't match the regexps above, check to see if the old - # SQL fragment matches the SQL of an existing column - foreach my $key (%{ $self->COLUMNS }) { - next unless exists $self->COLUMNS->{$key}->{name}; - return $key if $self->COLUMNS->{$key}->{name} eq $column; - } + if ($column =~ /\bAS\s+(\w+)$/i) { + return $1; + } - return $column; + # product, component, classification, assigned_to, qa_contact, reporter + elsif ($column =~ /map_(\w+?)s?\.(login_)?name/i) { + return $1; + } + + # If it doesn't match the regexps above, check to see if the old + # SQL fragment matches the SQL of an existing column + foreach my $key (%{$self->COLUMNS}) { + next unless exists $self->COLUMNS->{$key}->{name}; + return $key if $self->COLUMNS->{$key}->{name} eq $column; + } + + return $column; } # Returns an hashref of Bugzilla::Field objects the current user can search sub search_fields { - my ($params) = @_; + my ($params) = @_; - $params //= {}; - $params->{by_name} = 1; - my $user = delete $params->{user} // Bugzilla->user; - my $fields = Bugzilla->fields($params); + $params //= {}; + $params->{by_name} = 1; + my $user = delete $params->{user} // Bugzilla->user; + my $fields = Bugzilla->fields($params); - # if we're not in the time-tracking group, exclude time-tracking fields - if (!$user->is_timetracker) { - foreach my $field (TIMETRACKING_FIELDS) { - delete $fields->{$field}; - } + # if we're not in the time-tracking group, exclude time-tracking fields + if (!$user->is_timetracker) { + foreach my $field (TIMETRACKING_FIELDS) { + delete $fields->{$field}; } + } - # always exclude attachment data searching - delete $fields->{'attach_data.thedata'}; + # always exclude attachment data searching + delete $fields->{'attach_data.thedata'}; - return $fields; + return $fields; } # BMO - make product aliases lowercase -foreach my $name (keys %{ PRODUCT_ALIASES() }) { - PRODUCT_ALIASES->{lc($name)} = PRODUCT_ALIASES->{$name}; - delete PRODUCT_ALIASES->{$name}; +foreach my $name (keys %{PRODUCT_ALIASES()}) { + PRODUCT_ALIASES->{lc($name)} = PRODUCT_ALIASES->{$name}; + delete PRODUCT_ALIASES->{$name}; } 1; diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm index 4426ea576..b0eaddeb0 100644 --- a/Bugzilla/Search/Clause.pm +++ b/Bugzilla/Search/Clause.pm @@ -16,121 +16,123 @@ use Bugzilla::Search::Condition qw(condition); use Bugzilla::Util qw(trick_taint); sub new { - my ($class, $joiner) = @_; - if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') { - ThrowCodeError('search_invalid_joiner', { joiner => $joiner }); - } - # This will go into SQL directly so needs to be untainted. - trick_taint($joiner) if $joiner; - bless { joiner => $joiner || 'AND' }, $class; + my ($class, $joiner) = @_; + if ($joiner and $joiner ne 'OR' and $joiner ne 'AND') { + ThrowCodeError('search_invalid_joiner', {joiner => $joiner}); + } + + # This will go into SQL directly so needs to be untainted. + trick_taint($joiner) if $joiner; + bless {joiner => $joiner || 'AND'}, $class; } sub children { - my ($self) = @_; - $self->{children} ||= []; - return $self->{children}; + my ($self) = @_; + $self->{children} ||= []; + return $self->{children}; } sub update_search_args { - my ($self, $search_args) = @_; - # abstract + my ($self, $search_args) = @_; + + # abstract } sub joiner { return $_[0]->{joiner} } sub has_translated_conditions { - my ($self) = @_; - my $children = $self->children; - return 1 if grep { $_->isa('Bugzilla::Search::Condition') - && $_->translated } @$children; - foreach my $child (@$children) { - next if $child->isa('Bugzilla::Search::Condition'); - return 1 if $child->has_translated_conditions; - } - return 0; + my ($self) = @_; + my $children = $self->children; + return 1 + if grep { $_->isa('Bugzilla::Search::Condition') && $_->translated } + @$children; + foreach my $child (@$children) { + next if $child->isa('Bugzilla::Search::Condition'); + return 1 if $child->has_translated_conditions; + } + return 0; } sub add { - my $self = shift; - my $children = $self->children; - if (@_ == 3) { - push(@$children, condition(@_)); - return; - } - - my ($child) = @_; - return if !defined $child; - $child->isa(__PACKAGE__) || $child->isa('Bugzilla::Search::Condition') - || die 'child not the right type: ' . $child; - push(@{ $self->children }, $child); + my $self = shift; + my $children = $self->children; + if (@_ == 3) { + push(@$children, condition(@_)); + return; + } + + my ($child) = @_; + return if !defined $child; + $child->isa(__PACKAGE__) + || $child->isa('Bugzilla::Search::Condition') + || die 'child not the right type: ' . $child; + push(@{$self->children}, $child); } sub negate { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{negate} = $value ? 1 : 0; - } - return $self->{negate}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{negate} = $value ? 1 : 0; + } + return $self->{negate}; } sub walk_conditions { - my ($self, $callback) = @_; - foreach my $child (@{ $self->children }) { - if ($child->isa('Bugzilla::Search::Condition')) { - $callback->($self, $child); - } - else { - $child->walk_conditions($callback); - } + my ($self, $callback) = @_; + foreach my $child (@{$self->children}) { + if ($child->isa('Bugzilla::Search::Condition')) { + $callback->($self, $child); + } + else { + $child->walk_conditions($callback); } + } } sub as_string { - my ($self) = @_; - if (!$self->{sql}) { - my @strings; - foreach my $child (@{ $self->children }) { - next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions; - next if $child->isa('Bugzilla::Search::Condition') - && !$child->translated; - - my $string = $child->as_string; - next unless $string; - if ($self->joiner eq 'AND') { - $string = "( $string )" if $string =~ /OR/; - } - else { - $string = "( $string )" if $string =~ /AND/; - } - push(@strings, $string); - } - - my $sql = join(' ' . $self->joiner . ' ', @strings); - $sql = "NOT( $sql )" if $sql && $self->negate; - $self->{sql} = $sql; + my ($self) = @_; + if (!$self->{sql}) { + my @strings; + foreach my $child (@{$self->children}) { + next if $child->isa(__PACKAGE__) && !$child->has_translated_conditions; + next if $child->isa('Bugzilla::Search::Condition') && !$child->translated; + + my $string = $child->as_string; + next unless $string; + if ($self->joiner eq 'AND') { + $string = "( $string )" if $string =~ /OR/; + } + else { + $string = "( $string )" if $string =~ /AND/; + } + push(@strings, $string); } - return $self->{sql}; + + my $sql = join(' ' . $self->joiner . ' ', @strings); + $sql = "NOT( $sql )" if $sql && $self->negate; + $self->{sql} = $sql; + } + return $self->{sql}; } # Search.pm converts URL parameters to Clause objects. This helps do the # reverse. sub as_params { - my ($self) = @_; - my @params; - foreach my $child (@{ $self->children }) { - if ($child->isa(__PACKAGE__)) { - my %open_paren = (f => 'OP', n => scalar $child->negate, - j => $child->joiner); - push(@params, \%open_paren); - push(@params, $child->as_params); - my %close_paren = (f => 'CP'); - push(@params, \%close_paren); - } - else { - push(@params, $child->as_params); - } + my ($self) = @_; + my @params; + foreach my $child (@{$self->children}) { + if ($child->isa(__PACKAGE__)) { + my %open_paren = (f => 'OP', n => scalar $child->negate, j => $child->joiner); + push(@params, \%open_paren); + push(@params, $child->as_params); + my %close_paren = (f => 'CP'); + push(@params, \%close_paren); + } + else { + push(@params, $child->as_params); } - return @params; + } + return @params; } 1; diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm index c7d3e6ae7..5c063a803 100644 --- a/Bugzilla/Search/ClauseGroup.pm +++ b/Bugzilla/Search/ClauseGroup.pm @@ -19,79 +19,82 @@ use Bugzilla::Util qw(trick_taint); use List::MoreUtils qw(uniq); use constant UNSUPPORTED_FIELDS => qw( - classification - commenter - component - longdescs.count - product - owner_idle_time + classification + commenter + component + longdescs.count + product + owner_idle_time ); sub new { - my ($class) = @_; - my $self = bless({ joiner => 'AND' }, $class); - # Add a join back to the bugs table which will be used to group conditions - # for this clause - my $condition = Bugzilla::Search::Condition->new({}); - $condition->translated({ - joins => [{ - table => 'bugs', - as => 'bugs_g0', - from => 'bug_id', - to => 'bug_id', - extra => [], - }], - term => '1 = 1', - }); - $self->SUPER::add($condition); - $self->{group_condition} = $condition; - return $self; + my ($class) = @_; + my $self = bless({joiner => 'AND'}, $class); + + # Add a join back to the bugs table which will be used to group conditions + # for this clause + my $condition = Bugzilla::Search::Condition->new({}); + $condition->translated({ + joins => [{ + table => 'bugs', + as => 'bugs_g0', + from => 'bug_id', + to => 'bug_id', + extra => [], + }], + term => '1 = 1', + }); + $self->SUPER::add($condition); + $self->{group_condition} = $condition; + return $self; } sub add { - my ($self, @args) = @_; - my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field}; - - # We don't support nesting of conditions under this clause - if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) { - ThrowUserError('search_grouped_invalid_nesting'); - } - - # Ensure all conditions use the same field - if (!$self->{_field}) { - $self->{_field} = $field; - } elsif ($field ne $self->{_field}) { - ThrowUserError('search_grouped_field_mismatch'); - } - - # Unsupported fields - if (grep { $_ eq $field } UNSUPPORTED_FIELDS ) { - ThrowUserError('search_grouped_field_invalid', { field => $field }); - } - - $self->SUPER::add(@args); + my ($self, @args) = @_; + my $field = scalar(@args) == 3 ? $args[0] : $args[0]->{field}; + + # We don't support nesting of conditions under this clause + if (scalar(@args) == 1 && !$args[0]->isa('Bugzilla::Search::Condition')) { + ThrowUserError('search_grouped_invalid_nesting'); + } + + # Ensure all conditions use the same field + if (!$self->{_field}) { + $self->{_field} = $field; + } + elsif ($field ne $self->{_field}) { + ThrowUserError('search_grouped_field_mismatch'); + } + + # Unsupported fields + if (grep { $_ eq $field } UNSUPPORTED_FIELDS) { + ThrowUserError('search_grouped_field_invalid', {field => $field}); + } + + $self->SUPER::add(@args); } sub update_search_args { - my ($self, $search_args) = @_; + my ($self, $search_args) = @_; - # No need to change things if there's only one child condition - return unless scalar(@{ $self->children }) > 1; + # No need to change things if there's only one child condition + return unless scalar(@{$self->children}) > 1; - # we want all the terms to use the same join table - if (!exists $self->{_first_chart_id}) { - $self->{_first_chart_id} = $search_args->{chart_id}; - } else { - $search_args->{chart_id} = $self->{_first_chart_id}; - } + # we want all the terms to use the same join table + if (!exists $self->{_first_chart_id}) { + $self->{_first_chart_id} = $search_args->{chart_id}; + } + else { + $search_args->{chart_id} = $self->{_first_chart_id}; + } - my $suffix = '_g' . $self->{_first_chart_id}; - $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix"; + my $suffix = '_g' . $self->{_first_chart_id}; + $self->{group_condition}->{translated}->{joins}->[0]->{as} = "bugs$suffix"; - $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./; + $search_args->{full_field} =~ s/^bugs\./bugs$suffix\./; - $search_args->{table_suffix} = $suffix; - $search_args->{bugs_table} = "bugs$suffix"; + $search_args->{table_suffix} = $suffix; + $search_args->{bugs_table} = "bugs$suffix"; } 1; diff --git a/Bugzilla/Search/Condition.pm b/Bugzilla/Search/Condition.pm index 1c38c1f3e..33b0bfd8b 100644 --- a/Bugzilla/Search/Condition.pm +++ b/Bugzilla/Search/Condition.pm @@ -15,55 +15,59 @@ use base qw(Exporter); our @EXPORT_OK = qw(condition); sub new { - my ($class, $params) = @_; - my %self = %$params; - bless \%self, $class; - return \%self; + my ($class, $params) = @_; + my %self = %$params; + bless \%self, $class; + return \%self; } -sub field { return $_[0]->{field} } -sub value { return $_[0]->{value} } +sub field { return $_[0]->{field} } +sub value { return $_[0]->{value} } sub operator { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{operator} = $value; - } - return $self->{operator}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{operator} = $value; + } + return $self->{operator}; } sub fov { - my ($self) = @_; - return ($self->field, $self->operator, $self->value); + my ($self) = @_; + return ($self->field, $self->operator, $self->value); } sub translated { - my ($self, $params) = @_; - if (@_ == 2) { - $self->{translated} = $params; - } - return $self->{translated}; + my ($self, $params) = @_; + if (@_ == 2) { + $self->{translated} = $params; + } + return $self->{translated}; } sub as_string { - my ($self) = @_; - my $term = $self->translated->{term}; - $term = "NOT( $term )" if $term && $self->negate; - return $term; + my ($self) = @_; + my $term = $self->translated->{term}; + $term = "NOT( $term )" if $term && $self->negate; + return $term; } sub as_params { - my ($self) = @_; - return { f => $self->field, o => $self->operator, v => $self->value, - n => scalar $self->negate }; + my ($self) = @_; + return { + f => $self->field, + o => $self->operator, + v => $self->value, + n => scalar $self->negate + }; } sub negate { - my ($self, $value) = @_; - if (@_ == 2) { - $self->{negate} = $value ? 1 : 0; - } - return $self->{negate}; + my ($self, $value) = @_; + if (@_ == 2) { + $self->{negate} = $value ? 1 : 0; + } + return $self->{negate}; } ########################### @@ -71,9 +75,9 @@ sub negate { ########################### sub condition { - my ($field, $operator, $value) = @_; - return __PACKAGE__->new({ field => $field, operator => $operator, - value => $value }); + my ($field, $operator, $value) = @_; + return __PACKAGE__->new( + {field => $field, operator => $operator, value => $value}); } 1; diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm index 6c0253e27..f2bb83e2e 100644 --- a/Bugzilla/Search/Quicksearch.pm +++ b/Bugzilla/Search/Quicksearch.pm @@ -27,256 +27,265 @@ use base qw(Exporter); # Custom mappings for some fields. use constant MAPPINGS => { - # Status, Resolution, Platform, OS, Priority, Severity - "status" => "bug_status", - "platform" => "rep_platform", - "os" => "op_sys", - "severity" => "bug_severity", - - # People: AssignedTo, Reporter, QA Contact, CC, etc. - "assignee" => "assigned_to", - "owner" => "assigned_to", - "mentor" => "bug_mentor", - - # Product, Version, Component, Target Milestone - "milestone" => "target_milestone", - - # Summary, Description, URL, Status whiteboard, Keywords - "summary" => "short_desc", - "description" => "longdesc", - "comment" => "longdesc", - "url" => "bug_file_loc", - "whiteboard" => "status_whiteboard", - "sw" => "status_whiteboard", - "kw" => "keywords", - "group" => "bug_group", - - # Flags - "flag" => "flagtypes.name", - "requestee" => "requestees.login_name", - "setter" => "setters.login_name", - - # Attachments - "attachment" => "attachments.description", - "attachmentdesc" => "attachments.description", - "attachdesc" => "attachments.description", - "attachmentmimetype" => "attachments.mimetype", - "attachmimetype" => "attachments.mimetype" + + # Status, Resolution, Platform, OS, Priority, Severity + "status" => "bug_status", + "platform" => "rep_platform", + "os" => "op_sys", + "severity" => "bug_severity", + + # People: AssignedTo, Reporter, QA Contact, CC, etc. + "assignee" => "assigned_to", + "owner" => "assigned_to", + "mentor" => "bug_mentor", + + # Product, Version, Component, Target Milestone + "milestone" => "target_milestone", + + # Summary, Description, URL, Status whiteboard, Keywords + "summary" => "short_desc", + "description" => "longdesc", + "comment" => "longdesc", + "url" => "bug_file_loc", + "whiteboard" => "status_whiteboard", + "sw" => "status_whiteboard", + "kw" => "keywords", + "group" => "bug_group", + + # Flags + "flag" => "flagtypes.name", + "requestee" => "requestees.login_name", + "setter" => "setters.login_name", + + # Attachments + "attachment" => "attachments.description", + "attachmentdesc" => "attachments.description", + "attachdesc" => "attachments.description", + "attachmentmimetype" => "attachments.mimetype", + "attachmimetype" => "attachments.mimetype" }; sub FIELD_MAP { - my $cache = Bugzilla->request_cache; - return $cache->{quicksearch_fields} if $cache->{quicksearch_fields}; - - # Get all the fields whose names don't contain periods. (Fields that - # contain periods are always handled in MAPPINGS.) - my @db_fields = grep { $_->name !~ /\./ } - @{ Bugzilla->fields({ obsolete => 0 }) }; - my %full_map = (%{ MAPPINGS() }, map { $_->name => $_->name } @db_fields); - - # Eliminate the fields that start with bug_ or rep_, because those are - # handled by the MAPPINGS instead, and we don't want too many names - # for them. (Also, otherwise "rep" doesn't match "reporter".) - # - # Remove "status_whiteboard" because we have "whiteboard" for it in - # the mappings, and otherwise "stat" can't match "status". - # - # Also, don't allow searching the _accessible stuff via quicksearch - # (both because it's unnecessary and because otherwise - # "reporter_accessible" and "reporter" both match "rep". - delete @full_map{qw(rep_platform bug_status bug_file_loc bug_group - bug_severity bug_status - status_whiteboard - cclist_accessible reporter_accessible)}; - - Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map} ); - - $cache->{quicksearch_fields} = \%full_map; - - return $cache->{quicksearch_fields}; + my $cache = Bugzilla->request_cache; + return $cache->{quicksearch_fields} if $cache->{quicksearch_fields}; + + # Get all the fields whose names don't contain periods. (Fields that + # contain periods are always handled in MAPPINGS.) + my @db_fields = grep { $_->name !~ /\./ } @{Bugzilla->fields({obsolete => 0})}; + my %full_map = (%{MAPPINGS()}, map { $_->name => $_->name } @db_fields); + + # Eliminate the fields that start with bug_ or rep_, because those are + # handled by the MAPPINGS instead, and we don't want too many names + # for them. (Also, otherwise "rep" doesn't match "reporter".) + # + # Remove "status_whiteboard" because we have "whiteboard" for it in + # the mappings, and otherwise "stat" can't match "status". + # + # Also, don't allow searching the _accessible stuff via quicksearch + # (both because it's unnecessary and because otherwise + # "reporter_accessible" and "reporter" both match "rep". + delete @full_map{ + qw(rep_platform bug_status bug_file_loc bug_group + bug_severity bug_status + status_whiteboard + cclist_accessible reporter_accessible) + }; + + Bugzilla::Hook::process('quicksearch_map', {'map' => \%full_map}); + + $cache->{quicksearch_fields} = \%full_map; + + return $cache->{quicksearch_fields}; } # Certain fields, when specified like "field:value" get an operator other # than "substring" -use constant FIELD_OPERATOR => { - content => 'matches', - owner_idle_time => 'greaterthan', -}; +use constant FIELD_OPERATOR => + {content => 'matches', owner_idle_time => 'greaterthan',}; # Mappings for operators symbols to support operators other than "substring" use constant OPERATOR_SYMBOLS => { - ':' => 'substring', - '=' => 'equals', - '!=' => 'notequals', - '>=' => 'greaterthaneq', - '<=' => 'lessthaneq', - '>' => 'greaterthan', - '<' => 'lessthan', + ':' => 'substring', + '=' => 'equals', + '!=' => 'notequals', + '>=' => 'greaterthaneq', + '<=' => 'lessthaneq', + '>' => 'greaterthan', + '<' => 'lessthan', }; # We might want to put this into localconfig or somewhere use constant PRODUCT_EXCEPTIONS => ( - 'row', # [Browser] - # ^^^ - 'new', # [MailNews] - # ^^^ + 'row', # [Browser] + # ^^^ + 'new', # [MailNews] + # ^^^ ); use constant COMPONENT_EXCEPTIONS => ( - 'hang' # [Bugzilla: Component/Keyword Changes] - # ^^^^ + 'hang' # [Bugzilla: Component/Keyword Changes] + # ^^^^ ); # Quicksearch-wide globals for boolean charts. our ($chart, $and, $or, $fulltext, $bug_status_set, $ELASTIC); sub quicksearch { - my ($searchstring) = (@_); - my $cgi = Bugzilla->cgi; + my ($searchstring) = (@_); + my $cgi = Bugzilla->cgi; - $chart = 0; - $and = 0; - $or = 0; + $chart = 0; + $and = 0; + $or = 0; - # Remove leading and trailing commas and whitespace. - $searchstring =~ s/(^[\s,]+|[\s,]+$)//g; - ThrowUserError('buglist_parameters_required') unless ($searchstring); + # Remove leading and trailing commas and whitespace. + $searchstring =~ s/(^[\s,]+|[\s,]+$)//g; + ThrowUserError('buglist_parameters_required') unless ($searchstring); - if ($searchstring =~ m/^[0-9,\s]*$/) { - _bug_numbers_only($searchstring); - } - else { - _handle_alias($searchstring); - - # Retain backslashes and quotes, to know which strings are quoted, - # and which ones are not. - my @words = _parse_line('\s+', 1, $searchstring); - # If parse_line() returns no data, this means strings are badly quoted. - # Rather than trying to guess what the user wanted to do, we throw an error. - scalar(@words) - || ThrowUserError('quicksearch_unbalanced_quotes', - { string => $searchstring, quicksearch => $searchstring }); - - # A query cannot start with AND or OR, nor can it end with AND, OR or NOT. + if ($searchstring =~ m/^[0-9,\s]*$/) { + _bug_numbers_only($searchstring); + } + else { + _handle_alias($searchstring); + + # Retain backslashes and quotes, to know which strings are quoted, + # and which ones are not. + my @words = _parse_line('\s+', 1, $searchstring); + + # If parse_line() returns no data, this means strings are badly quoted. + # Rather than trying to guess what the user wanted to do, we throw an error. + scalar(@words) || ThrowUserError('quicksearch_unbalanced_quotes', + {string => $searchstring, quicksearch => $searchstring}); + + # A query cannot start with AND or OR, nor can it end with AND, OR or NOT. + ThrowUserError('quicksearch_invalid_query', {quicksearch => $searchstring}) + if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/); + + $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0; + + my (@qswords, @or_group); + while (scalar @words) { + my $word = shift @words; + + # AND is the default word separator, similar to a whitespace, + # but |a AND OR b| is not a valid combination. + if ($word eq 'AND') { ThrowUserError('quicksearch_invalid_query', - { quicksearch => $searchstring }) - if ($words[0] =~ /^(?:AND|OR)$/ || $words[$#words] =~ /^(?:AND|OR|NOT)$/); - - $fulltext = Bugzilla->user->setting('quicksearch_fulltext') eq 'on' ? 1 : 0; - - my (@qswords, @or_group); - while (scalar @words) { - my $word = shift @words; - # AND is the default word separator, similar to a whitespace, - # but |a AND OR b| is not a valid combination. - if ($word eq 'AND') { - ThrowUserError('quicksearch_invalid_query', - { operators => ['AND', 'OR'], quicksearch => $searchstring }) - if $words[0] eq 'OR'; - } - # |a OR AND b| is not a valid combination. - # |a OR OR b| is equivalent to |a OR b| and so is harmless. - elsif ($word eq 'OR') { - ThrowUserError('quicksearch_invalid_query', - { operators => ['OR', 'AND'], quicksearch => $searchstring }) - if $words[0] eq 'AND'; - } - # NOT negates the following word. - # |NOT AND| and |NOT OR| are not valid combinations. - # |NOT NOT| is fine but has no effect as they cancel themselves. - elsif ($word eq 'NOT') { - $word = shift @words; - next if $word eq 'NOT'; - if ($word eq 'AND' || $word eq 'OR') { - ThrowUserError('quicksearch_invalid_query', - { operators => ['NOT', $word], quicksearch => $searchstring }); - } - unshift(@words, "-$word"); - } - # --comment and ++comment disable or enable fulltext searching - elsif ($word =~ /^(--|\+\+)comments?$/i) { - $fulltext = $1 eq '--' ? 0 : 1; - } - else { - # OR groups words together, as OR has higher precedence than AND. - push(@or_group, $word); - # If the next word is not OR, then we are not in a OR group, - # or we are leaving it. - if (!defined $words[0] || $words[0] ne 'OR') { - push(@qswords, join('|', @or_group)); - @or_group = (); - } - } - } + {operators => ['AND', 'OR'], quicksearch => $searchstring}) + if $words[0] eq 'OR'; + } - _handle_status_and_resolution($qswords[0]); - shift(@qswords) if $bug_status_set; - - my (@unknownFields, %ambiguous_fields); - - # Loop over all main-level QuickSearch words. - foreach my $qsword (@qswords) { - my @or_operand = _parse_line('\|', 1, $qsword); - foreach my $term (@or_operand) { - next unless defined $term; - my $negate = substr($term, 0, 1) eq '-'; - if ($negate) { - $term = substr($term, 1); - } - - next if _handle_special_first_chars($term, $negate); - next if _handle_field_names($term, $negate, \@unknownFields, - \%ambiguous_fields); - - # Having ruled out the special cases, we may now split - # by comma, which is another legal boolean OR indicator. - # Remove quotes from quoted words, if any. - @words = _parse_line(',', 0, $term); - foreach my $word (@words) { - if (!_special_field_syntax($word, $negate)) { - _default_quicksearch_word($word, $negate); - } - _handle_urls($word, $negate); - } - } - $chart++; - $and = 0; - $or = 0; + # |a OR AND b| is not a valid combination. + # |a OR OR b| is equivalent to |a OR b| and so is harmless. + elsif ($word eq 'OR') { + ThrowUserError('quicksearch_invalid_query', + {operators => ['OR', 'AND'], quicksearch => $searchstring}) + if $words[0] eq 'AND'; + } + + # NOT negates the following word. + # |NOT AND| and |NOT OR| are not valid combinations. + # |NOT NOT| is fine but has no effect as they cancel themselves. + elsif ($word eq 'NOT') { + $word = shift @words; + next if $word eq 'NOT'; + if ($word eq 'AND' || $word eq 'OR') { + ThrowUserError('quicksearch_invalid_query', + {operators => ['NOT', $word], quicksearch => $searchstring}); } - - # If there is no mention of a bug status, we restrict the query - # to open bugs by default. - unless ($bug_status_set) { - $cgi->param('bug_status', BUG_STATE_OPEN); + unshift(@words, "-$word"); + } + + # --comment and ++comment disable or enable fulltext searching + elsif ($word =~ /^(--|\+\+)comments?$/i) { + $fulltext = $1 eq '--' ? 0 : 1; + } + else { + # OR groups words together, as OR has higher precedence than AND. + push(@or_group, $word); + + # If the next word is not OR, then we are not in a OR group, + # or we are leaving it. + if (!defined $words[0] || $words[0] ne 'OR') { + push(@qswords, join('|', @or_group)); + @or_group = (); } + } + } + + _handle_status_and_resolution($qswords[0]); + shift(@qswords) if $bug_status_set; + + my (@unknownFields, %ambiguous_fields); - # Inform user about any unknown fields - if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) { - ThrowUserError("quicksearch_unknown_field", - { unknown => \@unknownFields, - ambiguous => \%ambiguous_fields, - quicksearch => $searchstring }); + # Loop over all main-level QuickSearch words. + foreach my $qsword (@qswords) { + my @or_operand = _parse_line('\|', 1, $qsword); + foreach my $term (@or_operand) { + next unless defined $term; + my $negate = substr($term, 0, 1) eq '-'; + if ($negate) { + $term = substr($term, 1); } - # Make sure we have some query terms left - scalar($cgi->param())>0 || ThrowUserError("buglist_parameters_required"); + next if _handle_special_first_chars($term, $negate); + next + if _handle_field_names($term, $negate, \@unknownFields, \%ambiguous_fields); + + # Having ruled out the special cases, we may now split + # by comma, which is another legal boolean OR indicator. + # Remove quotes from quoted words, if any. + @words = _parse_line(',', 0, $term); + foreach my $word (@words) { + if (!_special_field_syntax($word, $negate)) { + _default_quicksearch_word($word, $negate); + } + _handle_urls($word, $negate); + } + } + $chart++; + $and = 0; + $or = 0; + } + + # If there is no mention of a bug status, we restrict the query + # to open bugs by default. + unless ($bug_status_set) { + $cgi->param('bug_status', BUG_STATE_OPEN); + } + + # Inform user about any unknown fields + if (scalar(@unknownFields) || scalar(keys %ambiguous_fields)) { + ThrowUserError( + "quicksearch_unknown_field", + { + unknown => \@unknownFields, + ambiguous => \%ambiguous_fields, + quicksearch => $searchstring + } + ); } - # List of quicksearch-specific CGI parameters to get rid of. - my @params_to_strip = ('quicksearch', 'load', 'run'); - my $modified_query_string = $cgi->canonicalise_query(@params_to_strip); + # Make sure we have some query terms left + scalar($cgi->param()) > 0 || ThrowUserError("buglist_parameters_required"); + } - if ($cgi->param('load')) { - my $urlbase = Bugzilla->localconfig->{urlbase}; - # Param 'load' asks us to display the query in the advanced search form. - print $cgi->redirect(-uri => "${urlbase}query.cgi?format=advanced&" - . $modified_query_string); - } + # List of quicksearch-specific CGI parameters to get rid of. + my @params_to_strip = ('quicksearch', 'load', 'run'); + my $modified_query_string = $cgi->canonicalise_query(@params_to_strip); + + if ($cgi->param('load')) { + my $urlbase = Bugzilla->localconfig->{urlbase}; - # Otherwise, pass the modified query string to the caller. - # We modified $cgi->params, so the caller can choose to look at that, too, - # and disregard the return value. - $cgi->delete(@params_to_strip); - return $modified_query_string; + # Param 'load' asks us to display the query in the advanced search form. + print $cgi->redirect( + -uri => "${urlbase}query.cgi?format=advanced&" . $modified_query_string); + } + + # Otherwise, pass the modified query string to the caller. + # We modified $cgi->params, so the caller can choose to look at that, too, + # and disregard the return value. + $cgi->delete(@params_to_strip); + return $modified_query_string; } ########################## @@ -284,336 +293,353 @@ sub quicksearch { ########################## sub _parse_line { - my ($delim, $keep, $line) = @_; - return () unless defined $line; - - # parse_line always treats ' as a quote character, making it impossible - # to sanely search for contractions. As this behavour isn't - # configurable, we replace ' with a placeholder to hide it from the - # parser. - - # only treat ' at the start or end of words as quotes - # it's easier to do this in reverse with regexes - $line =~ s/(^|\s|:)'/$1\001/g; - $line =~ s/'($|\s)/\001$1/g; - $line =~ s/\\?'/\000/g; - $line =~ tr/\001/'/; - - my @words = parse_line($delim, $keep, $line); - foreach my $word (@words) { - $word =~ tr/\000/'/ if defined $word; - } - return @words; + my ($delim, $keep, $line) = @_; + return () unless defined $line; + + # parse_line always treats ' as a quote character, making it impossible + # to sanely search for contractions. As this behavour isn't + # configurable, we replace ' with a placeholder to hide it from the + # parser. + + # only treat ' at the start or end of words as quotes + # it's easier to do this in reverse with regexes + $line =~ s/(^|\s|:)'/$1\001/g; + $line =~ s/'($|\s)/\001$1/g; + $line =~ s/\\?'/\000/g; + $line =~ tr/\001/'/; + + my @words = parse_line($delim, $keep, $line); + foreach my $word (@words) { + $word =~ tr/\000/'/ if defined $word; + } + return @words; } sub _bug_numbers_only { - my $searchstring = shift; - my $cgi = Bugzilla->cgi; - # Allow separation by comma or whitespace. - $searchstring =~ s/[,\s]+/,/g; - - if ($searchstring !~ /,/ && !i_am_webservice()) { - # Single bug number; shortcut to show_bug.cgi. - print $cgi->redirect( - -uri => Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$searchstring"); - exit; - } - else { - # List of bug numbers. - $cgi->param('bug_id', $searchstring); - $cgi->param('order', 'bugs.bug_id'); - $cgi->param('bug_id_type', 'anyexact'); - } + my $searchstring = shift; + my $cgi = Bugzilla->cgi; + + # Allow separation by comma or whitespace. + $searchstring =~ s/[,\s]+/,/g; + + if ($searchstring !~ /,/ && !i_am_webservice()) { + + # Single bug number; shortcut to show_bug.cgi. + print $cgi->redirect( + -uri => Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$searchstring"); + exit; + } + else { + # List of bug numbers. + $cgi->param('bug_id', $searchstring); + $cgi->param('order', 'bugs.bug_id'); + $cgi->param('bug_id_type', 'anyexact'); + } } sub _handle_alias { - my $searchstring = shift; - if ($searchstring =~ /^([^,\s]+)$/) { - my $alias = $1; - # We use this direct SQL because we want quicksearch to be VERY fast. - my $bug_id = Bugzilla->dbh->selectrow_array( - q{SELECT bug_id FROM bugs WHERE alias = ?}, undef, $alias); - # If the user cannot see the bug or if we are using a webservice, - # do not resolve its alias. - if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) { - $alias = url_quote($alias); - print Bugzilla->cgi->redirect( - -uri => Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$alias"); - exit; - } - } + my $searchstring = shift; + if ($searchstring =~ /^([^,\s]+)$/) { + my $alias = $1; + + # We use this direct SQL because we want quicksearch to be VERY fast. + my $bug_id + = Bugzilla->dbh->selectrow_array(q{SELECT bug_id FROM bugs WHERE alias = ?}, + undef, $alias); + + # If the user cannot see the bug or if we are using a webservice, + # do not resolve its alias. + if ($bug_id && Bugzilla->user->can_see_bug($bug_id) && !i_am_webservice()) { + $alias = url_quote($alias); + print Bugzilla->cgi->redirect( + -uri => Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=$alias"); + exit; + } + } } sub _handle_status_and_resolution { - my $word = shift; - my $legal_statuses = get_legal_field_values('bug_status'); - my (%states, %resolutions); - $bug_status_set = 1; - - if ($word =~ s/^(ALL|OPEN)\+$/$1/) { - Bugzilla->cgi->param('limit' => 0); - } - - if ($word eq 'OPEN') { - $states{$_} = 1 foreach BUG_STATE_OPEN; - } - # If we want all bugs, then there is nothing to do. - elsif ($word ne 'ALL' - && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses)) - { - $bug_status_set = 0; - } - - # If we have wanted resolutions, allow closed states - if (keys(%resolutions)) { - foreach my $status (@$legal_statuses) { - $states{$status} = 1 unless is_open_state($status); - } - } - - Bugzilla->cgi->param('bug_status', keys(%states)); - Bugzilla->cgi->param('resolution', keys(%resolutions)); + my $word = shift; + my $legal_statuses = get_legal_field_values('bug_status'); + my (%states, %resolutions); + $bug_status_set = 1; + + if ($word =~ s/^(ALL|OPEN)\+$/$1/) { + Bugzilla->cgi->param('limit' => 0); + } + + if ($word eq 'OPEN') { + $states{$_} = 1 foreach BUG_STATE_OPEN; + } + + # If we want all bugs, then there is nothing to do. + elsif ($word ne 'ALL' + && !matchPrefixes(\%states, \%resolutions, $word, $legal_statuses)) + { + $bug_status_set = 0; + } + + # If we have wanted resolutions, allow closed states + if (keys(%resolutions)) { + foreach my $status (@$legal_statuses) { + $states{$status} = 1 unless is_open_state($status); + } + } + + Bugzilla->cgi->param('bug_status', keys(%states)); + Bugzilla->cgi->param('resolution', keys(%resolutions)); } sub _handle_special_first_chars { - my ($qsword, $negate) = @_; - return 0 if !defined $qsword || length($qsword) <= 1; - - my $firstChar = substr($qsword, 0, 1); - my $baseWord = substr($qsword, 1); - my @subWords = split(/,/, $baseWord); - - if ($firstChar eq '#') { - addChart('short_desc', 'substring', $baseWord, $negate); - addChart('content', 'matches', _matches_phrase($baseWord), $negate) if $fulltext; - return 1; - } - if ($firstChar eq ':') { - foreach (@subWords) { - addChart('product', 'substring', $_, $negate); - addChart('component', 'substring', $_, $negate); - } - return 1; - } - if ($firstChar eq '@') { - addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords); - return 1; - } - if ($firstChar eq '[') { - addChart('short_desc', 'substring', $baseWord, $negate); - addChart('status_whiteboard', 'substring', $baseWord, $negate); - return 1; - } - if ($firstChar eq '!') { - addChart('keywords', 'anywords', $baseWord, $negate); - return 1; - } - return 0; + my ($qsword, $negate) = @_; + return 0 if !defined $qsword || length($qsword) <= 1; + + my $firstChar = substr($qsword, 0, 1); + my $baseWord = substr($qsword, 1); + my @subWords = split(/,/, $baseWord); + + if ($firstChar eq '#') { + addChart('short_desc', 'substring', $baseWord, $negate); + addChart('content', 'matches', _matches_phrase($baseWord), $negate) + if $fulltext; + return 1; + } + if ($firstChar eq ':') { + foreach (@subWords) { + addChart('product', 'substring', $_, $negate); + addChart('component', 'substring', $_, $negate); + } + return 1; + } + if ($firstChar eq '@') { + addChart('assigned_to', 'substring', $_, $negate) foreach (@subWords); + return 1; + } + if ($firstChar eq '[') { + addChart('short_desc', 'substring', $baseWord, $negate); + addChart('status_whiteboard', 'substring', $baseWord, $negate); + return 1; + } + if ($firstChar eq '!') { + addChart('keywords', 'anywords', $baseWord, $negate); + return 1; + } + return 0; } sub _handle_field_names { - my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_; - - # Flag and requestee shortcut - if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) { - # BMO: Do not treat custom fields as flags if value is ? - if ($1 !~ /^cf_/) { - my ($flagtype, $requestee) = ($1, $2); - addChart('flagtypes.name', 'substring', $flagtype, $negate); - if ($requestee) { - # AND - $chart++; - $and = $or = 0; - addChart('requestees.login_name', 'substring', $requestee, $negate); - } - return 1; + my ($or_operand, $negate, $unknownFields, $ambiguous_fields) = @_; + + # Flag and requestee shortcut + if ($or_operand =~ /^(?:flag:)?([^\?]+\?)([^\?]*)$/) { + + # BMO: Do not treat custom fields as flags if value is ? + if ($1 !~ /^cf_/) { + my ($flagtype, $requestee) = ($1, $2); + addChart('flagtypes.name', 'substring', $flagtype, $negate); + if ($requestee) { + + # AND + $chart++; + $and = $or = 0; + addChart('requestees.login_name', 'substring', $requestee, $negate); + } + return 1; + } + } + + # Generic field1,field2,field3:value1,value2 notation. + # We have to correctly ignore commas and colons in quotes. + # Longer operators must be tested first as we don't want single character + # operators such as <, > and = to be tested before <=, >= and !=. + my @operators = sort { length($b) <=> length($a) } keys %{OPERATOR_SYMBOLS()}; + + foreach my $symbol (@operators) { + my @field_values = _parse_line($symbol, 1, $or_operand); + next unless scalar @field_values == 2; + my @fields = _parse_line(',', 1, $field_values[0]); + my @values = _parse_line(',', 1, $field_values[1]); + foreach my $field (@fields) { + my $translated = _translate_field_name($field); + + # Skip and record any unknown fields + if (!defined $translated) { + push(@$unknownFields, $field); + } + + # If we got back an array, that means the substring is + # ambiguous and could match more than field name + elsif (ref $translated) { + $ambiguous_fields->{$field} = $translated; + } + else { + if ($translated eq 'bug_status' || $translated eq 'resolution') { + $bug_status_set = 1; } - } - - # Generic field1,field2,field3:value1,value2 notation. - # We have to correctly ignore commas and colons in quotes. - # Longer operators must be tested first as we don't want single character - # operators such as <, > and = to be tested before <=, >= and !=. - my @operators = sort { length($b) <=> length($a) } keys %{ OPERATOR_SYMBOLS() }; - - foreach my $symbol (@operators) { - my @field_values = _parse_line($symbol, 1, $or_operand); - next unless scalar @field_values == 2; - my @fields = _parse_line(',', 1, $field_values[0]); - my @values = _parse_line(',', 1, $field_values[1]); - foreach my $field (@fields) { - my $translated = _translate_field_name($field); - # Skip and record any unknown fields - if (!defined $translated) { - push(@$unknownFields, $field); - } - # If we got back an array, that means the substring is - # ambiguous and could match more than field name - elsif (ref $translated) { - $ambiguous_fields->{$field} = $translated; - } - else { - if ($translated eq 'bug_status' || $translated eq 'resolution') { - $bug_status_set = 1; - } - foreach my $value (@values) { - next unless defined $value; - my $operator = FIELD_OPERATOR->{$translated} - || OPERATOR_SYMBOLS->{$symbol} - || 'substring'; - # If the string was quoted to protect some special - # characters such as commas and colons, we need - # to remove quotes. - if ($value =~ /^(["'])(.+)\1$/) { - $value = $2; - $value =~ s/\\(["'])/$1/g; - } - # If the value is a pair of matching quotes, the person wanted the empty string - elsif ($value =~ /^(["'])\1$/ || $translated eq 'resolution' && $value eq '---') { - $value = ""; - $operator = "isempty"; - } - addChart($translated, $operator, $value, $negate); - } - } + foreach my $value (@values) { + next unless defined $value; + my $operator + = FIELD_OPERATOR->{$translated} || OPERATOR_SYMBOLS->{$symbol} || 'substring'; + + # If the string was quoted to protect some special + # characters such as commas and colons, we need + # to remove quotes. + if ($value =~ /^(["'])(.+)\1$/) { + $value = $2; + $value =~ s/\\(["'])/$1/g; + } + + # If the value is a pair of matching quotes, the person wanted the empty string + elsif ($value =~ /^(["'])\1$/ || $translated eq 'resolution' && $value eq '---') + { + $value = ""; + $operator = "isempty"; + } + addChart($translated, $operator, $value, $negate); } - return 1; + } } - return 0; + return 1; + } + return 0; } sub _translate_field_name { - my $field = shift; - $field = lc($field); - my $field_map = FIELD_MAP; - - # If the field exactly matches a mapping, just return right now. - return $field_map->{$field} if exists $field_map->{$field}; - - # Check if we match, as a starting substring, exactly one field. - my @field_names = keys %$field_map; - my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names; - # Eliminate duplicates that are actually the same field - # (otherwise "assi" matches both "assignee" and "assigned_to", and - # the lines below fail when they shouldn't.) - my %match_unique = map { $field_map->{$_} => $_ } @matches; - @matches = values %match_unique; - - if (scalar(@matches) == 1) { - return $field_map->{$matches[0]}; - } - elsif (scalar(@matches) > 1) { - return \@matches; - } - - # Check if we match exactly one custom field, ignoring the cf_ on the - # custom fields (to allow people to type things like "build" for - # "cf_build"). - my %cfless; - foreach my $name (@field_names) { - my $no_cf = $name; - if ($no_cf =~ s/^cf_//) { - if ($field eq $no_cf) { - return $field_map->{$name}; - } - $cfless{$no_cf} = $name; - } - } - - # See if we match exactly one substring of any of the cf_-less fields. - my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless); - - if (scalar(@cfless_matches) == 1) { - my $match = $cfless_matches[0]; - my $actual_field = $cfless{$match}; - return $field_map->{$actual_field}; - } - elsif (scalar(@matches) > 1) { - return \@matches; - } - - return undef; + my $field = shift; + $field = lc($field); + my $field_map = FIELD_MAP; + + # If the field exactly matches a mapping, just return right now. + return $field_map->{$field} if exists $field_map->{$field}; + + # Check if we match, as a starting substring, exactly one field. + my @field_names = keys %$field_map; + my @matches = grep { $_ =~ /^\Q$field\E/ } @field_names; + + # Eliminate duplicates that are actually the same field + # (otherwise "assi" matches both "assignee" and "assigned_to", and + # the lines below fail when they shouldn't.) + my %match_unique = map { $field_map->{$_} => $_ } @matches; + @matches = values %match_unique; + + if (scalar(@matches) == 1) { + return $field_map->{$matches[0]}; + } + elsif (scalar(@matches) > 1) { + return \@matches; + } + + # Check if we match exactly one custom field, ignoring the cf_ on the + # custom fields (to allow people to type things like "build" for + # "cf_build"). + my %cfless; + foreach my $name (@field_names) { + my $no_cf = $name; + if ($no_cf =~ s/^cf_//) { + if ($field eq $no_cf) { + return $field_map->{$name}; + } + $cfless{$no_cf} = $name; + } + } + + # See if we match exactly one substring of any of the cf_-less fields. + my @cfless_matches = grep { $_ =~ /^\Q$field\E/ } (keys %cfless); + + if (scalar(@cfless_matches) == 1) { + my $match = $cfless_matches[0]; + my $actual_field = $cfless{$match}; + return $field_map->{$actual_field}; + } + elsif (scalar(@matches) > 1) { + return \@matches; + } + + return undef; } sub _special_field_syntax { - my ($word, $negate) = @_; - return unless defined($word); - - # P1-5 Syntax - if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { - my ($p_start, $p_end) = ($1, $2); - my $legal_priorities = get_legal_field_values('priority'); - - # If Pn exists explicitly, use it. - my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities; - my $end; - $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end; - - # If Pn doesn't exist explicitly, then we mean the nth priority. - if ($start == -1) { - $start = max(0, $p_start - 1); - } - my $prios = $legal_priorities->[$start]; - - if (defined $end) { - # If Pn doesn't exist explicitly, then we mean the nth priority. - if ($end == -1) { - $end = min(scalar(@$legal_priorities), $p_end) - 1; - $end = max(0, $end); # Just in case the user typed P0. - } - ($start, $end) = ($end, $start) if $end < $start; - $prios = join(',', @$legal_priorities[$start..$end]) - } + my ($word, $negate) = @_; + return unless defined($word); - addChart('priority', 'anyexact', $prios, $negate); - return 1; - } - return 0; -} + # P1-5 Syntax + if ($word =~ m/^P(\d+)(?:-(\d+))?$/i) { + my ($p_start, $p_end) = ($1, $2); + my $legal_priorities = get_legal_field_values('priority'); -sub _default_quicksearch_word { - my ($word, $negate) = @_; - return unless defined($word); + # If Pn exists explicitly, use it. + my $start = firstidx { $_ eq "P$p_start" } @$legal_priorities; + my $end; + $end = firstidx { $_ eq "P$p_end" } @$legal_priorities if defined $p_end; - if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) { - addChart('product', 'substring', $word, $negate); + # If Pn doesn't exist explicitly, then we mean the nth priority. + if ($start == -1) { + $start = max(0, $p_start - 1); } + my $prios = $legal_priorities->[$start]; - if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) { - addChart('component', 'substring', $word, $negate); - } + if (defined $end) { - my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all); - if (grep { lc($word) eq lc($_) } @legal_keywords) { - addChart('keywords', 'substring', $word, $negate); + # If Pn doesn't exist explicitly, then we mean the nth priority. + if ($end == -1) { + $end = min(scalar(@$legal_priorities), $p_end) - 1; + $end = max(0, $end); # Just in case the user typed P0. + } + ($start, $end) = ($end, $start) if $end < $start; + $prios = join(',', @$legal_priorities[$start .. $end]); } - addChart('alias', 'substring', $word, $negate); - addChart('short_desc', 'substring', $word, $negate); - addChart('status_whiteboard', 'substring', $word, $negate); - addChart('longdesc', 'substring', $word, $negate) if $ELASTIC; - addChart('content', 'matches', _matches_phrase($word), $negate) if $fulltext && !$ELASTIC; + addChart('priority', 'anyexact', $prios, $negate); + return 1; + } + return 0; +} - # BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches - addChart('cf_crash_signature', 'substring', $word, $negate); +sub _default_quicksearch_word { + my ($word, $negate) = @_; + return unless defined($word); + + if (!grep { lc($word) eq $_ } PRODUCT_EXCEPTIONS and length($word) > 2) { + addChart('product', 'substring', $word, $negate); + } + + if (!grep { lc($word) eq $_ } COMPONENT_EXCEPTIONS and length($word) > 2) { + addChart('component', 'substring', $word, $negate); + } + + my @legal_keywords = map($_->name, Bugzilla::Keyword->get_all); + if (grep { lc($word) eq lc($_) } @legal_keywords) { + addChart('keywords', 'substring', $word, $negate); + } + + addChart('alias', 'substring', $word, $negate); + addChart('short_desc', 'substring', $word, $negate); + addChart('status_whiteboard', 'substring', $word, $negate); + addChart('longdesc', 'substring', $word, $negate) if $ELASTIC; + addChart('content', 'matches', _matches_phrase($word), $negate) + if $fulltext && !$ELASTIC; + +# BMO Bug 664124 - Include the crash signature (sig:) field in default quicksearches + addChart('cf_crash_signature', 'substring', $word, $negate); } sub _handle_urls { - my ($word, $negate) = @_; - return unless defined($word); - - # URL field (for IP addrs, host.names, - # scheme://urls) - if ($word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ - || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ - || $word =~ /:[\\\/][\\\/]/ - || $word =~ /localhost/ - || $word =~ /mailto[:]?/) - # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port - { - addChart('bug_file_loc', 'substring', $word, $negate); - } + my ($word, $negate) = @_; + return unless defined($word); + + # URL field (for IP addrs, host.names, + # scheme://urls) + if ( $word =~ m/[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/ + || $word =~ /^[A-Za-z]+(\.[A-Za-z]+)+/ + || $word =~ /:[\\\/][\\\/]/ + || $word =~ /localhost/ + || $word =~ /mailto[:]?/) + + # || $word =~ /[A-Za-z]+[:][0-9]+/ #host:port + { + addChart('bug_file_loc', 'substring', $word, $negate); + } } ########################################################################### @@ -622,77 +648,77 @@ sub _handle_urls { # Quote and escape a phrase appropriately for a "content matches" search. sub _matches_phrase { - my ($phrase) = @_; - return $phrase if $ELASTIC; - $phrase =~ s/"/\\"/g; - return "\"$phrase\""; + my ($phrase) = @_; + return $phrase if $ELASTIC; + $phrase =~ s/"/\\"/g; + return "\"$phrase\""; } # Expand found prefixes to states or resolutions sub matchPrefixes { - my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_; - return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*\+?$/; - - my @ar_prefixes = split(/,/, $word); - if ($ar_prefixes[-1] =~ s/\+$//) { - Bugzilla->cgi->param(limit => 0); - } - my $ar_check_resolutions = get_legal_field_values('resolution'); - my $foundMatch = 0; - - foreach my $prefix (@ar_prefixes) { - foreach (@$ar_check_states) { - if (/^$prefix/) { - $$hr_states{$_} = 1; - $foundMatch = 1; - } - } - foreach (@$ar_check_resolutions) { - if (/^$prefix/) { - $$hr_resolutions{$_} = 1; - $foundMatch = 1; - } - } - } - return $foundMatch; + my ($hr_states, $hr_resolutions, $word, $ar_check_states) = @_; + return unless $word =~ /^[A-Z_]+(,[A-Z_]+)*\+?$/; + + my @ar_prefixes = split(/,/, $word); + if ($ar_prefixes[-1] =~ s/\+$//) { + Bugzilla->cgi->param(limit => 0); + } + my $ar_check_resolutions = get_legal_field_values('resolution'); + my $foundMatch = 0; + + foreach my $prefix (@ar_prefixes) { + foreach (@$ar_check_states) { + if (/^$prefix/) { + $$hr_states{$_} = 1; + $foundMatch = 1; + } + } + foreach (@$ar_check_resolutions) { + if (/^$prefix/) { + $$hr_resolutions{$_} = 1; + $foundMatch = 1; + } + } + } + return $foundMatch; } # Negate comparison type sub negateComparisonType { - my $comparisonType = shift; - - if ($comparisonType eq 'anywords') { - return 'nowords'; - } - elsif ($comparisonType eq 'isempty') { - return 'isnotempty'; - } - return "not$comparisonType"; + my $comparisonType = shift; + + if ($comparisonType eq 'anywords') { + return 'nowords'; + } + elsif ($comparisonType eq 'isempty') { + return 'isnotempty'; + } + return "not$comparisonType"; } # Add a boolean chart sub addChart { - my ($field, $comparisonType, $value, $negate) = @_; - - $negate && ($comparisonType = negateComparisonType($comparisonType)); - makeChart("$chart-$and-$or", $field, $comparisonType, $value); - if ($negate) { - $and++; - $or = 0; - } - else { - $or++; - } + my ($field, $comparisonType, $value, $negate) = @_; + + $negate && ($comparisonType = negateComparisonType($comparisonType)); + makeChart("$chart-$and-$or", $field, $comparisonType, $value); + if ($negate) { + $and++; + $or = 0; + } + else { + $or++; + } } # Create the CGI parameters for a boolean chart sub makeChart { - my ($expr, $field, $type, $value) = @_; + my ($expr, $field, $type, $value) = @_; - my $cgi = Bugzilla->cgi; - $cgi->param("field$expr", $field); - $cgi->param("type$expr", $type); - $cgi->param("value$expr", $value); + my $cgi = Bugzilla->cgi; + $cgi->param("field$expr", $field); + $cgi->param("type$expr", $type); + $cgi->param("value$expr", $value); } 1; diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm index a5d9e2417..5738dc93f 100644 --- a/Bugzilla/Search/Recent.pm +++ b/Bugzilla/Search/Recent.pm @@ -21,24 +21,25 @@ use Bugzilla::Util; # Constants # ############# -use constant DB_TABLE => 'profile_search'; +use constant DB_TABLE => 'profile_search'; use constant LIST_ORDER => 'id DESC'; + # Do not track buglists viewed by users. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - user_id - bug_list - list_order + id + user_id + bug_list + list_order ); use constant VALIDATORS => { - user_id => \&_check_user_id, - bug_list => \&_check_bug_list, - list_order => \&_check_list_order, + user_id => \&_check_user_id, + bug_list => \&_check_bug_list, + list_order => \&_check_list_order, }; use constant UPDATE_COLUMNS => qw(bug_list list_order); @@ -51,29 +52,30 @@ use constant USE_MEMCACHED => 0; ################### sub create { - my $class = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $search = $class->SUPER::create(@_); - my $user_id = $search->user_id; - - # Enforce there only being SAVE_NUM_SEARCHES per user. - my @ids = @{ $dbh->selectcol_arrayref( - "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", - undef, $user_id) }; - if (scalar(@ids) > SAVE_NUM_SEARCHES) { - splice(@ids, - SAVE_NUM_SEARCHES); - $dbh->do( - "DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")"); - } - $dbh->bz_commit_transaction(); - return $search; + my $class = shift; + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my $search = $class->SUPER::create(@_); + my $user_id = $search->user_id; + + # Enforce there only being SAVE_NUM_SEARCHES per user. + my @ids = @{ + $dbh->selectcol_arrayref( + "SELECT id FROM profile_search WHERE user_id = ? ORDER BY id", undef, + $user_id + ) + }; + if (scalar(@ids) > SAVE_NUM_SEARCHES) { + splice(@ids, - SAVE_NUM_SEARCHES); + $dbh->do("DELETE FROM profile_search WHERE id IN (" . join(',', @ids) . ")"); + } + $dbh->bz_commit_transaction(); + return $search; } sub create_placeholder { - my $class = shift; - return $class->create({ user_id => Bugzilla->user->id, - bug_list => '' }); + my $class = shift; + return $class->create({user_id => Bugzilla->user->id, bug_list => ''}); } ############### @@ -81,41 +83,43 @@ sub create_placeholder { ############### sub check { - my $class = shift; - my $search = $class->SUPER::check(@_); - my $user = Bugzilla->user; - if ($search->user_id != $user->id) { - ThrowUserError('object_does_not_exist', { id => $search->id }); - } - return $search; + my $class = shift; + my $search = $class->SUPER::check(@_); + my $user = Bugzilla->user; + if ($search->user_id != $user->id) { + ThrowUserError('object_does_not_exist', {id => $search->id}); + } + return $search; } sub check_quietly { - my $class = shift; - my $error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $search = eval { $class->check(@_) }; - Bugzilla->error_mode($error_mode); - return $search; + my $class = shift; + my $error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $search = eval { $class->check(@_) }; + Bugzilla->error_mode($error_mode); + return $search; } sub new_from_cookie { - my ($invocant, $bug_ids) = @_; - my $class = ref($invocant) || $invocant; + my ($invocant, $bug_ids) = @_; + my $class = ref($invocant) || $invocant; - my $search = { id => 'cookie', - user_id => Bugzilla->user->id, - bug_list => join(',', @$bug_ids) }; + my $search = { + id => 'cookie', + user_id => Bugzilla->user->id, + bug_list => join(',', @$bug_ids) + }; - bless $search, $class; - return $search; + bless $search, $class; + return $search; } #################### # Simple Accessors # #################### -sub bug_list { return [split(',', $_[0]->{'bug_list'})]; } +sub bug_list { return [split(',', $_[0]->{'bug_list'})]; } sub list_order { return $_[0]->{'list_order'}; } sub user_id { return $_[0]->{'user_id'}; } @@ -131,17 +135,17 @@ sub set_list_order { $_[0]->set('list_order', $_[1]); } ############## sub _check_user_id { - my ($invocant, $id) = @_; - require Bugzilla::User; - return Bugzilla::User->check({ id => $id })->id; + my ($invocant, $id) = @_; + require Bugzilla::User; + return Bugzilla::User->check({id => $id})->id; } sub _check_bug_list { - my ($invocant, $list) = @_; + my ($invocant, $list) = @_; - my @bug_ids = ref($list) ? @$list : split(',', $list || ''); - detaint_natural($_) foreach @bug_ids; - return join(',', @bug_ids); + my @bug_ids = ref($list) ? @$list : split(',', $list || ''); + detaint_natural($_) foreach @bug_ids; + return join(',', @bug_ids); } sub _check_list_order { defined $_[1] ? trim($_[1]) : '' } diff --git a/Bugzilla/Search/Saved.pm b/Bugzilla/Search/Saved.pm index 1511cd87b..c24d333a8 100644 --- a/Bugzilla/Search/Saved.pm +++ b/Bugzilla/Search/Saved.pm @@ -28,22 +28,23 @@ use Scalar::Util qw(blessed); ############# use constant DB_TABLE => 'namedqueries'; + # Do not track buglists saved by users. use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - userid - name - query + id + userid + name + query ); use constant VALIDATORS => { - name => \&_check_name, - query => \&_check_query, - link_in_footer => \&_check_link_in_footer, + name => \&_check_name, + query => \&_check_query, + link_in_footer => \&_check_link_in_footer, }; use constant UPDATE_COLUMNS => qw(name query); @@ -53,56 +54,53 @@ use constant UPDATE_COLUMNS => qw(name query); ############### sub new { - my $class = shift; - my $param = shift; - my $dbh = Bugzilla->dbh; - - my $user; - if (ref $param) { - $user = $param->{user} || Bugzilla->user; - my $name = $param->{name}; - if (!defined $name) { - ThrowCodeError('bad_arg', - {argument => 'name', - function => "${class}::new"}); - } - my $condition = 'userid = ? AND name = ?'; - my $user_id = blessed $user ? $user->id : $user; - detaint_natural($user_id) - || ThrowCodeError('param_must_be_numeric', - {function => $class . '::_init', param => 'user'}); - my @values = ($user_id, $name); - $param = { condition => $condition, values => \@values }; - } - - unshift @_, $param; - my $self = $class->SUPER::new(@_); - if ($self) { - $self->{user} = $user if blessed $user; - - # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8 - # when it's coming out of the database, even though it has no UTF-8 - # characters in it, which prevents Bugzilla::CGI from later reading - # it correctly. - utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query}); + my $class = shift; + my $param = shift; + my $dbh = Bugzilla->dbh; + + my $user; + if (ref $param) { + $user = $param->{user} || Bugzilla->user; + my $name = $param->{name}; + if (!defined $name) { + ThrowCodeError('bad_arg', {argument => 'name', function => "${class}::new"}); } - return $self; + my $condition = 'userid = ? AND name = ?'; + my $user_id = blessed $user ? $user->id : $user; + detaint_natural($user_id) + || ThrowCodeError('param_must_be_numeric', + {function => $class . '::_init', param => 'user'}); + my @values = ($user_id, $name); + $param = {condition => $condition, values => \@values}; + } + + unshift @_, $param; + my $self = $class->SUPER::new(@_); + if ($self) { + $self->{user} = $user if blessed $user; + + # Some DBs (read: Oracle) incorrectly mark the query string as UTF-8 + # when it's coming out of the database, even though it has no UTF-8 + # characters in it, which prevents Bugzilla::CGI from later reading + # it correctly. + utf8::downgrade($self->{query}) if utf8::is_utf8($self->{query}); + } + return $self; } sub check { - my $class = shift; - my $search = $class->SUPER::check(@_); - my $user = Bugzilla->user; - return $search if $search->user->id == $user->id; - - if (!$search->shared_with_group - or !$user->in_group($search->shared_with_group)) - { - ThrowUserError('missing_query', { name => $search->name, - sharer_id => $search->user->id }); - } - - return $search; + my $class = shift; + my $search = $class->SUPER::check(@_); + my $user = Bugzilla->user; + return $search if $search->user->id == $user->id; + + if (!$search->shared_with_group or !$user->in_group($search->shared_with_group)) + { + ThrowUserError('missing_query', + {name => $search->name, sharer_id => $search->user->id}); + } + + return $search; } ############## @@ -112,24 +110,25 @@ sub check { sub _check_link_in_footer { return $_[1] ? 1 : 0; } sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowUserError("query_name_missing"); - $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); - if (length($name) > MAX_LEN_QUERY_NAME) { - ThrowUserError("query_name_too_long"); - } - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowUserError("query_name_missing"); + $name !~ /[<>&]/ || ThrowUserError("illegal_query_name"); + if (length($name) > MAX_LEN_QUERY_NAME) { + ThrowUserError("query_name_too_long"); + } + return $name; } sub _check_query { - my ($invocant, $query) = @_; - $query || ThrowUserError("buglist_parameters_required"); - my $cgi = new Bugzilla::CGI($query); - $cgi->clean_search_url; - # Don't store the query name as a parameter. - $cgi->delete('known_name'); - return $cgi->query_string; + my ($invocant, $query) = @_; + $query || ThrowUserError("buglist_parameters_required"); + my $cgi = new Bugzilla::CGI($query); + $cgi->clean_search_url; + + # Don't store the query name as a parameter. + $cgi->delete('known_name'); + return $cgi->query_string; } ######################### @@ -137,170 +136,180 @@ sub _check_query { ######################### sub create { - my $class = shift; - Bugzilla->login(LOGIN_REQUIRED); - my $dbh = Bugzilla->dbh; - $class->check_required_create_fields(@_); - $dbh->bz_start_transaction(); - my $params = $class->run_create_validators(@_); - - # Right now you can only create a Saved Search for the current user. - $params->{userid} = Bugzilla->user->id; - - my $lif = delete $params->{link_in_footer}; - my $obj = $class->insert_create_data($params); - if ($lif) { - $dbh->do('INSERT INTO namedqueries_link_in_footer - (user_id, namedquery_id) VALUES (?,?)', - undef, $params->{userid}, $obj->id); - } - $dbh->bz_commit_transaction(); - - return $obj; + my $class = shift; + Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + $class->check_required_create_fields(@_); + $dbh->bz_start_transaction(); + my $params = $class->run_create_validators(@_); + + # Right now you can only create a Saved Search for the current user. + $params->{userid} = Bugzilla->user->id; + + my $lif = delete $params->{link_in_footer}; + my $obj = $class->insert_create_data($params); + if ($lif) { + $dbh->do( + 'INSERT INTO namedqueries_link_in_footer + (user_id, namedquery_id) VALUES (?,?)', undef, $params->{userid}, + $obj->id + ); + } + $dbh->bz_commit_transaction(); + + return $obj; } sub rename_field_value { - my ($class, $field, $old_value, $new_value) = @_; - - my $old = url_quote($old_value); - my $new = url_quote($new_value); - my $old_sql = $old; - $old_sql =~ s/([_\%])/\\$1/g; - - my $table = $class->DB_TABLE; - my $id_field = $class->ID_FIELD; - - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - - my %queries = @{ $dbh->selectcol_arrayref( - "SELECT $id_field, query FROM $table WHERE query LIKE ?", - {Columns=>[1,2]}, "\%$old_sql\%") }; - foreach my $id (keys %queries) { - my $query = $queries{$id}; - $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi; - # Fix boolean charts. - while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) { - my $chart_id = $1; - # Note that this won't handle lists or substrings inside of - # boolean charts. Users will have to fix those themselves. - $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i; - } - $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", - undef, $query, $id); - Bugzilla->memcached->clear({ table => $table, id => $id }); + my ($class, $field, $old_value, $new_value) = @_; + + my $old = url_quote($old_value); + my $new = url_quote($new_value); + my $old_sql = $old; + $old_sql =~ s/([_\%])/\\$1/g; + + my $table = $class->DB_TABLE; + my $id_field = $class->ID_FIELD; + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + + my %queries = @{ + $dbh->selectcol_arrayref( + "SELECT $id_field, query FROM $table WHERE query LIKE ?", + {Columns => [1, 2]}, + "\%$old_sql\%" + ) + }; + foreach my $id (keys %queries) { + my $query = $queries{$id}; + $query =~ s/\b$field=\Q$old\E\b/$field=$new/gi; + + # Fix boolean charts. + while ($query =~ /\bfield(\d+-\d+-\d+)=\Q$field\E\b/gi) { + my $chart_id = $1; + + # Note that this won't handle lists or substrings inside of + # boolean charts. Users will have to fix those themselves. + $query =~ s/\bvalue\Q$chart_id\E=\Q$old\E\b/value$chart_id=$new/i; } + $dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?", undef, $query, $id); + Bugzilla->memcached->clear({table => $table, id => $id}); + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub preload { - my ($searches) = @_; - my $dbh = Bugzilla->dbh; + my ($searches) = @_; + my $dbh = Bugzilla->dbh; - return unless scalar @$searches; + return unless scalar @$searches; - my @query_ids = map { $_->id } @$searches; - my $queries_in_footer = $dbh->selectcol_arrayref( - 'SELECT namedquery_id + my @query_ids = map { $_->id } @$searches; + my $queries_in_footer = $dbh->selectcol_arrayref( + 'SELECT namedquery_id FROM namedqueries_link_in_footer WHERE ' . $dbh->sql_in('namedquery_id', \@query_ids) . ' AND user_id = ?', - undef, Bugzilla->user->id); + undef, Bugzilla->user->id + ); - my %links_in_footer = map { $_ => 1 } @$queries_in_footer; - foreach my $query (@$searches) { - $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0; - } + my %links_in_footer = map { $_ => 1 } @$queries_in_footer; + foreach my $query (@$searches) { + $query->{link_in_footer} = ($links_in_footer{$query->id}) ? 1 : 0; + } } ##################### # Complex Accessors # ##################### sub edit_link { - my ($self) = @_; - return $self->{edit_link} if defined $self->{edit_link}; - my $cgi = new Bugzilla::CGI($self->url); - if (!$cgi->param('query_type') - || !IsValidQueryType($cgi->param('query_type'))) - { - $cgi->param('query_type', 'advanced'); - } - $self->{edit_link} = $cgi->canonicalise_query; - return $self->{edit_link}; + my ($self) = @_; + return $self->{edit_link} if defined $self->{edit_link}; + my $cgi = new Bugzilla::CGI($self->url); + if (!$cgi->param('query_type') || !IsValidQueryType($cgi->param('query_type'))) + { + $cgi->param('query_type', 'advanced'); + } + $self->{edit_link} = $cgi->canonicalise_query; + return $self->{edit_link}; } sub used_in_whine { - my ($self) = @_; - return $self->{used_in_whine} if exists $self->{used_in_whine}; - ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array( - 'SELECT 1 FROM whine_events INNER JOIN whine_queries + my ($self) = @_; + return $self->{used_in_whine} if exists $self->{used_in_whine}; + ($self->{used_in_whine}) = Bugzilla->dbh->selectrow_array( + 'SELECT 1 FROM whine_events INNER JOIN whine_queries ON whine_events.id = whine_queries.eventid WHERE whine_events.owner_userid = ? AND query_name = ?', undef, - $self->{userid}, $self->name) || 0; - return $self->{used_in_whine}; + $self->{userid}, $self->name + ) || 0; + return $self->{used_in_whine}; } sub link_in_footer { - my ($self, $user) = @_; - # We only cache link_in_footer for the current Bugzilla->user. - return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user; - my $user_id = $user ? $user->id : Bugzilla->user->id; - my $link_in_footer = Bugzilla->dbh->selectrow_array( - 'SELECT 1 FROM namedqueries_link_in_footer - WHERE namedquery_id = ? AND user_id = ?', - undef, $self->id, $user_id) || 0; - $self->{link_in_footer} = $link_in_footer if !$user; - return $link_in_footer; + my ($self, $user) = @_; + + # We only cache link_in_footer for the current Bugzilla->user. + return $self->{link_in_footer} if exists $self->{link_in_footer} && !$user; + my $user_id = $user ? $user->id : Bugzilla->user->id; + my $link_in_footer = Bugzilla->dbh->selectrow_array( + 'SELECT 1 FROM namedqueries_link_in_footer + WHERE namedquery_id = ? AND user_id = ?', undef, $self->id, $user_id + ) || 0; + $self->{link_in_footer} = $link_in_footer if !$user; + return $link_in_footer; } sub shared_with_group { - my ($self) = @_; - return $self->{shared_with_group} if exists $self->{shared_with_group}; - # Bugzilla only currently supports sharing with one group, even - # though the database backend allows for an infinite number. - my ($group_id) = Bugzilla->dbh->selectrow_array( - 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?', - undef, $self->id); - $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) - : undef; - return $self->{shared_with_group}; + my ($self) = @_; + return $self->{shared_with_group} if exists $self->{shared_with_group}; + + # Bugzilla only currently supports sharing with one group, even + # though the database backend allows for an infinite number. + my ($group_id) + = Bugzilla->dbh->selectrow_array( + 'SELECT group_id FROM namedquery_group_map WHERE namedquery_id = ?', + undef, $self->id); + $self->{shared_with_group} = $group_id ? new Bugzilla::Group($group_id) : undef; + return $self->{shared_with_group}; } sub shared_with_users { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - if (!exists $self->{shared_with_users}) { - $self->{shared_with_users} = - $dbh->selectrow_array('SELECT COUNT(*) + if (!exists $self->{shared_with_users}) { + $self->{shared_with_users} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM namedqueries_link_in_footer INNER JOIN namedqueries ON namedquery_id = id WHERE namedquery_id = ? - AND user_id != userid', - undef, $self->id); - } - return $self->{shared_with_users}; + AND user_id != userid', undef, $self->id + ); + } + return $self->{shared_with_users}; } #################### # Simple Accessors # #################### -sub url { return $_[0]->{'query'}; } +sub url { return $_[0]->{'query'}; } sub user { - my ($self) = @_; - return $self->{user} ||= - Bugzilla::User->new({ id => $self->{userid}, cache => 1 }); + my ($self) = @_; + return $self->{user} + ||= Bugzilla::User->new({id => $self->{userid}, cache => 1}); } ############ # Mutators # ############ -sub set_name { $_[0]->set('name', $_[1]); } -sub set_url { $_[0]->set('query', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } +sub set_url { $_[0]->set('query', $_[1]); } 1; diff --git a/Bugzilla/Send/Sendmail.pm b/Bugzilla/Send/Sendmail.pm index 81c2190e5..fe27a0c70 100644 --- a/Bugzilla/Send/Sendmail.pm +++ b/Bugzilla/Send/Sendmail.pm @@ -16,80 +16,98 @@ use Return::Value; use Symbol qw(gensym); sub send { - my ($class, $message, @args) = @_; - my $mailer = $class->_find_sendmail; + my ($class, $message, @args) = @_; + my $mailer = $class->_find_sendmail; - return failure "Couldn't find 'sendmail' executable in your PATH" - ." and Email::Send::Sendmail::SENDMAIL is not set" - unless $mailer; + return failure "Couldn't find 'sendmail' executable in your PATH" + . " and Email::Send::Sendmail::SENDMAIL is not set" + unless $mailer; - return failure "Found $mailer but cannot execute it" - unless -x $mailer; + return failure "Found $mailer but cannot execute it" unless -x $mailer; - local $SIG{'CHLD'} = 'DEFAULT'; + local $SIG{'CHLD'} = 'DEFAULT'; - my $pipe = gensym; + my $pipe = gensym; - open($pipe, '|-', "$mailer -t -oi @args") - || return failure "Error executing $mailer: $!"; - print($pipe $message->as_string) - || return failure "Error printing via pipe to $mailer: $!"; - unless (close $pipe) { - return failure "error when closing pipe to $mailer: $!" if $!; - my ($error_message, $is_transient) = _map_exitcode($? >> 8); - if (Bugzilla->get_param_with_override('use_mailer_queue')) { - # Return success for errors which are fatal so Bugzilla knows to - # remove them from the queue - if ($is_transient) { - return failure "error when closing pipe to $mailer: $error_message"; - } else { - warn "error when closing pipe to $mailer: $error_message\n"; - return success; - } - } else { - return failure "error when closing pipe to $mailer: $error_message"; - } + open($pipe, '|-', "$mailer -t -oi @args") + || return failure "Error executing $mailer: $!"; + print($pipe $message->as_string) + || return failure "Error printing via pipe to $mailer: $!"; + unless (close $pipe) { + return failure "error when closing pipe to $mailer: $!" if $!; + my ($error_message, $is_transient) = _map_exitcode($? >> 8); + if (Bugzilla->get_param_with_override('use_mailer_queue')) { + + # Return success for errors which are fatal so Bugzilla knows to + # remove them from the queue + if ($is_transient) { + return failure "error when closing pipe to $mailer: $error_message"; + } + else { + warn "error when closing pipe to $mailer: $error_message\n"; + return success; + } + } + else { + return failure "error when closing pipe to $mailer: $error_message"; } - return success; + } + return success; } sub _map_exitcode { - # Returns (error message, is_transient) - # from the sendmail source (sendmail/sysexit.h) - my $code = shift; - if ($code == 64) { - return ("Command line usage error (EX_USAGE)", 1); - } elsif ($code == 65) { - return ("Data format error (EX_DATAERR)", 1); - } elsif ($code == 66) { - return ("Cannot open input (EX_NOINPUT)", 1); - } elsif ($code == 67) { - return ("Addressee unknown (EX_NOUSER)", 0); - } elsif ($code == 68) { - return ("Host name unknown (EX_NOHOST)", 0); - } elsif ($code == 69) { - return ("Service unavailable (EX_UNAVAILABLE)", 1); - } elsif ($code == 70) { - return ("Internal software error (EX_SOFTWARE)", 1); - } elsif ($code == 71) { - return ("System error (EX_OSERR)", 1); - } elsif ($code == 72) { - return ("Critical OS file missing (EX_OSFILE)", 1); - } elsif ($code == 73) { - return ("Can't create output file (EX_CANTCREAT)", 1); - } elsif ($code == 74) { - return ("Input/output error (EX_IOERR)", 1); - } elsif ($code == 75) { - return ("Temp failure (EX_TEMPFAIL)", 1); - } elsif ($code == 76) { - return ("Remote error in protocol (EX_PROTOCOL)", 1); - } elsif ($code == 77) { - return ("Permission denied (EX_NOPERM)", 1); - } elsif ($code == 78) { - return ("Configuration error (EX_CONFIG)", 1); - } else { - return ("Unknown Error ($code)", 1); - } + + # Returns (error message, is_transient) + # from the sendmail source (sendmail/sysexit.h) + my $code = shift; + if ($code == 64) { + return ("Command line usage error (EX_USAGE)", 1); + } + elsif ($code == 65) { + return ("Data format error (EX_DATAERR)", 1); + } + elsif ($code == 66) { + return ("Cannot open input (EX_NOINPUT)", 1); + } + elsif ($code == 67) { + return ("Addressee unknown (EX_NOUSER)", 0); + } + elsif ($code == 68) { + return ("Host name unknown (EX_NOHOST)", 0); + } + elsif ($code == 69) { + return ("Service unavailable (EX_UNAVAILABLE)", 1); + } + elsif ($code == 70) { + return ("Internal software error (EX_SOFTWARE)", 1); + } + elsif ($code == 71) { + return ("System error (EX_OSERR)", 1); + } + elsif ($code == 72) { + return ("Critical OS file missing (EX_OSFILE)", 1); + } + elsif ($code == 73) { + return ("Can't create output file (EX_CANTCREAT)", 1); + } + elsif ($code == 74) { + return ("Input/output error (EX_IOERR)", 1); + } + elsif ($code == 75) { + return ("Temp failure (EX_TEMPFAIL)", 1); + } + elsif ($code == 76) { + return ("Remote error in protocol (EX_PROTOCOL)", 1); + } + elsif ($code == 77) { + return ("Permission denied (EX_NOPERM)", 1); + } + elsif ($code == 78) { + return ("Configuration error (EX_CONFIG)", 1); + } + else { + return ("Unknown Error ($code)", 1); + } } 1; diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm index be30f6284..d89c2959a 100644 --- a/Bugzilla/Series.pm +++ b/Bugzilla/Series.pm @@ -27,224 +27,255 @@ use constant DB_TABLE => 'series'; use constant ID_FIELD => 'series_id'; 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); - - my $arg_count = scalar(@_); - - # new() can return undef if you pass in a series_id and the user doesn't - # have sufficient permissions. If you create a new series in this way, - # you need to check for an undef return, and act appropriately. - my $retval = $self; - - # There are three ways of creating Series objects. Two (CGI and Parameters) - # are for use when creating a new series. One (Database) is for retrieving - # information on existing series. - if ($arg_count == 1) { - if (ref($_[0])) { - # We've been given a CGI object to create a new Series from. - # This series may already exist - external code needs to check - # before it calls writeToDatabase(). - $self->initFromCGI($_[0]); - } - else { - # We've been given a series_id, which should represent an existing - # Series. - $retval = $self->initFromDatabase($_[0]); - } - } - elsif ($arg_count >= 6 && $arg_count <= 8) { - # We've been given a load of parameters to create a new Series from. - # Currently, undef is always passed as the first parameter; this allows - # you to call writeToDatabase() unconditionally. - # XXX - You cannot set category_id and subcategory_id from here. - $self->initFromParameters(@_); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + + # Create a ref to an empty hash and bless it + my $self = {}; + bless($self, $class); + + my $arg_count = scalar(@_); + + # new() can return undef if you pass in a series_id and the user doesn't + # have sufficient permissions. If you create a new series in this way, + # you need to check for an undef return, and act appropriately. + my $retval = $self; + + # There are three ways of creating Series objects. Two (CGI and Parameters) + # are for use when creating a new series. One (Database) is for retrieving + # information on existing series. + if ($arg_count == 1) { + if (ref($_[0])) { + + # We've been given a CGI object to create a new Series from. + # This series may already exist - external code needs to check + # before it calls writeToDatabase(). + $self->initFromCGI($_[0]); } else { - die("Bad parameters passed in - invalid number of args: $arg_count"); + # We've been given a series_id, which should represent an existing + # Series. + $retval = $self->initFromDatabase($_[0]); } - - return $retval; + } + elsif ($arg_count >= 6 && $arg_count <= 8) { + + # We've been given a load of parameters to create a new Series from. + # Currently, undef is always passed as the first parameter; this allows + # you to call writeToDatabase() unconditionally. + # XXX - You cannot set category_id and subcategory_id from here. + $self->initFromParameters(@_); + } + else { + die("Bad parameters passed in - invalid number of args: $arg_count"); + } + + return $retval; } sub initFromDatabase { - my ($self, $series_id) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - detaint_natural($series_id) - || ThrowCodeError("invalid_series_id", { 'series_id' => $series_id }); - - my $grouplist = $user->groups_as_string; - - my @series = $dbh->selectrow_array("SELECT series.series_id, cc1.name, " . - "cc2.name, series.name, series.creator, series.frequency, " . - "series.query, series.is_public, series.category, series.subcategory " . - "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 series.series_id = ? " . - " AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))", - undef, ($series_id, $user->id)); - - if (@series) { - $self->initFromParameters(@series); - return $self; - } - else { - return undef; - } + my ($self, $series_id) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + detaint_natural($series_id) + || ThrowCodeError("invalid_series_id", {'series_id' => $series_id}); + + my $grouplist = $user->groups_as_string; + + my @series = $dbh->selectrow_array( + "SELECT series.series_id, cc1.name, " + . "cc2.name, series.name, series.creator, series.frequency, " + . "series.query, series.is_public, series.category, series.subcategory " + . "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 series.series_id = ? " + . " AND (creator = ? OR (is_public = 1 AND cgm.category_id IS NULL))", + undef, + ($series_id, $user->id) + ); + + if (@series) { + $self->initFromParameters(@series); + return $self; + } + else { + return undef; + } } sub initFromParameters { - # Pass undef as the first parameter if you are creating a new series. - my $self = shift; - ($self->{'series_id'}, $self->{'category'}, $self->{'subcategory'}, - $self->{'name'}, $self->{'creator_id'}, $self->{'frequency'}, - $self->{'query'}, $self->{'public'}, $self->{'category_id'}, - $self->{'subcategory_id'}) = @_; + # Pass undef as the first parameter if you are creating a new series. + my $self = shift; - # If the first parameter is undefined, check if this series already - # exists and update it series_id accordingly - $self->{'series_id'} ||= $self->existsInDatabase(); + ( + $self->{'series_id'}, $self->{'category'}, $self->{'subcategory'}, + $self->{'name'}, $self->{'creator_id'}, $self->{'frequency'}, + $self->{'query'}, $self->{'public'}, $self->{'category_id'}, + $self->{'subcategory_id'} + ) = @_; + + # If the first parameter is undefined, check if this series already + # exists and update it series_id accordingly + $self->{'series_id'} ||= $self->existsInDatabase(); } sub initFromCGI { - my $self = shift; - my $cgi = shift; - - $self->{'series_id'} = $cgi->param('series_id') || undef; - if (defined($self->{'series_id'})) { - detaint_natural($self->{'series_id'}) - || ThrowCodeError("invalid_series_id", - { 'series_id' => $self->{'series_id'} }); - } + my $self = shift; + my $cgi = shift; + + $self->{'series_id'} = $cgi->param('series_id') || undef; + if (defined($self->{'series_id'})) { + detaint_natural($self->{'series_id'}) + || ThrowCodeError("invalid_series_id", {'series_id' => $self->{'series_id'}}); + } - $self->{'category'} = $cgi->param('category') - || $cgi->param('newcategory') - || ThrowUserError("missing_category"); + $self->{'category'} + = $cgi->param('category') + || $cgi->param('newcategory') + || ThrowUserError("missing_category"); - $self->{'subcategory'} = $cgi->param('subcategory') - || $cgi->param('newsubcategory') - || ThrowUserError("missing_subcategory"); + $self->{'subcategory'} + = $cgi->param('subcategory') + || $cgi->param('newsubcategory') + || ThrowUserError("missing_subcategory"); - $self->{'name'} = $cgi->param('name') - || ThrowUserError("missing_name"); + $self->{'name'} = $cgi->param('name') || ThrowUserError("missing_name"); - $self->{'creator_id'} = Bugzilla->user->id; + $self->{'creator_id'} = Bugzilla->user->id; - $self->{'frequency'} = $cgi->param('frequency'); - detaint_natural($self->{'frequency'}) - || ThrowUserError("missing_frequency"); + $self->{'frequency'} = $cgi->param('frequency'); + detaint_natural($self->{'frequency'}) || ThrowUserError("missing_frequency"); - $self->{'query'} = $cgi->canonicalise_query("format", "ctype", "action", - "category", "subcategory", "name", - "frequency", "public", "query_format"); - trick_taint($self->{'query'}); + $self->{'query'} = $cgi->canonicalise_query( + "format", "ctype", "action", "category", + "subcategory", "name", "frequency", "public", + "query_format" + ); + trick_taint($self->{'query'}); - $self->{'public'} = $cgi->param('public') ? 1 : 0; + $self->{'public'} = $cgi->param('public') ? 1 : 0; - # Change 'admin' here and in series.html.tmpl, or remove the check - # completely, if you want to change who can make series public. - $self->{'public'} = 0 unless Bugzilla->user->in_group('admin'); + # Change 'admin' here and in series.html.tmpl, or remove the check + # completely, if you want to change who can make series public. + $self->{'public'} = 0 unless Bugzilla->user->in_group('admin'); } sub writeToDatabase { - my $self = shift; + my $self = shift; - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); - my $category_id = getCategoryID($self->{'category'}); - my $subcategory_id = getCategoryID($self->{'subcategory'}); + my $category_id = getCategoryID($self->{'category'}); + my $subcategory_id = getCategoryID($self->{'subcategory'}); - my $exists; - if ($self->{'series_id'}) { - $exists = - $dbh->selectrow_array("SELECT series_id FROM series - WHERE series_id = $self->{'series_id'}"); - } + my $exists; + if ($self->{'series_id'}) { + $exists = $dbh->selectrow_array( + "SELECT series_id FROM series + WHERE series_id = $self->{'series_id'}" + ); + } - # Is this already in the database? - if ($exists) { - # Update existing series - my $dbh = Bugzilla->dbh; - $dbh->do("UPDATE series SET " . - "category = ?, subcategory = ?," . - "name = ?, frequency = ?, is_public = ? " . - "WHERE series_id = ?", undef, - $category_id, $subcategory_id, $self->{'name'}, - $self->{'frequency'}, $self->{'public'}, - $self->{'series_id'}); - } - else { - # Insert the new series into the series table - $dbh->do("INSERT INTO series (creator, category, subcategory, " . - "name, frequency, query, is_public) VALUES " . - "(?, ?, ?, ?, ?, ?, ?)", undef, - $self->{'creator_id'}, $category_id, $subcategory_id, $self->{'name'}, - $self->{'frequency'}, $self->{'query'}, $self->{'public'}); - - # Retrieve series_id - $self->{'series_id'} = $dbh->selectrow_array("SELECT MAX(series_id) " . - "FROM series"); - $self->{'series_id'} - || ThrowCodeError("missing_series_id", { 'series' => $self }); - } + # Is this already in the database? + if ($exists) { - $dbh->bz_commit_transaction(); + # Update existing series + my $dbh = Bugzilla->dbh; + $dbh->do( + "UPDATE series SET " + . "category = ?, subcategory = ?," + . "name = ?, frequency = ?, is_public = ? " + . "WHERE series_id = ?", + undef, + $category_id, + $subcategory_id, + $self->{'name'}, + $self->{'frequency'}, + $self->{'public'}, + $self->{'series_id'} + ); + } + else { + # Insert the new series into the series table + $dbh->do( + "INSERT INTO series (creator, category, subcategory, " + . "name, frequency, query, is_public) VALUES " + . "(?, ?, ?, ?, ?, ?, ?)", + undef, + $self->{'creator_id'}, + $category_id, + $subcategory_id, + $self->{'name'}, + $self->{'frequency'}, + $self->{'query'}, + $self->{'public'} + ); + + # Retrieve series_id + $self->{'series_id'} + = $dbh->selectrow_array("SELECT MAX(series_id) " . "FROM series"); + $self->{'series_id'} + || ThrowCodeError("missing_series_id", {'series' => $self}); + } + + $dbh->bz_commit_transaction(); } # Check whether a series with this name, category and subcategory exists in # the DB and, if so, returns its series_id. sub existsInDatabase { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - my $category_id = getCategoryID($self->{'category'}); - my $subcategory_id = getCategoryID($self->{'subcategory'}); + my $category_id = getCategoryID($self->{'category'}); + my $subcategory_id = getCategoryID($self->{'subcategory'}); - trick_taint($self->{'name'}); - my $series_id = $dbh->selectrow_array("SELECT series_id " . - "FROM series WHERE category = $category_id " . - "AND subcategory = $subcategory_id AND name = " . - $dbh->quote($self->{'name'})); + trick_taint($self->{'name'}); + my $series_id + = $dbh->selectrow_array("SELECT series_id " + . "FROM series WHERE category = $category_id " + . "AND subcategory = $subcategory_id AND name = " + . $dbh->quote($self->{'name'})); - return($series_id); + return ($series_id); } # Get a category or subcategory IDs, creating the category if it doesn't exist. sub getCategoryID { - my ($category) = @_; - my $category_id; - my $dbh = Bugzilla->dbh; + my ($category) = @_; + my $category_id; + my $dbh = Bugzilla->dbh; - # This seems for the best idiom for "Do A. Then maybe do B and A again." - while (1) { - # We are quoting this to put it in the DB, so we can remove taint - trick_taint($category); + # This seems for the best idiom for "Do A. Then maybe do B and A again." + while (1) { - $category_id = $dbh->selectrow_array("SELECT id " . - "from series_categories " . - "WHERE name =" . $dbh->quote($category)); + # We are quoting this to put it in the DB, so we can remove taint + trick_taint($category); - last if defined($category_id); + $category_id + = $dbh->selectrow_array("SELECT id " + . "from series_categories " + . "WHERE name =" + . $dbh->quote($category)); - $dbh->do("INSERT INTO series_categories (name) " . - "VALUES (" . $dbh->quote($category) . ")"); - } + last if defined($category_id); + + $dbh->do("INSERT INTO series_categories (name) " + . "VALUES (" + . $dbh->quote($category) + . ")"); + } - return $category_id; + return $category_id; } ########## @@ -254,20 +285,20 @@ sub id { return $_[0]->{'series_id'}; } sub name { return $_[0]->{'name'}; } sub creator { - my $self = shift; + my $self = shift; - if (!$self->{creator} && $self->{creator_id}) { - require Bugzilla::User; - $self->{creator} = new Bugzilla::User($self->{creator_id}); - } - return $self->{creator}; + if (!$self->{creator} && $self->{creator_id}) { + require Bugzilla::User; + $self->{creator} = new Bugzilla::User($self->{creator_id}); + } + return $self->{creator}; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id); + $dbh->do('DELETE FROM series WHERE series_id = ?', undef, $self->id); } 1; diff --git a/Bugzilla/Status.pm b/Bugzilla/Status.pm index ae04873a8..619594f5b 100644 --- a/Bugzilla/Status.pm +++ b/Bugzilla/Status.pm @@ -17,11 +17,11 @@ use warnings; # methods. use base qw(Bugzilla::Field::Choice Exporter); @Bugzilla::Status::EXPORT = qw( - BUG_STATE_OPEN - SPECIAL_STATUS_WORKFLOW_ACTIONS + BUG_STATE_OPEN + SPECIAL_STATUS_WORKFLOW_ACTIONS - is_open_state - closed_bug_statuses + is_open_state + closed_bug_statuses ); use Bugzilla::Error; @@ -31,25 +31,25 @@ use Bugzilla::Error; ################################ use constant SPECIAL_STATUS_WORKFLOW_ACTIONS => qw( - none - duplicate - change_resolution - clearresolution + none + duplicate + change_resolution + clearresolution ); use constant DB_TABLE => 'bug_status'; # This has all the standard Bugzilla::Field::Choice columns plus "is_open" sub DB_COLUMNS { - return ($_[0]->SUPER::DB_COLUMNS, 'is_open'); + return ($_[0]->SUPER::DB_COLUMNS, 'is_open'); } sub VALIDATORS { - my $invocant = shift; - my $validators = $invocant->SUPER::VALIDATORS; - $validators->{is_open} = \&Bugzilla::Object::check_boolean; - $validators->{value} = \&_check_value; - return $validators; + my $invocant = shift; + my $validators = $invocant->SUPER::VALIDATORS; + $validators->{is_open} = \&Bugzilla::Object::check_boolean; + $validators->{value} = \&_check_value; + return $validators; } ######################### @@ -57,24 +57,25 @@ sub VALIDATORS { ######################### sub create { - my $class = shift; - my $self = $class->SUPER::create(@_); - delete Bugzilla->request_cache->{status_bug_state_open}; - add_missing_bug_status_transitions(); - return $self; + my $class = shift; + my $self = $class->SUPER::create(@_); + delete Bugzilla->request_cache->{status_bug_state_open}; + add_missing_bug_status_transitions(); + return $self; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; - my $id = $self->id; - $dbh->bz_start_transaction(); - $self->SUPER::remove_from_db(); - $dbh->do('DELETE FROM status_workflow - WHERE old_status = ? OR new_status = ?', - undef, $id, $id); - $dbh->bz_commit_transaction(); - delete Bugzilla->request_cache->{status_bug_state_open}; + my $self = shift; + my $dbh = Bugzilla->dbh; + my $id = $self->id; + $dbh->bz_start_transaction(); + $self->SUPER::remove_from_db(); + $dbh->do( + 'DELETE FROM status_workflow + WHERE old_status = ? OR new_status = ?', undef, $id, $id + ); + $dbh->bz_commit_transaction(); + delete Bugzilla->request_cache->{status_bug_state_open}; } ############################### @@ -82,16 +83,16 @@ sub remove_from_db { ############################### sub is_active { return $_[0]->{'isactive'}; } -sub is_open { return $_[0]->{'is_open'}; } +sub is_open { return $_[0]->{'is_open'}; } sub is_static { - my $self = shift; - if ($self->name eq 'UNCONFIRMED' - || $self->name eq Bugzilla->params->{'duplicate_or_move_bug_status'}) - { - return 1; - } - return 0; + my $self = shift; + if ( $self->name eq 'UNCONFIRMED' + || $self->name eq Bugzilla->params->{'duplicate_or_move_bug_status'}) + { + return 1; + } + return 0; } ############## @@ -99,14 +100,14 @@ sub is_static { ############## sub _check_value { - my $invocant = shift; - my $value = $invocant->SUPER::_check_value(@_); - - if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) { - ThrowUserError('fieldvalue_reserved_word', - { field => $invocant->field, value => $value }); - } - return $value; + my $invocant = shift; + my $value = $invocant->SUPER::_check_value(@_); + + if (grep { lc($value) eq lc($_) } SPECIAL_STATUS_WORKFLOW_ACTIONS) { + ThrowUserError('fieldvalue_reserved_word', + {field => $invocant->field, value => $value}); + } + return $value; } @@ -115,118 +116,125 @@ sub _check_value { ############################### sub BUG_STATE_OPEN { - my $dbh = Bugzilla->dbh; - my $request_cache = Bugzilla->request_cache; - my $cache_key = 'status_bug_state_open'; - return @{ $request_cache->{$cache_key} } - if exists $request_cache->{$cache_key}; - - my $rows = Bugzilla->memcached->get_config({ key => $cache_key }); - if (!$rows) { - $rows = $dbh->selectcol_arrayref( - 'SELECT value FROM bug_status WHERE is_open = 1' - ); - Bugzilla->memcached->set_config({ key => $cache_key, data => $rows }); - } - - $request_cache->{$cache_key} = $rows; - return @$rows; + my $dbh = Bugzilla->dbh; + my $request_cache = Bugzilla->request_cache; + my $cache_key = 'status_bug_state_open'; + return @{$request_cache->{$cache_key}} if exists $request_cache->{$cache_key}; + + my $rows = Bugzilla->memcached->get_config({key => $cache_key}); + if (!$rows) { + $rows + = $dbh->selectcol_arrayref('SELECT value FROM bug_status WHERE is_open = 1'); + Bugzilla->memcached->set_config({key => $cache_key, data => $rows}); + } + + $request_cache->{$cache_key} = $rows; + return @$rows; } # Tells you whether or not the argument is a valid "open" state. sub is_open_state { - my ($state) = @_; - return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0); + my ($state) = @_; + return (grep($_ eq $state, BUG_STATE_OPEN) ? 1 : 0); } sub closed_bug_statuses { - my @bug_statuses = Bugzilla::Status->get_all; - @bug_statuses = grep { !$_->is_open } @bug_statuses; - return @bug_statuses; + my @bug_statuses = Bugzilla::Status->get_all; + @bug_statuses = grep { !$_->is_open } @bug_statuses; + return @bug_statuses; } sub can_change_to { - my $self = shift; - my $dbh = Bugzilla->dbh; - - if (!ref($self) || !defined $self->{'can_change_to'}) { - my ($cond, @args, $self_exists); - if (ref($self)) { - $cond = '= ?'; - push(@args, $self->id); - $self_exists = 1; - } - else { - $cond = 'IS NULL'; - # Let's do it so that the code below works in all cases. - $self = {}; - } - - my $new_status_ids = $dbh->selectcol_arrayref("SELECT new_status + my $self = shift; + my $dbh = Bugzilla->dbh; + + if (!ref($self) || !defined $self->{'can_change_to'}) { + my ($cond, @args, $self_exists); + if (ref($self)) { + $cond = '= ?'; + push(@args, $self->id); + $self_exists = 1; + } + else { + $cond = 'IS NULL'; + + # Let's do it so that the code below works in all cases. + $self = {}; + } + + my $new_status_ids = $dbh->selectcol_arrayref( + "SELECT new_status FROM status_workflow INNER JOIN bug_status ON id = new_status WHERE isactive = 1 AND old_status $cond - ORDER BY sortkey", - undef, @args); + ORDER BY sortkey", undef, @args + ); - # Allow the bug status to remain unchanged. - push(@$new_status_ids, $self->id) if $self_exists; - $self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids); - } + # Allow the bug status to remain unchanged. + push(@$new_status_ids, $self->id) if $self_exists; + $self->{'can_change_to'} = Bugzilla::Status->new_from_list($new_status_ids); + } - return $self->{'can_change_to'}; + return $self->{'can_change_to'}; } sub comment_required_on_change_from { - my ($self, $old_status) = @_; - my ($cond, $values) = $self->_status_condition($old_status); - - my ($require_comment) = Bugzilla->dbh->selectrow_array( - "SELECT require_comment FROM status_workflow - WHERE $cond", undef, @$values); - return $require_comment; + my ($self, $old_status) = @_; + my ($cond, $values) = $self->_status_condition($old_status); + + my ($require_comment) = Bugzilla->dbh->selectrow_array( + "SELECT require_comment FROM status_workflow + WHERE $cond", undef, @$values + ); + return $require_comment; } # Used as a helper for various functions that have to deal with old_status # sometimes being NULL and sometimes having a value. sub _status_condition { - my ($self, $old_status) = @_; - my @values; - my $cond = 'old_status IS NULL'; - # For newly-filed bugs - if ($old_status) { - $cond = 'old_status = ?'; - push(@values, $old_status->id); - } - $cond .= " AND new_status = ?"; - push(@values, $self->id); - return ($cond, \@values); + my ($self, $old_status) = @_; + my @values; + my $cond = 'old_status IS NULL'; + + # For newly-filed bugs + if ($old_status) { + $cond = 'old_status = ?'; + push(@values, $old_status->id); + } + $cond .= " AND new_status = ?"; + push(@values, $self->id); + return ($cond, \@values); } sub add_missing_bug_status_transitions { - my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'}; - my $dbh = Bugzilla->dbh; - my $new_status = new Bugzilla::Status({name => $bug_status}); - # Silently discard invalid bug statuses. - $new_status || return; + my $bug_status = shift || Bugzilla->params->{'duplicate_or_move_bug_status'}; + my $dbh = Bugzilla->dbh; + my $new_status = new Bugzilla::Status({name => $bug_status}); - my $missing_statuses = $dbh->selectcol_arrayref('SELECT id + # Silently discard invalid bug statuses. + $new_status || return; + + my $missing_statuses = $dbh->selectcol_arrayref( + 'SELECT id FROM bug_status LEFT JOIN status_workflow ON old_status = id AND new_status = ? WHERE old_status IS NULL', - undef, $new_status->id); - - my $sth = $dbh->prepare('INSERT INTO status_workflow - (old_status, new_status) VALUES (?, ?)'); - - foreach my $old_status_id (@$missing_statuses) { - next if ($old_status_id == $new_status->id); - $sth->execute($old_status_id, $new_status->id); - } + undef, $new_status->id + ); + + my $sth = $dbh->prepare( + 'INSERT INTO status_workflow + (old_status, new_status) VALUES (?, ?)' + ); + + foreach my $old_status_id (@$missing_statuses) { + next if ($old_status_id == $new_status->id); + $sth->execute($old_status_id, $new_status->id); + } } 1; diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm index 36435d637..1d597277e 100644 --- a/Bugzilla/Template.pm +++ b/Bugzilla/Template.pm @@ -19,7 +19,7 @@ use Bugzilla::Constants; use Bugzilla::Hook; use Bugzilla::Install::Requirements; use Bugzilla::Install::Util qw(install_string template_include_path - include_languages); + include_languages); use Bugzilla::Keyword; use Bugzilla::Util; use Bugzilla::User; @@ -45,16 +45,16 @@ use JSON::XS qw(encode_json); use parent qw(Template); use constant FORMAT_TRIPLE => '%19s|%-28s|%-28s'; -use constant FORMAT_3_SIZE => [19,28,28]; +use constant FORMAT_3_SIZE => [19, 28, 28]; use constant FORMAT_DOUBLE => '%19s %-55s'; -use constant FORMAT_2_SIZE => [19,55]; +use constant FORMAT_2_SIZE => [19, 55]; my %SHARED_PROVIDERS; # Pseudo-constant. sub SAFE_URL_REGEXP { - my $safe_protocols = join('|', SAFE_PROTOCOLS); - return qr/($safe_protocols):[^:\s<>\"][^\s<>\"]+[\w\/]/i; + my $safe_protocols = join('|', SAFE_PROTOCOLS); + return qr/($safe_protocols):[^:\s<>\"][^\s<>\"]+[\w\/]/i; } # Convert the constants in the Bugzilla::Constants module into a hash we can @@ -63,19 +63,19 @@ sub SAFE_URL_REGEXP { # traverse the arrays of exported and exportable symbols and ignoring the rest # (which, if Constants.pm exports only constants, as it should, will be nothing else). sub _load_constants { - my %constants; - foreach my $constant (@Bugzilla::Constants::EXPORT, - @Bugzilla::Constants::EXPORT_OK) - { - if (ref Bugzilla::Constants->$constant) { - $constants{$constant} = Bugzilla::Constants->$constant; - } - else { - my @list = (Bugzilla::Constants->$constant); - $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; - } + my %constants; + foreach + my $constant (@Bugzilla::Constants::EXPORT, @Bugzilla::Constants::EXPORT_OK) + { + if (ref Bugzilla::Constants->$constant) { + $constants{$constant} = Bugzilla::Constants->$constant; } - return \%constants; + else { + my @list = (Bugzilla::Constants->$constant); + $constants{$constant} = (scalar(@list) == 1) ? $list[0] : \@list; + } + } + return \%constants; } # Returns the path to the templates based on the Accept-Language @@ -83,52 +83,50 @@ sub _load_constants { # If no Accept-Language is present it uses the defined default # Templates may also be found in the extensions/ tree sub _include_path { - my $lang = shift || ''; - my $cache = Bugzilla->request_cache; - $cache->{"template_include_path_$lang"} ||= - template_include_path({ language => $lang }); - return $cache->{"template_include_path_$lang"}; + my $lang = shift || ''; + my $cache = Bugzilla->request_cache; + $cache->{"template_include_path_$lang"} + ||= template_include_path({language => $lang}); + return $cache->{"template_include_path_$lang"}; } sub get_format { - my $self = shift; - my ($template, $format, $ctype) = @_; - - $ctype ||= 'html'; - $format ||= ''; - - # Security - allow letters and a hyphen only - $ctype =~ s/[^a-zA-Z\-]//g; - $format =~ s/[^a-zA-Z\-]//g; - trick_taint($ctype); - trick_taint($format); - - $template .= ($format ? "-$format" : ""); - $template .= ".$ctype.tmpl"; - - # Now check that the template actually exists. We only want to check - # if the template exists; any other errors (eg parse errors) will - # end up being detected later. - eval { - $self->context->template($template); - }; - # This parsing may seem fragile, but it's OK: - # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html - # Even if it is wrong, any sort of error is going to cause a failure - # eventually, so the only issue would be an incorrect error message - if ($@ && $@->info =~ /: not found$/) { - ThrowUserError('format_not_found', {'format' => $format, - 'ctype' => $ctype}); - } - - # Else, just return the info - return - { - 'template' => $template, - 'format' => $format, - 'extension' => $ctype, - 'ctype' => Bugzilla::Constants::contenttypes->{$ctype} // 'application/octet-stream', - }; + my $self = shift; + my ($template, $format, $ctype) = @_; + + $ctype ||= 'html'; + $format ||= ''; + + # Security - allow letters and a hyphen only + $ctype =~ s/[^a-zA-Z\-]//g; + $format =~ s/[^a-zA-Z\-]//g; + trick_taint($ctype); + trick_taint($format); + + $template .= ($format ? "-$format" : ""); + $template .= ".$ctype.tmpl"; + + # Now check that the template actually exists. We only want to check + # if the template exists; any other errors (eg parse errors) will + # end up being detected later. + eval { $self->context->template($template); }; + + # This parsing may seem fragile, but it's OK: + # http://lists.template-toolkit.org/pipermail/templates/2003-March/004370.html + # Even if it is wrong, any sort of error is going to cause a failure + # eventually, so the only issue would be an incorrect error message + if ($@ && $@->info =~ /: not found$/) { + ThrowUserError('format_not_found', {'format' => $format, 'ctype' => $ctype}); + } + + # Else, just return the info + return { + 'template' => $template, + 'format' => $format, + 'extension' => $ctype, + 'ctype' => Bugzilla::Constants::contenttypes->{$ctype} + // 'application/octet-stream', + }; } # This routine quoteUrls contains inspirations from the HTML::FromText CPAN @@ -139,172 +137,182 @@ sub get_format { # If you want to modify this routine, read the comments carefully sub quoteUrls { - my ($text, $bug, $comment, $user, $bug_link_func) = @_; - return $text unless $text; - $user ||= Bugzilla->user; - $bug_link_func ||= \&get_bug_link; - - # We use /g for speed, but uris can have other things inside them - # (http://foo/bug#3 for example). Filtering that out filters valid - # bug refs out, so we have to do replacements. - # mailto can't contain space or #, so we don't have to bother for that - # Do this by replacing matches with \x{FDD2}$count\x{FDD3} - # \x{FDDx} is used because it's unlikely to occur in the text - # and are reserved unicode characters. We disable warnings for now - # until we require Perl 5.13.9 or newer. - no warnings 'utf8'; - - # If the comment is already wrapped, we should ignore newlines when - # looking for matching regexps. Else we should take them into account. - my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/; - - # However, note that adding the title (for buglinks) can affect things - # In particular, attachment matches go before bug titles, so that titles - # with 'attachment 1' don't double match. - # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur - # if it was substituted as a bug title (since that always involve leading - # and trailing text) - - # Because of entities, it's easier (and quicker) to do this before escaping - - my @things; - my $count = 0; - my $tmp; - - my @hook_regexes; - Bugzilla::Hook::process('bug_format_comment', - { text => \$text, bug => $bug, regexes => \@hook_regexes, - comment => $comment, user => $user }); - - foreach my $re (@hook_regexes) { - my ($match, $replace) = @$re{qw(match replace)}; - if (ref($replace) eq 'CODE') { - $text =~ s/$match/($things[$count++] = $replace->({matches => [ + my ($text, $bug, $comment, $user, $bug_link_func) = @_; + return $text unless $text; + $user ||= Bugzilla->user; + $bug_link_func ||= \&get_bug_link; + + # We use /g for speed, but uris can have other things inside them + # (http://foo/bug#3 for example). Filtering that out filters valid + # bug refs out, so we have to do replacements. + # mailto can't contain space or #, so we don't have to bother for that + # Do this by replacing matches with \x{FDD2}$count\x{FDD3} + # \x{FDDx} is used because it's unlikely to occur in the text + # and are reserved unicode characters. We disable warnings for now + # until we require Perl 5.13.9 or newer. + no warnings 'utf8'; + + # If the comment is already wrapped, we should ignore newlines when + # looking for matching regexps. Else we should take them into account. + my $s = ($comment && $comment->already_wrapped) ? qr/\s/ : qr/\h/; + + # However, note that adding the title (for buglinks) can affect things + # In particular, attachment matches go before bug titles, so that titles + # with 'attachment 1' don't double match. + # Dupe checks go afterwards, because that uses ^ and \Z, which won't occur + # if it was substituted as a bug title (since that always involve leading + # and trailing text) + + # Because of entities, it's easier (and quicker) to do this before escaping + + my @things; + my $count = 0; + my $tmp; + + my @hook_regexes; + Bugzilla::Hook::process( + 'bug_format_comment', + { + text => \$text, + bug => $bug, + regexes => \@hook_regexes, + comment => $comment, + user => $user + } + ); + + foreach my $re (@hook_regexes) { + my ($match, $replace) = @$re{qw(match replace)}; + if (ref($replace) eq 'CODE') { + $text =~ s/$match/($things[$count++] = $replace->({matches => [ $1, $2, $3, $4, $5, $6, $7, $8, $9, $10]})) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx; - } - else { - $text =~ s/$match/($things[$count++] = $replace) + } + else { + $text =~ s/$match/($things[$count++] = $replace) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}")/egx; - } } + } - # Provide tooltips for full bug links (Bug 74355) - my $urlbase_re = '(' . quotemeta(Bugzilla->localconfig->{urlbase}) . ')'; - $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b + # Provide tooltips for full bug links (Bug 74355) + my $urlbase_re = '(' . quotemeta(Bugzilla->localconfig->{urlbase}) . ')'; + $text =~ s~\b(${urlbase_re}\Qshow_bug.cgi?id=\E([0-9]+)(\#c([0-9]+))?)\b ~($things[$count++] = $bug_link_func->($3, $1, { comment_num => $5, user => $user })) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; - # non-mailto protocols - my $safe_protocols = SAFE_URL_REGEXP(); - $text =~ s~\b($safe_protocols) + # non-mailto protocols + my $safe_protocols = SAFE_URL_REGEXP(); + $text =~ s~\b($safe_protocols) ~($tmp = html_quote($1)) && ($things[$count++] = "$tmp") && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egox; - # We have to quote now, otherwise the html itself is escaped - # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH + # We have to quote now, otherwise the html itself is escaped + # THIS MEANS THAT A LITERAL ", <, >, ' MUST BE ESCAPED FOR A MATCH - $text = html_quote($text); + $text = html_quote($text); - # Color quoted text - $text =~ s~^(>.+)$~$1~mg; - $text =~ s~\n~\n~g; + # Color quoted text + $text =~ s~^(>.+)$~$1~mg; + $text =~ s~\n~\n~g; - # mailto: - # Use | so that $1 is defined regardless - # @ is the encoded '@' character. - $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b + # mailto: + # Use | so that $1 is defined regardless + # @ is the encoded '@' character. + $text =~ s~\b(mailto:|)?([\w\.\-\+\=]+&\#64;[\w\-]+(?:\.[\w\-]+)+)\b ~$1$2~igx; - # attachment links - # BMO: don't make diff view the default for patches (Bug 652332) - $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[diff\])?(?:\s+\[details\])?) + # attachment links + # BMO: don't make diff view the default for patches (Bug 652332) + $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[diff\])?(?:\s+\[details\])?) ~($things[$count++] = get_attachment_link($2, $1, $user)) && ("\x{FDD2}" . ($count-1) . "\x{FDD3}") ~egmxi; - # Current bug ID this comment belongs to - my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : ""; - - # This handles bug a, comment b type stuff. Because we're using /g - # we have to do this in one pattern, and so this is semi-messy. - # Also, we can't use $bug_re?$comment_re? because that will match the - # empty string - my $bug_word = template_var('terms')->{bug}; - my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i; - my $comment_re = qr/comment$s*\#?$s*(\d+)/i; - $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re) + # Current bug ID this comment belongs to + my $current_bugurl = $bug ? ("show_bug.cgi?id=" . $bug->id) : ""; + + # This handles bug a, comment b type stuff. Because we're using /g + # we have to do this in one pattern, and so this is semi-messy. + # Also, we can't use $bug_re?$comment_re? because that will match the + # empty string + my $bug_word = template_var('terms')->{bug}; + my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i; + my $comment_re = qr/comment$s*\#?$s*(\d+)/i; + $text =~ s~\b($bug_re(?:$s*,?$s*$comment_re)?|$comment_re) ~ # We have several choices. $1 here is the link, and $2-4 are set # depending on which part matched (defined($2) ? $bug_link_func->($2, $1, { comment_num => $3, user => $user }) : "$1") ~egx; - # Old duplicate markers. These don't use $bug_word because they are old - # and were never customizable. - $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ ) + # Old duplicate markers. These don't use $bug_word because they are old + # and were never customizable. + $text =~ s~(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ ) (\d+) (?=\ \*\*\*\Z) ~$bug_link_func->($1, $1, { user => $user }) ~egmx; - # Now remove the encoding hacks in reverse order - for (my $i = $#things; $i >= 0; $i--) { - $text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg; - } + # Now remove the encoding hacks in reverse order + for (my $i = $#things; $i >= 0; $i--) { + $text =~ s/\x{FDD2}($i)\x{FDD3}/$things[$i]/eg; + } - return $text; + return $text; } # Creates a link to an attachment, including its title. sub get_attachment_link { - my ($attachid, $link_text, $user) = @_; - my $dbh = Bugzilla->dbh; - $user ||= Bugzilla->user; - - my $attachment = new Bugzilla::Attachment({ id => $attachid, cache => 1 }); - - if ($attachment) { - my $title = ""; - my $className = ""; - if ($user->can_see_bug($attachment->bug_id) - && (!$attachment->isprivate || $user->is_insider)) - { - $title = $attachment->description; - } - if ($attachment->isobsolete) { - $className = "bz_obsolete"; - } - # Prevent code injection in the title. - $title = html_quote(clean_text($title)); - - $link_text =~ s/ \[details\]$//; - $link_text =~ s/ \[diff\]$//; - state $urlbase = Bugzilla->localconfig->{urlbase}; - my $linkval = "${urlbase}attachment.cgi?id=$attachid"; - - # If the attachment is a patch and patch_viewer feature is - # enabled, add link to the diff. - my $patchlink = ""; - if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) { - $patchlink = qq| [diff]|; - } + my ($attachid, $link_text, $user) = @_; + my $dbh = Bugzilla->dbh; + $user ||= Bugzilla->user; - # Whitespace matters here because these links are in
 tags.
-        return qq||
-               . qq|$link_text|
-               . qq| [details]|
-               . qq|${patchlink}|
-               . qq||;
+  my $attachment = new Bugzilla::Attachment({id => $attachid, cache => 1});
+
+  if ($attachment) {
+    my $title     = "";
+    my $className = "";
+    if ($user->can_see_bug($attachment->bug_id)
+      && (!$attachment->isprivate || $user->is_insider))
+    {
+      $title = $attachment->description;
     }
-    else {
-        return qq{$link_text};
+    if ($attachment->isobsolete) {
+      $className = "bz_obsolete";
+    }
+
+    # Prevent code injection in the title.
+    $title = html_quote(clean_text($title));
+
+    $link_text =~ s/ \[details\]$//;
+    $link_text =~ s/ \[diff\]$//;
+    state $urlbase = Bugzilla->localconfig->{urlbase};
+    my $linkval = "${urlbase}attachment.cgi?id=$attachid";
+
+    # If the attachment is a patch and patch_viewer feature is
+    # enabled, add link to the diff.
+    my $patchlink = "";
+    if ($attachment->ispatch and Bugzilla->feature('patch_viewer')) {
+      $patchlink
+        = qq| [diff]|;
     }
+
+    # Whitespace matters here because these links are in 
 tags.
+    return
+        qq||
+      . qq|$link_text|
+      . qq| [details]|
+      . qq|${patchlink}|
+      . qq||;
+  }
+  else {
+    return qq{$link_text};
+  }
 }
 
 # Creates a link to a bug, including its title.
@@ -315,51 +323,57 @@ sub get_attachment_link {
 #    comment in the bug
 
 sub get_bug_link {
-    my ($bug, $link_text, $options) = @_;
-    $options ||= {};
-    $options->{user} ||= Bugzilla->user;
-    my $dbh = Bugzilla->dbh;
-
-    if (defined $bug && $bug ne '') {
-        $bug = blessed($bug) ? $bug : new Bugzilla::Bug({ id => $bug, cache => 1 });
-        return $link_text if $bug->{error};
-    }
-
-    my $template = Bugzilla->template_inner;
-    my $linkified;
-    $template->process('bug/link.html.tmpl',
-        { bug => $bug, link_text => $link_text, %$options }, \$linkified);
-    return $linkified;
+  my ($bug, $link_text, $options) = @_;
+  $options ||= {};
+  $options->{user} ||= Bugzilla->user;
+  my $dbh = Bugzilla->dbh;
+
+  if (defined $bug && $bug ne '') {
+    $bug = blessed($bug) ? $bug : new Bugzilla::Bug({id => $bug, cache => 1});
+    return $link_text if $bug->{error};
+  }
+
+  my $template = Bugzilla->template_inner;
+  my $linkified;
+  $template->process('bug/link.html.tmpl',
+    {bug => $bug, link_text => $link_text, %$options},
+    \$linkified);
+  return $linkified;
 }
 
 # We use this instead of format because format doesn't deal well with
 # multi-byte languages.
 sub multiline_sprintf {
-    my ($format, $args, $sizes) = @_;
-    my @parts;
-    my @my_sizes = @$sizes; # Copy this so we don't modify the input array.
-    foreach my $string (@$args) {
-        my $size = shift @my_sizes;
-        my @pieces = split("\n", wrap_hard($string, $size));
-        push(@parts, \@pieces);
-    }
-
-    my $formatted;
-    while (1) {
-        # Get the first item of each part.
-        my @line = map { shift @$_ } @parts;
-        # If they're all undef, we're done.
-        last if !grep { defined $_ } @line;
-        # Make any single undef item into ''
-        @line = map { defined $_ ? $_ : '' } @line;
-        # And append a formatted line
-        $formatted .= sprintf($format, @line);
-        # Remove trailing spaces, or they become lots of =20's in
-        # quoted-printable emails.
-        $formatted =~ s/\s+$//;
-        $formatted .= "\n";
-    }
-    return $formatted;
+  my ($format, $args, $sizes) = @_;
+  my @parts;
+  my @my_sizes = @$sizes;    # Copy this so we don't modify the input array.
+  foreach my $string (@$args) {
+    my $size = shift @my_sizes;
+    my @pieces = split("\n", wrap_hard($string, $size));
+    push(@parts, \@pieces);
+  }
+
+  my $formatted;
+  while (1) {
+
+    # Get the first item of each part.
+    my @line = map { shift @$_ } @parts;
+
+    # If they're all undef, we're done.
+    last if !grep { defined $_ } @line;
+
+    # Make any single undef item into ''
+    @line = map { defined $_ ? $_ : '' } @line;
+
+    # And append a formatted line
+    $formatted .= sprintf($format, @line);
+
+    # Remove trailing spaces, or they become lots of =20's in
+    # quoted-printable emails.
+    $formatted =~ s/\s+$//;
+    $formatted .= "\n";
+  }
+  return $formatted;
 }
 
 #####################
@@ -367,8 +381,8 @@ sub multiline_sprintf {
 #####################
 
 sub version_filter {
-    my ($file_url) = @_;
-    return "static/v" . Bugzilla->VERSION . "/$file_url";
+  my ($file_url) = @_;
+  return "static/v" . Bugzilla->VERSION . "/$file_url";
 }
 
 # Set up the skin CSS cascade:
@@ -381,65 +395,65 @@ sub version_filter {
 #  6. Custom Bugzilla stylesheet set
 
 sub css_files {
-    my ($style_urls, $no_yui) = @_;
+  my ($style_urls, $no_yui) = @_;
 
-    # global.css belongs on every page
-    my @requested_css = ( 'skins/standard/global.css', @$style_urls );
+  # global.css belongs on every page
+  my @requested_css = ('skins/standard/global.css', @$style_urls);
 
-    unshift @requested_css, "skins/yui.css" unless $no_yui;
+  unshift @requested_css, "skins/yui.css" unless $no_yui;
 
-    my @css_sets = map { _css_link_set($_) } @requested_css;
+  my @css_sets = map { _css_link_set($_) } @requested_css;
 
-    my %by_type = (standard => [], skin => [], custom => []);
-    foreach my $set (@css_sets) {
-        foreach my $key (keys %$set) {
-            push(@{ $by_type{$key} }, $set->{$key});
-        }
+  my %by_type = (standard => [], skin => [], custom => []);
+  foreach my $set (@css_sets) {
+    foreach my $key (keys %$set) {
+      push(@{$by_type{$key}}, $set->{$key});
     }
+  }
 
-    return \%by_type;
+  return \%by_type;
 }
 
 sub _css_link_set {
-    my ($file_name) = @_;
+  my ($file_name) = @_;
 
-    my %set = (standard => version_filter($file_name));
-
-    # We use (?:^|/) to allow Extensions to use the skins system if they want.
-    if ($file_name !~ m{(?:^|/)skins/standard/}) {
-        return \%set;
-    }
-
-    my $skin = Bugzilla->user->settings->{skin}->{value};
-    my $cgi_path = bz_locations()->{'cgi_path'};
-    my $skin_file_name = $file_name;
-    $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/};
-    if (-f "$cgi_path/$skin_file_name") {
-        $set{skin} = version_filter($skin_file_name);
-    }
-
-    my $custom_file_name = $file_name;
-    $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
-    if (-f "$cgi_path/$custom_file_name") {
-        $set{custom} = version_filter($custom_file_name);
-    }
+  my %set = (standard => version_filter($file_name));
 
+  # We use (?:^|/) to allow Extensions to use the skins system if they want.
+  if ($file_name !~ m{(?:^|/)skins/standard/}) {
     return \%set;
+  }
+
+  my $skin           = Bugzilla->user->settings->{skin}->{value};
+  my $cgi_path       = bz_locations()->{'cgi_path'};
+  my $skin_file_name = $file_name;
+  $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/};
+  if (-f "$cgi_path/$skin_file_name") {
+    $set{skin} = version_filter($skin_file_name);
+  }
+
+  my $custom_file_name = $file_name;
+  $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
+  if (-f "$cgi_path/$custom_file_name") {
+    $set{custom} = version_filter($custom_file_name);
+  }
+
+  return \%set;
 }
 
 # YUI dependency resolution
 sub yui_resolve_deps {
-    my ($yui, $yui_deps) = @_;
+  my ($yui, $yui_deps) = @_;
 
-    my @yui_resolved;
-    foreach my $yui_name (@$yui) {
-        my $deps = $yui_deps->{$yui_name} || [];
-        foreach my $dep (reverse @$deps) {
-            push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved;
-        }
-        push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved;
+  my @yui_resolved;
+  foreach my $yui_name (@$yui) {
+    my $deps = $yui_deps->{$yui_name} || [];
+    foreach my $dep (reverse @$deps) {
+      push(@yui_resolved, $dep) if !grep { $_ eq $dep } @yui_resolved;
     }
-    return \@yui_resolved;
+    push(@yui_resolved, $yui_name) if !grep { $_ eq $yui_name } @yui_resolved;
+  }
+  return \@yui_resolved;
 }
 
 ###############################################################################
@@ -461,61 +475,57 @@ $Template::Stash::PRIVATE = undef;
 # Add "contains***" methods to list variables that search for one or more
 # items in a list and return boolean values representing whether or not
 # one/all/any item(s) were found.
-$Template::Stash::LIST_OPS->{ contains } =
-  sub {
-      my ($list, $item) = @_;
-      if (ref $item && $item->isa('Bugzilla::Object')) {
-          return grep($_->id == $item->id, @$list);
-      } else {
-          return grep($_ eq $item, @$list);
-      }
-  };
-
-$Template::Stash::LIST_OPS->{ containsany } =
-  sub {
-      my ($list, $items) = @_;
-      foreach my $item (@$items) {
-          if (ref $item && $item->isa('Bugzilla::Object')) {
-              return 1 if grep($_->id == $item->id, @$list);
-          } else {
-              return 1 if grep($_ eq $item, @$list);
-          }
-      }
-      return 0;
-  };
+$Template::Stash::LIST_OPS->{contains} = sub {
+  my ($list, $item) = @_;
+  if (ref $item && $item->isa('Bugzilla::Object')) {
+    return grep($_->id == $item->id, @$list);
+  }
+  else {
+    return grep($_ eq $item, @$list);
+  }
+};
+
+$Template::Stash::LIST_OPS->{containsany} = sub {
+  my ($list, $items) = @_;
+  foreach my $item (@$items) {
+    if (ref $item && $item->isa('Bugzilla::Object')) {
+      return 1 if grep($_->id == $item->id, @$list);
+    }
+    else {
+      return 1 if grep($_ eq $item, @$list);
+    }
+  }
+  return 0;
+};
 
 # Clone the array reference to leave the original one unaltered.
-$Template::Stash::LIST_OPS->{ clone } =
-  sub {
-      my $list = shift;
-      return [@$list];
-  };
+$Template::Stash::LIST_OPS->{clone} = sub {
+  my $list = shift;
+  return [@$list];
+};
 
 # Allow us to still get the scalar if we use the list operation ".0" on it,
 # as we often do for defaults in query.cgi and other places.
-$Template::Stash::SCALAR_OPS->{ 0 } =
-  sub {
-      return $_[0];
-  };
+$Template::Stash::SCALAR_OPS->{0} = sub {
+  return $_[0];
+};
 
 # Add a "truncate" method to the Template Toolkit's "scalar" object
 # that truncates a string to a certain length.
-$Template::Stash::SCALAR_OPS->{ truncate } =
-  sub {
-      my ($string, $length, $ellipsis) = @_;
-      return $string if !$length || length($string) <= $length;
-
-      $ellipsis ||= '';
-      my $strlen = $length - length($ellipsis);
-      my $newstr = substr($string, 0, $strlen) . $ellipsis;
-      return $newstr;
-  };
+$Template::Stash::SCALAR_OPS->{truncate} = sub {
+  my ($string, $length, $ellipsis) = @_;
+  return $string if !$length || length($string) <= $length;
+
+  $ellipsis ||= '';
+  my $strlen = $length - length($ellipsis);
+  my $newstr = substr($string, 0, $strlen) . $ellipsis;
+  return $newstr;
+};
 
 # Override the built in .lower() vmethod
-$Template::Stash::SCALAR_OPS->{ lower } =
-  sub {
-      return lc($_[0]);
-  };
+$Template::Stash::SCALAR_OPS->{lower} = sub {
+  return lc($_[0]);
+};
 
 # Create the template object that processes templates and specify
 # configuration parameters that apply to all templates.
@@ -525,18 +535,19 @@ $Template::Stash::SCALAR_OPS->{ lower } =
 our $is_processing = 0;
 
 sub process {
-    my $self = shift;
-    # All of this current_langs stuff allows template_inner to correctly
-    # determine what-language Template object it should instantiate.
-    my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= [];
-    unshift(@$current_langs, $self->context->{bz_language});
-    local $is_processing = 1;
-    local $SIG{__DIE__};
-    delete $SIG{__DIE__};
-    warn "WARNING: CGI::Carp makes templates slow" if $INC{"CGI/Carp.pm"};
-    my $retval = $self->SUPER::process(@_);
-    shift @$current_langs;
-    return $retval;
+  my $self = shift;
+
+  # All of this current_langs stuff allows template_inner to correctly
+  # determine what-language Template object it should instantiate.
+  my $current_langs = Bugzilla->request_cache->{template_current_lang} ||= [];
+  unshift(@$current_langs, $self->context->{bz_language});
+  local $is_processing = 1;
+  local $SIG{__DIE__};
+  delete $SIG{__DIE__};
+  warn "WARNING: CGI::Carp makes templates slow" if $INC{"CGI/Carp.pm"};
+  my $retval = $self->SUPER::process(@_);
+  shift @$current_langs;
+  return $retval;
 }
 
 # Construct the Template object
@@ -545,640 +556,663 @@ sub process {
 # since we won't have a template to use...
 
 sub create {
-    my $class = shift;
-    my %opts = @_;
-
-    # IMPORTANT - If you make any FILTER changes here, make sure to
-    # make them in t/004.template.t also, if required.
-
-    my $config = {
-        # Colon-separated list of directories containing templates.
-        INCLUDE_PATH => $opts{'include_path'}
-                        || _include_path($opts{'language'}),
-
-        # allow PERL/RAWPERL because doing so can boost performance
-        EVAL_PERL => 1,
-
-        # Remove white-space before template directives (PRE_CHOMP) and at the
-        # beginning and end of templates and template blocks (TRIM) for better
-        # looking, more compact content.  Use the plus sign at the beginning
-        # of directives to maintain white space (i.e. [%+ DIRECTIVE %]).
-        PRE_CHOMP => 1,
-        TRIM => 1,
-
-        ABSOLUTE => 1,
-        RELATIVE => 0,
-
-        # Only use an on-disk template cache if we're running as the web
-        # server.  This ensures the permissions of the cache remain correct.
-        COMPILE_DIR => is_webserver_group() ? bz_locations()->{'template_cache'} : undef,
-
-        # Don't check for a template update until 1 hour has passed since the
-        # last check.
-        STAT_TTL    => 60 * 60,
-
-        # Initialize templates (f.e. by loading plugins like Hook).
-        PRE_PROCESS => ["global/initialize.none.tmpl"],
-
-        ENCODING => 'UTF-8',
-
-        # Functions for processing text within templates in various ways.
-        # IMPORTANT!  When adding a filter here that does not override a
-        # built-in filter, please also add a stub filter to t/004template.t.
-        FILTERS => {
-
-            # Render text in required style.
-
-            inactive => [
-                sub {
-                    my($context, $isinactive) = @_;
-                    return sub {
-                        return $isinactive ? ''.$_[0].'' : $_[0];
-                    }
-                }, 1
-            ],
-
-            closed => [
-                sub {
-                    my($context, $isclosed) = @_;
-                    return sub {
-                        return $isclosed ? ''.$_[0].'' : $_[0];
-                    }
-                }, 1
-            ],
-
-            obsolete => [
-                sub {
-                    my($context, $isobsolete) = @_;
-                    return sub {
-                        return $isobsolete ? ''.$_[0].'' : $_[0];
-                    }
-                }, 1
-            ],
-
-            # Returns the text with backslashes, single/double quotes,
-            # and newlines/carriage returns escaped for use in JS strings.
-            js => sub {
-                my ($var) = @_;
-                no warnings 'utf8';
-                $var =~ s/([\\\'\"\/])/\\$1/g;
-                $var =~ s/\n/\\n/g;
-                $var =~ s/\r/\\r/g;
-                $var =~ s/\x{2028}/\\u2028/g; # unicode line separator
-                $var =~ s/\x{2029}/\\u2029/g; # unicode paragraph separator
-                $var =~ s/\@/\\x40/g; # anti-spam for email addresses
-                $var =~ s//\\x3e/g;
-                return $var;
-            },
-
-            # Sadly, different to the above. See http://www.json.org/
-            # for details.
-            json => sub {
-                my ($var) = @_;
-                no warnings 'utf8';
-                $var =~ s/([\\\"\/])/\\$1/g;
-                $var =~ s/\n/\\n/g;
-                $var =~ s/\r/\\r/g;
-                $var =~ s/\f/\\f/g;
-                $var =~ s/\t/\\t/g;
-                return $var;
-            },
-
-            # Converts data to base64
-            base64 => sub {
-                my ($data) = @_;
-                return encode_base64($data);
-            },
-
-            # Strips out control characters excepting whitespace
-            strip_control_chars => sub {
-                my ($data) = @_;
-                # Only run for utf8 to avoid issues with other multibyte encodings
-                # that may be reassigning meaning to ascii characters.
-                if (Bugzilla->params->{'utf8'}) {
-                    $data =~ s/(?![\t\r\n])[[:cntrl:]]//g;
-                }
-                return $data;
-            },
-
-            # HTML collapses newlines in element attributes to a single space,
-            # so form elements which may have whitespace (ie comments) need
-            # to be encoded using 
-            # See bugs 4928, 22983 and 32000 for more details
-            html_linebreak => sub {
-                my ($var) = @_;
-                $var = html_quote($var);
-                $var =~ s/\r\n/\
/g;
-                $var =~ s/\n\r/\
/g;
-                $var =~ s/\r/\
/g;
-                $var =~ s/\n/\
/g;
-                return $var;
-            },
-
-            # Prevents line break on hyphens and whitespaces.
-            no_break => sub {
-                my ($var) = @_;
-                $var =~ s/ /\ /g;
-                $var =~ s/-/\‑/g;
-                return $var;
-            },
-
-            # Insert `` HTML tags to camel and snake case words as well as
-            # words containing dots in the given string so a long bug summary,
-            # for example, will be wrapped in a preferred manner rather than
-            # overflowing or expanding the parent element. This conversion
-            # should exclude existing HTML tags such as links. Examples:
-            # * `test_switch_window_content.py`
-            # * `TestSwitchToWindowContent`
-            # * `mozilla.org`
-            wbr => sub {
-                my ($var) = @_;
-                $var =~ s/([a-z])([A-Z\._])(?![^<]*>)/$1$2/g;
-                return $var;
-            },
-
-            xml => \&Bugzilla::Util::xml_quote ,
-
-            # This filter is similar to url_quote but used a \ instead of a %
-            # as prefix. In addition it replaces a ' ' by a '_'.
-            css_class_quote => \&Bugzilla::Util::css_class_quote ,
-
-            # Removes control characters and trims extra whitespace.
-            clean_text => \&Bugzilla::Util::clean_text ,
-
-            quoteUrls => [ sub {
-                               my ($context, $bug, $comment, $user) = @_;
-                               return sub {
-                                   my $text = shift;
-                                   return quoteUrls($text, $bug, $comment, $user);
-                               };
-                           },
-                           1
-                         ],
-
-            bug_link => [ sub {
-                              my ($context, $bug, $options) = @_;
-                              return sub {
-                                  my $text = shift;
-                                  return get_bug_link($bug, $text, $options);
-                              };
-                          },
-                          1
-                        ],
-
-            bug_list_link => sub {
-                my ($buglist, $options) = @_;
-                return join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist)));
-            },
-
-            # In CSV, quotes are doubled, and any value containing a quote or a
-            # comma is enclosed in quotes.
-            # If a field starts with either "=", "+", "-" or "@", it is preceded
-            # by a space to prevent stupid formula execution from Excel & co.
-            csv => sub
-            {
-                my ($var) = @_;
-                $var = ' ' . $var if $var =~ /^[+=@-]/;
-                # backslash is not special to CSV, but it can be used to confuse some browsers...
-                # so we do not allow it to happen. We only do this for logged-in users.
-                $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id;
-                $var =~ s/\"/\"\"/g;
-                if ($var !~ /^-?(\d+\.)?\d*$/) {
-                    $var = "\"$var\"";
-                }
-                return $var;
-            } ,
-
-            # Format a filesize in bytes to a human readable value
-            unitconvert => sub
-            {
-                my ($data) = @_;
-                my $retval = "";
-                my %units = (
-                    'KB' => 1024,
-                    'MB' => 1024 * 1024,
-                    'GB' => 1024 * 1024 * 1024,
-                );
-
-                if ($data < 1024) {
-                    return "$data bytes";
-                }
-                else {
-                    my $u;
-                    foreach $u ('GB', 'MB', 'KB') {
-                        if ($data >= $units{$u}) {
-                            return sprintf("%.2f %s", $data/$units{$u}, $u);
-                        }
-                    }
-                }
-            },
-
-            # Format a time for display (more info in Bugzilla::Util)
-            time => [ sub {
-                          my ($context, $format, $timezone) = @_;
-                          return sub {
-                              my $time = shift;
-                              return format_time($time, $format, $timezone);
-                          };
-                      },
-                      1
-                    ],
-
-            html => \&Bugzilla::Util::html_quote,
-
-            html_light => \&Bugzilla::Util::html_light_quote,
-
-            email => \&Bugzilla::Util::email_filter,
-
-            version => \&version_filter,
-
-            # iCalendar contentline filter
-            ics => [ sub {
-                         my ($context, @args) = @_;
-                         return sub {
-                             my ($var) = shift;
-                             my ($par) = shift @args;
-                             my ($output) = "";
-
-                             $var =~ s/[\r\n]/ /g;
-                             $var =~ s/([;\\\",])/\\$1/g;
-
-                             if ($par) {
-                                 $output = sprintf("%s:%s", $par, $var);
-                             } else {
-                                 $output = $var;
-                             }
-
-                             $output =~ s/(.{75,75})/$1\n /g;
-
-                             return $output;
-                         };
-                     },
-                     1
-                     ],
-
-            # Note that using this filter is even more dangerous than
-            # using "none," and you should only use it when you're SURE
-            # the output won't be displayed directly to a web browser.
-            txt => sub {
-                my ($var) = @_;
-                # Trivial HTML tag remover
-                $var =~ s/<[^>]*>//g;
-                # And this basically reverses the html filter.
-                $var =~ s/\@/@/g;
-                $var =~ s/\<//g;
-                $var =~ s/\"/\"/g;
-                $var =~ s/\&/\&/g;
-                # Now remove extra whitespace...
-                my $collapse_filter = $Template::Filters::FILTERS->{collapse};
-                $var = $collapse_filter->($var);
-                # And if we're not in the WebService, wrap the message.
-                # (Wrapping the message in the WebService is unnecessary
-                # and causes awkward things like \n's appearing in error
-                # messages in JSON-RPC.)
-                unless (i_am_webservice()) {
-                    $var = wrap_comment($var, 72);
-                }
-                $var =~ s/\ / /g;
-
-                return $var;
-            },
-
-            # Wrap a displayed comment to the appropriate length
-            wrap_comment => [
-                sub {
-                    my ($context, $cols) = @_;
-                    return sub { wrap_comment($_[0], $cols) }
-                }, 1],
-
-            # We force filtering of every variable in key security-critical
-            # places; we have a none filter for people to use when they
-            # really, really don't want a variable to be changed.
-            none => sub { return $_[0]; } ,
+  my $class = shift;
+  my %opts  = @_;
+
+  # IMPORTANT - If you make any FILTER changes here, make sure to
+  # make them in t/004.template.t also, if required.
+
+  my $config = {
+
+    # Colon-separated list of directories containing templates.
+    INCLUDE_PATH => $opts{'include_path'} || _include_path($opts{'language'}),
+
+    # allow PERL/RAWPERL because doing so can boost performance
+    EVAL_PERL => 1,
+
+    # Remove white-space before template directives (PRE_CHOMP) and at the
+    # beginning and end of templates and template blocks (TRIM) for better
+    # looking, more compact content.  Use the plus sign at the beginning
+    # of directives to maintain white space (i.e. [%+ DIRECTIVE %]).
+    PRE_CHOMP => 1,
+    TRIM      => 1,
+
+    ABSOLUTE => 1,
+    RELATIVE => 0,
+
+    # Only use an on-disk template cache if we're running as the web
+    # server.  This ensures the permissions of the cache remain correct.
+    COMPILE_DIR => is_webserver_group()
+    ? bz_locations()->{'template_cache'}
+    : undef,
+
+    # Don't check for a template update until 1 hour has passed since the
+    # last check.
+    STAT_TTL => 60 * 60,
+
+    # Initialize templates (f.e. by loading plugins like Hook).
+    PRE_PROCESS => ["global/initialize.none.tmpl"],
+
+    ENCODING => 'UTF-8',
+
+    # Functions for processing text within templates in various ways.
+    # IMPORTANT!  When adding a filter here that does not override a
+    # built-in filter, please also add a stub filter to t/004template.t.
+    FILTERS => {
+
+      # Render text in required style.
+
+      inactive => [
+        sub {
+          my ($context, $isinactive) = @_;
+          return sub {
+            return $isinactive ? '' . $_[0] . '' : $_[0];
+            }
+        },
+        1
+      ],
+
+      closed => [
+        sub {
+          my ($context, $isclosed) = @_;
+          return sub {
+            return $isclosed ? '' . $_[0] . '' : $_[0];
+            }
+        },
+        1
+      ],
+
+      obsolete => [
+        sub {
+          my ($context, $isobsolete) = @_;
+          return sub {
+            return $isobsolete ? '' . $_[0] . '' : $_[0];
+            }
+        },
+        1
+      ],
+
+      # Returns the text with backslashes, single/double quotes,
+      # and newlines/carriage returns escaped for use in JS strings.
+      js => sub {
+        my ($var) = @_;
+        no warnings 'utf8';
+        $var =~ s/([\\\'\"\/])/\\$1/g;
+        $var =~ s/\n/\\n/g;
+        $var =~ s/\r/\\r/g;
+        $var =~ s/\x{2028}/\\u2028/g;    # unicode line separator
+        $var =~ s/\x{2029}/\\u2029/g;    # unicode paragraph separator
+        $var =~ s/\@/\\x40/g;            # anti-spam for email addresses
+        $var =~ s//\\x3e/g;
+        return $var;
+      },
+
+      # Sadly, different to the above. See http://www.json.org/
+      # for details.
+      json => sub {
+        my ($var) = @_;
+        no warnings 'utf8';
+        $var =~ s/([\\\"\/])/\\$1/g;
+        $var =~ s/\n/\\n/g;
+        $var =~ s/\r/\\r/g;
+        $var =~ s/\f/\\f/g;
+        $var =~ s/\t/\\t/g;
+        return $var;
+      },
+
+      # Converts data to base64
+      base64 => sub {
+        my ($data) = @_;
+        return encode_base64($data);
+      },
+
+      # Strips out control characters excepting whitespace
+      strip_control_chars => sub {
+        my ($data) = @_;
+
+        # Only run for utf8 to avoid issues with other multibyte encodings
+        # that may be reassigning meaning to ascii characters.
+        if (Bugzilla->params->{'utf8'}) {
+          $data =~ s/(?![\t\r\n])[[:cntrl:]]//g;
+        }
+        return $data;
+      },
+
+      # HTML collapses newlines in element attributes to a single space,
+      # so form elements which may have whitespace (ie comments) need
+      # to be encoded using 
+      # See bugs 4928, 22983 and 32000 for more details
+      html_linebreak => sub {
+        my ($var) = @_;
+        $var = html_quote($var);
+        $var =~ s/\r\n/\
/g;
+        $var =~ s/\n\r/\
/g;
+        $var =~ s/\r/\
/g;
+        $var =~ s/\n/\
/g;
+        return $var;
+      },
+
+      # Prevents line break on hyphens and whitespaces.
+      no_break => sub {
+        my ($var) = @_;
+        $var =~ s/ /\ /g;
+        $var =~ s/-/\‑/g;
+        return $var;
+      },
+
+      # Insert `` HTML tags to camel and snake case words as well as
+      # words containing dots in the given string so a long bug summary,
+      # for example, will be wrapped in a preferred manner rather than
+      # overflowing or expanding the parent element. This conversion
+      # should exclude existing HTML tags such as links. Examples:
+      # * `test_switch_window_content.py`
+      # * `TestSwitchToWindowContent`
+      # * `mozilla.org`
+      wbr => sub {
+        my ($var) = @_;
+        $var =~ s/([a-z])([A-Z\._])(?![^<]*>)/$1$2/g;
+        return $var;
+      },
+
+      xml => \&Bugzilla::Util::xml_quote,
+
+      # This filter is similar to url_quote but used a \ instead of a %
+      # as prefix. In addition it replaces a ' ' by a '_'.
+      css_class_quote => \&Bugzilla::Util::css_class_quote,
+
+      # Removes control characters and trims extra whitespace.
+      clean_text => \&Bugzilla::Util::clean_text,
+
+      quoteUrls => [
+        sub {
+          my ($context, $bug, $comment, $user) = @_;
+          return sub {
+            my $text = shift;
+            return quoteUrls($text, $bug, $comment, $user);
+          };
+        },
+        1
+      ],
+
+      bug_link => [
+        sub {
+          my ($context, $bug, $options) = @_;
+          return sub {
+            my $text = shift;
+            return get_bug_link($bug, $text, $options);
+          };
+        },
+        1
+      ],
+
+      bug_list_link => sub {
+        my ($buglist, $options) = @_;
+        return
+          join(", ", map(get_bug_link($_, $_, $options), split(/ *, */, $buglist)));
+      },
+
+      # In CSV, quotes are doubled, and any value containing a quote or a
+      # comma is enclosed in quotes.
+      # If a field starts with either "=", "+", "-" or "@", it is preceded
+      # by a space to prevent stupid formula execution from Excel & co.
+      csv => sub {
+        my ($var) = @_;
+        $var = ' ' . $var if $var =~ /^[+=@-]/;
+
+       # backslash is not special to CSV, but it can be used to confuse some browsers...
+       # so we do not allow it to happen. We only do this for logged-in users.
+        $var =~ s/\\/\x{FF3C}/g if Bugzilla->user->id;
+        $var =~ s/\"/\"\"/g;
+        if ($var !~ /^-?(\d+\.)?\d*$/) {
+          $var = "\"$var\"";
+        }
+        return $var;
+      },
+
+      # Format a filesize in bytes to a human readable value
+      unitconvert => sub {
+        my ($data) = @_;
+        my $retval = "";
+        my %units = ('KB' => 1024, 'MB' => 1024 * 1024, 'GB' => 1024 * 1024 * 1024,);
+
+        if ($data < 1024) {
+          return "$data bytes";
+        }
+        else {
+          my $u;
+          foreach $u ('GB', 'MB', 'KB') {
+            if ($data >= $units{$u}) {
+              return sprintf("%.2f %s", $data / $units{$u}, $u);
+            }
+          }
+        }
+      },
+
+      # Format a time for display (more info in Bugzilla::Util)
+      time => [
+        sub {
+          my ($context, $format, $timezone) = @_;
+          return sub {
+            my $time = shift;
+            return format_time($time, $format, $timezone);
+          };
+        },
+        1
+      ],
+
+      html => \&Bugzilla::Util::html_quote,
+
+      html_light => \&Bugzilla::Util::html_light_quote,
+
+      email => \&Bugzilla::Util::email_filter,
+
+      version => \&version_filter,
+
+      # iCalendar contentline filter
+      ics => [
+        sub {
+          my ($context, @args) = @_;
+          return sub {
+            my ($var)    = shift;
+            my ($par)    = shift @args;
+            my ($output) = "";
+
+            $var =~ s/[\r\n]/ /g;
+            $var =~ s/([;\\\",])/\\$1/g;
+
+            if ($par) {
+              $output = sprintf("%s:%s", $par, $var);
+            }
+            else {
+              $output = $var;
+            }
+
+            $output =~ s/(.{75,75})/$1\n /g;
+
+            return $output;
+          };
         },
+        1
+      ],
+
+      # Note that using this filter is even more dangerous than
+      # using "none," and you should only use it when you're SURE
+      # the output won't be displayed directly to a web browser.
+      txt => sub {
+        my ($var) = @_;
+
+        # Trivial HTML tag remover
+        $var =~ s/<[^>]*>//g;
+
+        # And this basically reverses the html filter.
+        $var =~ s/\@/@/g;
+        $var =~ s/\<//g;
+        $var =~ s/\"/\"/g;
+        $var =~ s/\&/\&/g;
+
+        # Now remove extra whitespace...
+        my $collapse_filter = $Template::Filters::FILTERS->{collapse};
+        $var = $collapse_filter->($var);
+
+        # And if we're not in the WebService, wrap the message.
+        # (Wrapping the message in the WebService is unnecessary
+        # and causes awkward things like \n's appearing in error
+        # messages in JSON-RPC.)
+        unless (i_am_webservice()) {
+          $var = wrap_comment($var, 72);
+        }
+        $var =~ s/\ / /g;
 
-        PLUGIN_BASE => 'Bugzilla::Template::Plugin',
-
-        # We don't want this feature.
-        CONSTANT_NAMESPACE => '__const',
-
-        # Default variables for all templates
-        VARIABLES => {
-            # Some of these are not really constants, and doing this messes up preloading.
-            # they are now fake constants.
-            constants => _load_constants(),
-
-            # Function for retrieving global parameters.
-            'Param' => sub { return Bugzilla->params->{$_[0]}; },
-
-            'bugzilla_version' => sub {
-                my $version = Bugzilla->VERSION;
-                if (my @ver = $version =~ /^(\d{4})(\d{2})(\d{2})\.(\d+)$/s) {
-                    if ($ver[3] eq '1') {
-                        return join('.', @ver[0,1,2]);
-                    }
-                    else {
-                        return join('.', @ver);
-                    }
-                }
-                else {
-                    return $version;
-                }
-            },
-
-            json_encode => sub {
-                return encode_json($_[0]);
-            },
-
-            # Function to create date strings
-            'time2str' => \&Date::Format::time2str,
-
-            # Fixed size column formatting for bugmail.
-            'format_columns' => sub {
-                my $cols = shift;
-                my $format = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE;
-                my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE;
-                return multiline_sprintf($format, \@_, $col_size);
-            },
-
-            # Generic linear search function
-            'lsearch' => sub {
-                my ($array, $item) = @_;
-                return firstidx { $_ eq $item } @$array;
-            },
-
-            # Currently logged in user, if any
-            # If an sudo session is in progress, this is the user we're faking
-            'user' => sub { return Bugzilla->user; },
-
-            # Currenly active language
-            'current_language' => sub { return Bugzilla->current_language; },
-
-            'script_nonce' => sub {
-                my $cgi = Bugzilla->cgi;
-                return $cgi->csp_nonce ? sprintf('nonce="%s"', $cgi->csp_nonce) : '';
-            },
-
-            # If an sudo session is in progress, this is the user who
-            # started the session.
-            'sudoer' => sub { return Bugzilla->sudoer; },
-
-            # Allow templates to access the "corect" URLBase value
-            'urlbase' => sub { return Bugzilla->localconfig->{urlbase}; },
-
-            # Allow templates to access docs url with users' preferred language
-            'docs_urlbase' => sub {
-                my $language = Bugzilla->current_language;
-                my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
-                $docs_urlbase =~ s/\%lang\%/$language/;
-                return $docs_urlbase;
-            },
-
-            # Check whether the URL is safe.
-            'is_safe_url' => sub {
-                my $url = shift;
-                return 0 unless $url;
-
-                my $safe_url_regexp = SAFE_URL_REGEXP();
-                return 1 if $url =~ /^$safe_url_regexp$/;
-                # Pointing to a local file with no colon in its name is fine.
-                return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i;
-                # If we come here, then we cannot guarantee it's safe.
-                return 0;
-            },
-
-            # Allow templates to generate a token themselves.
-            'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
-
-            'get_login_request_token' => sub {
-                my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie');
-                return $cookie ? issue_hash_token(['login_request', $cookie]) : '';
-            },
-
-            'get_api_token' => sub {
-                return '' unless Bugzilla->user->id;
-                my $cache = Bugzilla->request_cache;
-                return $cache->{api_token} //= issue_api_token();
-            },
-
-            # A way for all templates to get at Field data, cached.
-            'bug_fields' => sub {
-                my $cache = Bugzilla->request_cache;
-                $cache->{template_bug_fields} ||=
-                    Bugzilla->fields({ by_name => 1 });
-                return $cache->{template_bug_fields};
-            },
-
-            # A general purpose cache to store rendered templates for reuse.
-            # Make sure to not mix language-specific data.
-            'template_cache' => sub {
-                my $cache = Bugzilla->request_cache->{template_cache} ||= {};
-                $cache->{users} ||= {};
-                return $cache;
-            },
-
-            'css_files' => \&css_files,
-            yui_resolve_deps => \&yui_resolve_deps,
-
-            # Whether or not keywords are enabled, in this Bugzilla.
-            'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
-
-            # All the keywords
-            'all_keywords' => sub { return Bugzilla::Keyword->get_all(); },
-
-            # All the active keywords
-            'active_keywords' => sub {
-                return [grep { $_->is_active } Bugzilla::Keyword->get_all()];
-            },
-
-            'feature_enabled' => sub { return Bugzilla->feature(@_); },
-
-            'has_extension' => sub { return Bugzilla->has_extension(@_); },
-
-            # field_descs can be somewhat slow to generate, so we generate
-            # it only once per-language no matter how many times
-            # $template->process() is called.
-            'field_descs' => sub { return template_var('field_descs') },
-
-            # Calling bug/field-help.none.tmpl once per label is very
-            # expensive, so we generate it once per-language.
-            'help_html' => sub { return template_var('help_html') },
-
-            # This way we don't have to load field-descs.none.tmpl in
-            # many templates.
-            'display_value' => \&Bugzilla::Util::display_value,
-
-            'install_string' => \&Bugzilla::Install::Util::install_string,
-
-            'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
-
-            # These don't work as normal constants.
-            DB_MODULE        => \&Bugzilla::Constants::DB_MODULE,
-            'default_authorizer' => sub { return Bugzilla::Auth->new() },
-
-            # It is almost always better to do mobile feature detection, client side in js.
-            # However, we need to set the meta[name=viewport] server-side or the behavior is
-            # not as predictable. It is possible other parts of the frontend may use this feature too.
-            'is_mobile_browser' => sub { return Bugzilla->cgi->user_agent =~ /Mobi/ },
-
-            'socorro_lens_url' => sub {
-                my ($sigs) = @_;
-
-                # strip [@ ] from sigs
-                my @sigs = map { /^\[\@\s*(.+?)\s*\]$/ } @$sigs;
-
-                return '' unless @sigs;
-                # use a URI object to encode the query string part.
-                my $uri = URI->new(Bugzilla->localconfig->{urlbase} . 'metricsgraphics/socorro-lens.html');
-                $uri->query_form('s' => join("\\", @sigs));
-                return $uri;
-            },
+        return $var;
+      },
+
+      # Wrap a displayed comment to the appropriate length
+      wrap_comment => [
+        sub {
+          my ($context, $cols) = @_;
+          return sub { wrap_comment($_[0], $cols) }
         },
-    };
+        1
+      ],
+
+      # We force filtering of every variable in key security-critical
+      # places; we have a none filter for people to use when they
+      # really, really don't want a variable to be changed.
+      none => sub { return $_[0]; },
+    },
+
+    PLUGIN_BASE => 'Bugzilla::Template::Plugin',
+
+    # We don't want this feature.
+    CONSTANT_NAMESPACE => '__const',
+
+    # Default variables for all templates
+    VARIABLES => {
+
+      # Some of these are not really constants, and doing this messes up preloading.
+      # they are now fake constants.
+      constants => _load_constants(),
+
+      # Function for retrieving global parameters.
+      'Param' => sub { return Bugzilla->params->{$_[0]}; },
+
+      'bugzilla_version' => sub {
+        my $version = Bugzilla->VERSION;
+        if (my @ver = $version =~ /^(\d{4})(\d{2})(\d{2})\.(\d+)$/s) {
+          if ($ver[3] eq '1') {
+            return join('.', @ver[0, 1, 2]);
+          }
+          else {
+            return join('.', @ver);
+          }
+        }
+        else {
+          return $version;
+        }
+      },
+
+      json_encode => sub {
+        return encode_json($_[0]);
+      },
+
+      # Function to create date strings
+      'time2str' => \&Date::Format::time2str,
+
+      # Fixed size column formatting for bugmail.
+      'format_columns' => sub {
+        my $cols     = shift;
+        my $format   = ($cols == 3) ? FORMAT_TRIPLE : FORMAT_DOUBLE;
+        my $col_size = ($cols == 3) ? FORMAT_3_SIZE : FORMAT_2_SIZE;
+        return multiline_sprintf($format, \@_, $col_size);
+      },
+
+      # Generic linear search function
+      'lsearch' => sub {
+        my ($array, $item) = @_;
+        return firstidx { $_ eq $item } @$array;
+      },
+
+      # Currently logged in user, if any
+      # If an sudo session is in progress, this is the user we're faking
+      'user' => sub { return Bugzilla->user; },
+
+      # Currenly active language
+      'current_language' => sub { return Bugzilla->current_language; },
+
+      'script_nonce' => sub {
+        my $cgi = Bugzilla->cgi;
+        return $cgi->csp_nonce ? sprintf('nonce="%s"', $cgi->csp_nonce) : '';
+      },
+
+      # If an sudo session is in progress, this is the user who
+      # started the session.
+      'sudoer' => sub { return Bugzilla->sudoer; },
+
+      # Allow templates to access the "corect" URLBase value
+      'urlbase' => sub { return Bugzilla->localconfig->{urlbase}; },
+
+      # Allow templates to access docs url with users' preferred language
+      'docs_urlbase' => sub {
+        my $language     = Bugzilla->current_language;
+        my $docs_urlbase = Bugzilla->params->{'docs_urlbase'};
+        $docs_urlbase =~ s/\%lang\%/$language/;
+        return $docs_urlbase;
+      },
+
+      # Check whether the URL is safe.
+      'is_safe_url' => sub {
+        my $url = shift;
+        return 0 unless $url;
+
+        my $safe_url_regexp = SAFE_URL_REGEXP();
+        return 1 if $url =~ /^$safe_url_regexp$/;
+
+        # Pointing to a local file with no colon in its name is fine.
+        return 1 if $url =~ /^[^\s<>\":]+[\w\/]$/i;
+
+        # If we come here, then we cannot guarantee it's safe.
+        return 0;
+      },
+
+      # Allow templates to generate a token themselves.
+      'issue_hash_token' => \&Bugzilla::Token::issue_hash_token,
+
+      'get_login_request_token' => sub {
+        my $cookie = Bugzilla->cgi->cookie('Bugzilla_login_request_cookie');
+        return $cookie ? issue_hash_token(['login_request', $cookie]) : '';
+      },
+
+      'get_api_token' => sub {
+        return '' unless Bugzilla->user->id;
+        my $cache = Bugzilla->request_cache;
+        return $cache->{api_token} //= issue_api_token();
+      },
+
+      # A way for all templates to get at Field data, cached.
+      'bug_fields' => sub {
+        my $cache = Bugzilla->request_cache;
+        $cache->{template_bug_fields} ||= Bugzilla->fields({by_name => 1});
+        return $cache->{template_bug_fields};
+      },
+
+      # A general purpose cache to store rendered templates for reuse.
+      # Make sure to not mix language-specific data.
+      'template_cache' => sub {
+        my $cache = Bugzilla->request_cache->{template_cache} ||= {};
+        $cache->{users} ||= {};
+        return $cache;
+      },
+
+      'css_files'      => \&css_files,
+      yui_resolve_deps => \&yui_resolve_deps,
+
+      # Whether or not keywords are enabled, in this Bugzilla.
+      'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
+
+      # All the keywords
+      'all_keywords' => sub { return Bugzilla::Keyword->get_all(); },
+
+      # All the active keywords
+      'active_keywords' => sub {
+        return [grep { $_->is_active } Bugzilla::Keyword->get_all()];
+      },
+
+      'feature_enabled' => sub { return Bugzilla->feature(@_); },
+
+      'has_extension' => sub { return Bugzilla->has_extension(@_); },
+
+      # field_descs can be somewhat slow to generate, so we generate
+      # it only once per-language no matter how many times
+      # $template->process() is called.
+      'field_descs' => sub { return template_var('field_descs') },
+
+      # Calling bug/field-help.none.tmpl once per label is very
+      # expensive, so we generate it once per-language.
+      'help_html' => sub { return template_var('help_html') },
+
+      # This way we don't have to load field-descs.none.tmpl in
+      # many templates.
+      'display_value' => \&Bugzilla::Util::display_value,
+
+      'install_string' => \&Bugzilla::Install::Util::install_string,
+
+      'report_columns' => \&Bugzilla::Search::REPORT_COLUMNS,
+
+      # These don't work as normal constants.
+      DB_MODULE            => \&Bugzilla::Constants::DB_MODULE,
+      'default_authorizer' => sub { return Bugzilla::Auth->new() },
+
+# It is almost always better to do mobile feature detection, client side in js.
+# However, we need to set the meta[name=viewport] server-side or the behavior is
+# not as predictable. It is possible other parts of the frontend may use this feature too.
+      'is_mobile_browser' => sub { return Bugzilla->cgi->user_agent =~ /Mobi/ },
+
+      'socorro_lens_url' => sub {
+        my ($sigs) = @_;
+
+        # strip [@ ] from sigs
+        my @sigs = map {/^\[\@\s*(.+?)\s*\]$/} @$sigs;
+
+        return '' unless @sigs;
+
+        # use a URI object to encode the query string part.
+        my $uri = URI->new(
+          Bugzilla->localconfig->{urlbase} . 'metricsgraphics/socorro-lens.html');
+        $uri->query_form('s' => join("\\", @sigs));
+        return $uri;
+      },
+    },
+  };
 
-    # under mod_perl, use a provider (template loader) that preloads all templates into memory
-    my $provider_class
-        = $opts{preload}
-        ? 'Bugzilla::Template::PreloadProvider'
-        : 'Template::Provider';
+# under mod_perl, use a provider (template loader) that preloads all templates into memory
+  my $provider_class
+    = $opts{preload}
+    ? 'Bugzilla::Template::PreloadProvider'
+    : 'Template::Provider';
 
-    # Use a per-process provider to cache compiled templates in memory across
-    # requests.
-    my $provider_key = join(':', @{ $config->{INCLUDE_PATH} });
-    $SHARED_PROVIDERS{$provider_key} ||= $provider_class->new($config);
-    $config->{LOAD_TEMPLATES} = [ $SHARED_PROVIDERS{$provider_key} ];
+  # Use a per-process provider to cache compiled templates in memory across
+  # requests.
+  my $provider_key = join(':', @{$config->{INCLUDE_PATH}});
+  $SHARED_PROVIDERS{$provider_key} ||= $provider_class->new($config);
+  $config->{LOAD_TEMPLATES} = [$SHARED_PROVIDERS{$provider_key}];
 
-    local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
+  local $Template::Config::CONTEXT = 'Bugzilla::Template::Context';
 
-    Bugzilla::Hook::process('template_before_create', { config => $config });
-    my $template = $class->new($config)
-        || die("Template creation failed: " . $class->error());
+  Bugzilla::Hook::process('template_before_create', {config => $config});
+  my $template = $class->new($config)
+    || die("Template creation failed: " . $class->error());
 
-    # BMO - hook for defining new vmethods, etc
-    Bugzilla::Hook::process('template_after_create', { template => $template });
+  # BMO - hook for defining new vmethods, etc
+  Bugzilla::Hook::process('template_after_create', {template => $template});
 
-    # Pass on our current language to any template hooks or inner templates
-    # called by this Template object.
-    $template->context->{bz_language} = $opts{language} || '';
+  # Pass on our current language to any template hooks or inner templates
+  # called by this Template object.
+  $template->context->{bz_language} = $opts{language} || '';
 
-    return $template;
+  return $template;
 }
 
 # Used as part of the two subroutines below.
 our %_templates_to_precompile;
+
 sub precompile_templates {
-    my ($output) = @_;
+  my ($output) = @_;
+
+  return unless is_webserver_group();
 
-    return unless is_webserver_group();
+  # Remove the compiled templates.
+  my $cache_dir = bz_locations()->{'template_cache'};
+  my $datadir   = bz_locations()->{'datadir'};
+  if (-e $cache_dir) {
+    print install_string('template_removing_dir') . "\n" if $output;
 
-    # Remove the compiled templates.
-    my $cache_dir = bz_locations()->{'template_cache'};
-    my $datadir = bz_locations()->{'datadir'};
+    # This frequently fails if the webserver made the files, because
+    # then the webserver owns the directories.
+    rmtree($cache_dir);
+
+    # Check that the directory was really removed, and if not, move it
+    # into data/deleteme/.
     if (-e $cache_dir) {
-        print install_string('template_removing_dir') . "\n" if $output;
-
-        # This frequently fails if the webserver made the files, because
-        # then the webserver owns the directories.
-        rmtree($cache_dir);
-
-        # Check that the directory was really removed, and if not, move it
-        # into data/deleteme/.
-        if (-e $cache_dir) {
-            my $deleteme = "$datadir/deleteme";
-
-            print STDERR "\n\n",
-                install_string('template_removal_failed',
-                               { deleteme => $deleteme,
-                                 template_cache => $cache_dir }), "\n\n";
-            mkpath($deleteme);
-            my $random = generate_random_password();
-            rename($cache_dir, "$deleteme/$random")
-              or die "move failed: $!";
-        }
+      my $deleteme = "$datadir/deleteme";
+
+      print STDERR "\n\n",
+        install_string('template_removal_failed',
+        {deleteme => $deleteme, template_cache => $cache_dir}),
+        "\n\n";
+      mkpath($deleteme);
+      my $random = generate_random_password();
+      rename($cache_dir, "$deleteme/$random") or die "move failed: $!";
     }
+  }
 
-    print install_string('template_precompile') if $output;
+  print install_string('template_precompile') if $output;
 
-    # Pre-compile all available languages.
-    my $paths = template_include_path({ language => Bugzilla->languages });
+  # Pre-compile all available languages.
+  my $paths = template_include_path({language => Bugzilla->languages});
 
-    foreach my $dir (@$paths) {
-        my $template = Bugzilla::Template->create(include_path => [$dir]);
+  foreach my $dir (@$paths) {
+    my $template = Bugzilla::Template->create(include_path => [$dir]);
 
-        %_templates_to_precompile = ();
-        # Traverse the template hierarchy.
-        find({ wanted => \&_precompile_push, no_chdir => 1 }, $dir);
-        # The sort isn't totally necessary, but it makes debugging easier
-        # by making the templates always be compiled in the same order.
-        foreach my $file (sort keys %_templates_to_precompile) {
-            $file =~ s{^\Q$dir\E/}{};
-            # Compile the template but throw away the result. This has the side-
-            # effect of writing the compiled version to disk.
-            $template->context->template($file);
-        }
-    }
+    %_templates_to_precompile = ();
+
+    # Traverse the template hierarchy.
+    find({wanted => \&_precompile_push, no_chdir => 1}, $dir);
+
+    # The sort isn't totally necessary, but it makes debugging easier
+    # by making the templates always be compiled in the same order.
+    foreach my $file (sort keys %_templates_to_precompile) {
+      $file =~ s{^\Q$dir\E/}{};
 
-    # Under mod_perl, we look for templates using the absolute path of the
-    # template directory, which causes Template Toolkit to look for their
-    # *compiled* versions using the full absolute path under the data/template
-    # directory. (Like data/template/var/www/html/bugzilla/.) To avoid
-    # re-compiling templates under mod_perl, we symlink to the
-    # already-compiled templates. This doesn't work on Windows.
-    if (!ON_WINDOWS) {
-        # We do these separately in case they're in different locations.
-        _do_template_symlink(bz_locations()->{'templatedir'});
-        _do_template_symlink(bz_locations()->{'extensionsdir'});
+      # Compile the template but throw away the result. This has the side-
+      # effect of writing the compiled version to disk.
+      $template->context->template($file);
     }
+  }
 
-    # If anything created a Template object before now, clear it out.
-    delete Bugzilla->request_cache->{template};
+  # Under mod_perl, we look for templates using the absolute path of the
+  # template directory, which causes Template Toolkit to look for their
+  # *compiled* versions using the full absolute path under the data/template
+  # directory. (Like data/template/var/www/html/bugzilla/.) To avoid
+  # re-compiling templates under mod_perl, we symlink to the
+  # already-compiled templates. This doesn't work on Windows.
+  if (!ON_WINDOWS) {
 
-    # Clear out the cached Provider object
-    %SHARED_PROVIDERS = ();
+    # We do these separately in case they're in different locations.
+    _do_template_symlink(bz_locations()->{'templatedir'});
+    _do_template_symlink(bz_locations()->{'extensionsdir'});
+  }
 
-    print install_string('done') . "\n" if $output;
+  # If anything created a Template object before now, clear it out.
+  delete Bugzilla->request_cache->{template};
+
+  # Clear out the cached Provider object
+  %SHARED_PROVIDERS = ();
+
+  print install_string('done') . "\n" if $output;
 }
 
 # Helper for precompile_templates
 sub _precompile_push {
-    my $name = $File::Find::name;
-    return if (-d $name);
-    return if ($name =~ /\/CVS\//);
-    return if ($name !~ /\.tmpl$/);
-    $_templates_to_precompile{$name} = 1;
+  my $name = $File::Find::name;
+  return if (-d $name);
+  return if ($name =~ /\/CVS\//);
+  return if ($name !~ /\.tmpl$/);
+  $_templates_to_precompile{$name} = 1;
 }
 
 # Helper for precompile_templates
 sub _do_template_symlink {
-    my $dir_to_symlink = shift;
-
-    my $abs_path = abs_path($dir_to_symlink);
-
-    # If $dir_to_symlink is already an absolute path (as might happen
-    # with packagers who set $libpath to an absolute path), then we don't
-    # need to do this symlink.
-    return if ($abs_path eq $dir_to_symlink);
-
-    my $abs_root  = dirname($abs_path);
-    my $dir_name  = basename($abs_path);
-    my $cache_dir   = bz_locations()->{'template_cache'};
-    my $container = "$cache_dir$abs_root";
-    mkpath($container);
-    my $target = "$cache_dir/$dir_name";
-    # Check if the directory exists, because if there are no extensions,
-    # there won't be an "data/template/extensions" directory to link to.
-    if (-d $target) {
-        # We use abs2rel so that the symlink will look like
-        # "../../../../template" which works, while just
-        # "data/template/template/" doesn't work.
-        my $relative_target = File::Spec->abs2rel($target, $container);
-
-        my $link_name = "$container/$dir_name";
-        symlink($relative_target, $link_name)
-          or warn "Could not make $link_name a symlink to $relative_target: $!";
-    }
+  my $dir_to_symlink = shift;
+
+  my $abs_path = abs_path($dir_to_symlink);
+
+  # If $dir_to_symlink is already an absolute path (as might happen
+  # with packagers who set $libpath to an absolute path), then we don't
+  # need to do this symlink.
+  return if ($abs_path eq $dir_to_symlink);
+
+  my $abs_root  = dirname($abs_path);
+  my $dir_name  = basename($abs_path);
+  my $cache_dir = bz_locations()->{'template_cache'};
+  my $container = "$cache_dir$abs_root";
+  mkpath($container);
+  my $target = "$cache_dir/$dir_name";
+
+  # Check if the directory exists, because if there are no extensions,
+  # there won't be an "data/template/extensions" directory to link to.
+  if (-d $target) {
+
+    # We use abs2rel so that the symlink will look like
+    # "../../../../template" which works, while just
+    # "data/template/template/" doesn't work.
+    my $relative_target = File::Spec->abs2rel($target, $container);
+
+    my $link_name = "$container/$dir_name";
+    symlink($relative_target, $link_name)
+      or warn "Could not make $link_name a symlink to $relative_target: $!";
+  }
 }
 
 1;
diff --git a/Bugzilla/Template/Context.pm b/Bugzilla/Template/Context.pm
index 21e2b7ec8..f2db23702 100644
--- a/Bugzilla/Template/Context.pm
+++ b/Bugzilla/Template/Context.pm
@@ -18,23 +18,24 @@ use Bugzilla::Hook;
 use Scalar::Util qw(blessed);
 
 sub process {
-    my $self = shift;
-    # We don't want to run the template_before_process hook for
-    # template hooks (but we do want it to run if a hook calls
-    # PROCESS inside itself). The problem is that the {component}->{name} of
-    # hooks is unreliable--sometimes it starts with ./ and it's the
-    # full path to the hook template, and sometimes it's just the relative
-    # name (like hook/global/field-descs-end.none.tmpl). Also, calling
-    # template_before_process for hook templates doesn't seem too useful,
-    # because that's already part of the extension and they should be able
-    # to modify their hook if they want (or just modify the variables in the
-    # calling template).
-    if (not delete $self->{bz_in_hook}) {
-        $self->{bz_in_process} = 1;
-    }
-    my $result = $self->SUPER::process(@_);
-    delete $self->{bz_in_process};
-    return $result;
+  my $self = shift;
+
+  # We don't want to run the template_before_process hook for
+  # template hooks (but we do want it to run if a hook calls
+  # PROCESS inside itself). The problem is that the {component}->{name} of
+  # hooks is unreliable--sometimes it starts with ./ and it's the
+  # full path to the hook template, and sometimes it's just the relative
+  # name (like hook/global/field-descs-end.none.tmpl). Also, calling
+  # template_before_process for hook templates doesn't seem too useful,
+  # because that's already part of the extension and they should be able
+  # to modify their hook if they want (or just modify the variables in the
+  # calling template).
+  if (not delete $self->{bz_in_hook}) {
+    $self->{bz_in_process} = 1;
+  }
+  my $result = $self->SUPER::process(@_);
+  delete $self->{bz_in_process};
+  return $result;
 }
 
 # This method is called by Template-Toolkit exactly once per template or
@@ -46,57 +47,58 @@ sub process {
 # in the PROCESS or INCLUDE directive haven't been set, and if we're
 # in an INCLUDE, the stash is not yet localized during process().
 sub stash {
-    my $self = shift;
-    my $stash = $self->SUPER::stash(@_);
+  my $self  = shift;
+  my $stash = $self->SUPER::stash(@_);
 
-    my $name = $stash->{component}->{name};
-    my $pre_process = $self->config->{PRE_PROCESS};
+  my $name        = $stash->{component}->{name};
+  my $pre_process = $self->config->{PRE_PROCESS};
 
-    # Checking bz_in_process tells us that we were indeed called as part of a
-    # Context::process, and not at some other point.
-    #
-    # Checking $name makes sure that we're processing a file, and not just a
-    # block, by checking that the name has a period in it. We don't allow
-    # blocks because their names are too unreliable--an extension could have
-    # a block with the same name, or multiple files could have a same-named
-    # block, and then your extension would malfunction.
-    #
-    # We also make sure that we don't run, ever, during the PRE_PROCESS
-    # templates, because if somebody calls Throw*Error globally inside of
-    # template_before_process, that causes an infinite recursion into
-    # the PRE_PROCESS templates (because Bugzilla, while inside
-    # global/intialize.none.tmpl, loads the template again to create the
-    # template object for Throw*Error).
-    #
-    # Checking Bugzilla::Hook::in prevents infinite recursion on this hook.
-    if ($self->{bz_in_process} and $name =~ /\./
-        and !grep($_ eq $name, @$pre_process)
-        and !Bugzilla::Hook::in('template_before_process'))
-    {
-        Bugzilla::Hook::process("template_before_process",
-                                { vars => $stash, context => $self,
-                                  file => $name });
-    }
+  # Checking bz_in_process tells us that we were indeed called as part of a
+  # Context::process, and not at some other point.
+  #
+  # Checking $name makes sure that we're processing a file, and not just a
+  # block, by checking that the name has a period in it. We don't allow
+  # blocks because their names are too unreliable--an extension could have
+  # a block with the same name, or multiple files could have a same-named
+  # block, and then your extension would malfunction.
+  #
+  # We also make sure that we don't run, ever, during the PRE_PROCESS
+  # templates, because if somebody calls Throw*Error globally inside of
+  # template_before_process, that causes an infinite recursion into
+  # the PRE_PROCESS templates (because Bugzilla, while inside
+  # global/intialize.none.tmpl, loads the template again to create the
+  # template object for Throw*Error).
+  #
+  # Checking Bugzilla::Hook::in prevents infinite recursion on this hook.
+  if (  $self->{bz_in_process}
+    and $name =~ /\./
+    and !grep($_ eq $name, @$pre_process)
+    and !Bugzilla::Hook::in('template_before_process'))
+  {
+    Bugzilla::Hook::process("template_before_process",
+      {vars => $stash, context => $self, file => $name});
+  }
 
-    # This prevents other calls to stash() that might somehow happen
-    # later in the file from also triggering the hook.
-    delete $self->{bz_in_process};
+  # This prevents other calls to stash() that might somehow happen
+  # later in the file from also triggering the hook.
+  delete $self->{bz_in_process};
 
-    return $stash;
+  return $stash;
 }
 
 sub filter {
-    my ($self, $name, $args) = @_;
-    # If we pass an alias for the filter name, the filter code is cached
-    # instead of looking for it at each call.
-    # If the filter has arguments, then we can't cache it.
-    $self->SUPER::filter($name, $args, $args ? undef : $name);
+  my ($self, $name, $args) = @_;
+
+  # If we pass an alias for the filter name, the filter code is cached
+  # instead of looking for it at each call.
+  # If the filter has arguments, then we can't cache it.
+  $self->SUPER::filter($name, $args, $args ? undef : $name);
 }
 
 # We need a DESTROY sub for the same reason that Bugzilla::CGI does.
 sub DESTROY {
-    my $self = shift;
-    $self->SUPER::DESTROY(@_);
-};
+  my $self = shift;
+  $self->SUPER::DESTROY(@_);
+}
 
 1;
diff --git a/Bugzilla/Template/Plugin/Bugzilla.pm b/Bugzilla/Template/Plugin/Bugzilla.pm
index 752aa9dfa..110d3d352 100644
--- a/Bugzilla/Template/Plugin/Bugzilla.pm
+++ b/Bugzilla/Template/Plugin/Bugzilla.pm
@@ -16,20 +16,20 @@ use base qw(Template::Plugin);
 use Bugzilla;
 
 sub new {
-    my ($class, $context) = @_;
+  my ($class, $context) = @_;
 
-    return bless {}, $class;
+  return bless {}, $class;
 }
 
 sub AUTOLOAD {
-    my $class = shift;
-    our $AUTOLOAD;
+  my $class = shift;
+  our $AUTOLOAD;
 
-    $AUTOLOAD =~ s/^.*:://;
+  $AUTOLOAD =~ s/^.*:://;
 
-    return if $AUTOLOAD eq 'DESTROY';
+  return if $AUTOLOAD eq 'DESTROY';
 
-    return Bugzilla->$AUTOLOAD(@_);
+  return Bugzilla->$AUTOLOAD(@_);
 }
 
 1;
diff --git a/Bugzilla/Template/Plugin/Hook.pm b/Bugzilla/Template/Plugin/Hook.pm
index a2b76a80f..e20ca1016 100644
--- a/Bugzilla/Template/Plugin/Hook.pm
+++ b/Bugzilla/Template/Plugin/Hook.pm
@@ -21,74 +21,73 @@ use Bugzilla::Error;
 use File::Spec;
 
 sub new {
-    my ($class, $context) = @_;
-    return bless { _CONTEXT => $context }, $class;
+  my ($class, $context) = @_;
+  return bless {_CONTEXT => $context}, $class;
 }
 
 sub _context { return $_[0]->{_CONTEXT} }
 
 sub process {
-    my ($self, $hook_name, $template) = @_;
-    my $context = $self->_context();
-    $template ||= $context->stash->{component}->{name};
-
-    # sanity check:
-    if (!$template =~ /[\w\.\/\-_\\]+/) {
-        ThrowCodeError('template_invalid', { name => $template });
-    }
-
-    my (undef, $path, $filename) = File::Spec->splitpath($template);
-    $path ||= '';
-    $filename =~ m/(.+)\.(.+)\.tmpl$/;
-    my $template_name = $1;
-    my $type = $2;
-
-    # Hooks are named like this:
-    my $extension_template = "$path$template_name-$hook_name.$type.tmpl";
-
-    # Get the hooks out of the cache if they exist. Otherwise, read them
-    # from the disk.
-    my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
-    my $lang = $context->{bz_language} || '';
-    $cache->{"${lang}__$extension_template"}
-        ||= $self->_get_hooks($extension_template);
-
-    # process() accepts an arrayref of templates, so we just pass the whole
-    # arrayref.
-    $context->{bz_in_hook} = 1; # See Bugzilla::Template::Context
-    return $context->process($cache->{"${lang}__$extension_template"});
+  my ($self, $hook_name, $template) = @_;
+  my $context = $self->_context();
+  $template ||= $context->stash->{component}->{name};
+
+  # sanity check:
+  if (!$template =~ /[\w\.\/\-_\\]+/) {
+    ThrowCodeError('template_invalid', {name => $template});
+  }
+
+  my (undef, $path, $filename) = File::Spec->splitpath($template);
+  $path ||= '';
+  $filename =~ m/(.+)\.(.+)\.tmpl$/;
+  my $template_name = $1;
+  my $type          = $2;
+
+  # Hooks are named like this:
+  my $extension_template = "$path$template_name-$hook_name.$type.tmpl";
+
+  # Get the hooks out of the cache if they exist. Otherwise, read them
+  # from the disk.
+  my $cache = Bugzilla->request_cache->{template_plugin_hook_cache} ||= {};
+  my $lang = $context->{bz_language} || '';
+  $cache->{"${lang}__$extension_template"}
+    ||= $self->_get_hooks($extension_template);
+
+  # process() accepts an arrayref of templates, so we just pass the whole
+  # arrayref.
+  $context->{bz_in_hook} = 1;    # See Bugzilla::Template::Context
+  return $context->process($cache->{"${lang}__$extension_template"});
 }
 
 sub _get_hooks {
-    my ($self, $extension_template) = @_;
-
-    my $template_sets = $self->_template_hook_include_path();
-    my @hooks;
-    foreach my $dir_set (@$template_sets) {
-        foreach my $template_dir (@$dir_set) {
-            my $file = "$template_dir/hook/$extension_template";
-            if (-e $file) {
-                my $template = $self->_context->template($file);
-                push(@hooks, $template);
-                # Don't run the hook for more than one language.
-                last;
-            }
-        }
+  my ($self, $extension_template) = @_;
+
+  my $template_sets = $self->_template_hook_include_path();
+  my @hooks;
+  foreach my $dir_set (@$template_sets) {
+    foreach my $template_dir (@$dir_set) {
+      my $file = "$template_dir/hook/$extension_template";
+      if (-e $file) {
+        my $template = $self->_context->template($file);
+        push(@hooks, $template);
+
+        # Don't run the hook for more than one language.
+        last;
+      }
     }
+  }
 
-    return \@hooks;
+  return \@hooks;
 }
 
 sub _template_hook_include_path {
-    my $self = shift;
-    my $cache = Bugzilla->request_cache;
-    my $language = $self->_context->{bz_language} || '';
-    my $cache_key = "template_plugin_hook_include_path_$language";
-    $cache->{$cache_key} ||= template_include_path({
-        language => $language,
-        hook     => 1,
-    });
-    return $cache->{$cache_key};
+  my $self      = shift;
+  my $cache     = Bugzilla->request_cache;
+  my $language  = $self->_context->{bz_language} || '';
+  my $cache_key = "template_plugin_hook_include_path_$language";
+  $cache->{$cache_key}
+    ||= template_include_path({language => $language, hook => 1,});
+  return $cache->{$cache_key};
 }
 
 1;
diff --git a/Bugzilla/Template/Plugin/User.pm b/Bugzilla/Template/Plugin/User.pm
index 09452d899..ac22f0861 100644
--- a/Bugzilla/Template/Plugin/User.pm
+++ b/Bugzilla/Template/Plugin/User.pm
@@ -31,20 +31,20 @@ use base qw(Template::Plugin);
 use Bugzilla::User;
 
 sub new {
-    my ($class, $context) = @_;
+  my ($class, $context) = @_;
 
-    return bless {}, $class;
+  return bless {}, $class;
 }
 
 sub AUTOLOAD {
-    my $class = shift;
-    our $AUTOLOAD;
+  my $class = shift;
+  our $AUTOLOAD;
 
-    $AUTOLOAD =~ s/^.*:://;
+  $AUTOLOAD =~ s/^.*:://;
 
-    return if $AUTOLOAD eq 'DESTROY';
+  return if $AUTOLOAD eq 'DESTROY';
 
-    return Bugzilla::User->$AUTOLOAD(@_);
+  return Bugzilla::User->$AUTOLOAD(@_);
 }
 
 1;
diff --git a/Bugzilla/Template/PreloadProvider.pm b/Bugzilla/Template/PreloadProvider.pm
index bddabfa2e..6d963f31f 100644
--- a/Bugzilla/Template/PreloadProvider.pm
+++ b/Bugzilla/Template/PreloadProvider.pm
@@ -15,7 +15,7 @@ use warnings;
 use base qw(Template::Provider);
 
 use File::Find ();
-use Cwd ();
+use Cwd        ();
 use File::Spec;
 use Template::Constants qw( STATUS_ERROR );
 use Template::Document;
@@ -24,88 +24,88 @@ use Template::Config;
 use Bugzilla::Util qw(trick_taint);
 
 sub _init {
-    my $self = shift;
-    $self->SUPER::_init(@_);
-
-    my $path   = $self->{INCLUDE_PATH};
-    my $cache  = $self->{_BZ_CACHE} = {};
-    my $search = $self->{_BZ_SEARCH} = {};
-
-    foreach my $template_dir (@$path) {
-        $template_dir = Cwd::realpath($template_dir);
-        my $wanted = sub {
-            my ( $name, $dir ) = ($File::Find::name, $File::Find::dir);
-            if ( $name =~ /\.tmpl$/ ) {
-                my $key = $name;
-                $key =~ s/^\Q$template_dir\///;
-                unless ($search->{$key}) {
-                    $search->{$key} = $name;
-                }
-                trick_taint($name);
-                my $data = {
-                    path => $name,
-                    name => $key,
-                    text => do {
-                        open my $fh, '<:utf8', $name or die "cannot open $name";
-                        local $/ = undef;
-                        scalar <$fh>; # $fh is closed it goes out of scope
-                    },
-                    time => (stat($name))[9],
-                };
-                trick_taint($data->{text}) if $data->{text};
-                $cache->{$name} = $self->_bz_compile($data) or die "compile error: $name";
-            }
+  my $self = shift;
+  $self->SUPER::_init(@_);
+
+  my $path   = $self->{INCLUDE_PATH};
+  my $cache  = $self->{_BZ_CACHE} = {};
+  my $search = $self->{_BZ_SEARCH} = {};
+
+  foreach my $template_dir (@$path) {
+    $template_dir = Cwd::realpath($template_dir);
+    my $wanted = sub {
+      my ($name, $dir) = ($File::Find::name, $File::Find::dir);
+      if ($name =~ /\.tmpl$/) {
+        my $key = $name;
+        $key =~ s/^\Q$template_dir\///;
+        unless ($search->{$key}) {
+          $search->{$key} = $name;
+        }
+        trick_taint($name);
+        my $data = {
+          path => $name,
+          name => $key,
+          text => do {
+            open my $fh, '<:utf8', $name or die "cannot open $name";
+            local $/ = undef;
+            scalar <$fh>;    # $fh is closed it goes out of scope
+          },
+          time => (stat($name))[9],
         };
-        File::Find::find( { wanted => $wanted, no_chdir => 1 }, $template_dir );
-    }
-
-    return $self;
+        trick_taint($data->{text}) if $data->{text};
+        $cache->{$name} = $self->_bz_compile($data) or die "compile error: $name";
+      }
+    };
+    File::Find::find({wanted => $wanted, no_chdir => 1}, $template_dir);
+  }
+
+  return $self;
 }
 
 sub fetch {
-    my ($self, $name, $prefix) = @_;
-    my $file;
-    if (File::Spec->file_name_is_absolute($name)) {
-        $file = $name;
-    }
-    elsif ($name =~ m#^\./#) {
-        $file = File::Spec->rel2abs($name);
-    }
-    else {
-        $file = $self->{_BZ_SEARCH}{$name};
-    }
-
-    if (not $file) {
-        return ("cannot find file - $name ($file)", STATUS_ERROR);
-    }
-
-    if ($self->{_BZ_CACHE}{$file}) {
-        return ($self->{_BZ_CACHE}{$file}, undef);
-    }
-    else {
-        return ("unknown file - $file", STATUS_ERROR);
-    }
+  my ($self, $name, $prefix) = @_;
+  my $file;
+  if (File::Spec->file_name_is_absolute($name)) {
+    $file = $name;
+  }
+  elsif ($name =~ m#^\./#) {
+    $file = File::Spec->rel2abs($name);
+  }
+  else {
+    $file = $self->{_BZ_SEARCH}{$name};
+  }
+
+  if (not $file) {
+    return ("cannot find file - $name ($file)", STATUS_ERROR);
+  }
+
+  if ($self->{_BZ_CACHE}{$file}) {
+    return ($self->{_BZ_CACHE}{$file}, undef);
+  }
+  else {
+    return ("unknown file - $file", STATUS_ERROR);
+  }
 }
 
 sub _bz_compile {
-    my ($self, $data) = @_;
+  my ($self, $data) = @_;
 
-    my $parser = $self->{PARSER} ||= Template::Config->parser( $self->{PARAMS} )
-        || return ( Template::Config->error(), STATUS_ERROR );
+  my $parser = $self->{PARSER} ||= Template::Config->parser($self->{PARAMS})
+    || return (Template::Config->error(), STATUS_ERROR);
 
-    # discard the template text - we don't need it any more
-    my $text = delete $data->{text};
+  # discard the template text - we don't need it any more
+  my $text = delete $data->{text};
 
-    # call parser to compile template into Perl code
-    if (my $parsedoc = $parser->parse($text, $data)) {
-        $parsedoc->{METADATA} = {
-            'name' => $data->{name},
-            'modtime' => $data->{time},
-            %{ $parsedoc->{METADATA} },
-        };
+  # call parser to compile template into Perl code
+  if (my $parsedoc = $parser->parse($text, $data)) {
+    $parsedoc->{METADATA} = {
+      'name'    => $data->{name},
+      'modtime' => $data->{time},
+      %{$parsedoc->{METADATA}},
+    };
 
-        return Template::Document->new($parsedoc);
-    }
+    return Template::Document->new($parsedoc);
+  }
 }
 
 1;
diff --git a/Bugzilla/Test/MockDB.pm b/Bugzilla/Test/MockDB.pm
index fb7873ccf..db55b5d1e 100644
--- a/Bugzilla/Test/MockDB.pm
+++ b/Bugzilla/Test/MockDB.pm
@@ -11,110 +11,117 @@ use warnings;
 use Try::Tiny;
 use Capture::Tiny qw(capture_merged);
 
-use Bugzilla::Test::MockLocalconfig (
-    db_driver => 'sqlite',
-    db_name => ':memory:',
-);
+use Bugzilla::Test::MockLocalconfig (db_driver => 'sqlite',
+  db_name => ':memory:',);
 use Bugzilla;
-BEGIN { Bugzilla->extensions };
-use Bugzilla::Test::MockParams (
-    emailsuffix => '',
-    emailregexp => '.+',
-);
+BEGIN { Bugzilla->extensions }
+use Bugzilla::Test::MockParams (emailsuffix => '', emailregexp => '.+',);
 
 sub import {
-    require Bugzilla::Install;
-    require Bugzilla::Install::DB;
-    require Bugzilla::Field;;
+  require Bugzilla::Install;
+  require Bugzilla::Install::DB;
+  require Bugzilla::Field;
 
-    state $first_time = 0;
+  state $first_time = 0;
 
-    return undef if $first_time++;
+  return undef if $first_time++;
 
-    return capture_merged {
-        Bugzilla->dbh->bz_setup_database();
+  return capture_merged {
+    Bugzilla->dbh->bz_setup_database();
 
-        # Populate the tables that hold the values for the  fields.
+    Bugzilla->dbh->bz_populate_enum_tables();
 
-        Bugzilla::Install::DB::update_fielddefs_definition();
-        Bugzilla::Field::populate_field_definitions();
-        Bugzilla::Install::init_workflow();
-        Bugzilla::Install::DB->update_table_definitions({});
-        Bugzilla::Install::update_system_groups();
+    Bugzilla::Install::DB::update_fielddefs_definition();
+    Bugzilla::Field::populate_field_definitions();
+    Bugzilla::Install::init_workflow();
+    Bugzilla::Install::DB->update_table_definitions({});
+    Bugzilla::Install::update_system_groups();
 
-        Bugzilla->set_user(Bugzilla::User->super_user);
+    Bugzilla->set_user(Bugzilla::User->super_user);
 
-        Bugzilla::Install::update_settings();
+    Bugzilla::Install::update_settings();
 
-        my $dbh = Bugzilla->dbh;
-        if ( !$dbh->selectrow_array("SELECT 1 FROM priority WHERE value = 'P1'") ) {
-            $dbh->do("DELETE FROM priority");
-            my $count = 100;
-            foreach my $priority (map { "P$_" } 1..5) {
-                $dbh->do( "INSERT INTO priority (value, sortkey) VALUES (?, ?)", undef, ( $priority, $count + 100 ) );
-            }
-        }
-        my @flagtypes = (
-            {
-                name             => 'review',
-                desc             => 'The patch has passed review by a module owner or peer.',
-                is_requestable   => 1,
-                is_requesteeble  => 1,
-                is_multiplicable => 1,
-                grant_group      => '',
-                target_type      => 'a',
-                cc_list          => '',
-                inclusions       => ['']
-            },
-            {
-                name             => 'feedback',
-                desc             => 'A particular person\'s input is requested for a patch, ' .
-                                    'but that input does not amount to an official review.',
-                is_requestable   => 1,
-                is_requesteeble  => 1,
-                is_multiplicable => 1,
-                grant_group      => '',
-                target_type      => 'a',
-                cc_list          => '',
-                inclusions       => ['']
-            }
-        );
+    my $dbh = Bugzilla->dbh;
+    if (!$dbh->selectrow_array("SELECT 1 FROM priority WHERE value = 'P1'")) {
+      $dbh->do("DELETE FROM priority");
+      my $count = 100;
+      foreach my $priority (map {"P$_"} 1 .. 5) {
+        $dbh->do("INSERT INTO priority (value, sortkey) VALUES (?, ?)",
+          undef, ($priority, $count + 100));
+      }
+    }
+    my @flagtypes = (
+      {
+        name             => 'review',
+        desc             => 'The patch has passed review by a module owner or peer.',
+        is_requestable   => 1,
+        is_requesteeble  => 1,
+        is_multiplicable => 1,
+        grant_group      => '',
+        target_type      => 'a',
+        cc_list          => '',
+        inclusions       => ['']
+      },
+      {
+        name => 'feedback',
+        desc => 'A particular person\'s input is requested for a patch, '
+          . 'but that input does not amount to an official review.',
+        is_requestable   => 1,
+        is_requesteeble  => 1,
+        is_multiplicable => 1,
+        grant_group      => '',
+        target_type      => 'a',
+        cc_list          => '',
+        inclusions       => ['']
+      }
+    );
 
-        foreach my $flag (@flagtypes) {
-            next if Bugzilla::FlagType->new({ name => $flag->{name} });
-            my $grant_group_id = $flag->{grant_group}
-                                ? Bugzilla::Group->new({ name => $flag->{grant_group} })->id
-                                : undef;
-            my $request_group_id = $flag->{request_group}
-                                ? Bugzilla::Group->new({ name => $flag->{request_group} })->id
-                                : undef;
+    foreach my $flag (@flagtypes) {
+      next if Bugzilla::FlagType->new({name => $flag->{name}});
+      my $grant_group_id
+        = $flag->{grant_group}
+        ? Bugzilla::Group->new({name => $flag->{grant_group}})->id
+        : undef;
+      my $request_group_id
+        = $flag->{request_group}
+        ? Bugzilla::Group->new({name => $flag->{request_group}})->id
+        : undef;
 
-            $dbh->do('INSERT INTO flagtypes (name, description, cc_list, target_type, is_requestable,
+      $dbh->do(
+        'INSERT INTO flagtypes (name, description, cc_list, target_type, is_requestable,
                                             is_requesteeble, is_multiplicable, grant_group_id, request_group_id)
                                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
-                    undef, ($flag->{name}, $flag->{desc}, $flag->{cc_list}, $flag->{target_type},
-                            $flag->{is_requestable}, $flag->{is_requesteeble}, $flag->{is_multiplicable},
-                            $grant_group_id, $request_group_id));
+        undef,
+        (
+          $flag->{name},             $flag->{desc},
+          $flag->{cc_list},          $flag->{target_type},
+          $flag->{is_requestable},   $flag->{is_requesteeble},
+          $flag->{is_multiplicable}, $grant_group_id,
+          $request_group_id
+        )
+      );
 
-            my $type_id = $dbh->bz_last_key('flagtypes', 'id');
+      my $type_id = $dbh->bz_last_key('flagtypes', 'id');
 
-            foreach my $inclusion (@{$flag->{inclusions}}) {
-                my ($product, $component) = split(':', $inclusion);
-                my ($prod_id, $comp_id);
-                if ($product) {
-                    my $prod_obj = Bugzilla::Product->new({ name => $product });
-                    $prod_id = $prod_obj->id;
-                    if ($component) {
-                        $comp_id = Bugzilla::Component->new({ name => $component, product => $prod_obj})->id;
-                    }
-                }
-                $dbh->do('INSERT INTO flaginclusions (type_id, product_id, component_id)
-                        VALUES (?, ?, ?)',
-                        undef, ($type_id, $prod_id, $comp_id));
-            }
+      foreach my $inclusion (@{$flag->{inclusions}}) {
+        my ($product, $component) = split(':', $inclusion);
+        my ($prod_id, $comp_id);
+        if ($product) {
+          my $prod_obj = Bugzilla::Product->new({name => $product});
+          $prod_id = $prod_obj->id;
+          if ($component) {
+            $comp_id
+              = Bugzilla::Component->new({name => $component, product => $prod_obj})->id;
+          }
         }
-    };
+        $dbh->do(
+          'INSERT INTO flaginclusions (type_id, product_id, component_id)
+                        VALUES (?, ?, ?)', undef, ($type_id, $prod_id, $comp_id)
+        );
+      }
+    }
+  };
 }
 
 1;
diff --git a/Bugzilla/Test/MockLocalconfig.pm b/Bugzilla/Test/MockLocalconfig.pm
index a32aea0d4..080fdeef8 100644
--- a/Bugzilla/Test/MockLocalconfig.pm
+++ b/Bugzilla/Test/MockLocalconfig.pm
@@ -10,9 +10,9 @@ use strict;
 use warnings;
 
 sub import {
-    my ($class, %lc) = @_;
-    $ENV{LOCALCONFIG_ENV} = 'BMO';
-    $ENV{"BMO_$_"} = $lc{$_} for keys %lc;
+  my ($class, %lc) = @_;
+  $ENV{LOCALCONFIG_ENV} = 'BMO';
+  $ENV{"BMO_$_"} = $lc{$_} for keys %lc;
 }
 
 1;
diff --git a/Bugzilla/Test/MockParams.pm b/Bugzilla/Test/MockParams.pm
index 2d064c616..8738f78d4 100644
--- a/Bugzilla/Test/MockParams.pm
+++ b/Bugzilla/Test/MockParams.pm
@@ -16,56 +16,48 @@ use Bugzilla::Config;
 use Safe;
 
 our $Params;
+
 BEGIN {
-    our $Mock = mock 'Bugzilla::Config' => (
-        override => [
-            'read_param_file' => sub {
-                my ($class) = @_;
-                return {} unless $Params;
-                my $s = Safe->new;
-                $s->reval($Params);
-                die "Error evaluating params: $@" if $@;
-                return { %{ $s->varglob('param') } };
-            },
-            '_write_file' => sub {
-                my ($class, $str) = @_;
-                $Params = $str;
-            },
-        ],
-    );
+  our $Mock = mock 'Bugzilla::Config' => (
+    override => [
+      'read_param_file' => sub {
+        my ($class) = @_;
+        return {} unless $Params;
+        my $s = Safe->new;
+        $s->reval($Params);
+        die "Error evaluating params: $@" if $@;
+        return {%{$s->varglob('param')}};
+      },
+      '_write_file' => sub {
+        my ($class, $str) = @_;
+        $Params = $str;
+      },
+    ],
+  );
 }
 
 sub import {
-    my ($self, %answers) = @_;
-    state $first_time = 0;
+  my ($self, %answers) = @_;
+  state $first_time = 0;
 
-    require Bugzilla::Field;
-    require Bugzilla::Status;
-    require Bugzilla;
-    my $Bugzilla = mock 'Bugzilla' => (
-        override => [
-            installation_answers => sub { \%answers },
-        ],
-    );
-    my $BugzillaField = mock 'Bugzilla::Field' => (
-        override => [
-            get_legal_field_values => sub { [] },
-        ],
-    );
-    my $BugzillaStatus = mock 'Bugzilla::Status' => (
-        override => [
-            closed_bug_statuses => sub { die "no database" },
-        ],
-    );
+  require Bugzilla::Field;
+  require Bugzilla::Status;
+  require Bugzilla;
+  my $Bugzilla = mock 'Bugzilla' =>
+    (override => [installation_answers => sub { \%answers },],);
+  my $BugzillaField = mock 'Bugzilla::Field' =>
+    (override => [get_legal_field_values => sub { [] },],);
+  my $BugzillaStatus = mock 'Bugzilla::Status' =>
+    (override => [closed_bug_statuses => sub { die "no database" },],);
 
-    if ($first_time++) {
-        capture_merged {
-            Bugzilla::Config::update_params();
-        };
-    }
-    else {
-        Bugzilla::Config::SetParam($_, $answers{$_}) for keys %answers;
-    }
+  if ($first_time++) {
+    capture_merged {
+      Bugzilla::Config::update_params();
+    };
+  }
+  else {
+    Bugzilla::Config::SetParam($_, $answers{$_}) for keys %answers;
+  }
 }
 
-1;
\ No newline at end of file
+1;
diff --git a/Bugzilla/Test/Util.pm b/Bugzilla/Test/Util.pm
index 9fbc151f7..995cff0be 100644
--- a/Bugzilla/Test/Util.pm
+++ b/Bugzilla/Test/Util.pm
@@ -20,50 +20,51 @@ use Mojo::Message::Response;
 use Test2::Tools::Mock qw(mock);
 
 sub create_user {
-    my ($login, $password, %extra) = @_;
-    require Bugzilla;
-    return Bugzilla::User->create({
-        login_name    => $login,
-        cryptpassword => $password,
-        disabledtext  => "",
-        disable_mail  => 0,
-        extern_id     => undef,
-        %extra,
-    });
+  my ($login, $password, %extra) = @_;
+  require Bugzilla;
+  return Bugzilla::User->create({
+    login_name    => $login,
+    cryptpassword => $password,
+    disabledtext  => "",
+    disable_mail  => 0,
+    extern_id     => undef,
+    %extra,
+  });
 }
 
 sub issue_api_key {
-    my ($login, $given_api_key) = @_;
-    my $user = Bugzilla::User->check({ name => $login });
+  my ($login, $given_api_key) = @_;
+  my $user = Bugzilla::User->check({name => $login});
 
-    my $params = {
-        user_id     => $user->id,
-        description => 'Bugzilla::Test::Util::issue_api_key',
-        api_key     => $given_api_key,
-    };
+  my $params = {
+    user_id     => $user->id,
+    description => 'Bugzilla::Test::Util::issue_api_key',
+    api_key     => $given_api_key,
+  };
 
-    if ($given_api_key) {
-        return Bugzilla::User::APIKey->create_special($params);
-    } else {
-        return Bugzilla::User::APIKey->create($params);
-    }
+  if ($given_api_key) {
+    return Bugzilla::User::APIKey->create_special($params);
+  }
+  else {
+    return Bugzilla::User::APIKey->create($params);
+  }
 }
 
 sub _json_content_type { $_->headers->content_type('application/json') }
 
 sub mock_useragent_tx {
-    my ($body, $modify) = @_;
-    $modify //= \&_json_content_type;
+  my ($body, $modify) = @_;
+  $modify //= \&_json_content_type;
 
-    my $res = Mojo::Message::Response->new;
-    $res->code(200);
-    $res->body($body);
-    if ($modify) {
-        local $_ = $res;
-        $modify->($res);
-    }
+  my $res = Mojo::Message::Response->new;
+  $res->code(200);
+  $res->body($body);
+  if ($modify) {
+    local $_ = $res;
+    $modify->($res);
+  }
 
-    return mock({result => $res});
+  return mock({result => $res});
 }
 
 1;
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index 8e51db45d..3398e236a 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -27,12 +27,12 @@ use JSON qw(encode_json decode_json);
 use base qw(Exporter);
 
 @Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token
-                              issue_short_lived_session_token
-                              issue_auth_delegation_token check_auth_delegation_token
-                              check_token_data delete_token
-                              issue_hash_token check_hash_token
-                              issue_hash_sig   check_hash_sig
-                              set_token_extra_data get_token_extra_data);
+  issue_short_lived_session_token
+  issue_auth_delegation_token check_auth_delegation_token
+  check_token_data delete_token
+  issue_hash_token check_hash_token
+  issue_hash_sig   check_hash_sig
+  set_token_extra_data get_token_extra_data);
 
 # 128 bits password:
 # 128 * log10(2) / log10(62) = 21.49, round up to 22.
@@ -45,407 +45,439 @@ use constant TOKEN_LENGTH => 22;
 
 # Create a token used for internal API authentication
 sub issue_api_token {
-    # Generates a random token, adds it to the tokens table if one does not
-    # already exist, and returns the token to the caller.
-    my $dbh  = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-    my ($token) = $dbh->selectrow_array("
+
+  # Generates a random token, adds it to the tokens table if one does not
+  # already exist, and returns the token to the caller.
+  my $dbh     = Bugzilla->dbh;
+  my $user    = Bugzilla->user;
+  my ($token) = $dbh->selectrow_array("
         SELECT token FROM tokens
          WHERE userid = ? AND tokentype = 'api_token'
-               AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()",
-        undef, $user->id);
-    return $token // _create_token($user->id, 'api_token', '');
+               AND ("
+      . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR')
+      . ") > NOW()", undef, $user->id);
+  return $token // _create_token($user->id, 'api_token', '');
 }
 
 sub issue_auth_delegation_token {
-    my ($uri) = @_;
-    my $dbh  = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-    my $checksum = hmac_sha256_base64($user->id, $uri, Bugzilla->localconfig->{'site_wide_secret'});
+  my ($uri)    = @_;
+  my $dbh      = Bugzilla->dbh;
+  my $user     = Bugzilla->user;
+  my $checksum = hmac_sha256_base64($user->id, $uri,
+    Bugzilla->localconfig->{'site_wide_secret'});
 
-    return _create_token($user->id, 'auth_delegation', $checksum);
+  return _create_token($user->id, 'auth_delegation', $checksum);
 }
 
 sub check_auth_delegation_token {
-    my ($token, $uri) = @_;
-    my $dbh  = Bugzilla->dbh;
-    my $user = Bugzilla->user;
+  my ($token, $uri) = @_;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
 
-    my ($eventdata) = $dbh->selectrow_array("
+  my ($eventdata) = $dbh->selectrow_array("
         SELECT eventdata FROM tokens
          WHERE token = ? AND tokentype = 'auth_delegation'
-               AND (" . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR') . ") > NOW()",
-        undef, $token);
-
-    if ($eventdata) {
-        my $checksum = hmac_sha256_base64($user->id, $uri, Bugzilla->localconfig->{'site_wide_secret'});
-        if ($eventdata eq $checksum) {
-            delete_token($token);
-            return 1;
-        }
+               AND ("
+      . $dbh->sql_date_math('issuedate', '+', (MAX_TOKEN_AGE * 24 - 12), 'HOUR')
+      . ") > NOW()", undef, $token);
+
+  if ($eventdata) {
+    my $checksum = hmac_sha256_base64($user->id, $uri,
+      Bugzilla->localconfig->{'site_wide_secret'});
+    if ($eventdata eq $checksum) {
+      delete_token($token);
+      return 1;
     }
+  }
 
-    return 0;
+  return 0;
 }
 
 # Creates and sends a token to create a new user account.
 # It assumes that the login has the correct format and is not already in use.
 sub issue_new_user_account_token {
-    my $login_name = shift;
-    my $dbh = Bugzilla->dbh;
-    my $template = Bugzilla->template;
-    my $vars = {};
-
-    # Is there already a pending request for this login name? If yes, do not throw
-    # an error because the user may have lost his email with the token inside.
-    # But to prevent using this way to mailbomb an email address, make sure
-    # the last request is at least 10 minutes old before sending a new email.
-
-    my $pending_requests = $dbh->selectrow_array(
-        'SELECT COUNT(*)
+  my $login_name = shift;
+  my $dbh        = Bugzilla->dbh;
+  my $template   = Bugzilla->template;
+  my $vars       = {};
+
+  # Is there already a pending request for this login name? If yes, do not throw
+  # an error because the user may have lost his email with the token inside.
+  # But to prevent using this way to mailbomb an email address, make sure
+  # the last request is at least 10 minutes old before sending a new email.
+
+  my $pending_requests = $dbh->selectrow_array(
+    'SELECT COUNT(*)
            FROM tokens
           WHERE tokentype = ?
                 AND ' . $dbh->sql_istrcmp('eventdata', '?') . '
                 AND issuedate > '
-                    . $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'),
-        undef, ('account', $login_name));
+      . $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'), undef,
+    ('account', $login_name)
+  );
 
-    ThrowUserError('too_soon_for_new_token', {'type' => 'account'}) if $pending_requests;
+  ThrowUserError('too_soon_for_new_token', {'type' => 'account'})
+    if $pending_requests;
 
-    my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
+  my ($token, $token_ts) = _create_token(undef, 'account', $login_name);
 
-    $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'};
-    $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
-    $vars->{'token'} = $token;
+  $vars->{'email'}         = $login_name . Bugzilla->params->{'emailsuffix'};
+  $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+  $vars->{'token'}         = $token;
 
-    my $message;
-    $template->process('account/email/request-new.txt.tmpl', $vars, \$message)
-      || ThrowTemplateError($template->error());
+  my $message;
+  $template->process('account/email/request-new.txt.tmpl', $vars, \$message)
+    || ThrowTemplateError($template->error());
 
-    # In 99% of cases, the user getting the confirmation email is the same one
-    # who made the request, and so it is reasonable to send the email in the same
-    # language used to view the "Create a New Account" page (we cannot use his
-    # user prefs as the user has no account yet!).
-    MessageToMTA($message);
+  # In 99% of cases, the user getting the confirmation email is the same one
+  # who made the request, and so it is reasonable to send the email in the same
+  # language used to view the "Create a New Account" page (we cannot use his
+  # user prefs as the user has no account yet!).
+  MessageToMTA($message);
 }
 
 sub IssueEmailChangeToken {
-    my ($user, $new_email) = @_;
-    my $email_suffix = Bugzilla->params->{'emailsuffix'};
-    my $old_email = $user->login;
+  my ($user, $new_email) = @_;
+  my $email_suffix = Bugzilla->params->{'emailsuffix'};
+  my $old_email    = $user->login;
 
-    my ($token, $token_ts) = _create_token($user->id, 'emailold', $old_email . ":" . $new_email);
+  my ($token, $token_ts)
+    = _create_token($user->id, 'emailold', $old_email . ":" . $new_email);
 
-    my $newtoken = _create_token($user->id, 'emailnew', $old_email . ":" . $new_email);
+  my $newtoken
+    = _create_token($user->id, 'emailnew', $old_email . ":" . $new_email);
 
-    # Mail the user the token along with instructions for using it.
+  # Mail the user the token along with instructions for using it.
 
-    my $template = Bugzilla->template_inner($user->setting('lang'));
-    my $vars = {};
+  my $template = Bugzilla->template_inner($user->setting('lang'));
+  my $vars     = {};
 
-    $vars->{'oldemailaddress'} = $old_email . $email_suffix;
-    $vars->{'newemailaddress'} = $new_email . $email_suffix;
-    $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
-    $vars->{'token'} = $token;
-    # For SecureMail extension
-    $vars->{'to_user'} = $user;
-    $vars->{'emailaddress'} = $old_email . $email_suffix;
+  $vars->{'oldemailaddress'} = $old_email . $email_suffix;
+  $vars->{'newemailaddress'} = $new_email . $email_suffix;
+  $vars->{'expiration_ts'}   = ctime($token_ts + MAX_TOKEN_AGE * 86400);
+  $vars->{'token'}           = $token;
 
-    my $message;
-    $template->process("account/email/change-old.txt.tmpl", $vars, \$message)
-      || ThrowTemplateError($template->error());
+  # For SecureMail extension
+  $vars->{'to_user'}      = $user;
+  $vars->{'emailaddress'} = $old_email . $email_suffix;
 
-    MessageToMTA($message);
+  my $message;
+  $template->process("account/email/change-old.txt.tmpl", $vars, \$message)
+    || ThrowTemplateError($template->error());
 
-    $vars->{'token'} = $newtoken;
-    $vars->{'emailaddress'} = $new_email . $email_suffix;
+  MessageToMTA($message);
 
-    $message = "";
-    $template->process("account/email/change-new.txt.tmpl", $vars, \$message)
-      || ThrowTemplateError($template->error());
+  $vars->{'token'}        = $newtoken;
+  $vars->{'emailaddress'} = $new_email . $email_suffix;
 
-    MessageToMTA($message);
+  $message = "";
+  $template->process("account/email/change-new.txt.tmpl", $vars, \$message)
+    || ThrowTemplateError($template->error());
+
+  MessageToMTA($message);
 }
 
 # Generates a random token, adds it to the tokens table, and sends it
 # to the user with instructions for using it to change their password.
 sub IssuePasswordToken {
-    my $user = shift;
-    my $dbh = Bugzilla->dbh;
+  my $user = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    my $too_soon = $dbh->selectrow_array(
-        'SELECT 1 FROM tokens
+  my $too_soon = $dbh->selectrow_array(
+    'SELECT 1 FROM tokens
           WHERE userid = ? AND tokentype = ?
                 AND issuedate > '
-                    . $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'),
-        undef, ($user->id, 'password'));
+      . $dbh->sql_date_math('NOW()', '-', 10, 'MINUTE'), undef,
+    ($user->id, 'password')
+  );
 
-    ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon;
+  ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon;
 
-    my ($token, $token_ts) = _create_token($user->id, 'password', remote_ip());
+  my ($token, $token_ts) = _create_token($user->id, 'password', remote_ip());
 
-    # Mail the user the token along with instructions for using it.
-    my $template = Bugzilla->template_inner($user->setting('lang'));
-    my $vars = {};
+  # Mail the user the token along with instructions for using it.
+  my $template = Bugzilla->template_inner($user->setting('lang'));
+  my $vars     = {};
 
-    $vars->{'token'} = $token;
-    $vars->{'emailaddress'} = $user->email;
-    $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
-    # The user is not logged in (else he wouldn't request a new password).
-    # So we have to pass this information to the template.
-    $vars->{'timezone'} = $user->timezone;
+  $vars->{'token'}         = $token;
+  $vars->{'emailaddress'}  = $user->email;
+  $vars->{'expiration_ts'} = ctime($token_ts + MAX_TOKEN_AGE * 86400);
 
-    my $message = "";
-    $template->process("account/password/forgotten-password.txt.tmpl",
-                                                               $vars, \$message)
-      || ThrowTemplateError($template->error());
+  # The user is not logged in (else he wouldn't request a new password).
+  # So we have to pass this information to the template.
+  $vars->{'timezone'} = $user->timezone;
 
-    MessageToMTA($message);
+  my $message = "";
+  $template->process("account/password/forgotten-password.txt.tmpl",
+    $vars, \$message)
+    || ThrowTemplateError($template->error());
+
+  MessageToMTA($message);
 }
 
 sub issue_session_token {
-    my ($data, $user) = @_;
-    # Generates a random token, adds it to the tokens table, and returns
-    # the token to the caller.
+  my ($data, $user) = @_;
+
+  # Generates a random token, adds it to the tokens table, and returns
+  # the token to the caller.
 
-    $user //= Bugzilla->user;
-    return _create_token($user->id, 'session', $data);
+  $user //= Bugzilla->user;
+  return _create_token($user->id, 'session', $data);
 }
 
 sub issue_short_lived_session_token {
-    my ($data, $user) = @_;
-    # Generates a random token, adds it to the tokens table, and returns
-    # the token to the caller.
+  my ($data, $user) = @_;
+
+  # Generates a random token, adds it to the tokens table, and returns
+  # the token to the caller.
 
-    $user //= Bugzilla->user;
-    return _create_token($user->id ? $user->id : undef, 'session.short', $data);
+  $user //= Bugzilla->user;
+  return _create_token($user->id ? $user->id : undef, 'session.short', $data);
 }
 
 sub issue_hash_sig {
-    my ($type, $data, $salt) = @_;
-    $data //= "";
-    $salt //= generate_random_password(16);
-
-    my $hmac = hmac_sha256_base64(
-        $salt,
-        $type,
-        $data,
-        Bugzilla->localconfig->{site_wide_secret}
-    );
-    return sprintf("%s|%s|%x", $salt, $hmac, length($data));
+  my ($type, $data, $salt) = @_;
+  $data //= "";
+  $salt //= generate_random_password(16);
+
+  my $hmac = hmac_sha256_base64($salt, $type, $data,
+    Bugzilla->localconfig->{site_wide_secret});
+  return sprintf("%s|%s|%x", $salt, $hmac, length($data));
 }
 
 sub check_hash_sig {
-    my ($type, $sig, $data) = @_;
-    return 0 unless defined $sig && defined $data;
-    my ($salt, undef, $len) = split(/\|/, $sig, 3);
-    return length($data) == hex($len) && $sig eq issue_hash_sig($type, $data, $salt);
+  my ($type, $sig, $data) = @_;
+  return 0 unless defined $sig && defined $data;
+  my ($salt, undef, $len) = split(/\|/, $sig, 3);
+  return
+    length($data) == hex($len) && $sig eq issue_hash_sig($type, $data, $salt);
 }
 
 sub issue_hash_token {
-    my ($data, $time) = @_;
-    $data ||= [];
-    $time ||= time();
-
-    # For the user ID, use the actual ID if the user is logged in.
-    # Otherwise, use the remote IP, in case this is for something
-    # such as creating an account or logging in.
-    my $user_id = Bugzilla->user->id || remote_ip();
-
-    # The concatenated string is of the form
-    # token creation time + user ID (either ID or remote IP) + data
-    my @args = ($time, $user_id, @$data);
-
-    my $token = join('*', @args);
-    # $token needs to be a byte string.
-    utf8::encode($token);
-    $token = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'});
-    $token =~ s/\+/-/g;
-    $token =~ s/\//_/g;
-
-    # Prepend the token creation time, unencrypted, so that the token
-    # lifetime can be validated.
-    return $time . '-' . $token;
+  my ($data, $time) = @_;
+  $data ||= [];
+  $time ||= time();
+
+  # For the user ID, use the actual ID if the user is logged in.
+  # Otherwise, use the remote IP, in case this is for something
+  # such as creating an account or logging in.
+  my $user_id = Bugzilla->user->id || remote_ip();
+
+  # The concatenated string is of the form
+  # token creation time + user ID (either ID or remote IP) + data
+  my @args = ($time, $user_id, @$data);
+
+  my $token = join('*', @args);
+
+  # $token needs to be a byte string.
+  utf8::encode($token);
+  $token
+    = hmac_sha256_base64($token, Bugzilla->localconfig->{'site_wide_secret'});
+  $token =~ s/\+/-/g;
+  $token =~ s/\//_/g;
+
+  # Prepend the token creation time, unencrypted, so that the token
+  # lifetime can be validated.
+  return $time . '-' . $token;
 }
 
 sub check_hash_token {
-    my ($token, $data) = @_;
-    $data ||= [];
-    my ($time, $expected_token);
-
-    if ($token) {
-        ($time, undef) = split(/-/, $token);
-        # Regenerate the token based on the information we have.
-        $expected_token = issue_hash_token($data, $time);
-    }
+  my ($token, $data) = @_;
+  $data ||= [];
+  my ($time, $expected_token);
 
-    if (!$token
-        || $expected_token ne $token
-        || time() - $time > MAX_TOKEN_AGE * 86400)
-    {
-        my $template = Bugzilla->template;
-        my $vars = {};
-        $vars->{'script_name'} = basename($0);
-        $vars->{'token'} = issue_hash_token($data);
-        $vars->{'reason'} = (!$token) ?                   'missing_token' :
-                            ($expected_token ne $token) ? 'invalid_token' :
-                                                          'expired_token';
-        print Bugzilla->cgi->header();
-        $template->process('global/confirm-action.html.tmpl', $vars)
-          || ThrowTemplateError($template->error());
-        exit;
-    }
+  if ($token) {
+    ($time, undef) = split(/-/, $token);
+
+    # Regenerate the token based on the information we have.
+    $expected_token = issue_hash_token($data, $time);
+  }
+
+  if (!$token
+    || $expected_token ne $token
+    || time() - $time > MAX_TOKEN_AGE * 86400)
+  {
+    my $template = Bugzilla->template;
+    my $vars     = {};
+    $vars->{'script_name'} = basename($0);
+    $vars->{'token'}       = issue_hash_token($data);
+    $vars->{'reason'}
+      = (!$token) ? 'missing_token'
+      : ($expected_token ne $token) ? 'invalid_token'
+      :                               'expired_token';
+    print Bugzilla->cgi->header();
+    $template->process('global/confirm-action.html.tmpl', $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
 
-    # If we come here, then the token is valid and not too old.
-    return 1;
+  # If we come here, then the token is valid and not too old.
+  return 1;
 }
 
 sub CleanTokenTable {
-    my $dbh = Bugzilla->dbh;
-    $dbh->do("DELETE FROM tokens WHERE " .
-             $dbh->sql_date_math('issuedate', '+', '?', 'HOUR') . " <= NOW()",
-             undef, MAX_TOKEN_AGE * 24);
-    $dbh->do("DELETE FROM tokens WHERE tokentype = ? AND " .
-             $dbh->sql_date_math('issuedate', '+', '?', 'HOUR') . " <= NOW()",
-             undef, 'session.short', MAX_SHORT_TOKEN_HOURS);
+  my $dbh = Bugzilla->dbh;
+  $dbh->do(
+    "DELETE FROM tokens WHERE "
+      . $dbh->sql_date_math('issuedate', '+', '?', 'HOUR')
+      . " <= NOW()",
+    undef,
+    MAX_TOKEN_AGE * 24
+  );
+  $dbh->do(
+    "DELETE FROM tokens WHERE tokentype = ? AND "
+      . $dbh->sql_date_math('issuedate', '+', '?', 'HOUR')
+      . " <= NOW()",
+    undef, 'session.short', MAX_SHORT_TOKEN_HOURS
+  );
 }
 
 sub GenerateUniqueToken {
-    # Generates a unique random token.  Uses generate_random_password
-    # for the tokens themselves and checks uniqueness by searching for
-    # the token in the "tokens" table.  Gives up if it can't come up
-    # with a token after about one hundred tries.
-    my ($table, $column) = @_;
-
-    my $token;
-    my $duplicate = 1;
-    my $tries = 0;
-    $table ||= "tokens";
-    $column ||= "token";
-
-    my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?");
-
-    while ($duplicate) {
-        ++$tries;
-        if ($tries > 100) {
-            ThrowCodeError("token_generation_error");
-        }
-        $token = generate_random_password(TOKEN_LENGTH);
-        $sth->execute($token);
-        $duplicate = $sth->fetchrow_array;
+
+  # Generates a unique random token.  Uses generate_random_password
+  # for the tokens themselves and checks uniqueness by searching for
+  # the token in the "tokens" table.  Gives up if it can't come up
+  # with a token after about one hundred tries.
+  my ($table, $column) = @_;
+
+  my $token;
+  my $duplicate = 1;
+  my $tries     = 0;
+  $table  ||= "tokens";
+  $column ||= "token";
+
+  my $dbh = Bugzilla->dbh;
+  my $sth = $dbh->prepare("SELECT 1 FROM $table WHERE $column = ?");
+
+  while ($duplicate) {
+    ++$tries;
+    if ($tries > 100) {
+      ThrowCodeError("token_generation_error");
     }
-    return $token;
+    $token = generate_random_password(TOKEN_LENGTH);
+    $sth->execute($token);
+    $duplicate = $sth->fetchrow_array;
+  }
+  return $token;
 }
 
 # Cancels a previously issued token and notifies the user.
 # This should only happen when the user accidentally makes a token request
 # or when a malicious hacker makes a token request on behalf of a user.
 sub Cancel {
-    my ($token, $cancelaction, $vars) = @_;
-    my $dbh = Bugzilla->dbh;
-    $vars ||= {};
-
-    # Get information about the token being canceled.
-    trick_taint($token);
-    my ($db_token, $issuedate, $tokentype, $eventdata, $userid) =
-        $dbh->selectrow_array('SELECT token, ' . $dbh->sql_date_format('issuedate') . ',
+  my ($token, $cancelaction, $vars) = @_;
+  my $dbh = Bugzilla->dbh;
+  $vars ||= {};
+
+  # Get information about the token being canceled.
+  trick_taint($token);
+  my ($db_token, $issuedate, $tokentype, $eventdata, $userid)
+    = $dbh->selectrow_array(
+    'SELECT token, '
+      . $dbh->sql_date_format('issuedate') . ',
                                       tokentype, eventdata, userid
                                  FROM tokens
-                                WHERE token = ?',
-                                undef, $token);
-
-    # Some DBs such as MySQL are case-insensitive by default so we do
-    # a quick comparison to make sure the tokens are indeed the same.
-    (defined $db_token && $db_token eq $token)
-        || ThrowCodeError("cancel_token_does_not_exist");
-
-    # If we are canceling the creation of a new user account, then there
-    # is no entry in the 'profiles' table.
-    my $user = new Bugzilla::User($userid);
-
-    $vars->{'emailaddress'} = $userid ? $user->email : $eventdata;
-    $vars->{'remoteaddress'} = remote_ip();
-    $vars->{'token'} = $token;
-    $vars->{'tokentype'} = $tokentype;
-    $vars->{'issuedate'} = $issuedate;
-    # The user is probably not logged in.
-    # So we have to pass this information to the template.
-    $vars->{'timezone'} = $user->timezone;
-    $vars->{'eventdata'} = $eventdata;
-    $vars->{'cancelaction'} = $cancelaction;
-
-    # Notify the user via email about the cancellation.
-    my $template = Bugzilla->template_inner($user->setting('lang'));
-
-    my $message;
-    $template->process("account/cancel-token.txt.tmpl", $vars, \$message)
-      || ThrowTemplateError($template->error());
+                                WHERE token = ?', undef, $token
+    );
 
-    MessageToMTA($message);
+  # Some DBs such as MySQL are case-insensitive by default so we do
+  # a quick comparison to make sure the tokens are indeed the same.
+  (defined $db_token && $db_token eq $token)
+    || ThrowCodeError("cancel_token_does_not_exist");
 
-    # Delete the token from the database.
-    delete_token($token);
+  # If we are canceling the creation of a new user account, then there
+  # is no entry in the 'profiles' table.
+  my $user = new Bugzilla::User($userid);
+
+  $vars->{'emailaddress'}  = $userid ? $user->email : $eventdata;
+  $vars->{'remoteaddress'} = remote_ip();
+  $vars->{'token'}         = $token;
+  $vars->{'tokentype'}     = $tokentype;
+  $vars->{'issuedate'}     = $issuedate;
+
+  # The user is probably not logged in.
+  # So we have to pass this information to the template.
+  $vars->{'timezone'}     = $user->timezone;
+  $vars->{'eventdata'}    = $eventdata;
+  $vars->{'cancelaction'} = $cancelaction;
+
+  # Notify the user via email about the cancellation.
+  my $template = Bugzilla->template_inner($user->setting('lang'));
+
+  my $message;
+  $template->process("account/cancel-token.txt.tmpl", $vars, \$message)
+    || ThrowTemplateError($template->error());
+
+  MessageToMTA($message);
+
+  # Delete the token from the database.
+  delete_token($token);
 }
 
 sub DeletePasswordTokens {
-    my ($userid, $reason) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($userid, $reason) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    detaint_natural($userid);
-    my $tokens = $dbh->selectcol_arrayref('SELECT token FROM tokens
+  detaint_natural($userid);
+  my $tokens = $dbh->selectcol_arrayref(
+    'SELECT token FROM tokens
                                            WHERE userid = ? AND tokentype = ?',
-                                           undef, ($userid, 'password'));
+    undef, ($userid, 'password')
+  );
 
-    foreach my $token (@$tokens) {
-        Bugzilla::Token::Cancel($token, $reason);
-    }
+  foreach my $token (@$tokens) {
+    Bugzilla::Token::Cancel($token, $reason);
+  }
 }
 
 # Returns an email change token if the user has one.
 sub HasEmailChangeToken {
-    my $userid = shift;
-    my $dbh = Bugzilla->dbh;
+  my $userid = shift;
+  my $dbh    = Bugzilla->dbh;
 
-    my $token = $dbh->selectrow_array('SELECT token FROM tokens
+  my $token = $dbh->selectrow_array(
+    'SELECT token FROM tokens
                                        WHERE userid = ?
-                                       AND (tokentype = ? OR tokentype = ?) ' .
-                                       $dbh->sql_limit(1),
-                                       undef, ($userid, 'emailnew', 'emailold'));
-    return $token;
+                                       AND (tokentype = ? OR tokentype = ?) '
+      . $dbh->sql_limit(1), undef, ($userid, 'emailnew', 'emailold')
+  );
+  return $token;
 }
 
 # Returns the userid, issuedate and eventdata for the specified token
 sub GetTokenData {
-    my ($token) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($token) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    return unless defined $token;
-    $token = clean_text($token);
-    trick_taint($token);
+  return unless defined $token;
+  $token = clean_text($token);
+  trick_taint($token);
 
-    my @token_data = $dbh->selectrow_array(
-        "SELECT token, userid, " . $dbh->sql_date_format('issuedate') . ", eventdata, tokentype
+  my @token_data = $dbh->selectrow_array(
+        "SELECT token, userid, "
+      . $dbh->sql_date_format('issuedate')
+      . ", eventdata, tokentype
          FROM   tokens
-         WHERE  token = ?", undef, $token);
+         WHERE  token = ?", undef, $token
+  );
 
-    # Some DBs such as MySQL are case-insensitive by default so we do
-    # a quick comparison to make sure the tokens are indeed the same.
-    my $db_token = shift @token_data;
-    return undef if (!defined $db_token || $db_token ne $token);
+  # Some DBs such as MySQL are case-insensitive by default so we do
+  # a quick comparison to make sure the tokens are indeed the same.
+  my $db_token = shift @token_data;
+  return undef if (!defined $db_token || $db_token ne $token);
 
-    return @token_data;
+  return @token_data;
 }
 
 # Deletes specified token
 sub delete_token {
-    my ($token) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($token) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    return unless defined $token;
-    trick_taint($token);
+  return unless defined $token;
+  trick_taint($token);
 
-    $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token);
+  $dbh->do("DELETE FROM tokens WHERE token = ?", undef, $token);
 }
 
 # Given a token, makes sure it comes from the currently logged in user
@@ -453,73 +485,73 @@ sub delete_token {
 # Note: this routine must not be called while tables are locked as it will try
 # to lock some tables itself, see CleanTokenTable().
 sub check_token_data {
-    my ($token, $expected_action, $alternate_script) = @_;
-    my $user = Bugzilla->user;
-    my $template = Bugzilla->template;
-    my $cgi = Bugzilla->cgi;
-
-    my ($creator_id, $date, $token_action) = GetTokenData($token);
-    unless ($creator_id
-            && $creator_id == $user->id
-            && $token_action eq $expected_action)
-    {
-        # Something is going wrong. Ask confirmation before processing.
-        # It is possible that someone tried to trick an administrator.
-        # In this case, we want to know his name!
-        require Bugzilla::User;
-
-        my $vars = {};
-        $vars->{'abuser'} = Bugzilla::User->new($creator_id)->identity;
-        $vars->{'token_action'} = $token_action;
-        $vars->{'expected_action'} = $expected_action;
-        $vars->{'script_name'} = basename($0);
-        $vars->{'alternate_script'} = $alternate_script || basename($0);
-
-        # Now is a good time to remove old tokens from the DB.
-        CleanTokenTable();
-
-        # If no token was found, create a valid token for the given action.
-        unless ($creator_id) {
-            $token = issue_session_token($expected_action);
-            $cgi->param('token', $token);
-        }
-
-        print $cgi->header();
-
-        $template->process('admin/confirm-action.html.tmpl', $vars)
-          || ThrowTemplateError($template->error());
-        exit;
+  my ($token, $expected_action, $alternate_script) = @_;
+  my $user     = Bugzilla->user;
+  my $template = Bugzilla->template;
+  my $cgi      = Bugzilla->cgi;
+
+  my ($creator_id, $date, $token_action) = GetTokenData($token);
+  unless ($creator_id
+    && $creator_id == $user->id
+    && $token_action eq $expected_action)
+  {
+    # Something is going wrong. Ask confirmation before processing.
+    # It is possible that someone tried to trick an administrator.
+    # In this case, we want to know his name!
+    require Bugzilla::User;
+
+    my $vars = {};
+    $vars->{'abuser'}           = Bugzilla::User->new($creator_id)->identity;
+    $vars->{'token_action'}     = $token_action;
+    $vars->{'expected_action'}  = $expected_action;
+    $vars->{'script_name'}      = basename($0);
+    $vars->{'alternate_script'} = $alternate_script || basename($0);
+
+    # Now is a good time to remove old tokens from the DB.
+    CleanTokenTable();
+
+    # If no token was found, create a valid token for the given action.
+    unless ($creator_id) {
+      $token = issue_session_token($expected_action);
+      $cgi->param('token', $token);
     }
-    return 1;
+
+    print $cgi->header();
+
+    $template->process('admin/confirm-action.html.tmpl', $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
+  return 1;
 }
 
 sub set_token_extra_data {
-    my ($token, $data) = @_;
+  my ($token, $data) = @_;
 
-    $data = encode_json($data) if ref($data);
+  $data = encode_json($data) if ref($data);
 
-    # extra_data is MEDIUMTEXT, max 16M
-    if (length($data) > 16_777_215) {
-        ThrowCodeError('token_data_too_big');
-    }
+  # extra_data is MEDIUMTEXT, max 16M
+  if (length($data) > 16_777_215) {
+    ThrowCodeError('token_data_too_big');
+  }
 
-    Bugzilla->dbh->do(
-        "INSERT INTO token_data (token, extra_data) VALUES (?, ?) ON DUPLICATE KEY UPDATE extra_data = ?",
-        undef, $token, $data, $data);
+  Bugzilla->dbh->do(
+    "INSERT INTO token_data (token, extra_data) VALUES (?, ?) ON DUPLICATE KEY UPDATE extra_data = ?",
+    undef, $token, $data, $data
+  );
 }
 
 sub get_token_extra_data {
-    my ($token) = @_;
-    trick_taint($token);
-    my ($data) = Bugzilla->dbh->selectrow_array(
-        "SELECT extra_data FROM token_data WHERE token = ?",
-        undef, $token);
-    return undef unless defined $data;
-    $data = encode('UTF-8', $data);
-    eval {
-        $data = decode_json($data);
-    };
-    return $data;
+  my ($token) = @_;
+  trick_taint($token);
+  my ($data)
+    = Bugzilla->dbh->selectrow_array(
+    "SELECT extra_data FROM token_data WHERE token = ?",
+    undef, $token);
+  return undef unless defined $data;
+  $data = encode('UTF-8', $data);
+  eval { $data = decode_json($data); };
+  return $data;
 }
 
 ################################################################################
@@ -529,34 +561,38 @@ sub get_token_extra_data {
 # Generates a unique token and inserts it into the database
 # Returns the token and the token timestamp
 sub _create_token {
-    my ($userid, $tokentype, $eventdata) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($userid, $tokentype, $eventdata) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    detaint_natural($userid) if defined $userid;
-    trick_taint($tokentype);
-    trick_taint($eventdata);
+  detaint_natural($userid) if defined $userid;
+  trick_taint($tokentype);
+  trick_taint($eventdata);
 
-    my $is_shadow = Bugzilla->is_shadow_db;
-    $dbh = Bugzilla->switch_to_main_db() if $is_shadow;
+  my $is_shadow = Bugzilla->is_shadow_db;
+  $dbh = Bugzilla->switch_to_main_db() if $is_shadow;
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    my $token = GenerateUniqueToken();
+  my $token = GenerateUniqueToken();
 
-    $dbh->do("INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
-        VALUES (?, NOW(), ?, ?, ?)", undef, ($userid, $token, $tokentype, $eventdata));
+  $dbh->do(
+    "INSERT INTO tokens (userid, issuedate, token, tokentype, eventdata)
+        VALUES (?, NOW(), ?, ?, ?)", undef,
+    ($userid, $token, $tokentype, $eventdata)
+  );
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 
-    if (wantarray) {
-        my (undef, $token_ts, undef) = GetTokenData($token);
-        $token_ts = str2time($token_ts);
-        Bugzilla->switch_to_shadow_db() if $is_shadow;
-        return ($token, $token_ts);
-    } else {
-        Bugzilla->switch_to_shadow_db() if $is_shadow;
-        return $token;
-    }
+  if (wantarray) {
+    my (undef, $token_ts, undef) = GetTokenData($token);
+    $token_ts = str2time($token_ts);
+    Bugzilla->switch_to_shadow_db() if $is_shadow;
+    return ($token, $token_ts);
+  }
+  else {
+    Bugzilla->switch_to_shadow_db() if $is_shadow;
+    return $token;
+  }
 }
 
 1;
diff --git a/Bugzilla/Types.pm b/Bugzilla/Types.pm
index 93d699f49..e4868d227 100644
--- a/Bugzilla/Types.pm
+++ b/Bugzilla/Types.pm
@@ -11,17 +11,16 @@ use 5.10.1;
 use strict;
 use warnings;
 
-use Type::Library
-    -base,
-    -declare => qw( Bug User Group Attachment Comment JSONBool );
+use Type::Library -base,
+  -declare => qw( Bug User Group Attachment Comment JSONBool );
 use Type::Utils -all;
 use Types::Standard -types;
 
-class_type Bug,        { class => 'Bugzilla::Bug' };
-class_type User,       { class => 'Bugzilla::User' };
-class_type Group,      { class => 'Bugzilla::Group' };
-class_type Attachment, { class => 'Bugzilla::Attachment' };
-class_type Comment,    { class => 'Bugzilla::Comment' };
-class_type JSONBool,   { class => 'JSON::PP::Boolean' };
+class_type Bug,        {class => 'Bugzilla::Bug'};
+class_type User,       {class => 'Bugzilla::User'};
+class_type Group,      {class => 'Bugzilla::Group'};
+class_type Attachment, {class => 'Bugzilla::Attachment'};
+class_type Comment,    {class => 'Bugzilla::Comment'};
+class_type JSONBool,   {class => 'JSON::PP::Boolean'};
 
 1;
diff --git a/Bugzilla/Update.pm b/Bugzilla/Update.pm
index 72a7108a8..9f9288162 100644
--- a/Bugzilla/Update.pm
+++ b/Bugzilla/Update.pm
@@ -13,149 +13,159 @@ use warnings;
 
 use Bugzilla::Constants;
 
-use constant TIME_INTERVAL => 86400; # Default is one day, in seconds.
-use constant TIMEOUT       => 5; # Number of seconds before timeout.
+use constant TIME_INTERVAL => 86400;    # Default is one day, in seconds.
+use constant TIMEOUT       => 5;        # Number of seconds before timeout.
 
 # Look for new releases and notify logged in administrators about them.
 sub get_notifications {
-    return if !Bugzilla->feature('updates');
-    return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
-
-    my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
-    # Update the local XML file if this one doesn't exist or if
-    # the last modification time (stat[9]) is older than TIME_INTERVAL.
-    if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
-        unlink $local_file; # Make sure the old copy is away.
-        return { 'error' => 'no_update' } if (-e $local_file);
-
-        my $error = _synchronize_data();
-        # If an error is returned, leave now.
-        return $error if $error;
+  return if !Bugzilla->feature('updates');
+  return if (Bugzilla->params->{'upgrade_notification'} eq 'disabled');
+
+  my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
+
+  # Update the local XML file if this one doesn't exist or if
+  # the last modification time (stat[9]) is older than TIME_INTERVAL.
+  if (!-e $local_file || (time() - (stat($local_file))[9] > TIME_INTERVAL)) {
+    unlink $local_file;    # Make sure the old copy is away.
+    return {'error' => 'no_update'} if (-e $local_file);
+
+    my $error = _synchronize_data();
+
+    # If an error is returned, leave now.
+    return $error if $error;
+  }
+
+  # If we cannot access the local XML file, ignore it.
+  return {'error' => 'no_access'} unless (-r $local_file);
+
+  my $twig = XML::Twig->new();
+  $twig->safe_parsefile($local_file);
+
+  # If the XML file is invalid, return.
+  return {'error' => 'corrupted'} if $@;
+  my $root = $twig->root;
+
+  my @releases;
+  foreach my $branch ($root->children('branch')) {
+    my $release = {
+      'branch_ver' => $branch->{'att'}->{'id'},
+      'latest_ver' => $branch->{'att'}->{'vid'},
+      'status'     => $branch->{'att'}->{'status'},
+      'url'        => $branch->{'att'}->{'url'},
+      'date'       => $branch->{'att'}->{'date'}
+    };
+    push(@releases, $release);
+  }
+
+  # On which branch is the current installation running?
+  my @current_version
+    = (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+
+  my @release;
+  if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
+    @release = grep { $_->{'status'} eq 'development' } @releases;
+
+    # If there is no development snapshot available, then we are in the
+    # process of releasing a release candidate. That's the release we want.
+    unless (scalar(@release)) {
+      @release = grep { $_->{'status'} eq 'release-candidate' } @releases;
     }
-
-    # If we cannot access the local XML file, ignore it.
-    return { 'error' => 'no_access' } unless (-r $local_file);
-
-    my $twig = XML::Twig->new();
-    $twig->safe_parsefile($local_file);
-    # If the XML file is invalid, return.
-    return { 'error' => 'corrupted' } if $@;
-    my $root = $twig->root;
-
-    my @releases;
-    foreach my $branch ($root->children('branch')) {
-        my $release = {
-            'branch_ver' => $branch->{'att'}->{'id'},
-            'latest_ver' => $branch->{'att'}->{'vid'},
-            'status'     => $branch->{'att'}->{'status'},
-            'url'        => $branch->{'att'}->{'url'},
-            'date'       => $branch->{'att'}->{'date'}
-        };
-        push(@releases, $release);
-    }
-
-    # On which branch is the current installation running?
-    my @current_version =
-        (BUGZILLA_VERSION =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
-
-    my @release;
-    if (Bugzilla->params->{'upgrade_notification'} eq 'development_snapshot') {
-        @release = grep {$_->{'status'} eq 'development'} @releases;
-        # If there is no development snapshot available, then we are in the
-        # process of releasing a release candidate. That's the release we want.
-        unless (scalar(@release)) {
-            @release = grep {$_->{'status'} eq 'release-candidate'} @releases;
-        }
-    }
-    elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
-        @release = grep {$_->{'status'} eq 'stable'} @releases;
-    }
-    elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') {
-        # We want the latest stable version for the current branch.
-        # If we are running a development snapshot, we won't match anything.
-        my $branch_version = $current_version[0] . '.' . $current_version[1];
-
-        # We do a string comparison instead of a numerical one, because
-        # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
-        @release = grep {$_->{'branch_ver'} eq $branch_version} @releases;
-
-        # If the branch is now closed, we should strongly suggest
-        # to upgrade to the latest stable release available.
-        if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
-            @release = grep {$_->{'status'} eq 'stable'} @releases;
-            return {'data' => $release[0], 'deprecated' => $branch_version};
-        }
+  }
+  elsif (Bugzilla->params->{'upgrade_notification'} eq 'latest_stable_release') {
+    @release = grep { $_->{'status'} eq 'stable' } @releases;
+  }
+  elsif (Bugzilla->params->{'upgrade_notification'} eq 'stable_branch_release') {
+
+    # We want the latest stable version for the current branch.
+    # If we are running a development snapshot, we won't match anything.
+    my $branch_version = $current_version[0] . '.' . $current_version[1];
+
+    # We do a string comparison instead of a numerical one, because
+    # e.g. 2.2 == 2.20, but 2.2 ne 2.20 (and 2.2 is indeed much older).
+    @release = grep { $_->{'branch_ver'} eq $branch_version } @releases;
+
+    # If the branch is now closed, we should strongly suggest
+    # to upgrade to the latest stable release available.
+    if (scalar(@release) && $release[0]->{'status'} eq 'closed') {
+      @release = grep { $_->{'status'} eq 'stable' } @releases;
+      return {'data' => $release[0], 'deprecated' => $branch_version};
     }
-    else {
-      # Unknown parameter.
-      return {'error' => 'unknown_parameter'};
-    }
-
-    # Return if no new release is available.
-    return unless scalar(@release);
-
-    # Only notify the administrator if the latest version available
-    # is newer than the current one.
-    my @new_version =
-        ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
-
-    # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order
-    # to compare versions easily.
-    $current_version[2] = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1;
-    $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1;
-
-    my $is_newer = _compare_versions(\@current_version, \@new_version);
-    return ($is_newer == 1) ? {'data' => $release[0]} : undef;
+  }
+  else {
+    # Unknown parameter.
+    return {'error' => 'unknown_parameter'};
+  }
+
+  # Return if no new release is available.
+  return unless scalar(@release);
+
+  # Only notify the administrator if the latest version available
+  # is newer than the current one.
+  my @new_version
+    = ($release[0]->{'latest_ver'} =~ m/^(\d+)\.(\d+)(?:(rc|\.)(\d+))?\+?$/);
+
+  # We convert release candidates 'rc' to integers (rc ? 0 : 1) in order
+  # to compare versions easily.
+  $current_version[2]
+    = ($current_version[2] && $current_version[2] eq 'rc') ? 0 : 1;
+  $new_version[2] = ($new_version[2] && $new_version[2] eq 'rc') ? 0 : 1;
+
+  my $is_newer = _compare_versions(\@current_version, \@new_version);
+  return ($is_newer == 1) ? {'data' => $release[0]} : undef;
 }
 
 sub _synchronize_data {
-    my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
-
-    my $ua = LWP::UserAgent->new();
-    $ua->timeout(TIMEOUT);
-    $ua->protocols_allowed(['http', 'https']);
-    # If the URL of the proxy is given, use it, else get this information
-    # from the environment variable.
-    my $proxy_url = Bugzilla->params->{'proxy_url'};
-    if ($proxy_url) {
-        $ua->proxy(['http', 'https'], $proxy_url);
-    }
-    else {
-        $ua->env_proxy;
-    }
-    my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) };
-
-    # $ua->mirror() forces the modification time of the local XML file
-    # to match the modification time of the remote one.
-    # So we have to update it manually to reflect that a newer version
-    # of the file has effectively been requested. This will avoid
-    # any new download for the next TIME_INTERVAL.
-    if (-e $local_file) {
-        # Try to alter its last modification time.
-        my $can_alter = utime(undef, undef, $local_file);
-        # This error should never happen.
-        $can_alter || return { 'error' => 'no_update' };
-    }
-    elsif ($response && $response->is_error) {
-        # We have been unable to download the file.
-        return { 'error' => 'cannot_download', 'reason' => $response->status_line };
-    }
-    else {
-        return { 'error' => 'no_write', 'reason' => $@ };
-    }
-
-    # Everything went well.
-    return 0;
+  my $local_file = bz_locations()->{'datadir'} . '/' . LOCAL_FILE;
+
+  my $ua = LWP::UserAgent->new();
+  $ua->timeout(TIMEOUT);
+  $ua->protocols_allowed(['http', 'https']);
+
+  # If the URL of the proxy is given, use it, else get this information
+  # from the environment variable.
+  my $proxy_url = Bugzilla->params->{'proxy_url'};
+  if ($proxy_url) {
+    $ua->proxy(['http', 'https'], $proxy_url);
+  }
+  else {
+    $ua->env_proxy;
+  }
+  my $response = eval { $ua->mirror(REMOTE_FILE, $local_file) };
+
+  # $ua->mirror() forces the modification time of the local XML file
+  # to match the modification time of the remote one.
+  # So we have to update it manually to reflect that a newer version
+  # of the file has effectively been requested. This will avoid
+  # any new download for the next TIME_INTERVAL.
+  if (-e $local_file) {
+
+    # Try to alter its last modification time.
+    my $can_alter = utime(undef, undef, $local_file);
+
+    # This error should never happen.
+    $can_alter || return {'error' => 'no_update'};
+  }
+  elsif ($response && $response->is_error) {
+
+    # We have been unable to download the file.
+    return {'error' => 'cannot_download', 'reason' => $response->status_line};
+  }
+  else {
+    return {'error' => 'no_write', 'reason' => $@};
+  }
+
+  # Everything went well.
+  return 0;
 }
 
 sub _compare_versions {
-    my ($old_ver, $new_ver) = @_;
-    while (scalar(@$old_ver) && scalar(@$new_ver)) {
-        my $old = shift(@$old_ver) || 0;
-        my $new = shift(@$new_ver) || 0;
-        return $new <=> $old if ($new <=> $old);
-    }
-    return scalar(@$new_ver) <=> scalar(@$old_ver);
+  my ($old_ver, $new_ver) = @_;
+  while (scalar(@$old_ver) && scalar(@$new_ver)) {
+    my $old = shift(@$old_ver) || 0;
+    my $new = shift(@$new_ver) || 0;
+    return $new <=> $old if ($new <=> $old);
+  }
+  return scalar(@$new_ver) <=> scalar(@$old_ver);
 
 }
 
diff --git a/Bugzilla/User.pm b/Bugzilla/User.pm
index afd310eb0..fdfc7f8d0 100644
--- a/Bugzilla/User.pm
+++ b/Bugzilla/User.pm
@@ -34,9 +34,9 @@ use Role::Tiny::With;
 
 use base qw(Bugzilla::Object Exporter);
 @Bugzilla::User::EXPORT = qw(is_available_username
-    login_to_id user_id_to_login
-    USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
-    MATCH_SKIP_CONFIRM
+  login_to_id user_id_to_login
+  USER_MATCH_MULTIPLE USER_MATCH_FAILED USER_MATCH_SUCCESS
+  MATCH_SKIP_CONFIRM
 );
 
 #####################################################################
@@ -47,16 +47,16 @@ use constant USER_MATCH_MULTIPLE => -1;
 use constant USER_MATCH_FAILED   => 0;
 use constant USER_MATCH_SUCCESS  => 1;
 
-use constant MATCH_SKIP_CONFIRM  => 1;
+use constant MATCH_SKIP_CONFIRM => 1;
 
 use constant DEFAULT_USER => {
-    'userid'         => 0,
-    'realname'       => '',
-    'login_name'     => '',
-    'showmybugslink' => 0,
-    'disabledtext'   => '',
-    'disable_mail'   => 0,
-    'is_enabled'     => 1,
+  'userid'         => 0,
+  'realname'       => '',
+  'login_name'     => '',
+  'showmybugslink' => 0,
+  'disabledtext'   => '',
+  'disable_mail'   => 0,
+  'is_enabled'     => 1,
 };
 
 use constant DB_TABLE => 'profiles';
@@ -66,23 +66,24 @@ use constant DB_TABLE => 'profiles';
 # Bugzilla::User used "name" for the realname field. This should be
 # fixed one day.
 sub DB_COLUMNS {
-    my $dbh = Bugzilla->dbh;
-    return (
-        'profiles.userid',
-        'profiles.login_name',
-        'profiles.realname',
-        'profiles.mybugslink AS showmybugslink',
-        'profiles.disabledtext',
-        'profiles.disable_mail',
-        'profiles.extern_id',
-        'profiles.is_enabled',
-        $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
-        'profiles.password_change_required',
-        'profiles.password_change_reason',
-        'profiles.mfa',
-        'profiles.mfa_required_date',
-        'profiles.nickname'
+  my $dbh = Bugzilla->dbh;
+  return (
+    'profiles.userid',
+    'profiles.login_name',
+    'profiles.realname',
+    'profiles.mybugslink AS showmybugslink',
+    'profiles.disabledtext',
+    'profiles.disable_mail',
+    'profiles.extern_id',
+    'profiles.is_enabled',
+    $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date',
+    'profiles.password_change_required',
+    'profiles.password_change_reason',
+    'profiles.mfa',
+    'profiles.mfa_required_date',
+    'profiles.nickname'
     ),
+    ;
 }
 
 use constant NAME_FIELD => 'login_name';
@@ -90,41 +91,41 @@ use constant ID_FIELD   => 'userid';
 use constant LIST_ORDER => NAME_FIELD;
 
 use constant VALIDATORS => {
-    cryptpassword            => \&_check_password,
-    disable_mail             => \&_check_disable_mail,
-    disabledtext             => \&_check_disabledtext,
-    login_name               => \&check_login_name_for_creation,
-    realname                 => \&_check_realname,
-    nickname                 => \&_check_realname,
-    extern_id                => \&_check_extern_id,
-    is_enabled               => \&_check_is_enabled,
-    password_change_required => \&Bugzilla::Object::check_boolean,
-    password_change_reason   => \&_check_password_change_reason,
-    mfa                      => \&_check_mfa,
+  cryptpassword            => \&_check_password,
+  disable_mail             => \&_check_disable_mail,
+  disabledtext             => \&_check_disabledtext,
+  login_name               => \&check_login_name_for_creation,
+  realname                 => \&_check_realname,
+  nickname                 => \&_check_realname,
+  extern_id                => \&_check_extern_id,
+  is_enabled               => \&_check_is_enabled,
+  password_change_required => \&Bugzilla::Object::check_boolean,
+  password_change_reason   => \&_check_password_change_reason,
+  mfa                      => \&_check_mfa,
 };
 
 sub UPDATE_COLUMNS {
-    my $self = shift;
-    my @cols = qw(
-        disable_mail
-        disabledtext
-        login_name
-        realname
-        extern_id
-        is_enabled
-        password_change_required
-        password_change_reason
-        mfa
-        mfa_required_date
-        nickname
-    );
-    push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
-    return @cols;
-};
+  my $self = shift;
+  my @cols = qw(
+    disable_mail
+    disabledtext
+    login_name
+    realname
+    extern_id
+    is_enabled
+    password_change_required
+    password_change_reason
+    mfa
+    mfa_required_date
+    nickname
+  );
+  push(@cols, 'cryptpassword') if exists $self->{cryptpassword};
+  return @cols;
+}
 
 use constant VALIDATOR_DEPENDENCIES => {
-    is_enabled             => [ 'disabledtext' ],
-    password_change_reason => [ 'password_change_required' ],
+  is_enabled             => ['disabledtext'],
+  password_change_reason => ['password_change_required'],
 };
 
 use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
@@ -132,18 +133,18 @@ use constant EXTRA_REQUIRED_FIELDS => qw(is_enabled);
 with 'Bugzilla::Elastic::Role::Object';
 
 sub ES_INDEX {
-    my ($class) = @_;
-    sprintf("%s_%s", Bugzilla->params->{elasticsearch_index}, $class->ES_TYPE);
+  my ($class) = @_;
+  sprintf("%s_%s", Bugzilla->params->{elasticsearch_index}, $class->ES_TYPE);
 }
 
-sub ES_TYPE { 'user' }
+sub ES_TYPE {'user'}
 
-sub ES_OBJECTS_AT_ONCE { 5000 }
+sub ES_OBJECTS_AT_ONCE {5000}
 
 sub ES_SELECT_UPDATED_SQL {
-    my ($class, $mtime) = @_;
+  my ($class, $mtime) = @_;
 
-    my $sql = q{
+  my $sql = q{
         SELECT DISTINCT
             object_id
         FROM
@@ -151,221 +152,227 @@ sub ES_SELECT_UPDATED_SQL {
         WHERE
             class = 'Bugzilla::User' AND at_time > FROM_UNIXTIME(?)
     };
-    return ($sql, [$mtime]);
+  return ($sql, [$mtime]);
 }
 
 sub ES_SELECT_ALL_SQL {
-    my ($class, $last_id) = @_;
+  my ($class, $last_id) = @_;
 
-    my $id = $class->ID_FIELD;
-    my $table = $class->DB_TABLE;
+  my $id    = $class->ID_FIELD;
+  my $table = $class->DB_TABLE;
 
-    return ("SELECT $id FROM $table WHERE $id > ? AND is_enabled AND NOT disabledtext ORDER BY $id", [$last_id // 0]);
+  return (
+    "SELECT $id FROM $table WHERE $id > ? AND is_enabled AND NOT disabledtext ORDER BY $id",
+    [$last_id // 0]
+  );
 }
 
 sub ES_SETTINGS {
-    return {
-        number_of_shards => 2,
-        analysis         => {
-            filter => {
-                asciifolding_original => {
-                    type              => "asciifolding",
-                    preserve_original => \1,
-                },
-            },
-            analyzer => {
-                autocomplete => {
-                    type      => 'custom',
-                    tokenizer => 'keyword',
-                    filter    => [ 'lowercase', 'asciifolding_original' ],
-                },
-                folding => {
-                    tokenizer => 'standard',
-                    filter    => [ 'standard', 'lowercase', 'asciifolding_original' ],
-                },
-            }
-        }
-    };
+  return {
+    number_of_shards => 2,
+    analysis         => {
+      filter => {
+        asciifolding_original => {type => "asciifolding", preserve_original => \1,},
+      },
+      analyzer => {
+        autocomplete => {
+          type      => 'custom',
+          tokenizer => 'keyword',
+          filter    => ['lowercase', 'asciifolding_original'],
+        },
+        folding => {
+          tokenizer => 'standard',
+          filter    => ['standard', 'lowercase', 'asciifolding_original'],
+        },
+      }
+    }
+  };
 }
 
 sub ES_PROPERTIES {
-    return {
-        suggest_user => {
-            type            => 'completion',
-            analyzer        => 'folding',
-            search_analyzer => 'folding',
-            payloads        => \1,
-        },
-        suggest_nick => {
-            type            => 'completion',
-            analyzer        => 'autocomplete',
-            payloads        => \1,
-        },
-        login      => { type => 'string' },
-        name       => { type => 'string' },
-        is_enabled => { type => 'boolean' },
-    };
+  return {
+    suggest_user => {
+      type            => 'completion',
+      analyzer        => 'folding',
+      search_analyzer => 'folding',
+      payloads        => \1,
+    },
+    suggest_nick =>
+      {type => 'completion', analyzer => 'autocomplete', payloads => \1,},
+    login      => {type => 'string'},
+    name       => {type => 'string'},
+    is_enabled => {type => 'boolean'},
+  };
 }
 
 sub es_document {
-    my ( $self, $timestamp ) = @_;
-    my $doc = {
-        login        => $self->login,
-        name         => $self->name,
-        is_enabled   => $self->is_enabled,
-        suggest_user => {
-            input => [ $self->login, $self->name ],
-            output => $self->identity,
-            payload => { name => $self->login, real_name => $self->name },
-        },
+  my ($self, $timestamp) = @_;
+  my $doc = {
+    login        => $self->login,
+    name         => $self->name,
+    is_enabled   => $self->is_enabled,
+    suggest_user => {
+      input   => [$self->login, $self->name],
+      output  => $self->identity,
+      payload => {name => $self->login, real_name => $self->name},
+    },
+  };
+  my $name  = $self->name;
+  my @nicks = extract_nicks($name);
+
+  if (@nicks) {
+    $doc->{suggest_nick} = {
+      input   => \@nicks,
+      output  => $self->login,
+      payload => {name => $self->login, real_name => $self->name},
     };
-    my $name = $self->name;
-    my @nicks = extract_nicks($name);
-
-    if (@nicks) {
-        $doc->{suggest_nick} = {
-            input => \@nicks,
-            output => $self->login,
-            payload => { name => $self->login, real_name => $self->name },
-        };
-    }
+  }
 
-    return $doc;
+  return $doc;
 }
 ################################################################################
 # Functions
 ################################################################################
 
 sub new {
-    my $invocant = shift;
-    my $class = ref($invocant) || $invocant;
-    my ($param) = @_;
-
-    my $user = { %{ DEFAULT_USER() } };
-    bless ($user, $class);
-    return $user unless $param;
-
-    if (ref($param) eq 'HASH') {
-        if (defined $param->{extern_id}) {
-            $param = { condition => 'extern_id = ?' , values => [$param->{extern_id}] };
-            $_[0] = $param;
-        }
+  my $invocant = shift;
+  my $class    = ref($invocant) || $invocant;
+  my ($param)  = @_;
+
+  my $user = {%{DEFAULT_USER()}};
+  bless($user, $class);
+  return $user unless $param;
+
+  if (ref($param) eq 'HASH') {
+    if (defined $param->{extern_id}) {
+      $param = {condition => 'extern_id = ?', values => [$param->{extern_id}]};
+      $_[0] = $param;
     }
-    return $class->SUPER::new(@_);
+  }
+  return $class->SUPER::new(@_);
 }
 
 sub super_user {
-    my $invocant = shift;
-    my $class = ref($invocant) || $invocant;
-    my ($param) = @_;
+  my $invocant = shift;
+  my $class    = ref($invocant) || $invocant;
+  my ($param)  = @_;
 
-    my $user = { %{ DEFAULT_USER() } };
-    $user->{groups} = [Bugzilla::Group->get_all];
-    $user->{bless_groups} = [Bugzilla::Group->get_all];
-    bless $user, $class;
-    return $user;
+  my $user = {%{DEFAULT_USER()}};
+  $user->{groups}       = [Bugzilla::Group->get_all];
+  $user->{bless_groups} = [Bugzilla::Group->get_all];
+  bless $user, $class;
+  return $user;
 }
 
 sub _update_groups {
-    my $self = shift;
-    my $group_changes = shift;
-    my $changes = shift;
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-
-    # Update group settings.
-    my $sth_add_mapping = $dbh->prepare(
-        qq{INSERT INTO user_group_map (
+  my $self          = shift;
+  my $group_changes = shift;
+  my $changes       = shift;
+  my $dbh           = Bugzilla->dbh;
+  my $user          = Bugzilla->user;
+
+  # Update group settings.
+  my $sth_add_mapping = $dbh->prepare(
+    qq{INSERT INTO user_group_map (
                   user_id, group_id, isbless, grant_type
                  ) VALUES (
                   ?, ?, ?, ?
                  )
-          });
-    my $sth_remove_mapping = $dbh->prepare(
-        qq{DELETE FROM user_group_map
+          }
+  );
+  my $sth_remove_mapping = $dbh->prepare(
+    qq{DELETE FROM user_group_map
             WHERE user_id = ?
               AND group_id = ?
               AND isbless = ?
               AND grant_type = ?
-          });
+          }
+  );
 
-    foreach my $is_bless (keys %$group_changes) {
-        my ($removed, $added) = @{$group_changes->{$is_bless}};
+  foreach my $is_bless (keys %$group_changes) {
+    my ($removed, $added) = @{$group_changes->{$is_bless}};
 
-        foreach my $group (@$removed) {
-            $sth_remove_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
-            Bugzilla->audit(sprintf('%s <%s> removed group %s from %s', $user->login, remote_ip(), $group->name, $self->login));
-        }
-        foreach my $group (@$added) {
-            $sth_add_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
-            Bugzilla->audit(sprintf('%s <%s> added group %s to %s', $user->login, remote_ip(), $group->name, $self->login));
-        }
+    foreach my $group (@$removed) {
+      $sth_remove_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
+      Bugzilla->audit(sprintf(
+        '%s <%s> removed group %s from %s',
+        $user->login, remote_ip(), $group->name, $self->login
+      ));
+    }
+    foreach my $group (@$added) {
+      $sth_add_mapping->execute($self->id, $group->id, $is_bless, GRANT_DIRECT);
+      Bugzilla->audit(sprintf(
+        '%s <%s> added group %s to %s',
+        $user->login, remote_ip(), $group->name, $self->login
+      ));
+    }
 
-        if (! $is_bless) {
-            my $query = qq{
+    if (!$is_bless) {
+      my $query = qq{
                 INSERT INTO profiles_activity
                     (userid, who, profiles_when, fieldid, oldvalue, newvalue)
                 VALUES ( ?, ?, now(), ?, ?, ?)
             };
 
-            $dbh->do(
-                $query, undef,
-                $self->id, $user->id,
-                get_field_id('bug_group'),
-                join(', ', map { $_->name } @$removed),
-                join(', ', map { $_->name } @$added)
-            );
-        }
-        else {
-            # XXX: should create profiles_activity entries for blesser changes.
-        }
+      $dbh->do(
+        $query, undef, $self->id, $user->id,
+        get_field_id('bug_group'),
+        join(', ', map { $_->name } @$removed),
+        join(', ', map { $_->name } @$added)
+      );
+    }
+    else {
+      # XXX: should create profiles_activity entries for blesser changes.
+    }
 
-        Bugzilla->memcached->clear_config({ key => 'user_groups.' . $self->id });
+    Bugzilla->memcached->clear_config({key => 'user_groups.' . $self->id});
 
-        my $type = $is_bless ? 'bless_groups' : 'groups';
-        $changes->{$type} = [
-            [ map { $_->name } @$removed ],
-            [ map { $_->name } @$added ],
-        ];
-    }
+    my $type = $is_bless ? 'bless_groups' : 'groups';
+    $changes->{$type} = [[map { $_->name } @$removed], [map { $_->name } @$added],];
+  }
 }
 
 sub update {
-    my $self = shift;
-    my $options = shift;
+  my $self    = shift;
+  my $options = shift;
 
-    my $group_changes = delete $self->{_group_changes};
+  my $group_changes = delete $self->{_group_changes};
 
-    my $changes = $self->SUPER::update(@_);
-    my $dbh = Bugzilla->dbh;
-    $self->_update_groups($group_changes, $changes);
+  my $changes = $self->SUPER::update(@_);
+  my $dbh     = Bugzilla->dbh;
+  $self->_update_groups($group_changes, $changes);
 
-    if (exists $changes->{login_name}) {
-        # Delete all the tokens related to the userid
-        $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id)
-            unless $options->{keep_tokens};
-        # And rederive regex groups
-        $self->derive_regexp_groups();
-    }
+  if (exists $changes->{login_name}) {
 
-    if (exists $changes->{mfa} && $self->mfa eq '') {
-        if (Bugzilla->user->id != $self->id) {
-            Bugzilla->audit(sprintf('%s disabled 2FA for %s', Bugzilla->user->login, $self->login));
-        }
-        $dbh->do("DELETE FROM profile_mfa WHERE user_id = ?", undef, $self->id);
+    # Delete all the tokens related to the userid
+    $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $self->id)
+      unless $options->{keep_tokens};
+
+    # And rederive regex groups
+    $self->derive_regexp_groups();
+  }
+
+  if (exists $changes->{mfa} && $self->mfa eq '') {
+    if (Bugzilla->user->id != $self->id) {
+      Bugzilla->audit(
+        sprintf('%s disabled 2FA for %s', Bugzilla->user->login, $self->login));
     }
+    $dbh->do("DELETE FROM profile_mfa WHERE user_id = ?", undef, $self->id);
+  }
 
-    # Logout the user if necessary.
-    Bugzilla->logout_user($self)
-        if (!$options->{keep_session}
-            && (exists $changes->{login_name}
-                || exists $changes->{disabledtext}
-                || exists $changes->{cryptpassword}));
+  # Logout the user if necessary.
+  Bugzilla->logout_user($self)
+    if (
+    !$options->{keep_session}
+    && ( exists $changes->{login_name}
+      || exists $changes->{disabledtext}
+      || exists $changes->{cryptpassword})
+    );
 
-    # XXX Can update profiles_activity here as soon as it understands
-    #     field names like login_name.
+  # XXX Can update profiles_activity here as soon as it understands
+  #     field names like login_name.
 
-    return $changes;
+  return $changes;
 }
 
 ################################################################################
@@ -373,288 +380,294 @@ sub update {
 ################################################################################
 
 sub _check_disable_mail {
-    my ($invocant, $value) = @_;
-    return 1 if ref($invocant) && !$invocant->is_enabled;
-    return $value ? 1 : 0;
+  my ($invocant, $value) = @_;
+  return 1 if ref($invocant) && !$invocant->is_enabled;
+  return $value ? 1 : 0;
 }
 
 sub _check_disabledtext { return trim($_[1]) || ''; }
 
 # Check whether the extern_id is unique.
 sub _check_extern_id {
-    my ($invocant, $extern_id) = @_;
-    $extern_id = trim($extern_id);
-    return undef unless defined($extern_id) && $extern_id ne "";
-    if (!ref($invocant) || $invocant->extern_id ne $extern_id) {
-        my $existing_login = $invocant->new({ extern_id => $extern_id });
-        if ($existing_login) {
-            ThrowUserError( 'extern_id_exists',
-                            { extern_id => $extern_id,
-                              existing_login_name => $existing_login->login });
-        }
+  my ($invocant, $extern_id) = @_;
+  $extern_id = trim($extern_id);
+  return undef unless defined($extern_id) && $extern_id ne "";
+  if (!ref($invocant) || $invocant->extern_id ne $extern_id) {
+    my $existing_login = $invocant->new({extern_id => $extern_id});
+    if ($existing_login) {
+      ThrowUserError('extern_id_exists',
+        {extern_id => $extern_id, existing_login_name => $existing_login->login});
     }
-    return $extern_id;
+  }
+  return $extern_id;
 }
 
 # This is public since createaccount.cgi needs to use it before issuing
 # a token for account creation.
 sub check_login_name_for_creation {
-    my ($invocant, $name) = @_;
-    $name = trim($name);
-    $name || ThrowUserError('user_login_required');
-    validate_email_syntax($name)
-        || ThrowUserError('illegal_email_address', { addr => $name });
-
-    # Check the name if it's a new user, or if we're changing the name.
-    if (!ref($invocant) || $invocant->login ne $name) {
-        is_available_username($name)
-            || ThrowUserError('account_exists', { email => $name });
-    }
+  my ($invocant, $name) = @_;
+  $name = trim($name);
+  $name || ThrowUserError('user_login_required');
+  validate_email_syntax($name)
+    || ThrowUserError('illegal_email_address', {addr => $name});
 
-    return $name;
+  # Check the name if it's a new user, or if we're changing the name.
+  if (!ref($invocant) || $invocant->login ne $name) {
+    is_available_username($name)
+      || ThrowUserError('account_exists', {email => $name});
+  }
+
+  return $name;
 }
 
 sub _check_password {
-    my ($self, $pass) = @_;
+  my ($self, $pass) = @_;
 
-    # If the password is '*', do not encrypt it or validate it further--we
-    # are creating a user who should not be able to log in using DB
-    # authentication.
-    return $pass if $pass eq '*';
+  # If the password is '*', do not encrypt it or validate it further--we
+  # are creating a user who should not be able to log in using DB
+  # authentication.
+  return $pass if $pass eq '*';
 
-    Bugzilla->assert_password_is_secure($pass);
-    my $cryptpassword = bz_crypt($pass);
-    return $cryptpassword;
+  Bugzilla->assert_password_is_secure($pass);
+  my $cryptpassword = bz_crypt($pass);
+  return $cryptpassword;
 }
 
 sub _check_realname { return trim($_[1]) || ''; }
 
 sub _check_is_enabled {
-    my ($invocant, $is_enabled, undef, $params) = @_;
-    # is_enabled is set automatically on creation depending on whether
-    # disabledtext is empty (enabled) or not empty (disabled).
-    # When updating the user, is_enabled is set by calling set_disabledtext().
-    # Any value passed into this validator is ignored.
-    my $disabledtext = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext};
-    return $disabledtext ? 0 : 1;
+  my ($invocant, $is_enabled, undef, $params) = @_;
+
+  # is_enabled is set automatically on creation depending on whether
+  # disabledtext is empty (enabled) or not empty (disabled).
+  # When updating the user, is_enabled is set by calling set_disabledtext().
+  # Any value passed into this validator is ignored.
+  my $disabledtext
+    = ref($invocant) ? $invocant->disabledtext : $params->{disabledtext};
+  return $disabledtext ? 0 : 1;
 }
 
 sub _check_password_change_reason {
-    my ($self, $value) = @_;
-    return $self->password_change_required
-        ? trim($_[1]) || ''
-        : '';
+  my ($self, $value) = @_;
+  return $self->password_change_required ? trim($_[1]) || '' : '';
 }
 
 sub _check_mfa {
-    my ($self, $provider) = @_;
-    $provider = lc($provider // '');
-    return 'TOTP' if $provider eq 'totp';
-    return 'Duo' if $provider eq 'duo';
-
-    # you must be member of the bz_can_disable_mfa group to disable mfa for
-    # other accounts.
-    if ($provider eq '') {
-        my $user = Bugzilla->user;
-        if ($user->id != $self->id && !$user->in_group('bz_can_disable_mfa')) {
-            ThrowUserError('mfa_disable_denied');
-        }
+  my ($self, $provider) = @_;
+  $provider = lc($provider // '');
+  return 'TOTP' if $provider eq 'totp';
+  return 'Duo'  if $provider eq 'duo';
+
+  # you must be member of the bz_can_disable_mfa group to disable mfa for
+  # other accounts.
+  if ($provider eq '') {
+    my $user = Bugzilla->user;
+    if ($user->id != $self->id && !$user->in_group('bz_can_disable_mfa')) {
+      ThrowUserError('mfa_disable_denied');
     }
+  }
 
-    return '';
+  return '';
 }
 
 ################################################################################
 # Mutators
 ################################################################################
 
-sub set_disable_mail             { $_[0]->set('disable_mail', $_[1]);             }
-sub set_email_enabled            { $_[0]->set('disable_mail', !$_[1]);            }
-sub set_extern_id                { $_[0]->set('extern_id', $_[1]);                }
-sub set_password_change_required { $_[0]->set('password_change_required', $_[1]); }
-sub set_password_change_reason   { $_[0]->set('password_change_reason', $_[1]);   }
+sub set_disable_mail  { $_[0]->set('disable_mail', $_[1]); }
+sub set_email_enabled { $_[0]->set('disable_mail', !$_[1]); }
+sub set_extern_id     { $_[0]->set('extern_id',    $_[1]); }
+
+sub set_password_change_required {
+  $_[0]->set('password_change_required', $_[1]);
+}
+sub set_password_change_reason { $_[0]->set('password_change_reason', $_[1]); }
 
 sub set_login {
-    my ($self, $login) = @_;
-    $self->set('login_name', $login);
-    delete $self->{identity};
-    delete $self->{nick};
+  my ($self, $login) = @_;
+  $self->set('login_name', $login);
+  delete $self->{identity};
+  delete $self->{nick};
 }
 
 sub _generate_nickname {
-    my ($name, $login, $id) = @_;
-    my ($nick) = extract_nicks($name);
-    if (!$nick) {
-        $nick = "";
-    }
-    my ($count) = Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM profiles WHERE nickname = ? AND userid != ?', undef, $nick, $id);
-    if ($count) {
-        $nick = "";
-    }
-    return $nick;
+  my ($name, $login, $id) = @_;
+  my ($nick) = extract_nicks($name);
+  if (!$nick) {
+    $nick = "";
+  }
+  my ($count)
+    = Bugzilla->dbh->selectrow_array(
+    'SELECT COUNT(*) FROM profiles WHERE nickname = ? AND userid != ?',
+    undef, $nick, $id);
+  if ($count) {
+    $nick = "";
+  }
+  return $nick;
 }
 
 sub set_name {
-    my ($self, $name) = @_;
-    $self->set('realname', $name);
-    delete $self->{identity};
-    $self->set('nickname', _generate_nickname($name, $self->login, $self->id));
+  my ($self, $name) = @_;
+  $self->set('realname', $name);
+  delete $self->{identity};
+  $self->set('nickname', _generate_nickname($name, $self->login, $self->id));
 }
 
 sub set_nick {
-    my ($self, $nick) = @_;
-    $self->set('nickname', $nick);
+  my ($self, $nick) = @_;
+  $self->set('nickname', $nick);
 }
 
 sub set_password {
-    my ($self, $password) = @_;
-    $self->set('cryptpassword', $password);
-    $self->set('password_change_required', 0);
-    $self->set('password_change_reason', '');
+  my ($self, $password) = @_;
+  $self->set('cryptpassword',            $password);
+  $self->set('password_change_required', 0);
+  $self->set('password_change_reason',   '');
 }
 
 sub set_disabledtext {
-    my ($self, $text) = @_;
-    $self->set('disabledtext', $text);
-    $self->set('is_enabled', trim($text) eq '' ? 0 : 1);
-    $self->set('disable_mail', 1) if !$self->is_enabled;
+  my ($self, $text) = @_;
+  $self->set('disabledtext', $text);
+  $self->set('is_enabled', trim($text) eq '' ? 0 : 1);
+  $self->set('disable_mail', 1) if !$self->is_enabled;
 }
 
 sub set_mfa {
-    my ($self, $value) = @_;
-    $self->set('mfa', $value);
-    delete $self->{mfa_provider};
+  my ($self, $value) = @_;
+  $self->set('mfa', $value);
+  delete $self->{mfa_provider};
 }
 
 sub set_mfa_required_date {
-    my ($self, $value) = @_;
-    $self->set('mfa_required_date', $value);
+  my ($self, $value) = @_;
+  $self->set('mfa_required_date', $value);
 }
 
 sub set_groups {
-    my $self = shift;
-    $self->_set_groups(GROUP_MEMBERSHIP, @_);
+  my $self = shift;
+  $self->_set_groups(GROUP_MEMBERSHIP, @_);
 }
 
 sub set_bless_groups {
-    my $self = shift;
+  my $self = shift;
 
-    # The person making the change needs to be in the editusers group
-    Bugzilla->user->in_group('editusers')
-        || ThrowUserError("auth_failure", {group  => "editusers",
-                                           reason => "cant_bless",
-                                           action => "edit",
-                                           object => "users"});
+  # The person making the change needs to be in the editusers group
+  Bugzilla->user->in_group('editusers') || ThrowUserError(
+    "auth_failure",
+    {
+      group  => "editusers",
+      reason => "cant_bless",
+      action => "edit",
+      object => "users"
+    }
+  );
 
-    $self->_set_groups(GROUP_BLESS, @_);
+  $self->_set_groups(GROUP_BLESS, @_);
 }
 
 sub _set_groups {
-    my $self     = shift;
-    my $is_bless = shift;
-    my $changes  = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self     = shift;
+  my $is_bless = shift;
+  my $changes  = shift;
+  my $dbh      = Bugzilla->dbh;
 
-    use Data::Dumper;
+  use Data::Dumper;
 
-    # The person making the change is $user, $self is the person being changed
-    my $user = Bugzilla->user;
+  # The person making the change is $user, $self is the person being changed
+  my $user = Bugzilla->user;
 
-    # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array
-    # is a list of group ids and/or names.
+  # Input is a hash of arrays. Key is 'set', 'add' or 'remove'. The array
+  # is a list of group ids and/or names.
 
-    # First turn the arrays into group objects.
-    $changes = $self->_set_groups_to_object($changes);
+  # First turn the arrays into group objects.
+  $changes = $self->_set_groups_to_object($changes);
 
-    # Get a list of the groups the user currently is a member of
-    my $ids = $dbh->selectcol_arrayref(
-        q{SELECT DISTINCT group_id
+  # Get a list of the groups the user currently is a member of
+  my $ids = $dbh->selectcol_arrayref(
+    q{SELECT DISTINCT group_id
             FROM user_group_map
-           WHERE user_id = ? AND isbless = ? AND grant_type = ?},
-        undef, $self->id, $is_bless, GRANT_DIRECT);
-
-    my $current_groups = Bugzilla::Group->new_from_list($ids);
-    my $new_groups = dclone($current_groups);
-
-    # Record the changes
-    if (exists $changes->{set}) {
-        $new_groups = $changes->{set};
-
-        # We need to check the user has bless rights on the existing groups
-        # If they don't, then we need to add them back to new_groups
-        foreach my $group (@$current_groups) {
-            if (! $user->can_bless($group->id)) {
-                push @$new_groups, $group
-                    unless grep { $_->id eq $group->id } @$new_groups;
-            }
-        }
+           WHERE user_id = ? AND isbless = ? AND grant_type = ?}, undef, $self->id,
+    $is_bless, GRANT_DIRECT
+  );
+
+  my $current_groups = Bugzilla::Group->new_from_list($ids);
+  my $new_groups     = dclone($current_groups);
+
+  # Record the changes
+  if (exists $changes->{set}) {
+    $new_groups = $changes->{set};
+
+    # We need to check the user has bless rights on the existing groups
+    # If they don't, then we need to add them back to new_groups
+    foreach my $group (@$current_groups) {
+      if (!$user->can_bless($group->id)) {
+        push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups;
+      }
     }
-    else {
-        foreach my $group (@{$changes->{remove} // []}) {
-            @$new_groups = grep { $_->id ne $group->id } @$new_groups;
-        }
-        foreach my $group (@{$changes->{add} // []}) {
-            push @$new_groups, $group
-                unless grep { $_->id eq $group->id } @$new_groups;
-        }
+  }
+  else {
+    foreach my $group (@{$changes->{remove} // []}) {
+      @$new_groups = grep { $_->id ne $group->id } @$new_groups;
     }
-
-    # Stash the changes, so self->update can actually make them
-    my @diffs = diff_arrays($current_groups, $new_groups, 'id');
-    if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) {
-        $self->{_group_changes}{$is_bless} = \@diffs;
+    foreach my $group (@{$changes->{add} // []}) {
+      push @$new_groups, $group unless grep { $_->id eq $group->id } @$new_groups;
     }
+  }
+
+  # Stash the changes, so self->update can actually make them
+  my @diffs = diff_arrays($current_groups, $new_groups, 'id');
+  if (scalar(@{$diffs[0]}) || scalar(@{$diffs[1]})) {
+    $self->{_group_changes}{$is_bless} = \@diffs;
+  }
 }
 
 sub _set_groups_to_object {
-    my $self = shift;
-    my $changes = shift;
-    my $user = Bugzilla->user;
+  my $self    = shift;
+  my $changes = shift;
+  my $user    = Bugzilla->user;
 
-    foreach my $key (keys %$changes) {
-        # Check we were given an array
-        unless (ref($changes->{$key}) eq 'ARRAY') {
-            ThrowCodeError(
-                'param_invalid',
-                { param => $changes->{$key}, function => $key }
-            );
-        }
+  foreach my $key (keys %$changes) {
 
-        # Go through the array, and turn items into group objects
-        my @groups = ();
-        foreach my $value (@{$changes->{$key}}) {
-            my $type = $value =~ /^\d+$/ ? 'id' : 'name';
-            my $group = Bugzilla::Group->new({$type => $value});
-
-            if (! $group || ! $user->can_bless($group->id)) {
-                ThrowUserError('auth_failure',
-                    { group  => $value, reason => 'cant_bless',
-                      action => 'edit', object => 'users' });
-            }
-            push @groups, $group;
-        }
-        $changes->{$key} = \@groups;
+    # Check we were given an array
+    unless (ref($changes->{$key}) eq 'ARRAY') {
+      ThrowCodeError('param_invalid', {param => $changes->{$key}, function => $key});
     }
 
-    return $changes;
+    # Go through the array, and turn items into group objects
+    my @groups = ();
+    foreach my $value (@{$changes->{$key}}) {
+      my $type = $value =~ /^\d+$/ ? 'id' : 'name';
+      my $group = Bugzilla::Group->new({$type => $value});
+
+      if (!$group || !$user->can_bless($group->id)) {
+        ThrowUserError('auth_failure',
+          {group => $value, reason => 'cant_bless', action => 'edit', object => 'users'});
+      }
+      push @groups, $group;
+    }
+    $changes->{$key} = \@groups;
+  }
+
+  return $changes;
 }
 
 sub update_last_seen_date {
-    my $self = shift;
-    return unless $self->id;
-    my $dbh = Bugzilla->dbh;
-    my $date = $dbh->selectrow_array(
-        'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
-
-    if (!$self->last_seen_date or $date ne $self->last_seen_date) {
-        $self->{last_seen_date} = $date;
-        # We don't use the normal update() routine here as we only
-        # want to update the last_seen_date column, not any other
-        # pending changes
-        $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?",
-                 undef, $date, $self->id);
-        Bugzilla->memcached->clear({ table => 'profiles', id => $self->id });
-    }
+  my $self = shift;
+  return unless $self->id;
+  my $dbh  = Bugzilla->dbh;
+  my $date = $dbh->selectrow_array(
+    'SELECT ' . $dbh->sql_date_format('NOW()', '%Y-%m-%d'));
+
+  if (!$self->last_seen_date or $date ne $self->last_seen_date) {
+    $self->{last_seen_date} = $date;
+
+    # We don't use the normal update() routine here as we only
+    # want to update the last_seen_date column, not any other
+    # pending changes
+    $dbh->do("UPDATE profiles SET last_seen_date = ? WHERE userid = ?",
+      undef, $date, $self->id);
+    Bugzilla->memcached->clear({table => 'profiles', id => $self->id});
+  }
 }
 
 ################################################################################
@@ -662,201 +675,212 @@ sub update_last_seen_date {
 ################################################################################
 
 # Accessors for user attributes
-sub name  { $_[0]->{realname};   }
-sub login { $_[0]->{login_name}; }
-sub extern_id { $_[0]->{extern_id}; }
-sub email { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
-sub disabledtext { $_[0]->{'disabledtext'}; }
-sub is_enabled { $_[0]->{'is_enabled'} ? 1 : 0; }
+sub name           { $_[0]->{realname}; }
+sub login          { $_[0]->{login_name}; }
+sub extern_id      { $_[0]->{extern_id}; }
+sub email          { $_[0]->login . Bugzilla->params->{'emailsuffix'}; }
+sub disabledtext   { $_[0]->{'disabledtext'}; }
+sub is_enabled     { $_[0]->{'is_enabled'} ? 1 : 0; }
 sub showmybugslink { $_[0]->{showmybugslink}; }
 sub email_disabled { $_[0]->{disable_mail} || !$_[0]->{is_enabled}; }
-sub email_enabled { !$_[0]->email_disabled; }
+sub email_enabled  { !$_[0]->email_disabled; }
 sub last_seen_date { $_[0]->{last_seen_date}; }
 sub password_change_required { $_[0]->{password_change_required}; }
-sub password_change_reason { $_[0]->{password_change_reason}; }
+sub password_change_reason   { $_[0]->{password_change_reason}; }
 
 sub cryptpassword {
-    my $self = shift;
-    # We don't store it because we never want it in the object (we
-    # don't want to accidentally dump even the hash somewhere).
-    my ($pw) = Bugzilla->dbh->selectrow_array(
-        'SELECT cryptpassword FROM profiles WHERE userid = ?',
-        undef, $self->id);
-    return $pw;
+  my $self = shift;
+
+  # We don't store it because we never want it in the object (we
+  # don't want to accidentally dump even the hash somewhere).
+  my ($pw)
+    = Bugzilla->dbh->selectrow_array(
+    'SELECT cryptpassword FROM profiles WHERE userid = ?',
+    undef, $self->id);
+  return $pw;
 }
 
 sub set_authorizer {
-    my ($self, $authorizer) = @_;
-    $self->{authorizer} = $authorizer;
+  my ($self, $authorizer) = @_;
+  $self->{authorizer} = $authorizer;
 }
+
 sub authorizer {
-    my ($self) = @_;
-    if (!$self->{authorizer}) {
-        require Bugzilla::Auth;
-        $self->{authorizer} = new Bugzilla::Auth();
-    }
-    return $self->{authorizer};
+  my ($self) = @_;
+  if (!$self->{authorizer}) {
+    require Bugzilla::Auth;
+    $self->{authorizer} = new Bugzilla::Auth();
+  }
+  return $self->{authorizer};
 }
 
 sub mfa { $_[0]->{mfa} }
 
 sub mfa_required_date {
-    my $self = shift;
-    return $self->{mfa_required_date} ? datetime_from($self->{mfa_required_date}, @_) : undef;
+  my $self = shift;
+  return $self->{mfa_required_date}
+    ? datetime_from($self->{mfa_required_date}, @_)
+    : undef;
 }
 
 sub mfa_provider {
-    my ($self) = @_;
-    my $mfa = $self->{mfa} || return undef;
-    return $self->{mfa_provider} if exists $self->{mfa_provider};
-    require Bugzilla::MFA;
-    $self->{mfa_provider} = Bugzilla::MFA->new_from($self, $mfa);
-    return $self->{mfa_provider};
+  my ($self) = @_;
+  my $mfa = $self->{mfa} || return undef;
+  return $self->{mfa_provider} if exists $self->{mfa_provider};
+  require Bugzilla::MFA;
+  $self->{mfa_provider} = Bugzilla::MFA->new_from($self, $mfa);
+  return $self->{mfa_provider};
 }
 
 
 sub in_mfa_group {
-    my $self = shift;
-    return $self->{in_mfa_group} if exists $self->{in_mfa_group};
+  my $self = shift;
+  return $self->{in_mfa_group} if exists $self->{in_mfa_group};
 
-    my $mfa_group = Bugzilla->params->{mfa_group};
-    return $self->{in_mfa_group} = ($mfa_group && $self->in_group($mfa_group));
+  my $mfa_group = Bugzilla->params->{mfa_group};
+  return $self->{in_mfa_group} = ($mfa_group && $self->in_group($mfa_group));
 }
 
 sub name_or_login {
-    my $self = shift;
+  my $self = shift;
 
-    return $self->name || $self->login;
+  return $self->name || $self->login;
 }
 
 # Generate a string to identify the user by name + login if the user
 # has a name or by login only if she doesn't.
 sub identity {
-    my $self = shift;
+  my $self = shift;
 
-    return "" unless $self->id;
+  return "" unless $self->id;
 
-    if (!defined $self->{identity}) {
-        $self->{identity} =
-          $self->name ? $self->name . " <" . $self->login. ">" : $self->login;
-    }
+  if (!defined $self->{identity}) {
+    $self->{identity}
+      = $self->name ? $self->name . " <" . $self->login . ">" : $self->login;
+  }
 
-    return $self->{identity};
+  return $self->{identity};
 }
 
 sub nick {
-    my $self = shift;
+  my $self = shift;
 
-    return "" unless $self->id;
-    return $self->{nickname} if $self->{nickname};
-    return $self->{nick} //= (split(/@/, $self->login, 2))[0];
+  return "" unless $self->id;
+  return $self->{nickname} if $self->{nickname};
+  return $self->{nick} //= (split(/@/, $self->login, 2))[0];
 }
 
 sub queries {
-    my $self = shift;
-    return $self->{queries} if defined $self->{queries};
-    return [] unless $self->id;
+  my $self = shift;
+  return $self->{queries} if defined $self->{queries};
+  return [] unless $self->id;
 
-    my $dbh = Bugzilla->dbh;
-    my $query_ids = $dbh->selectcol_arrayref(
-        'SELECT id FROM namedqueries WHERE userid = ?', undef, $self->id);
-    require Bugzilla::Search::Saved;
-    $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids);
+  my $dbh = Bugzilla->dbh;
+  my $query_ids
+    = $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?',
+    undef, $self->id);
+  require Bugzilla::Search::Saved;
+  $self->{queries} = Bugzilla::Search::Saved->new_from_list($query_ids);
 
-    # We preload link_in_footer from here as this information is always requested.
-    # This only works if the user object represents the current logged in user.
-    Bugzilla::Search::Saved::preload($self->{queries}) if $self->id == Bugzilla->user->id;
+  # We preload link_in_footer from here as this information is always requested.
+  # This only works if the user object represents the current logged in user.
+  Bugzilla::Search::Saved::preload($self->{queries})
+    if $self->id == Bugzilla->user->id;
 
-    return $self->{queries};
+  return $self->{queries};
 }
 
 sub queries_subscribed {
-    my $self = shift;
-    return $self->{queries_subscribed} if defined $self->{queries_subscribed};
-    return [] unless $self->id;
-
-    # Exclude the user's own queries.
-    my @my_query_ids = map($_->id, @{$self->queries});
-    my $query_id_string = join(',', @my_query_ids) || '-1';
-
-    # Only show subscriptions that we can still actually see. If a
-    # user changes the shared group of a query, our subscription
-    # will remain but we won't have access to the query anymore.
-    my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref(
-        "SELECT lif.namedquery_id
+  my $self = shift;
+  return $self->{queries_subscribed} if defined $self->{queries_subscribed};
+  return [] unless $self->id;
+
+  # Exclude the user's own queries.
+  my @my_query_ids = map($_->id, @{$self->queries});
+  my $query_id_string = join(',', @my_query_ids) || '-1';
+
+  # Only show subscriptions that we can still actually see. If a
+  # user changes the shared group of a query, our subscription
+  # will remain but we won't have access to the query anymore.
+  my $subscribed_query_ids = Bugzilla->dbh->selectcol_arrayref(
+    "SELECT lif.namedquery_id
            FROM namedqueries_link_in_footer lif
                 INNER JOIN namedquery_group_map ngm
                 ON ngm.namedquery_id = lif.namedquery_id
           WHERE lif.user_id = ?
                 AND lif.namedquery_id NOT IN ($query_id_string)
-                AND " . $self->groups_in_sql,
-          undef, $self->id);
-    require Bugzilla::Search::Saved;
-    $self->{queries_subscribed} =
-        Bugzilla::Search::Saved->new_from_list($subscribed_query_ids);
-    return $self->{queries_subscribed};
+                AND " . $self->groups_in_sql, undef, $self->id
+  );
+  require Bugzilla::Search::Saved;
+  $self->{queries_subscribed}
+    = Bugzilla::Search::Saved->new_from_list($subscribed_query_ids);
+  return $self->{queries_subscribed};
 }
 
 sub queries_available {
-    my $self = shift;
-    return $self->{queries_available} if defined $self->{queries_available};
-    return [] unless $self->id;
+  my $self = shift;
+  return $self->{queries_available} if defined $self->{queries_available};
+  return [] unless $self->id;
 
-    # Exclude the user's own queries.
-    my @my_query_ids = map($_->id, @{$self->queries});
-    my $query_id_string = join(',', @my_query_ids) || '-1';
+  # Exclude the user's own queries.
+  my @my_query_ids = map($_->id, @{$self->queries});
+  my $query_id_string = join(',', @my_query_ids) || '-1';
 
-    my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref(
-        'SELECT namedquery_id FROM namedquery_group_map
-          WHERE '  . $self->groups_in_sql . "
-                AND namedquery_id NOT IN ($query_id_string)");
-    require Bugzilla::Search::Saved;
-    $self->{queries_available} =
-        Bugzilla::Search::Saved->new_from_list($avail_query_ids);
-    return $self->{queries_available};
+  my $avail_query_ids = Bugzilla->dbh->selectcol_arrayref(
+    'SELECT namedquery_id FROM namedquery_group_map
+          WHERE ' . $self->groups_in_sql . "
+                AND namedquery_id NOT IN ($query_id_string)"
+  );
+  require Bugzilla::Search::Saved;
+  $self->{queries_available}
+    = Bugzilla::Search::Saved->new_from_list($avail_query_ids);
+  return $self->{queries_available};
 }
 
 sub tags {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
-
-    if (!defined $self->{tags}) {
-        # We must use LEFT JOIN instead of INNER JOIN as we may be
-        # in the process of inserting a new tag to some bugs,
-        # in which case there are no bugs with this tag yet.
-        $self->{tags} = $dbh->selectall_hashref(
-            'SELECT name, id, COUNT(bug_id) AS bug_count
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+
+  if (!defined $self->{tags}) {
+
+    # We must use LEFT JOIN instead of INNER JOIN as we may be
+    # in the process of inserting a new tag to some bugs,
+    # in which case there are no bugs with this tag yet.
+    $self->{tags} = $dbh->selectall_hashref(
+      'SELECT name, id, COUNT(bug_id) AS bug_count
                FROM tag
           LEFT JOIN bug_tag ON bug_tag.tag_id = tag.id
-              WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'),
-            'name', undef, $self->id);
-    }
-    return $self->{tags};
+              WHERE user_id = ? ' . $dbh->sql_group_by('id', 'name'), 'name', undef,
+      $self->id
+    );
+  }
+  return $self->{tags};
 }
 
 sub bugs_ignored {
-    my ($self) = @_;
-    my $dbh = Bugzilla->dbh;
-    if (!defined $self->{'bugs_ignored'}) {
-        $self->{'bugs_ignored'} = $dbh->selectall_arrayref(
-            'SELECT bugs.bug_id AS id,
+  my ($self) = @_;
+  my $dbh = Bugzilla->dbh;
+  if (!defined $self->{'bugs_ignored'}) {
+    $self->{'bugs_ignored'} = $dbh->selectall_arrayref(
+      'SELECT bugs.bug_id AS id,
                     bugs.bug_status AS status,
                     bugs.short_desc AS summary
                FROM bugs
                     INNER JOIN email_bug_ignore
                     ON bugs.bug_id = email_bug_ignore.bug_id
-              WHERE user_id = ?',
-            { Slice => {} }, $self->id);
-        # Go ahead and load these into the visible bugs cache
-        # to speed up can_see_bug checks later
-        $self->visible_bugs([ map { $_->{'id'} } @{ $self->{'bugs_ignored'} } ]);
-    }
-    return $self->{'bugs_ignored'};
+              WHERE user_id = ?', {Slice => {}}, $self->id
+    );
+
+    # Go ahead and load these into the visible bugs cache
+    # to speed up can_see_bug checks later
+    $self->visible_bugs([map { $_->{'id'} } @{$self->{'bugs_ignored'}}]);
+  }
+  return $self->{'bugs_ignored'};
 }
 
 sub is_bug_ignored {
-    my ($self, $bug_id) = @_;
-    return (grep {$_->{'id'} == $bug_id} @{$self->bugs_ignored}) ? 1 : 0;
+  my ($self, $bug_id) = @_;
+  return (grep { $_->{'id'} == $bug_id } @{$self->bugs_ignored}) ? 1 : 0;
 }
 
 ##########################
@@ -864,280 +888,288 @@ sub is_bug_ignored {
 ##########################
 
 sub recent_searches {
-    my $self = shift;
-    $self->{recent_searches} ||=
-        Bugzilla::Search::Recent->match({ user_id => $self->id });
-    return $self->{recent_searches};
+  my $self = shift;
+  $self->{recent_searches}
+    ||= Bugzilla::Search::Recent->match({user_id => $self->id});
+  return $self->{recent_searches};
 }
 
 sub recent_search_containing {
-    my ($self, $bug_id) = @_;
-    my $searches = $self->recent_searches;
+  my ($self, $bug_id) = @_;
+  my $searches = $self->recent_searches;
 
-    foreach my $search (@$searches) {
-        return $search if grep($_ == $bug_id, @{ $search->bug_list });
-    }
+  foreach my $search (@$searches) {
+    return $search if grep($_ == $bug_id, @{$search->bug_list});
+  }
 
-    return undef;
+  return undef;
 }
 
 sub recent_search_for {
-    my ($self, $bug) = @_;
-    my $params = Bugzilla->input_params;
-    my $cgi = Bugzilla->cgi;
-
-    if ($self->id) {
-        # First see if there's a list_id parameter in the query string.
-        my $list_id = $params->{list_id};
-        if (!$list_id) {
-            # If not, check for "list_id" in the query string of the referer.
-            my $referer = $cgi->referer;
-            if ($referer) {
-                my $uri = URI->new($referer);
-                if ($uri->path =~ /buglist\.cgi$/) {
-                    $list_id = $uri->query_param('list_id')
-                               || $uri->query_param('regetlastlist');
-                }
-            }
+  my ($self, $bug) = @_;
+  my $params = Bugzilla->input_params;
+  my $cgi    = Bugzilla->cgi;
+
+  if ($self->id) {
+
+    # First see if there's a list_id parameter in the query string.
+    my $list_id = $params->{list_id};
+    if (!$list_id) {
+
+      # If not, check for "list_id" in the query string of the referer.
+      my $referer = $cgi->referer;
+      if ($referer) {
+        my $uri = URI->new($referer);
+        if ($uri->path =~ /buglist\.cgi$/) {
+          $list_id = $uri->query_param('list_id') || $uri->query_param('regetlastlist');
         }
+      }
+    }
 
-        if ($list_id && $list_id ne 'cookie') {
-            # If we got a bad list_id (either some other user's or an expired
-            # one) don't crash, just don't return that list.
-            my $search = Bugzilla::Search::Recent->check_quietly(
-                { id => $list_id });
-            return $search if $search;
-        }
+    if ($list_id && $list_id ne 'cookie') {
 
-        # If there's no list_id, see if the current bug's id is contained
-        # in any of the user's saved lists.
-        my $search = $self->recent_search_containing($bug->id);
-        return $search if $search;
+      # If we got a bad list_id (either some other user's or an expired
+      # one) don't crash, just don't return that list.
+      my $search = Bugzilla::Search::Recent->check_quietly({id => $list_id});
+      return $search if $search;
     }
 
-    # Finally (or always, if we're logged out), if there's a BUGLIST cookie
-    # and the selected bug is in the list, then return the cookie as a fake
-    # Search::Recent object.
-    if (my $list = $cgi->cookie('BUGLIST')) {
-        # Also split on colons, which was used as a separator in old cookies.
-        my @bug_ids = split(/[:-]/, $list);
-        if (grep { $_ == $bug->id } @bug_ids) {
-            my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids);
-            return $search;
-        }
+    # If there's no list_id, see if the current bug's id is contained
+    # in any of the user's saved lists.
+    my $search = $self->recent_search_containing($bug->id);
+    return $search if $search;
+  }
+
+  # Finally (or always, if we're logged out), if there's a BUGLIST cookie
+  # and the selected bug is in the list, then return the cookie as a fake
+  # Search::Recent object.
+  if (my $list = $cgi->cookie('BUGLIST')) {
+
+    # Also split on colons, which was used as a separator in old cookies.
+    my @bug_ids = split(/[:-]/, $list);
+    if (grep { $_ == $bug->id } @bug_ids) {
+      my $search = Bugzilla::Search::Recent->new_from_cookie(\@bug_ids);
+      return $search;
     }
+  }
 
-    return undef;
+  return undef;
 }
 
 sub save_last_search {
-    my ($self, $params) = @_;
-    my ($bug_ids, $order, $vars, $list_id) =
-        @$params{qw(bugs order vars list_id)};
-
-    my $cgi = Bugzilla->cgi;
-    if ($order) {
-        $cgi->send_cookie(-name => 'LASTORDER',
-                          -value => $order,
-                          -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
-    }
+  my ($self, $params) = @_;
+  my ($bug_ids, $order, $vars, $list_id) = @$params{qw(bugs order vars list_id)};
+
+  my $cgi = Bugzilla->cgi;
+  if ($order) {
+    $cgi->send_cookie(
+      -name    => 'LASTORDER',
+      -value   => $order,
+      -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+    );
+  }
 
-    return if !@$bug_ids;
-
-    my $search;
-    if ($self->id) {
-        on_main_db {
-            if ($list_id) {
-                $search = Bugzilla::Search::Recent->check_quietly({ id => $list_id });
-            }
-
-            if ($search) {
-                if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
-                    $search->set_bug_list($bug_ids);
-                }
-                if (!$search->list_order || $order ne $search->list_order) {
-                    $search->set_list_order($order);
-                }
-                $search->update();
-            }
-            else {
-                # If we already have an existing search with a totally
-                # identical bug list, then don't create a new one. This
-                # prevents people from writing over their whole
-                # recent-search list by just refreshing a saved search
-                # (which doesn't have list_id in the header) over and over.
-                my $list_string = join(',', @$bug_ids);
-                my $existing_search = Bugzilla::Search::Recent->match({
-                    user_id => $self->id, bug_list => $list_string });
-
-                if (!scalar(@$existing_search)) {
-                    $search = Bugzilla::Search::Recent->create({
-                        user_id    => $self->id,
-                        bug_list   => $bug_ids,
-                        list_order => $order });
-                }
-                else {
-                    $search = $existing_search->[0];
-                }
-            }
-        };
-        delete $self->{recent_searches};
-    }
-    # Logged-out users use a cookie to store a single last search. We don't
-    # override that cookie with the logged-in user's latest search, because
-    # if they did one search while logged out and another while logged in,
-    # they may still want to navigate through the search they made while
-    # logged out.
-    else {
-        my $bug_list = join('-', @$bug_ids);
-        if (length($bug_list) < 4000) {
-            $cgi->send_cookie(-name => 'BUGLIST',
-                              -value => $bug_list,
-                              -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
+  return if !@$bug_ids;
+
+  my $search;
+  if ($self->id) {
+    on_main_db {
+      if ($list_id) {
+        $search = Bugzilla::Search::Recent->check_quietly({id => $list_id});
+      }
+
+      if ($search) {
+        if (join(',', @{$search->bug_list}) ne join(',', @$bug_ids)) {
+          $search->set_bug_list($bug_ids);
+        }
+        if (!$search->list_order || $order ne $search->list_order) {
+          $search->set_list_order($order);
+        }
+        $search->update();
+      }
+      else {
+        # If we already have an existing search with a totally
+        # identical bug list, then don't create a new one. This
+        # prevents people from writing over their whole
+        # recent-search list by just refreshing a saved search
+        # (which doesn't have list_id in the header) over and over.
+        my $list_string = join(',', @$bug_ids);
+        my $existing_search = Bugzilla::Search::Recent->match(
+          {user_id => $self->id, bug_list => $list_string});
+
+        if (!scalar(@$existing_search)) {
+          $search
+            = Bugzilla::Search::Recent->create({
+            user_id => $self->id, bug_list => $bug_ids, list_order => $order
+            });
         }
         else {
-            $cgi->remove_cookie('BUGLIST');
-            $vars->{'toolong'} = 1;
+          $search = $existing_search->[0];
         }
+      }
+    };
+    delete $self->{recent_searches};
+  }
+
+  # Logged-out users use a cookie to store a single last search. We don't
+  # override that cookie with the logged-in user's latest search, because
+  # if they did one search while logged out and another while logged in,
+  # they may still want to navigate through the search they made while
+  # logged out.
+  else {
+    my $bug_list = join('-', @$bug_ids);
+    if (length($bug_list) < 4000) {
+      $cgi->send_cookie(
+        -name    => 'BUGLIST',
+        -value   => $bug_list,
+        -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+      );
     }
-    return $search;
+    else {
+      $cgi->remove_cookie('BUGLIST');
+      $vars->{'toolong'} = 1;
+    }
+  }
+  return $search;
 }
 
 sub settings {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'settings'} if (defined $self->{'settings'});
+  return $self->{'settings'} if (defined $self->{'settings'});
 
-    # IF the user is logged in
-    # THEN get the user's settings
-    # ELSE get default settings
-    if ($self->id) {
-        $self->{'settings'} = get_all_settings($self->id);
-    } else {
-        $self->{'settings'} = get_defaults();
-    }
+  # IF the user is logged in
+  # THEN get the user's settings
+  # ELSE get default settings
+  if ($self->id) {
+    $self->{'settings'} = get_all_settings($self->id);
+  }
+  else {
+    $self->{'settings'} = get_defaults();
+  }
 
-    return $self->{'settings'};
+  return $self->{'settings'};
 }
 
 sub setting {
-    my ($self, $name) = @_;
-    return $self->settings->{$name}->{'value'};
+  my ($self, $name) = @_;
+  return $self->settings->{$name}->{'value'};
 }
 
 sub timezone {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{timezone}) {
-        my $tz = $self->setting('timezone');
-        if ($tz eq 'local') {
-            # The user wants the local timezone of the server.
-            $self->{timezone} = Bugzilla->local_timezone;
-        }
-        else {
-            $self->{timezone} = DateTime::TimeZone->new(name => $tz);
-        }
+  if (!defined $self->{timezone}) {
+    my $tz = $self->setting('timezone');
+    if ($tz eq 'local') {
+
+      # The user wants the local timezone of the server.
+      $self->{timezone} = Bugzilla->local_timezone;
+    }
+    else {
+      $self->{timezone} = DateTime::TimeZone->new(name => $tz);
     }
-    return $self->{timezone};
+  }
+  return $self->{timezone};
 }
 
 sub flush_queries_cache {
-    my $self = shift;
+  my $self = shift;
 
-    delete $self->{queries};
-    delete $self->{queries_subscribed};
-    delete $self->{queries_available};
+  delete $self->{queries};
+  delete $self->{queries_subscribed};
+  delete $self->{queries_available};
 }
 
 sub groups {
-    my $self = shift;
+  my $self = shift;
 
-    return $self->{groups} if defined $self->{groups};
-    return [] unless $self->id;
+  return $self->{groups} if defined $self->{groups};
+  return [] unless $self->id;
 
-    my $user_groups_key = "user_groups." . $self->id;
-    my $groups = Bugzilla->memcached->get_config({
-        key => $user_groups_key
-    });
+  my $user_groups_key = "user_groups." . $self->id;
+  my $groups = Bugzilla->memcached->get_config({key => $user_groups_key});
 
-    if (!$groups) {
-        my $dbh = Bugzilla->dbh;
-        my $groups_to_check = $dbh->selectcol_arrayref(
-            "SELECT DISTINCT group_id
+  if (!$groups) {
+    my $dbh             = Bugzilla->dbh;
+    my $groups_to_check = $dbh->selectcol_arrayref(
+      "SELECT DISTINCT group_id
                FROM user_group_map
-              WHERE user_id = ? AND isbless = 0", undef, $self->id);
-
-        my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP;
-        my $membership_rows = Bugzilla->memcached->get_config({
-            key => $grant_type_key,
-        });
-        if (!$membership_rows) {
-            $membership_rows = $dbh->selectall_arrayref(
-                "SELECT DISTINCT grantor_id, member_id
+              WHERE user_id = ? AND isbless = 0", undef, $self->id
+    );
+
+    my $grant_type_key = 'group_grant_type_' . GROUP_MEMBERSHIP;
+    my $membership_rows
+      = Bugzilla->memcached->get_config({key => $grant_type_key,});
+    if (!$membership_rows) {
+      $membership_rows = $dbh->selectall_arrayref(
+        "SELECT DISTINCT grantor_id, member_id
                 FROM group_group_map
-                WHERE grant_type = " . GROUP_MEMBERSHIP);
-            Bugzilla->memcached->set_config({
-                key  => $grant_type_key,
-                data => $membership_rows,
-            });
-        }
+                WHERE grant_type = " . GROUP_MEMBERSHIP
+      );
+      Bugzilla->memcached->set_config({
+        key => $grant_type_key, data => $membership_rows,
+      });
+    }
 
-        my %group_membership;
-        foreach my $row (@$membership_rows) {
-            my ($grantor_id, $member_id) = @$row;
-            push (@{ $group_membership{$member_id} }, $grantor_id);
-        }
+    my %group_membership;
+    foreach my $row (@$membership_rows) {
+      my ($grantor_id, $member_id) = @$row;
+      push(@{$group_membership{$member_id}}, $grantor_id);
+    }
 
-        # Let's walk the groups hierarchy tree (using FIFO)
-        # On the first iteration it's pre-filled with direct groups
-        # membership. Later on, each group can add its own members into the
-        # FIFO. Circular dependencies are eliminated by checking
-        # $checked_groups{$member_id} hash values.
-        # As a result, %groups will have all the groups we are the member of.
-        my %checked_groups;
-        my %groups;
-        while (scalar(@$groups_to_check) > 0) {
-            # Pop the head group from FIFO
-            my $member_id = shift @$groups_to_check;
-
-            # Skip the group if we have already checked it
-            if (!$checked_groups{$member_id}) {
-                # Mark group as checked
-                $checked_groups{$member_id} = 1;
-
-                # Add all its members to the FIFO check list
-                # %group_membership contains arrays of group members
-                # for all groups. Accessible by group number.
-                my $members = $group_membership{$member_id};
-                my @new_to_check = grep(!$checked_groups{$_}, @$members);
-                push(@$groups_to_check, @new_to_check);
-
-                $groups{$member_id} = 1;
-            }
-        }
-        $groups = [ keys %groups ];
+    # Let's walk the groups hierarchy tree (using FIFO)
+    # On the first iteration it's pre-filled with direct groups
+    # membership. Later on, each group can add its own members into the
+    # FIFO. Circular dependencies are eliminated by checking
+    # $checked_groups{$member_id} hash values.
+    # As a result, %groups will have all the groups we are the member of.
+    my %checked_groups;
+    my %groups;
+    while (scalar(@$groups_to_check) > 0) {
+
+      # Pop the head group from FIFO
+      my $member_id = shift @$groups_to_check;
+
+      # Skip the group if we have already checked it
+      if (!$checked_groups{$member_id}) {
 
-        Bugzilla->memcached->set_config({
-            key  => $user_groups_key,
-            data => $groups,
-        });
+        # Mark group as checked
+        $checked_groups{$member_id} = 1;
+
+        # Add all its members to the FIFO check list
+        # %group_membership contains arrays of group members
+        # for all groups. Accessible by group number.
+        my $members = $group_membership{$member_id};
+        my @new_to_check = grep(!$checked_groups{$_}, @$members);
+        push(@$groups_to_check, @new_to_check);
+
+        $groups{$member_id} = 1;
+      }
     }
+    $groups = [keys %groups];
+
+    Bugzilla->memcached->set_config({key => $user_groups_key, data => $groups,});
+  }
 
-    $self->{groups} = Bugzilla::Group->new_from_list($groups);
-    return $self->{groups};
+  $self->{groups} = Bugzilla::Group->new_from_list($groups);
+  return $self->{groups};
 }
 
 sub force_bug_dissociation {
-    my ($self, $nobody, $groups, $timestamp) = @_;
-    my $dbh       = Bugzilla->dbh;
-    my $auto_user = Bugzilla->user;
-    $timestamp //= $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-
-    my $group_marks = join(", ", ('?') x @$groups);
-    my $user_id = $self->id;
-    my @params = ($user_id, $user_id, $user_id, $user_id,
-                  map { blessed $_ ? $_->id : $_ } @$groups);
-    my $bugs = $dbh->selectall_arrayref(qq{
+  my ($self, $nobody, $groups, $timestamp) = @_;
+  my $dbh       = Bugzilla->dbh;
+  my $auto_user = Bugzilla->user;
+  $timestamp //= $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+  my $group_marks = join(", ", ('?') x @$groups);
+  my $user_id     = $self->id;
+  my @params      = (
+    $user_id, $user_id, $user_id, $user_id,
+    map { blessed $_ ? $_->id : $_ } @$groups
+  );
+  my $bugs = $dbh->selectall_arrayref(
+    qq{
         SELECT
             bugs.bug_id,
             bugs.reporter_accessible,
@@ -1157,99 +1189,103 @@ sub force_bug_dissociation {
             OR match_assignee
             OR match_qa_contact
             OR match_cc
-    }, { Slice => {} }, @params);
-
-    my @reporter_bugs = map { $_->{bug_id} } grep { $_->{match_reporter}   } @$bugs;
-    my @assignee_bugs = map { $_->{bug_id} } grep { $_->{match_assignee}   } @$bugs;
-    my @qa_bugs       = map { $_->{bug_id} } grep { $_->{match_qa_contact} } @$bugs;
-    my @cc_bugs       = map { $_->{bug_id} } grep { $_->{match_cc}         } @$bugs;
-
-    # Reporter - set reporter_accessible to false
-    my $reporter_accessible_field_id = get_field_id('reporter_accessible');
-    foreach my $bug_id (@reporter_bugs) {
-        $dbh->do(
-            q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
-            VALUES (?, ?, ?, ?, ?, ?)},
-            undef, $bug_id, $auto_user->id, $timestamp, $reporter_accessible_field_id, 1, 0);
-        $dbh->do(
-            q{UPDATE bugs SET reporter_accessible = 0, delta_ts = ?, lastdiffed = ?
-            WHERE bug_id = ?},
-            undef, $timestamp, $timestamp, $bug_id);
-    }
-
-    # Assignee
-    my $assigned_to_field_id = get_field_id('assigned_to');
-    foreach my $bug_id (@assignee_bugs) {
-        $dbh->do(
-            q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
-            VALUES (?, ?, ?, ?, ?, ?)},
-            undef, $bug_id, $auto_user->id, $timestamp, $assigned_to_field_id,
-                $self->login, $auto_user->login);
-        $dbh->do(
-            q{UPDATE bugs SET assigned_to = ?, delta_ts = ?, lastdiffed = ?
-            WHERE bug_id = ?},
-            undef, $nobody->id, $timestamp, $timestamp, $bug_id);
-    }
-
-    # QA Contact
-    my $qa_field_id = get_field_id('qa_contact');
-    foreach my $bug_id (@qa_bugs) {
-        $dbh->do(
-            q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
-            VALUES (?, ?, ?, ?, ?, '')},
-            undef, $bug_id, $auto_user->id, $timestamp, $qa_field_id, $self->login);
-        $dbh->do(
-            q{UPDATE bugs SET qa_contact = NULL, delta_ts = ?, lastdiffed = ?
-            WHERE bug_id = ?},
-            undef, $timestamp, $timestamp, $bug_id);
-    }
-
-    # CC list
-    my $cc_field_id = get_field_id('cc');
-    foreach my $bug_id (@cc_bugs) {
-        $dbh->do(
-            q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
-            VALUES (?, ?, ?, ?, ?, '')},
-            undef, $bug_id, $auto_user->id, $timestamp, $cc_field_id, $self->login);
-        $dbh->do(q{DELETE FROM cc WHERE bug_id = ? AND who = ?},
-                undef, $bug_id, $self->id);
-    }
+    }, {Slice => {}}, @params
+  );
+
+  my @reporter_bugs = map { $_->{bug_id} } grep { $_->{match_reporter} } @$bugs;
+  my @assignee_bugs = map { $_->{bug_id} } grep { $_->{match_assignee} } @$bugs;
+  my @qa_bugs       = map { $_->{bug_id} } grep { $_->{match_qa_contact} } @$bugs;
+  my @cc_bugs       = map { $_->{bug_id} } grep { $_->{match_cc} } @$bugs;
+
+  # Reporter - set reporter_accessible to false
+  my $reporter_accessible_field_id = get_field_id('reporter_accessible');
+  foreach my $bug_id (@reporter_bugs) {
+    $dbh->do(
+      q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
+            VALUES (?, ?, ?, ?, ?, ?)}, undef, $bug_id, $auto_user->id, $timestamp,
+      $reporter_accessible_field_id, 1, 0
+    );
+    $dbh->do(
+      q{UPDATE bugs SET reporter_accessible = 0, delta_ts = ?, lastdiffed = ?
+            WHERE bug_id = ?}, undef, $timestamp, $timestamp, $bug_id
+    );
+  }
+
+  # Assignee
+  my $assigned_to_field_id = get_field_id('assigned_to');
+  foreach my $bug_id (@assignee_bugs) {
+    $dbh->do(
+      q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
+            VALUES (?, ?, ?, ?, ?, ?)}, undef, $bug_id, $auto_user->id, $timestamp,
+      $assigned_to_field_id, $self->login, $auto_user->login
+    );
+    $dbh->do(
+      q{UPDATE bugs SET assigned_to = ?, delta_ts = ?, lastdiffed = ?
+            WHERE bug_id = ?}, undef, $nobody->id, $timestamp, $timestamp, $bug_id
+    );
+  }
+
+  # QA Contact
+  my $qa_field_id = get_field_id('qa_contact');
+  foreach my $bug_id (@qa_bugs) {
+    $dbh->do(
+      q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
+            VALUES (?, ?, ?, ?, ?, '')}, undef, $bug_id, $auto_user->id, $timestamp,
+      $qa_field_id, $self->login
+    );
+    $dbh->do(
+      q{UPDATE bugs SET qa_contact = NULL, delta_ts = ?, lastdiffed = ?
+            WHERE bug_id = ?}, undef, $timestamp, $timestamp, $bug_id
+    );
+  }
+
+  # CC list
+  my $cc_field_id = get_field_id('cc');
+  foreach my $bug_id (@cc_bugs) {
+    $dbh->do(
+      q{INSERT INTO bugs_activity (bug_id, who, bug_when, fieldid, removed, added)
+            VALUES (?, ?, ?, ?, ?, '')}, undef, $bug_id, $auto_user->id, $timestamp,
+      $cc_field_id, $self->login
+    );
+    $dbh->do(q{DELETE FROM cc WHERE bug_id = ? AND who = ?},
+      undef, $bug_id, $self->id);
+  }
 
-    if (@reporter_bugs || @assignee_bugs || @qa_bugs || @cc_bugs) {
-        $self->clear_last_statistics_ts();
+  if (@reporter_bugs || @assignee_bugs || @qa_bugs || @cc_bugs) {
+    $self->clear_last_statistics_ts();
 
-        # It's complex to determine which items now need to be flushed from memcached.
-        # As this is expected to be a rare event, we just flush the entire cache.
-        Bugzilla->memcached->clear_all();
-    }
+    # It's complex to determine which items now need to be flushed from memcached.
+    # As this is expected to be a rare event, we just flush the entire cache.
+    Bugzilla->memcached->clear_all();
+  }
 
-    return $bugs;
+  return $bugs;
 }
 
 sub last_visited {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return Bugzilla::BugUserLastVisit->match({ user_id => $self->id });
+  return Bugzilla::BugUserLastVisit->match({user_id => $self->id});
 }
 
 sub is_involved_in_bug {
-    my ($self, $bug) = @_;
-    my $user_id    = $self->id;
-    my $user_login = $self->login;
+  my ($self, $bug) = @_;
+  my $user_id    = $self->id;
+  my $user_login = $self->login;
 
-    return unless $user_id;
-    return 1 if $user_id == $bug->assigned_to->id;
-    return 1 if $user_id == $bug->reporter->id;
+  return unless $user_id;
+  return 1 if $user_id == $bug->assigned_to->id;
+  return 1 if $user_id == $bug->reporter->id;
 
-    if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) {
-        return 1 if $user_id == $bug->qa_contact->id;
-    }
+  if (Bugzilla->params->{'useqacontact'} and $bug->qa_contact) {
+    return 1 if $user_id == $bug->qa_contact->id;
+  }
 
-    # BMO - Bug mentors are considered involved with the bug
-    return 1 if $bug->is_mentor($self);
+  # BMO - Bug mentors are considered involved with the bug
+  return 1 if $bug->is_mentor($self);
 
-    return unless $bug->cc;
-    return any { $user_login eq $_ } @{ $bug->cc };
+  return unless $bug->cc;
+  return any { $user_login eq $_ } @{$bug->cc};
 }
 
 # It turns out that calling ->id on objects a few hundred thousand
@@ -1257,228 +1293,239 @@ sub is_involved_in_bug {
 # when profiling xt/search.t.) So we cache the group ids separately from
 # groups for functions that need the group ids.
 sub _group_ids {
-    my ($self) = @_;
-    $self->{group_ids} ||= [map { $_->id } @{ $self->groups }];
-    return $self->{group_ids};
+  my ($self) = @_;
+  $self->{group_ids} ||= [map { $_->id } @{$self->groups}];
+  return $self->{group_ids};
 }
 
 sub groups_as_string {
-    my $self = shift;
-    my $ids = $self->_group_ids;
-    return scalar(@$ids) ? join(',', @$ids) : '-1';
+  my $self = shift;
+  my $ids  = $self->_group_ids;
+  return scalar(@$ids) ? join(',', @$ids) : '-1';
 }
 
 sub groups_in_sql {
-    my ($self, $field) = @_;
-    $field ||= 'group_id';
-    my $ids = $self->_group_ids;
-    $ids = [-1] if !scalar @$ids;
-    return Bugzilla->dbh->sql_in($field, $ids);
+  my ($self, $field) = @_;
+  $field ||= 'group_id';
+  my $ids = $self->_group_ids;
+  $ids = [-1] if !scalar @$ids;
+  return Bugzilla->dbh->sql_in($field, $ids);
 }
 
 sub groups_owned {
-    my $self = shift;
-    return $self->{groups_owned} //= Bugzilla::Group->match({ owner_user_id => $self->id });
+  my $self = shift;
+  return $self->{groups_owned}
+    //= Bugzilla::Group->match({owner_user_id => $self->id});
 }
 
 sub bless_groups {
-    my $self = shift;
+  my $self = shift;
 
-    return $self->{'bless_groups'} if defined $self->{'bless_groups'};
-    return [] unless $self->id;
+  return $self->{'bless_groups'} if defined $self->{'bless_groups'};
+  return [] unless $self->id;
 
-    if ($self->in_group('admin')) {
-        # Users having admin permissions may bless all groups.
-        $self->{'bless_groups'} = [Bugzilla::Group->get_all];
-        return $self->{'bless_groups'};
-    }
+  if ($self->in_group('admin')) {
 
-    if (Bugzilla->params->{usevisibilitygroups}
-        && !$self->visible_groups_inherited) {
-        return [];
-    }
+    # Users having admin permissions may bless all groups.
+    $self->{'bless_groups'} = [Bugzilla::Group->get_all];
+    return $self->{'bless_groups'};
+  }
+
+  if (Bugzilla->params->{usevisibilitygroups} && !$self->visible_groups_inherited)
+  {
+    return [];
+  }
 
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
 
-    # Get all groups for the user where they have direct bless privileges.
-    my $query = "
+  # Get all groups for the user where they have direct bless privileges.
+  my $query = "
         SELECT DISTINCT group_id
           FROM user_group_map
          WHERE user_id = ?
                AND isbless = 1";
-    if (Bugzilla->params->{usevisibilitygroups}) {
-        $query .= " AND "
-            . $dbh->sql_in('group_id', $self->visible_groups_inherited);
-    }
-
-    # Get all groups for the user where they are a member of a group that
-    # inherits bless privs.
-    my @group_ids = map { $_->id } @{ $self->groups };
-    if (@group_ids) {
-        $query .= "
+  if (Bugzilla->params->{usevisibilitygroups}) {
+    $query .= " AND " . $dbh->sql_in('group_id', $self->visible_groups_inherited);
+  }
+
+  # Get all groups for the user where they are a member of a group that
+  # inherits bless privs.
+  my @group_ids = map { $_->id } @{$self->groups};
+  if (@group_ids) {
+    $query .= "
             UNION
             SELECT DISTINCT grantor_id
             FROM group_group_map
             WHERE grant_type = " . GROUP_BLESS . "
                 AND " . $dbh->sql_in('member_id', \@group_ids);
-        if (Bugzilla->params->{usevisibilitygroups}) {
-            $query .= " AND "
-                . $dbh->sql_in('grantor_id', $self->visible_groups_inherited);
-        }
+    if (Bugzilla->params->{usevisibilitygroups}) {
+      $query .= " AND " . $dbh->sql_in('grantor_id', $self->visible_groups_inherited);
     }
+  }
 
-    my $ids = $dbh->selectcol_arrayref($query, undef, $self->id);
-    return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids);
+  my $ids = $dbh->selectcol_arrayref($query, undef, $self->id);
+  return $self->{bless_groups} = Bugzilla::Group->new_from_list($ids);
 }
 
 sub in_group {
-    my ($self, $group, $product_id) = @_;
-    $group = $group->name if blessed $group;
-    $self->{in_group} //= { map { $_->name  => $_ } @{ $self->groups } };
+  my ($self, $group, $product_id) = @_;
+  $group = $group->name if blessed $group;
+  $self->{in_group} //= {map { $_->name => $_ } @{$self->groups}};
 
-    if ($self->{in_group}{$group}) {
-        return 1;
-    }
-    elsif ($product_id && detaint_natural($product_id)) {
-        # Make sure $group exists on a per-product basis.
-        return 0 unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
-
-        $self->{"product_$product_id"} = {} unless exists $self->{"product_$product_id"};
-        if (!defined $self->{"product_$product_id"}->{$group}) {
-            my $dbh = Bugzilla->dbh;
-            my $in_group = $dbh->selectrow_array(
-                           "SELECT 1
+  if ($self->{in_group}{$group}) {
+    return 1;
+  }
+  elsif ($product_id && detaint_natural($product_id)) {
+
+    # Make sure $group exists on a per-product basis.
+    return 0 unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES);
+
+    $self->{"product_$product_id"} = {}
+      unless exists $self->{"product_$product_id"};
+    if (!defined $self->{"product_$product_id"}->{$group}) {
+      my $dbh      = Bugzilla->dbh;
+      my $in_group = $dbh->selectrow_array(
+        "SELECT 1
                               FROM group_control_map
                              WHERE product_id = ?
                                    AND $group != 0
-                                   AND " . $self->groups_in_sql . ' ' .
-                              $dbh->sql_limit(1),
-                             undef, $product_id);
+                                   AND "
+          . $self->groups_in_sql . ' ' . $dbh->sql_limit(1), undef, $product_id
+      );
 
-            $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0;
-        }
-        return $self->{"product_$product_id"}->{$group};
+      $self->{"product_$product_id"}->{$group} = $in_group ? 1 : 0;
     }
-    # If we come here, then the user is not in the requested group.
-    return 0;
+    return $self->{"product_$product_id"}->{$group};
+  }
+
+  # If we come here, then the user is not in the requested group.
+  return 0;
 }
 
 sub in_group_id {
-    my ($self, $id) = @_;
-    return grep($_->id == $id, @{ $self->groups }) ? 1 : 0;
+  my ($self, $id) = @_;
+  return grep($_->id == $id, @{$self->groups}) ? 1 : 0;
 }
 
 # This is a helper to get all groups which have an icon to be displayed
 # besides the name of the commenter.
 sub groups_with_icon {
-    my $self = shift;
+  my $self = shift;
 
-    return $self->{groups_with_icon} //= [grep { $_->icon_url } @{ $self->groups }];
+  return $self->{groups_with_icon} //= [grep { $_->icon_url } @{$self->groups}];
 }
 
 sub get_products_by_permission {
-    my ($self, $group) = @_;
-    # Make sure $group exists on a per-product basis.
-    return [] unless (grep {$_ eq $group} PER_PRODUCT_PRIVILEGES);
+  my ($self, $group) = @_;
 
-    my $product_ids = Bugzilla->dbh->selectcol_arrayref(
-                          "SELECT DISTINCT product_id
+  # Make sure $group exists on a per-product basis.
+  return [] unless (grep { $_ eq $group } PER_PRODUCT_PRIVILEGES);
+
+  my $product_ids = Bugzilla->dbh->selectcol_arrayref(
+    "SELECT DISTINCT product_id
                              FROM group_control_map
                             WHERE $group != 0
-                              AND " . $self->groups_in_sql);
+                              AND " . $self->groups_in_sql
+  );
 
-    # No need to go further if the user has no "special" privs.
-    return [] unless scalar(@$product_ids);
-    my %product_map = map { $_ => 1 } @$product_ids;
+  # No need to go further if the user has no "special" privs.
+  return [] unless scalar(@$product_ids);
+  my %product_map = map { $_ => 1 } @$product_ids;
 
-    # We will restrict the list to products the user can see.
-    my $selectable_products = $self->get_selectable_products;
-    my @products = grep { $product_map{$_->id} } @$selectable_products;
-    return \@products;
+  # We will restrict the list to products the user can see.
+  my $selectable_products = $self->get_selectable_products;
+  my @products = grep { $product_map{$_->id} } @$selectable_products;
+  return \@products;
 }
 
 sub can_see_user {
-    my ($self, $otherUser) = @_;
-    my $query;
+  my ($self, $otherUser) = @_;
+  my $query;
 
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        # If the user can see no groups, then no users are visible either.
-        my $visibleGroups = $self->visible_groups_as_string() || return 0;
-        $query = qq{SELECT COUNT(DISTINCT userid)
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+
+    # If the user can see no groups, then no users are visible either.
+    my $visibleGroups = $self->visible_groups_as_string() || return 0;
+    $query = qq{SELECT COUNT(DISTINCT userid)
                     FROM profiles, user_group_map
                     WHERE userid = ?
                     AND user_id = userid
                     AND isbless = 0
                     AND group_id IN ($visibleGroups)
                    };
-    } else {
-        $query = qq{SELECT COUNT(userid)
+  }
+  else {
+    $query = qq{SELECT COUNT(userid)
                     FROM profiles
                     WHERE userid = ?
                    };
-    }
-    return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id);
+  }
+  return Bugzilla->dbh->selectrow_array($query, undef, $otherUser->id);
 }
 
 sub can_edit_product {
-    my ($self, $prod_id) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($self, $prod_id) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    my $has_external_groups =
-      $dbh->selectrow_array('SELECT 1
+  my $has_external_groups = $dbh->selectrow_array(
+    'SELECT 1
                                FROM group_control_map
                               WHERE product_id = ?
                                 AND canedit != 0
-                                AND group_id NOT IN(' . $self->groups_as_string . ')',
-                             undef, $prod_id);
+                                AND group_id NOT IN('
+      . $self->groups_as_string . ')', undef, $prod_id
+  );
 
-    return !$has_external_groups;
+  return !$has_external_groups;
 }
 
 sub can_see_bug {
-    my ($self, $bug_id) = @_;
-    return @{ $self->visible_bugs([$bug_id]) } ? 1 : 0;
+  my ($self, $bug_id) = @_;
+  return @{$self->visible_bugs([$bug_id])} ? 1 : 0;
 }
 
 sub visible_bugs {
-    my ($self, $bugs) = @_;
-    # Allow users to pass in Bug objects and bug ids both.
-    my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs;
-
-    # We only check the visibility of bugs that we haven't
-    # checked yet.
-    # Bugzilla::Bug->update automatically removes updated bugs
-    # from the cache to force them to be checked again.
-    my $visible_cache = $self->{_visible_bugs_cache} ||= {};
-    my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids);
-
-    if (@check_ids) {
-        my $dbh = Bugzilla->dbh;
-        my $user_id = $self->id;
-
-        foreach my $id (@check_ids) {
-            my $orig_id = $id;
-            detaint_natural($id)
-                || ThrowCodeError('param_must_be_numeric', { param    => $orig_id,
-                                                             function => 'Bugzilla::User->visible_bugs'});
-        }
+  my ($self, $bugs) = @_;
 
-        my $sth;
-        # Speed up the can_see_bug case.
-        if (scalar(@check_ids) == 1) {
-            $sth = $self->{_sth_one_visible_bug};
-        }
-        $sth ||= $dbh->prepare(
-            # This checks for groups that the bug is in that the user
-            # *isn't* in. Then, in the Perl code below, we check if
-            # the user can otherwise access the bug (for example, by being
-            # the assignee or QA Contact).
-            #
-            # The DISTINCT exists because the bug could be in *several*
-            # groups that the user isn't in, but they will all return the
-            # same result for bug_group_map.bug_id (so DISTINCT filters
-            # out duplicate rows).
-            "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact,
+  # Allow users to pass in Bug objects and bug ids both.
+  my @bug_ids = map { blessed $_ ? $_->id : $_ } @$bugs;
+
+  # We only check the visibility of bugs that we haven't
+  # checked yet.
+  # Bugzilla::Bug->update automatically removes updated bugs
+  # from the cache to force them to be checked again.
+  my $visible_cache = $self->{_visible_bugs_cache} ||= {};
+  my @check_ids = grep(!exists $visible_cache->{$_}, @bug_ids);
+
+  if (@check_ids) {
+    my $dbh     = Bugzilla->dbh;
+    my $user_id = $self->id;
+
+    foreach my $id (@check_ids) {
+      my $orig_id = $id;
+      detaint_natural($id)
+        || ThrowCodeError('param_must_be_numeric',
+        {param => $orig_id, function => 'Bugzilla::User->visible_bugs'});
+    }
+
+    my $sth;
+
+    # Speed up the can_see_bug case.
+    if (scalar(@check_ids) == 1) {
+      $sth = $self->{_sth_one_visible_bug};
+    }
+    $sth ||= $dbh->prepare(
+
+      # This checks for groups that the bug is in that the user
+      # *isn't* in. Then, in the Perl code below, we check if
+      # the user can otherwise access the bug (for example, by being
+      # the assignee or QA Contact).
+      #
+      # The DISTINCT exists because the bug could be in *several*
+      # groups that the user isn't in, but they will all return the
+      # same result for bug_group_map.bug_id (so DISTINCT filters
+      # out duplicate rows).
+      "SELECT DISTINCT bugs.bug_id, reporter, assigned_to, qa_contact,
                     reporter_accessible, cclist_accessible, cc.who,
                     bug_group_map.bug_id
                FROM bugs
@@ -1488,1107 +1535,1173 @@ sub visible_bugs {
                     LEFT JOIN bug_group_map
                               ON bugs.bug_id = bug_group_map.bug_id
                                  AND bug_group_map.group_id NOT IN ("
-                                     . $self->groups_as_string . ')
+        . $self->groups_as_string . ')
               WHERE bugs.bug_id IN (' . join(',', ('?') x @check_ids) . ')
-                    AND creation_ts IS NOT NULL ');
-        if (scalar(@check_ids) == 1) {
-            $self->{_sth_one_visible_bug} = $sth;
-        }
+                    AND creation_ts IS NOT NULL '
+    );
+    if (scalar(@check_ids) == 1) {
+      $self->{_sth_one_visible_bug} = $sth;
+    }
 
-        $sth->execute(@check_ids);
-        my $use_qa_contact = Bugzilla->params->{'useqacontact'};
-        while (my $row = $sth->fetchrow_arrayref) {
-            my ($bug_id, $reporter, $owner, $qacontact, $reporter_access,
-                $cclist_access, $isoncclist, $missinggroup) = @$row;
-            $visible_cache->{$bug_id} ||=
-                ((($reporter == $user_id) && $reporter_access)
-                 || ($use_qa_contact
-                     && $qacontact && ($qacontact == $user_id))
-                 || ($owner == $user_id)
-                 || ($isoncclist && $cclist_access)
-                 || !$missinggroup) ? 1 : 0;
-        }
+    $sth->execute(@check_ids);
+    my $use_qa_contact = Bugzilla->params->{'useqacontact'};
+    while (my $row = $sth->fetchrow_arrayref) {
+      my ($bug_id, $reporter, $owner, $qacontact, $reporter_access, $cclist_access,
+        $isoncclist, $missinggroup)
+        = @$row;
+      $visible_cache->{$bug_id}
+        ||= ((($reporter == $user_id) && $reporter_access)
+          || ($use_qa_contact && $qacontact && ($qacontact == $user_id))
+          || ($owner == $user_id)
+          || ($isoncclist && $cclist_access)
+          || !$missinggroup) ? 1 : 0;
     }
+  }
 
-    return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
+  return [grep { $visible_cache->{blessed $_ ? $_->id : $_} } @$bugs];
 }
 
 sub clear_product_cache {
-    my $self = shift;
-    delete $self->{enterable_products};
-    delete $self->{selectable_products};
-    delete $self->{selectable_classifications};
+  my $self = shift;
+  delete $self->{enterable_products};
+  delete $self->{selectable_products};
+  delete $self->{selectable_classifications};
 }
 
 sub can_see_product {
-    my ($self, $product_name) = @_;
+  my ($self, $product_name) = @_;
 
-    return any { $_->name eq $product_name } @{$self->get_selectable_products};
+  return any { $_->name eq $product_name } @{$self->get_selectable_products};
 }
 
 sub get_selectable_products {
-    my $self = shift;
-    my $class_id = shift;
-    my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id;
-
-    if (!defined $self->{selectable_products}) {
-        my $query = "SELECT id " .
-                    "  FROM products " .
-                 "LEFT JOIN group_control_map " .
-                        "ON group_control_map.product_id = products.id " .
-                      " AND group_control_map.membercontrol = " . CONTROLMAPMANDATORY .
-                      " AND group_id NOT IN(" . $self->groups_as_string . ") " .
-                  "   WHERE group_id IS NULL " .
-                  "ORDER BY name";
-
-        my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query);
-        $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
-    }
-
-    # Restrict the list of products to those being in the classification, if any.
-    if ($class_restricted) {
-        return [grep {$_->classification_id == $class_id} @{$self->{selectable_products}}];
-    }
-    # If we come here, then we want all selectable products.
-    return $self->{selectable_products};
+  my $self             = shift;
+  my $class_id         = shift;
+  my $class_restricted = Bugzilla->params->{'useclassification'} && $class_id;
+
+  if (!defined $self->{selectable_products}) {
+    my $query
+      = "SELECT id "
+      . "  FROM products "
+      . "LEFT JOIN group_control_map "
+      . "ON group_control_map.product_id = products.id "
+      . " AND group_control_map.membercontrol = "
+      . CONTROLMAPMANDATORY
+      . " AND group_id NOT IN("
+      . $self->groups_as_string . ") "
+      . "   WHERE group_id IS NULL "
+      . "ORDER BY name";
+
+    my $prod_ids = Bugzilla->dbh->selectcol_arrayref($query);
+    $self->{selectable_products} = Bugzilla::Product->new_from_list($prod_ids);
+  }
+
+  # Restrict the list of products to those being in the classification, if any.
+  if ($class_restricted) {
+    return [grep { $_->classification_id == $class_id }
+        @{$self->{selectable_products}}];
+  }
+
+  # If we come here, then we want all selectable products.
+  return $self->{selectable_products};
 }
 
 sub get_selectable_classifications {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    if (!defined $self->{selectable_classifications}) {
-        my $products = $self->get_selectable_products;
-        my %class_ids = map { $_->classification_id => 1 } @$products;
+  if (!defined $self->{selectable_classifications}) {
+    my $products = $self->get_selectable_products;
+    my %class_ids = map { $_->classification_id => 1 } @$products;
 
-        $self->{selectable_classifications} = Bugzilla::Classification->new_from_list([keys %class_ids]);
-    }
-    return $self->{selectable_classifications};
+    $self->{selectable_classifications}
+      = Bugzilla::Classification->new_from_list([keys %class_ids]);
+  }
+  return $self->{selectable_classifications};
 }
 
 sub can_enter_product {
-    my ($self, $input, $warn) = @_;
-    my $dbh = Bugzilla->dbh;
-    $warn ||= 0;
-
-    $input = trim($input) if !ref $input;
-    if (!defined $input or $input eq '') {
-        return unless $warn == THROW_ERROR;
-        ThrowUserError('object_not_specified',
-                       { class => 'Bugzilla::Product' });
-    }
+  my ($self, $input, $warn) = @_;
+  my $dbh = Bugzilla->dbh;
+  $warn ||= 0;
 
-    if (!scalar @{ $self->get_enterable_products }) {
-        return unless $warn == THROW_ERROR;
-        ThrowUserError('no_products');
-    }
+  $input = trim($input) if !ref $input;
+  if (!defined $input or $input eq '') {
+    return unless $warn == THROW_ERROR;
+    ThrowUserError('object_not_specified', {class => 'Bugzilla::Product'});
+  }
 
-    my $product = blessed($input) ? $input
-                                  : new Bugzilla::Product({ name => $input });
-    my $can_enter =
-      $product && grep($_->name eq $product->name,
-                       @{ $self->get_enterable_products });
+  if (!scalar @{$self->get_enterable_products}) {
+    return unless $warn == THROW_ERROR;
+    ThrowUserError('no_products');
+  }
 
-    return $product if $can_enter;
+  my $product
+    = blessed($input) ? $input : new Bugzilla::Product({name => $input});
+  my $can_enter = $product
+    && grep($_->name eq $product->name, @{$self->get_enterable_products});
 
-    return 0 unless $warn == THROW_ERROR;
+  return $product if $can_enter;
 
-    # Check why access was denied. These checks are slow,
-    # but that's fine, because they only happen if we fail.
+  return 0 unless $warn == THROW_ERROR;
 
-    # We don't just use $product->name for error messages, because if it
-    # changes case from $input, then that's a clue that the product does
-    # exist but is hidden.
-    my $name = blessed($input) ? $input->name : $input;
+  # Check why access was denied. These checks are slow,
+  # but that's fine, because they only happen if we fail.
 
-    # The product could not exist or you could be denied...
-    if (!$product || !$product->user_has_access($self)) {
-        ThrowUserError('entry_access_denied', { product => $name });
-    }
-    # It could be closed for bug entry...
-    elsif (!$product->is_active) {
-        ThrowUserError('product_disabled', { product => $product });
-    }
-    # It could have no components...
-    elsif (!@{$product->components}
-           || !grep { $_->is_active } @{$product->components})
-    {
-        ThrowUserError('missing_component', { product => $product });
-    }
-    # It could have no versions...
-    elsif (!@{$product->versions}
-           || !grep { $_->is_active } @{$product->versions})
-    {
-        ThrowUserError ('missing_version', { product => $product });
-    }
+  # We don't just use $product->name for error messages, because if it
+  # changes case from $input, then that's a clue that the product does
+  # exist but is hidden.
+  my $name = blessed($input) ? $input->name : $input;
+
+  # The product could not exist or you could be denied...
+  if (!$product || !$product->user_has_access($self)) {
+    ThrowUserError('entry_access_denied', {product => $name});
+  }
+
+  # It could be closed for bug entry...
+  elsif (!$product->is_active) {
+    ThrowUserError('product_disabled', {product => $product});
+  }
+
+  # It could have no components...
+  elsif (!@{$product->components}
+    || !grep { $_->is_active } @{$product->components})
+  {
+    ThrowUserError('missing_component', {product => $product});
+  }
+
+  # It could have no versions...
+  elsif (!@{$product->versions} || !grep { $_->is_active } @{$product->versions})
+  {
+    ThrowUserError('missing_version', {product => $product});
+  }
 
-    die "can_enter_product reached an unreachable location.";
+  die "can_enter_product reached an unreachable location.";
 }
 
 sub get_enterable_products {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (defined $self->{enterable_products}) {
-        return $self->{enterable_products};
-    }
+  if (defined $self->{enterable_products}) {
+    return $self->{enterable_products};
+  }
 
-     # All products which the user has "Entry" access to.
-     my $enterable_ids = $dbh->selectcol_arrayref(
-           'SELECT products.id FROM products
+  # All products which the user has "Entry" access to.
+  my $enterable_ids = $dbh->selectcol_arrayref(
+    'SELECT products.id FROM products
          LEFT JOIN group_control_map
                    ON group_control_map.product_id = products.id
                       AND group_control_map.entry != 0
                       AND group_id NOT IN (' . $self->groups_as_string . ')
             WHERE group_id IS NULL
-                  AND products.isactive = 1');
-
-    if (scalar @$enterable_ids) {
-        # And all of these products must have at least one component
-        # and one version.
-        $enterable_ids = $dbh->selectcol_arrayref(
-            'SELECT DISTINCT products.id FROM products
-              WHERE ' . $dbh->sql_in('products.id', $enterable_ids) .
-              ' AND products.id IN (SELECT DISTINCT components.product_id
+                  AND products.isactive = 1'
+  );
+
+  if (scalar @$enterable_ids) {
+
+    # And all of these products must have at least one component
+    # and one version.
+    $enterable_ids = $dbh->selectcol_arrayref(
+      'SELECT DISTINCT products.id FROM products
+              WHERE '
+        . $dbh->sql_in('products.id', $enterable_ids)
+        . ' AND products.id IN (SELECT DISTINCT components.product_id
                                       FROM components
                                      WHERE components.isactive = 1)
                 AND products.id IN (SELECT DISTINCT versions.product_id
                                       FROM versions
-                                     WHERE versions.isactive = 1)');
-    }
+                                     WHERE versions.isactive = 1)'
+    );
+  }
 
-    $self->{enterable_products} =
-         Bugzilla::Product->new_from_list($enterable_ids);
-    return $self->{enterable_products};
+  $self->{enterable_products} = Bugzilla::Product->new_from_list($enterable_ids);
+  return $self->{enterable_products};
 }
 
 sub can_access_product {
-    my ($self, $product) = @_;
-    my $product_name = blessed($product) ? $product->name : $product;
-    return scalar(grep {$_->name eq $product_name} @{$self->get_accessible_products});
+  my ($self, $product) = @_;
+  my $product_name = blessed($product) ? $product->name : $product;
+  return
+    scalar(grep { $_->name eq $product_name } @{$self->get_accessible_products});
 }
 
 sub get_accessible_products {
-    my $self = shift;
+  my $self = shift;
 
-    # Map the objects into a hash using the ids as keys
-    my %products = map { $_->id => $_ }
-                       @{$self->get_selectable_products},
-                       @{$self->get_enterable_products};
+  # Map the objects into a hash using the ids as keys
+  my %products = map { $_->id => $_ } @{$self->get_selectable_products},
+    @{$self->get_enterable_products};
 
-    return [ sort { $a->name cmp $b->name } values %products ];
+  return [sort { $a->name cmp $b->name } values %products];
 }
 
 sub check_can_admin_product {
-    my ($self, $product_name) = @_;
+  my ($self, $product_name) = @_;
 
-    # First make sure the product name is valid.
-    my $product = Bugzilla::Product->check($product_name);
+  # First make sure the product name is valid.
+  my $product = Bugzilla::Product->check($product_name);
 
-    ($self->in_group('editcomponents', $product->id)
-       && $self->can_see_product($product->name))
-         || ThrowUserError('product_admin_denied', {product => $product->name});
+  (      $self->in_group('editcomponents', $product->id)
+      && $self->can_see_product($product->name))
+    || ThrowUserError('product_admin_denied', {product => $product->name});
 
-    # Return the validated product object.
-    return $product;
+  # Return the validated product object.
+  return $product;
 }
 
 sub check_can_admin_flagtype {
-    my ($self, $flagtype_id) = @_;
-
-    my $flagtype = Bugzilla::FlagType->check({ id => $flagtype_id });
-    my $can_fully_edit = 1;
-
-    if (!$self->in_group('editcomponents')) {
-        my $products = $self->get_products_by_permission('editcomponents');
-        # You need editcomponents privs for at least one product to have
-        # a chance to edit the flagtype.
-        scalar(@$products)
-          || ThrowUserError('auth_failure', {group  => 'editcomponents',
-                                             action => 'edit',
-                                             object => 'flagtypes'});
-        my $can_admin = 0;
-        my $i = $flagtype->inclusions_as_hash;
-        my $e = $flagtype->exclusions_as_hash;
-
-        # If there is at least one product for which the user doesn't have
-        # editcomponents privs, then don't allow him to do everything with
-        # this flagtype, independently of whether this product is in the
-        # exclusion list or not.
-        my %product_ids;
-        map { $product_ids{$_->id} = 1 } @$products;
-        $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i;
-
-        unless ($e->{0}->{0}) {
-            foreach my $product (@$products) {
-                my $id = $product->id;
-                next if $e->{$id}->{0};
-                # If we are here, the product has not been explicitly excluded.
-                # Check whether it's explicitly included, or at least one of
-                # its components.
-                $can_admin = ($i->{0}->{0} || $i->{$id}->{0}
-                              || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}}));
-                last if $can_admin;
-            }
-        }
-        $can_admin || ThrowUserError('flag_type_not_editable', { flagtype => $flagtype });
-    }
-    return wantarray ? ($flagtype, $can_fully_edit) : $flagtype;
+  my ($self, $flagtype_id) = @_;
+
+  my $flagtype = Bugzilla::FlagType->check({id => $flagtype_id});
+  my $can_fully_edit = 1;
+
+  if (!$self->in_group('editcomponents')) {
+    my $products = $self->get_products_by_permission('editcomponents');
+
+    # You need editcomponents privs for at least one product to have
+    # a chance to edit the flagtype.
+    scalar(@$products)
+      || ThrowUserError('auth_failure',
+      {group => 'editcomponents', action => 'edit', object => 'flagtypes'});
+    my $can_admin = 0;
+    my $i         = $flagtype->inclusions_as_hash;
+    my $e         = $flagtype->exclusions_as_hash;
+
+    # If there is at least one product for which the user doesn't have
+    # editcomponents privs, then don't allow him to do everything with
+    # this flagtype, independently of whether this product is in the
+    # exclusion list or not.
+    my %product_ids;
+    map { $product_ids{$_->id} = 1 } @$products;
+    $can_fully_edit = 0 if grep { !$product_ids{$_} } keys %$i;
+
+    unless ($e->{0}->{0}) {
+      foreach my $product (@$products) {
+        my $id = $product->id;
+        next if $e->{$id}->{0};
+
+        # If we are here, the product has not been explicitly excluded.
+        # Check whether it's explicitly included, or at least one of
+        # its components.
+        $can_admin
+          = (  $i->{0}->{0}
+            || $i->{$id}->{0}
+            || scalar(grep { !$e->{$id}->{$_} } keys %{$i->{$id}}));
+        last if $can_admin;
+      }
+    }
+    $can_admin || ThrowUserError('flag_type_not_editable', {flagtype => $flagtype});
+  }
+  return wantarray ? ($flagtype, $can_fully_edit) : $flagtype;
 }
 
 sub can_change_flag {
-    my ($self, $flag_type, $old_status, $new_status) = @_;
+  my ($self, $flag_type, $old_status, $new_status) = @_;
 
-    # "old_status:new_status" => [OR conditions
-    state $flag_transitions = {
-        'X:-' => ['grant_group'],
-        'X:+' => ['grant_group'],
-        'X:?' => ['request_group'],
+  # "old_status:new_status" => [OR conditions
+  state $flag_transitions = {
+    'X:-' => ['grant_group'],
+    'X:+' => ['grant_group'],
+    'X:?' => ['request_group'],
 
-        '?:X' => ['request_group', 'is_setter'],
-        '?:-' => ['grant_group'],
-        '?:+' => ['grant_group'],
+    '?:X' => ['request_group', 'is_setter'],
+    '?:-' => ['grant_group'],
+    '?:+' => ['grant_group'],
 
-        '+:X' => ['grant_group'],
-        '+:-' => ['grant_group'],
-        '+:?' => ['grant_group'],
+    '+:X' => ['grant_group'],
+    '+:-' => ['grant_group'],
+    '+:?' => ['grant_group'],
 
-        '-:X' => ['grant_group'],
-        '-:+' => ['grant_group'],
-        '-:?' => ['grant_group'],
-    };
+    '-:X' => ['grant_group'],
+    '-:+' => ['grant_group'],
+    '-:?' => ['grant_group'],
+  };
 
-    return 1 if $new_status eq $old_status;
+  return 1 if $new_status eq $old_status;
 
-    my $action = "$old_status:$new_status";
-    my %bool = (
-        request_group => $self->can_request_flag($flag_type),
-        grant_group   => $self->can_set_flag($flag_type),
-        is_setter     => $self->id == Bugzilla->user->id,
-    );
+  my $action = "$old_status:$new_status";
+  my %bool   = (
+    request_group => $self->can_request_flag($flag_type),
+    grant_group   => $self->can_set_flag($flag_type),
+    is_setter     => $self->id == Bugzilla->user->id,
+  );
 
-    my $cond = $flag_transitions->{$action};
-    if ($cond) {
-        if (any { $bool{ $_ } } @$cond) {
-            return 1;
-        }
-        else {
-            return 0;
-        }
+  my $cond = $flag_transitions->{$action};
+  if ($cond) {
+    if (any { $bool{$_} } @$cond) {
+      return 1;
     }
     else {
-        warn "unknown flag transition blocked: $action";
-        return 0;
+      return 0;
     }
+  }
+  else {
+    warn "unknown flag transition blocked: $action";
+    return 0;
+  }
 }
 
 sub can_request_flag {
-    my ($self, $flag_type) = @_;
+  my ($self, $flag_type) = @_;
 
-    return ($self->can_set_flag($flag_type)
-            || !$flag_type->request_group_id
-            || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0;
+  return ($self->can_set_flag($flag_type)
+      || !$flag_type->request_group_id
+      || $self->in_group_id($flag_type->request_group_id)) ? 1 : 0;
 }
 
 sub can_set_flag {
-    my ($self, $flag_type) = @_;
+  my ($self, $flag_type) = @_;
 
-    return (!$flag_type->grant_group_id
-            || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0;
+  return (!$flag_type->grant_group_id
+      || $self->in_group_id($flag_type->grant_group_id)) ? 1 : 0;
 }
 
 sub can_unset_flag {
-    my ($self, $flag_type, $flag_status) = @_;
-    return 1 if !$flag_type->grant_group_id;
-    return 1 if ($flag_status ne '+' && $flag_status ne '-');
-    return $self->in_group_id($flag_type->grant_group_id) ? 1 : 0;
+  my ($self, $flag_type, $flag_status) = @_;
+  return 1 if !$flag_type->grant_group_id;
+  return 1 if ($flag_status ne '+' && $flag_status ne '-');
+  return $self->in_group_id($flag_type->grant_group_id) ? 1 : 0;
 }
 
 # visible_groups_inherited returns a reference to a list of all the groups
 # whose members are visible to this user.
 sub visible_groups_inherited {
-    my $self = shift;
-    return $self->{visible_groups_inherited} if defined $self->{visible_groups_inherited};
-    return [] unless $self->id;
-    my @visgroups = @{$self->visible_groups_direct};
-    @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)};
-    $self->{visible_groups_inherited} = \@visgroups;
-    return $self->{visible_groups_inherited};
+  my $self = shift;
+  return $self->{visible_groups_inherited}
+    if defined $self->{visible_groups_inherited};
+  return [] unless $self->id;
+  my @visgroups = @{$self->visible_groups_direct};
+  @visgroups = @{Bugzilla::Group->flatten_group_membership(@visgroups)};
+  $self->{visible_groups_inherited} = \@visgroups;
+  return $self->{visible_groups_inherited};
 }
 
 # visible_groups_direct returns a reference to a list of all the groups that
 # are visible to this user.
 sub visible_groups_direct {
-    my $self = shift;
-    my @visgroups = ();
-    return $self->{visible_groups_direct} if defined $self->{visible_groups_direct};
-    return [] unless $self->id;
+  my $self      = shift;
+  my @visgroups = ();
+  return $self->{visible_groups_direct} if defined $self->{visible_groups_direct};
+  return [] unless $self->id;
 
-    my $dbh = Bugzilla->dbh;
-    my $sth;
+  my $dbh = Bugzilla->dbh;
+  my $sth;
 
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        $sth = $dbh->prepare("SELECT DISTINCT grantor_id
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+    $sth = $dbh->prepare(
+      "SELECT DISTINCT grantor_id
                                  FROM group_group_map
                                 WHERE " . $self->groups_in_sql('member_id') . "
-                                  AND grant_type=" . GROUP_VISIBLE);
-    }
-    else {
-        # All groups are visible if usevisibilitygroups is off.
-        $sth = $dbh->prepare('SELECT id FROM groups');
-    }
-    $sth->execute();
+                                  AND grant_type=" . GROUP_VISIBLE
+    );
+  }
+  else {
+    # All groups are visible if usevisibilitygroups is off.
+    $sth = $dbh->prepare('SELECT id FROM groups');
+  }
+  $sth->execute();
 
-    while (my ($row) = $sth->fetchrow_array) {
-        push @visgroups,$row;
-    }
-    $self->{visible_groups_direct} = \@visgroups;
+  while (my ($row) = $sth->fetchrow_array) {
+    push @visgroups, $row;
+  }
+  $self->{visible_groups_direct} = \@visgroups;
 
-    return $self->{visible_groups_direct};
+  return $self->{visible_groups_direct};
 }
 
 sub visible_groups_as_string {
-    my $self = shift;
-    return join(', ', @{$self->visible_groups_inherited()});
+  my $self = shift;
+  return join(', ', @{$self->visible_groups_inherited()});
 }
 
 # This function defines the groups a user may share a query with.
 # More restrictive sites may want to build this reference to a list of group IDs
 # from bless_groups instead of mirroring visible_groups_inherited, perhaps.
 sub queryshare_groups {
-    my $self = shift;
-    my @queryshare_groups;
-
-    return $self->{queryshare_groups} if defined $self->{queryshare_groups};
-
-    if ($self->in_group(Bugzilla->params->{'querysharegroup'})) {
-        # We want to be allowed to share with groups we're in only.
-        # If usevisibilitygroups is on, then we need to restrict this to groups
-        # we may see.
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            foreach(@{$self->visible_groups_inherited()}) {
-                next unless $self->in_group_id($_);
-                push(@queryshare_groups, $_);
-            }
-        }
-        else {
-            @queryshare_groups = @{ $self->_group_ids };
-        }
+  my $self = shift;
+  my @queryshare_groups;
+
+  return $self->{queryshare_groups} if defined $self->{queryshare_groups};
+
+  if ($self->in_group(Bugzilla->params->{'querysharegroup'})) {
+
+    # We want to be allowed to share with groups we're in only.
+    # If usevisibilitygroups is on, then we need to restrict this to groups
+    # we may see.
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      foreach (@{$self->visible_groups_inherited()}) {
+        next unless $self->in_group_id($_);
+        push(@queryshare_groups, $_);
+      }
     }
+    else {
+      @queryshare_groups = @{$self->_group_ids};
+    }
+  }
 
-    return $self->{queryshare_groups} = \@queryshare_groups;
+  return $self->{queryshare_groups} = \@queryshare_groups;
 }
 
 sub queryshare_groups_as_string {
-    my $self = shift;
-    return join(', ', @{$self->queryshare_groups()});
+  my $self = shift;
+  return join(', ', @{$self->queryshare_groups()});
 }
 
 sub derive_regexp_groups {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    my $id = $self->id;
-    return unless $id;
+  my $id = $self->id;
+  return unless $id;
 
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
 
-    my $sth;
+  my $sth;
 
-    # add derived records for any matching regexps
+  # add derived records for any matching regexps
 
-    $sth = $dbh->prepare("SELECT id, userregexp, user_group_map.group_id
+  $sth = $dbh->prepare(
+    "SELECT id, userregexp, user_group_map.group_id
                             FROM groups
                        LEFT JOIN user_group_map
                               ON groups.id = user_group_map.group_id
                              AND user_group_map.user_id = ?
-                             AND user_group_map.grant_type = ?");
-    $sth->execute($id, GRANT_REGEXP);
+                             AND user_group_map.grant_type = ?"
+  );
+  $sth->execute($id, GRANT_REGEXP);
 
-    my $group_insert = $dbh->prepare(q{INSERT INTO user_group_map
+  my $group_insert = $dbh->prepare(
+    q{INSERT INTO user_group_map
                                        (user_id, group_id, isbless, grant_type)
-                                       VALUES (?, ?, 0, ?)});
-    my $group_delete = $dbh->prepare(q{DELETE FROM user_group_map
+                                       VALUES (?, ?, 0, ?)}
+  );
+  my $group_delete = $dbh->prepare(
+    q{DELETE FROM user_group_map
                                        WHERE user_id = ?
                                          AND group_id = ?
                                          AND isbless = 0
-                                         AND grant_type = ?});
-    while (my ($group, $regexp, $present) = $sth->fetchrow_array()) {
-        if (($regexp ne '') && ($self->login =~ m/$regexp/i)) {
-            $group_insert->execute($id, $group, GRANT_REGEXP) unless $present;
-        } else {
-            $group_delete->execute($id, $group, GRANT_REGEXP) if $present;
-        }
+                                         AND grant_type = ?}
+  );
+  while (my ($group, $regexp, $present) = $sth->fetchrow_array()) {
+    if (($regexp ne '') && ($self->login =~ m/$regexp/i)) {
+      $group_insert->execute($id, $group, GRANT_REGEXP) unless $present;
+    }
+    else {
+      $group_delete->execute($id, $group, GRANT_REGEXP) if $present;
     }
+  }
 
-    Bugzilla->memcached->clear_config({ key => "user_groups.$id" });
+  Bugzilla->memcached->clear_config({key => "user_groups.$id"});
 }
 
 sub product_responsibilities {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    return $self->{'product_resp'} if defined $self->{'product_resp'};
-    return [] unless $self->id;
+  return $self->{'product_resp'} if defined $self->{'product_resp'};
+  return [] unless $self->id;
 
-    my $list = $dbh->selectall_arrayref('SELECT components.product_id, components.id
+  my $list = $dbh->selectall_arrayref(
+    'SELECT components.product_id, components.id
                                            FROM components
                                            LEFT JOIN component_cc
                                            ON components.id = component_cc.component_id
                                           WHERE components.initialowner = ?
                                              OR components.initialqacontact = ?
                                              OR component_cc.user_id = ?',
-                                  {Slice => {}}, ($self->id, $self->id, $self->id));
-
-    unless ($list) {
-        $self->{'product_resp'} = [];
-        return $self->{'product_resp'};
-    }
+    {Slice => {}}, ($self->id, $self->id, $self->id)
+  );
 
-    my @prod_ids = map {$_->{'product_id'}} @$list;
-    my $products = Bugzilla::Product->new_from_list(\@prod_ids);
-    # We cannot |use| it, because Component.pm already |use|s User.pm.
-    require Bugzilla::Component;
-    my @comp_ids = map {$_->{'id'}} @$list;
-    my $components = Bugzilla::Component->new_from_list(\@comp_ids);
-
-    my @prod_list;
-    # @$products is already sorted alphabetically.
-    foreach my $prod (@$products) {
-        # We use @components instead of $prod->components because we only want
-        # components where the user is either the default assignee or QA contact.
-        push(@prod_list, {product    => $prod,
-                          components => [grep {$_->product_id == $prod->id} @$components]});
-    }
-    $self->{'product_resp'} = \@prod_list;
+  unless ($list) {
+    $self->{'product_resp'} = [];
     return $self->{'product_resp'};
+  }
+
+  my @prod_ids = map { $_->{'product_id'} } @$list;
+  my $products = Bugzilla::Product->new_from_list(\@prod_ids);
+
+  # We cannot |use| it, because Component.pm already |use|s User.pm.
+  require Bugzilla::Component;
+  my @comp_ids = map { $_->{'id'} } @$list;
+  my $components = Bugzilla::Component->new_from_list(\@comp_ids);
+
+  my @prod_list;
+
+  # @$products is already sorted alphabetically.
+  foreach my $prod (@$products) {
+
+    # We use @components instead of $prod->components because we only want
+    # components where the user is either the default assignee or QA contact.
+    push(
+      @prod_list,
+      {
+        product    => $prod,
+        components => [grep { $_->product_id == $prod->id } @$components]
+      }
+    );
+  }
+  $self->{'product_resp'} = \@prod_list;
+  return $self->{'product_resp'};
 }
 
 sub can_bless {
-    my $self = shift;
+  my $self = shift;
 
-    if (!scalar(@_)) {
-        # If we're called without an argument, just return
-        # whether or not we can bless at all.
-        return scalar(@{ $self->bless_groups }) ? 1 : 0;
-    }
+  if (!scalar(@_)) {
 
-    # Otherwise, we're checking a specific group
-    my $group_id = shift;
-    return grep($_->id == $group_id, @{ $self->bless_groups }) ? 1 : 0;
+    # If we're called without an argument, just return
+    # whether or not we can bless at all.
+    return scalar(@{$self->bless_groups}) ? 1 : 0;
+  }
+
+  # Otherwise, we're checking a specific group
+  my $group_id = shift;
+  return grep($_->id == $group_id, @{$self->bless_groups}) ? 1 : 0;
 }
 
 sub match {
-    # Generates a list of users whose login name (email address) or real name
-    # matches a substring or wildcard.
-    # This is also called if matches are disabled (for error checking), but
-    # in this case only the exact match code will end up running.
-
-    # $str contains the string to match, while $limit contains the
-    # maximum number of records to retrieve.
-    my ($str, $limit, $exclude_disabled) = @_;
-    my $user = Bugzilla->user;
-    my $dbh = Bugzilla->dbh;
 
-    $str = trim($str);
+  # Generates a list of users whose login name (email address) or real name
+  # matches a substring or wildcard.
+  # This is also called if matches are disabled (for error checking), but
+  # in this case only the exact match code will end up running.
 
-    my @users = ();
-    return \@users if $str =~ /^\s*$/;
+  # $str contains the string to match, while $limit contains the
+  # maximum number of records to retrieve.
+  my ($str, $limit, $exclude_disabled) = @_;
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
 
-    # The search order is wildcards, then exact match, then substring search.
-    # Wildcard matching is skipped if there is no '*', and exact matches will
-    # not (?) have a '*' in them.  If any search comes up with something, the
-    # ones following it will not execute.
+  $str = trim($str);
 
-    # first try wildcards
-    my $wildstr = $str;
+  my @users = ();
+  return \@users if $str =~ /^\s*$/;
 
-    # Do not do wildcards if there is no '*' in the string.
-    if ($wildstr =~ s/\*/\%/g && $user->id) {
-        # Build the query.
-        trick_taint($wildstr);
-        my $query  = "SELECT DISTINCT userid FROM profiles ";
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            $query .= "INNER JOIN user_group_map
-                               ON user_group_map.user_id = profiles.userid ";
-        }
-        $query .= "WHERE ("
-            . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR " .
-              $dbh->sql_istrcmp('realname', '?', "LIKE") . ") ";
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            $query .= "AND isbless = 0 " .
-                      "AND group_id IN(" .
-                      join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
-        }
-        $query    .= " AND is_enabled = 1 " if $exclude_disabled;
-        $query    .= $dbh->sql_limit($limit) if $limit;
+  # The search order is wildcards, then exact match, then substring search.
+  # Wildcard matching is skipped if there is no '*', and exact matches will
+  # not (?) have a '*' in them.  If any search comes up with something, the
+  # ones following it will not execute.
 
-        # Execute the query, retrieve the results, and make them into
-        # User objects.
-        my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
-        @users = @{Bugzilla::User->new_from_list($user_ids)};
-    }
-    else {    # try an exact match
-        # Exact matches don't care if a user is disabled.
-        trick_taint($str);
-        my $user_id = $dbh->selectrow_array('SELECT userid FROM profiles
-                                             WHERE ' . $dbh->sql_istrcmp('login_name', '?'),
-                                             undef, $str);
-
-        push(@users, new Bugzilla::User($user_id)) if $user_id;
+  # first try wildcards
+  my $wildstr = $str;
+
+  # Do not do wildcards if there is no '*' in the string.
+  if ($wildstr =~ s/\*/\%/g && $user->id) {
+
+    # Build the query.
+    trick_taint($wildstr);
+    my $query = "SELECT DISTINCT userid FROM profiles ";
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      $query .= "INNER JOIN user_group_map
+                               ON user_group_map.user_id = profiles.userid ";
     }
+    $query
+      .= "WHERE ("
+      . $dbh->sql_istrcmp('login_name', '?', "LIKE") . " OR "
+      . $dbh->sql_istrcmp('realname',   '?', "LIKE") . ") ";
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      $query
+        .= "AND isbless = 0 "
+        . "AND group_id IN("
+        . join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
+    }
+    $query .= " AND is_enabled = 1 "  if $exclude_disabled;
+    $query .= $dbh->sql_limit($limit) if $limit;
+
+    # Execute the query, retrieve the results, and make them into
+    # User objects.
+    my $user_ids = $dbh->selectcol_arrayref($query, undef, ($wildstr, $wildstr));
+    @users = @{Bugzilla::User->new_from_list($user_ids)};
+  }
+  else {    # try an exact match
+            # Exact matches don't care if a user is disabled.
+    trick_taint($str);
+    my $user_id = $dbh->selectrow_array(
+      'SELECT userid FROM profiles
+                                             WHERE '
+        . $dbh->sql_istrcmp('login_name', '?'), undef, $str
+    );
+
+    push(@users, new Bugzilla::User($user_id)) if $user_id;
+  }
 
-    # then try substring search
-    if (!scalar(@users) && length($str) >= 3 && $user->id) {
-        trick_taint($str);
+  # then try substring search
+  if (!scalar(@users) && length($str) >= 3 && $user->id) {
+    trick_taint($str);
 
-        my $query   = "SELECT DISTINCT userid FROM profiles ";
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            $query .= "INNER JOIN user_group_map
+    my $query = "SELECT DISTINCT userid FROM profiles ";
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      $query .= "INNER JOIN user_group_map
                                ON user_group_map.user_id = profiles.userid ";
-        }
-        $query     .= " WHERE (" .
-                $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR " .
-                $dbh->sql_iposition('?', 'realname') . " > 0) ";
-        if (Bugzilla->params->{'usevisibilitygroups'}) {
-            $query .= " AND isbless = 0" .
-                      " AND group_id IN(" .
-                join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
-        }
-        $query     .= " AND is_enabled = 1 " if $exclude_disabled;
-        $query     .= $dbh->sql_limit($limit) if $limit;
-        my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str));
-        @users = @{Bugzilla::User->new_from_list($user_ids)};
     }
-    return \@users;
+    $query
+      .= " WHERE ("
+      . $dbh->sql_iposition('?', 'login_name') . " > 0" . " OR "
+      . $dbh->sql_iposition('?', 'realname')
+      . " > 0) ";
+    if (Bugzilla->params->{'usevisibilitygroups'}) {
+      $query
+        .= " AND isbless = 0"
+        . " AND group_id IN("
+        . join(', ', (-1, @{$user->visible_groups_inherited})) . ") ";
+    }
+    $query .= " AND is_enabled = 1 "  if $exclude_disabled;
+    $query .= $dbh->sql_limit($limit) if $limit;
+    my $user_ids = $dbh->selectcol_arrayref($query, undef, ($str, $str));
+    @users = @{Bugzilla::User->new_from_list($user_ids)};
+  }
+  return \@users;
 }
 
 sub match_field {
-    my $fields       = shift;   # arguments as a hash
-    my $data         = shift || Bugzilla->input_params; # hash to look up fields in
-    my $behavior     = shift || 0; # A constant that tells us how to act
-    my $matches      = {};      # the values sent to the template
-    my $matchsuccess = 1;       # did the match fail?
-    my $need_confirm = 0;       # whether to display confirmation screen
-    my $match_multiple = 0;     # whether we ever matched more than one user
-    my @non_conclusive_fields;  # fields which don't have a unique user.
-
-    my $params = Bugzilla->params;
-
-    # prepare default form values
-
-    # Fields can be regular expressions matching multiple form fields
-    # (f.e. "requestee-(\d+)"), so expand each non-literal field
-    # into the list of form fields it matches.
-    my $expanded_fields = {};
-    foreach my $field_pattern (keys %{$fields}) {
-        # Check if the field has any non-word characters.  Only those fields
-        # can be regular expressions, so don't expand the field if it doesn't
-        # have any of those characters.
-        if ($field_pattern =~ /^\w+$/) {
-            $expanded_fields->{$field_pattern} = $fields->{$field_pattern};
-        }
-        else {
-            my @field_names = grep(/$field_pattern/, keys %$data);
-
-            foreach my $field_name (@field_names) {
-                $expanded_fields->{$field_name} =
-                  { type => $fields->{$field_pattern}->{'type'} };
-
-                # The field is a requestee field; in order for its name
-                # to show up correctly on the confirmation page, we need
-                # to find out the name of its flag type.
-                if ($field_name =~ /^requestee(_type)?-(\d+)$/) {
-                    my $flag_type;
-                    if ($1) {
-                        require Bugzilla::FlagType;
-                        $flag_type = new Bugzilla::FlagType($2);
-                    }
-                    else {
-                        require Bugzilla::Flag;
-                        my $flag = new Bugzilla::Flag($2);
-                        $flag_type = $flag->type if $flag;
-                    }
-                    if ($flag_type) {
-                        $expanded_fields->{$field_name}->{'flag_type'} = $flag_type;
-                    }
-                    else {
-                        # No need to look for a valid requestee if the flag(type)
-                        # has been deleted (may occur in race conditions).
-                        delete $expanded_fields->{$field_name};
-                        delete $data->{$field_name};
-                    }
-                }
-            }
+  my $fields = shift;                            # arguments as a hash
+  my $data = shift || Bugzilla->input_params;    # hash to look up fields in
+  my $behavior       = shift || 0;    # A constant that tells us how to act
+  my $matches        = {};            # the values sent to the template
+  my $matchsuccess   = 1;             # did the match fail?
+  my $need_confirm   = 0;             # whether to display confirmation screen
+  my $match_multiple = 0;             # whether we ever matched more than one user
+  my @non_conclusive_fields;          # fields which don't have a unique user.
+
+  my $params = Bugzilla->params;
+
+  # prepare default form values
+
+  # Fields can be regular expressions matching multiple form fields
+  # (f.e. "requestee-(\d+)"), so expand each non-literal field
+  # into the list of form fields it matches.
+  my $expanded_fields = {};
+  foreach my $field_pattern (keys %{$fields}) {
+
+    # Check if the field has any non-word characters.  Only those fields
+    # can be regular expressions, so don't expand the field if it doesn't
+    # have any of those characters.
+    if ($field_pattern =~ /^\w+$/) {
+      $expanded_fields->{$field_pattern} = $fields->{$field_pattern};
+    }
+    else {
+      my @field_names = grep(/$field_pattern/, keys %$data);
+
+      foreach my $field_name (@field_names) {
+        $expanded_fields->{$field_name} = {type => $fields->{$field_pattern}->{'type'}};
+
+        # The field is a requestee field; in order for its name
+        # to show up correctly on the confirmation page, we need
+        # to find out the name of its flag type.
+        if ($field_name =~ /^requestee(_type)?-(\d+)$/) {
+          my $flag_type;
+          if ($1) {
+            require Bugzilla::FlagType;
+            $flag_type = new Bugzilla::FlagType($2);
+          }
+          else {
+            require Bugzilla::Flag;
+            my $flag = new Bugzilla::Flag($2);
+            $flag_type = $flag->type if $flag;
+          }
+          if ($flag_type) {
+            $expanded_fields->{$field_name}->{'flag_type'} = $flag_type;
+          }
+          else {
+            # No need to look for a valid requestee if the flag(type)
+            # has been deleted (may occur in race conditions).
+            delete $expanded_fields->{$field_name};
+            delete $data->{$field_name};
+          }
         }
+      }
     }
-    $fields = $expanded_fields;
+  }
+  $fields = $expanded_fields;
 
-    foreach my $field (keys %{$fields}) {
-        next unless defined $data->{$field};
+  foreach my $field (keys %{$fields}) {
+    next unless defined $data->{$field};
 
-        #Concatenate login names, so that we have a common way to handle them.
-        my $raw_field;
-        if (ref $data->{$field}) {
-            $raw_field = join(",", @{$data->{$field}});
-        }
-        else {
-            $raw_field = $data->{$field};
-        }
-        $raw_field = clean_text($raw_field || '');
-
-        # Now we either split $raw_field by spaces/commas and put the list
-        # into @queries, or in the case of fields which only accept single
-        # entries, we simply use the verbatim text.
-        my @queries;
-        if ($fields->{$field}->{'type'} eq 'single') {
-            @queries = ($raw_field);
-            # We will repopulate it later if a match is found, else it must
-            # be set to an empty string so that the field remains defined.
-            $data->{$field} = '';
-        }
-        elsif ($fields->{$field}->{'type'} eq 'multi') {
-            @queries =  split(/[,;]+/, $raw_field);
-            # We will repopulate it later if a match is found, else it must
-            # be undefined.
-            delete $data->{$field};
-        }
-        else {
-            # bad argument
-            ThrowCodeError('bad_arg',
-                           { argument => $fields->{$field}->{'type'},
-                             function =>  'Bugzilla::User::match_field',
-                           });
-        }
+    #Concatenate login names, so that we have a common way to handle them.
+    my $raw_field;
+    if (ref $data->{$field}) {
+      $raw_field = join(",", @{$data->{$field}});
+    }
+    else {
+      $raw_field = $data->{$field};
+    }
+    $raw_field = clean_text($raw_field || '');
 
-        # Tolerate fields that do not exist (in case you specify
-        # e.g. the QA contact, and it's currently not in use).
-        next unless (defined $raw_field && $raw_field ne '');
+    # Now we either split $raw_field by spaces/commas and put the list
+    # into @queries, or in the case of fields which only accept single
+    # entries, we simply use the verbatim text.
+    my @queries;
+    if ($fields->{$field}->{'type'} eq 'single') {
+      @queries = ($raw_field);
 
-        my $limit = 0;
-        if ($params->{'maxusermatches'}) {
-            $limit = $params->{'maxusermatches'} + 1;
-        }
+      # We will repopulate it later if a match is found, else it must
+      # be set to an empty string so that the field remains defined.
+      $data->{$field} = '';
+    }
+    elsif ($fields->{$field}->{'type'} eq 'multi') {
+      @queries = split(/[,;]+/, $raw_field);
 
-        my @logins;
-        for my $query (@queries) {
-            $query = trim($query);
-            next if $query eq '';
-
-            my $users = match(
-                $query,   # match string
-                $limit,   # match limit
-                1         # exclude_disabled
-            );
-
-            # here is where it checks for multiple matches
-            if (scalar(@{$users}) == 1) { # exactly one match
-                push(@logins, @{$users}[0]->login);
-
-                # skip confirmation for exact matches
-                next if (lc(@{$users}[0]->login) eq lc($query));
-
-                $matches->{$field}->{$query}->{'status'} = 'success';
-                $need_confirm = 1 if $params->{'confirmuniqueusermatch'};
-
-            }
-            elsif ((scalar(@{$users}) > 1)
-                    && ($params->{'maxusermatches'} != 1)) {
-                $need_confirm = 1;
-                $match_multiple = 1;
-                push(@non_conclusive_fields, $field);
-
-                if (($params->{'maxusermatches'})
-                   && (scalar(@{$users}) > $params->{'maxusermatches'}))
-                {
-                    $matches->{$field}->{$query}->{'status'} = 'trunc';
-                    pop @{$users};  # take the last one out
-                }
-                else {
-                    $matches->{$field}->{$query}->{'status'} = 'success';
-                }
-
-            }
-            else {
-                # everything else fails
-                $matchsuccess = 0; # fail
-                push(@non_conclusive_fields, $field);
-                $matches->{$field}->{$query}->{'status'} = 'fail';
-                $need_confirm = 1;  # confirmation screen shows failures
-            }
-
-            $matches->{$field}->{$query}->{'users'}  = $users;
+      # We will repopulate it later if a match is found, else it must
+      # be undefined.
+      delete $data->{$field};
+    }
+    else {
+      # bad argument
+      ThrowCodeError(
+        'bad_arg',
+        {
+          argument => $fields->{$field}->{'type'},
+          function => 'Bugzilla::User::match_field',
         }
+      );
+    }
+
+    # Tolerate fields that do not exist (in case you specify
+    # e.g. the QA contact, and it's currently not in use).
+    next unless (defined $raw_field && $raw_field ne '');
+
+    my $limit = 0;
+    if ($params->{'maxusermatches'}) {
+      $limit = $params->{'maxusermatches'} + 1;
+    }
+
+    my @logins;
+    for my $query (@queries) {
+      $query = trim($query);
+      next if $query eq '';
+
+      my $users = match(
+        $query,    # match string
+        $limit,    # match limit
+        1          # exclude_disabled
+      );
 
-        # If no match or more than one match has been found for a field
-        # expecting only one match (type eq "single"), we set it back to ''
-        # so that the caller of this function can still check whether this
-        # field was defined or not (and it was if we came here).
-        if ($fields->{$field}->{'type'} eq 'single') {
-            $data->{$field} = $logins[0] || '';
+      # here is where it checks for multiple matches
+      if (scalar(@{$users}) == 1) {    # exactly one match
+        push(@logins, @{$users}[0]->login);
+
+        # skip confirmation for exact matches
+        next if (lc(@{$users}[0]->login) eq lc($query));
+
+        $matches->{$field}->{$query}->{'status'} = 'success';
+        $need_confirm = 1 if $params->{'confirmuniqueusermatch'};
+
+      }
+      elsif ((scalar(@{$users}) > 1) && ($params->{'maxusermatches'} != 1)) {
+        $need_confirm   = 1;
+        $match_multiple = 1;
+        push(@non_conclusive_fields, $field);
+
+        if ( ($params->{'maxusermatches'})
+          && (scalar(@{$users}) > $params->{'maxusermatches'}))
+        {
+          $matches->{$field}->{$query}->{'status'} = 'trunc';
+          pop @{$users};    # take the last one out
         }
-        elsif (scalar @logins) {
-            $data->{$field} = \@logins;
+        else {
+          $matches->{$field}->{$query}->{'status'} = 'success';
         }
+
+      }
+      else {
+        # everything else fails
+        $matchsuccess = 0;    # fail
+        push(@non_conclusive_fields, $field);
+        $matches->{$field}->{$query}->{'status'} = 'fail';
+        $need_confirm = 1;    # confirmation screen shows failures
+      }
+
+      $matches->{$field}->{$query}->{'users'} = $users;
     }
 
-    my $retval;
-    if (!$matchsuccess) {
-        $retval = USER_MATCH_FAILED;
+    # If no match or more than one match has been found for a field
+    # expecting only one match (type eq "single"), we set it back to ''
+    # so that the caller of this function can still check whether this
+    # field was defined or not (and it was if we came here).
+    if ($fields->{$field}->{'type'} eq 'single') {
+      $data->{$field} = $logins[0] || '';
     }
-    elsif ($match_multiple) {
-        $retval = USER_MATCH_MULTIPLE;
-    }
-    else {
-        $retval = USER_MATCH_SUCCESS;
+    elsif (scalar @logins) {
+      $data->{$field} = \@logins;
     }
+  }
 
-    # Skip confirmation if we were told to, or if we don't need to confirm.
-    if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) {
-        return wantarray ? ($retval, \@non_conclusive_fields) : $retval;
-    }
+  my $retval;
+  if (!$matchsuccess) {
+    $retval = USER_MATCH_FAILED;
+  }
+  elsif ($match_multiple) {
+    $retval = USER_MATCH_MULTIPLE;
+  }
+  else {
+    $retval = USER_MATCH_SUCCESS;
+  }
 
-    my $template = Bugzilla->template;
-    my $cgi = Bugzilla->cgi;
-    my $vars = {};
+  # Skip confirmation if we were told to, or if we don't need to confirm.
+  if ($behavior == MATCH_SKIP_CONFIRM || !$need_confirm) {
+    return wantarray ? ($retval, \@non_conclusive_fields) : $retval;
+  }
 
-    $vars->{'script'}        = $cgi->url(-relative => 1); # for self-referencing URLs
-    $vars->{'fields'}        = $fields; # fields being matched
-    $vars->{'matches'}       = $matches; # matches that were made
-    $vars->{'matchsuccess'}  = $matchsuccess; # continue or fail
-    $vars->{'matchmultiple'} = $match_multiple;
+  my $template = Bugzilla->template;
+  my $cgi      = Bugzilla->cgi;
+  my $vars     = {};
 
-    print $cgi->header();
+  $vars->{'script'}       = $cgi->url(-relative => 1); # for self-referencing URLs
+  $vars->{'fields'}       = $fields;                   # fields being matched
+  $vars->{'matches'}      = $matches;                  # matches that were made
+  $vars->{'matchsuccess'} = $matchsuccess;             # continue or fail
+  $vars->{'matchmultiple'} = $match_multiple;
 
-    $template->process("global/confirm-user-match.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  print $cgi->header();
+
+  $template->process("global/confirm-user-match.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 
 }
 
 # Changes in some fields automatically trigger events. The field names are
 # from the fielddefs table.
 our %names_to_events = (
-    'resolution'              => EVT_OPENED_CLOSED,
-    'keywords'                => EVT_KEYWORD,
-    'cc'                      => EVT_CC,
-    'bug_severity'            => EVT_PROJ_MANAGEMENT,
-    'priority'                => EVT_PROJ_MANAGEMENT,
-    'bug_status'              => EVT_PROJ_MANAGEMENT,
-    'target_milestone'        => EVT_PROJ_MANAGEMENT,
-    'attachments.description' => EVT_ATTACHMENT_DATA,
-    'attachments.mimetype'    => EVT_ATTACHMENT_DATA,
-    'attachments.ispatch'     => EVT_ATTACHMENT_DATA,
-    'dependson'               => EVT_DEPEND_BLOCK,
-    'blocked'                 => EVT_DEPEND_BLOCK,
-    'product'                 => EVT_COMPONENT,
-    'component'               => EVT_COMPONENT);
+  'resolution'              => EVT_OPENED_CLOSED,
+  'keywords'                => EVT_KEYWORD,
+  'cc'                      => EVT_CC,
+  'bug_severity'            => EVT_PROJ_MANAGEMENT,
+  'priority'                => EVT_PROJ_MANAGEMENT,
+  'bug_status'              => EVT_PROJ_MANAGEMENT,
+  'target_milestone'        => EVT_PROJ_MANAGEMENT,
+  'attachments.description' => EVT_ATTACHMENT_DATA,
+  'attachments.mimetype'    => EVT_ATTACHMENT_DATA,
+  'attachments.ispatch'     => EVT_ATTACHMENT_DATA,
+  'dependson'               => EVT_DEPEND_BLOCK,
+  'blocked'                 => EVT_DEPEND_BLOCK,
+  'product'                 => EVT_COMPONENT,
+  'component'               => EVT_COMPONENT
+);
 
 # Returns true if the user wants mail for a given bug change.
 # Note: the "+" signs before the constants suppress bareword quoting.
 sub wants_bug_mail {
-    my $self = shift;
-    my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_;
-
-    # is_silent_user is true if the username is mentioned in the param `silent_users`
-    return 0 if $changer && $changer->is_silent_user;
-
-    # Make a list of the events which have happened during this bug change,
-    # from the point of view of this user.
-    my %events;
-    foreach my $change (@$fieldDiffs) {
-        my $fieldName = $change->{field_name};
-        # A change to any of the above fields sets the corresponding event
-        if (defined($names_to_events{$fieldName})) {
-            $events{$names_to_events{$fieldName}} = 1;
-        }
-        else {
-            # Catch-all for any change not caught by a more specific event
-            $events{+EVT_OTHER} = 1;
-        }
+  my $self = shift;
+  my ($bug, $relationship, $fieldDiffs, $comments, $dep_mail, $changer) = @_;
 
-        # If the user is in a particular role and the value of that role
-        # changed, we need the ADDED_REMOVED event.
-        if (($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE) ||
-            ($fieldName eq "qa_contact" && $relationship == REL_QA))
-        {
-            $events{+EVT_ADDED_REMOVED} = 1;
-        }
+ # is_silent_user is true if the username is mentioned in the param `silent_users`
+  return 0 if $changer && $changer->is_silent_user;
 
-        if ($fieldName eq "cc") {
-            my $login = $self->login;
-            my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
-            my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
-            if ($inold != $innew)
-            {
-                $events{+EVT_ADDED_REMOVED} = 1;
-            }
-        }
-    }
+  # Make a list of the events which have happened during this bug change,
+  # from the point of view of this user.
+  my %events;
+  foreach my $change (@$fieldDiffs) {
+    my $fieldName = $change->{field_name};
 
-    if (!$bug->lastdiffed) {
-        # Notify about new bugs.
-        $events{+EVT_BUG_CREATED} = 1;
-
-        # You role is new if the bug itself is.
-        # Only makes sense for the assignee, QA contact and the CC list.
-        if ($relationship == REL_ASSIGNEE
-            || $relationship == REL_QA
-            || $relationship == REL_CC)
-        {
-            $events{+EVT_ADDED_REMOVED} = 1;
-        }
-    }
-
-    if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
-        $events{+EVT_ATTACHMENT} = 1;
+    # A change to any of the above fields sets the corresponding event
+    if (defined($names_to_events{$fieldName})) {
+      $events{$names_to_events{$fieldName}} = 1;
     }
-    elsif (defined($$comments[0])) {
-        $events{+EVT_COMMENT} = 1;
+    else {
+      # Catch-all for any change not caught by a more specific event
+      $events{+EVT_OTHER} = 1;
     }
 
-    # Dependent changed bugmails must have an event to ensure the bugmail is
-    # emailed.
-    if ($dep_mail) {
-        $events{+EVT_DEPEND_BLOCK} = 1;
+    # If the user is in a particular role and the value of that role
+    # changed, we need the ADDED_REMOVED event.
+    if ( ($fieldName eq "assigned_to" && $relationship == REL_ASSIGNEE)
+      || ($fieldName eq "qa_contact" && $relationship == REL_QA))
+    {
+      $events{+EVT_ADDED_REMOVED} = 1;
     }
 
-    my @event_list = keys %events;
+    if ($fieldName eq "cc") {
+      my $login = $self->login;
+      my $inold = ($change->{old} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
+      my $innew = ($change->{new} =~ /^(.*,\s*)?\Q$login\E(,.*)?$/);
+      if ($inold != $innew) {
+        $events{+EVT_ADDED_REMOVED} = 1;
+      }
+    }
+  }
 
-    my $wants_mail = $self->wants_mail(\@event_list, $relationship);
+  if (!$bug->lastdiffed) {
 
-    # The negative events are handled separately - they can't be incorporated
-    # into the first wants_mail call, because they are of the opposite sense.
-    #
-    # We do them separately because if _any_ of them are set, we don't want
-    # the mail.
-    if ($wants_mail && $changer && ($self->id == $changer->id)) {
-        $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship);
-    }
+    # Notify about new bugs.
+    $events{+EVT_BUG_CREATED} = 1;
 
-    if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') {
-        $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship);
+    # You role is new if the bug itself is.
+    # Only makes sense for the assignee, QA contact and the CC list.
+    if ( $relationship == REL_ASSIGNEE
+      || $relationship == REL_QA
+      || $relationship == REL_CC)
+    {
+      $events{+EVT_ADDED_REMOVED} = 1;
+    }
+  }
+
+  if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) {
+    $events{+EVT_ATTACHMENT} = 1;
+  }
+  elsif (defined($$comments[0])) {
+    $events{+EVT_COMMENT} = 1;
+  }
+
+  # Dependent changed bugmails must have an event to ensure the bugmail is
+  # emailed.
+  if ($dep_mail) {
+    $events{+EVT_DEPEND_BLOCK} = 1;
+  }
+
+  my @event_list = keys %events;
+
+  my $wants_mail = $self->wants_mail(\@event_list, $relationship);
+
+  # The negative events are handled separately - they can't be incorporated
+  # into the first wants_mail call, because they are of the opposite sense.
+  #
+  # We do them separately because if _any_ of them are set, we don't want
+  # the mail.
+  if ($wants_mail && $changer && ($self->id == $changer->id)) {
+    $wants_mail &= $self->wants_mail([EVT_CHANGED_BY_ME], $relationship);
+  }
+
+  if ($wants_mail && $bug->bug_status eq 'UNCONFIRMED') {
+    $wants_mail &= $self->wants_mail([EVT_UNCONFIRMED], $relationship);
+  }
+
+  # BMO: add a hook to allow custom bugmail filtering
+  Bugzilla::Hook::process(
+    "user_wants_mail",
+    {
+      user         => $self,
+      wants_mail   => \$wants_mail,
+      bug          => $bug,
+      relationship => $relationship,
+      fieldDiffs   => $fieldDiffs,
+      comments     => $comments,
+      dep_mail     => $dep_mail,
+      changer      => $changer,
     }
+  );
 
-    # BMO: add a hook to allow custom bugmail filtering
-    Bugzilla::Hook::process("user_wants_mail", {
-        user            => $self,
-        wants_mail      => \$wants_mail,
-        bug             => $bug,
-        relationship    => $relationship,
-        fieldDiffs      => $fieldDiffs,
-        comments        => $comments,
-        dep_mail        => $dep_mail,
-        changer         => $changer,
-    });
-
-    return $wants_mail;
+  return $wants_mail;
 }
 
 # Returns true if the user wants mail for a given set of events.
 sub wants_mail {
-    my $self = shift;
-    my ($events, $relationship) = @_;
+  my $self = shift;
+  my ($events, $relationship) = @_;
 
-    # Don't send any mail, ever, if account is disabled
-    # XXX Temporary Compatibility Change 1 of 2:
-    # This code is disabled for the moment to make the behaviour like the old
-    # system, which sent bugmail to disabled accounts.
-    # return 0 if $self->{'disabledtext'};
+  # Don't send any mail, ever, if account is disabled
+  # XXX Temporary Compatibility Change 1 of 2:
+  # This code is disabled for the moment to make the behaviour like the old
+  # system, which sent bugmail to disabled accounts.
+  # return 0 if $self->{'disabledtext'};
 
-    # No mail if there are no events
-    return 0 if !scalar(@$events);
+  # No mail if there are no events
+  return 0 if !scalar(@$events);
 
-    # If a relationship isn't given, default to REL_ANY.
-    if (!defined($relationship)) {
-        $relationship = REL_ANY;
-    }
+  # If a relationship isn't given, default to REL_ANY.
+  if (!defined($relationship)) {
+    $relationship = REL_ANY;
+  }
 
-    # Skip DB query if relationship is explicit
-    return 1 if $relationship == REL_GLOBAL_WATCHER;
+  # Skip DB query if relationship is explicit
+  return 1 if $relationship == REL_GLOBAL_WATCHER;
 
-    my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events;
-    return $wants_mail ? 1 : 0;
+  my $wants_mail = grep { $self->mail_settings->{$relationship}{$_} } @$events;
+  return $wants_mail ? 1 : 0;
 }
 
 sub mail_settings {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
-
-    if (!defined $self->{'mail_settings'}) {
-        my $data =
-          $dbh->selectall_arrayref('SELECT relationship, event FROM email_setting
-                                    WHERE user_id = ?', undef, $self->id);
-        my %mail;
-        # The hash is of the form $mail{$relationship}{$event} = 1.
-        $mail{$_->[0]}{$_->[1]} = 1 foreach @$data;
-
-        $self->{'mail_settings'} = \%mail;
-    }
-    return $self->{'mail_settings'};
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+
+  if (!defined $self->{'mail_settings'}) {
+    my $data = $dbh->selectall_arrayref(
+      'SELECT relationship, event FROM email_setting
+                                    WHERE user_id = ?', undef, $self->id
+    );
+    my %mail;
+
+    # The hash is of the form $mail{$relationship}{$event} = 1.
+    $mail{$_->[0]}{$_->[1]} = 1 foreach @$data;
+
+    $self->{'mail_settings'} = \%mail;
+  }
+  return $self->{'mail_settings'};
 }
 
 sub has_audit_entries {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (!exists $self->{'has_audit_entries'}) {
-        $self->{'has_audit_entries'} =
-            $dbh->selectrow_array('SELECT 1 FROM audit_log WHERE user_id = ? ' .
-                                   $dbh->sql_limit(1), undef, $self->id);
-    }
-    return $self->{'has_audit_entries'};
+  if (!exists $self->{'has_audit_entries'}) {
+    $self->{'has_audit_entries'}
+      = $dbh->selectrow_array(
+      'SELECT 1 FROM audit_log WHERE user_id = ? ' . $dbh->sql_limit(1),
+      undef, $self->id);
+  }
+  return $self->{'has_audit_entries'};
 }
 
 sub is_insider {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{'is_insider'}) {
-        my $insider_group = Bugzilla->params->{'insidergroup'};
-        $self->{'is_insider'} =
-            ($insider_group && $self->in_group($insider_group)) ? 1 : 0;
-    }
-    return $self->{'is_insider'};
+  if (!defined $self->{'is_insider'}) {
+    my $insider_group = Bugzilla->params->{'insidergroup'};
+    $self->{'is_insider'}
+      = ($insider_group && $self->in_group($insider_group)) ? 1 : 0;
+  }
+  return $self->{'is_insider'};
 }
 
 sub is_global_watcher {
-    my $self = shift;
+  my $self = shift;
 
-    if (!exists $self->{'is_global_watcher'}) {
-        my @watchers = split(/\s*,\s*/, Bugzilla->params->{'globalwatchers'});
-        $self->{'is_global_watcher'} = (any { $_ eq $self->login } @watchers) ? 1 : 0;
-    }
-    return  $self->{'is_global_watcher'};
+  if (!exists $self->{'is_global_watcher'}) {
+    my @watchers = split(/\s*,\s*/, Bugzilla->params->{'globalwatchers'});
+    $self->{'is_global_watcher'} = (any { $_ eq $self->login } @watchers) ? 1 : 0;
+  }
+  return $self->{'is_global_watcher'};
 }
 
 sub is_silent_user {
-    my $self = shift;
+  my $self = shift;
 
-    if (!exists $self->{'is_silent_user'}) {
-        my @users = split(/\s*,\s*/, Bugzilla->params->{'silent_users'});
-        $self->{'is_silent_user'} = (any { $self->login eq $_ } @users) ? 1 : 0;
-    }
+  if (!exists $self->{'is_silent_user'}) {
+    my @users = split(/\s*,\s*/, Bugzilla->params->{'silent_users'});
+    $self->{'is_silent_user'} = (any { $self->login eq $_ } @users) ? 1 : 0;
+  }
 
-    return  $self->{'is_silent_user'};
+  return $self->{'is_silent_user'};
 }
 
 sub is_timetracker {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{'is_timetracker'}) {
-        my $tt_group = Bugzilla->params->{'timetrackinggroup'};
-        $self->{'is_timetracker'} =
-            ($tt_group && $self->in_group($tt_group)) ? 1 : 0;
-    }
-    return $self->{'is_timetracker'};
+  if (!defined $self->{'is_timetracker'}) {
+    my $tt_group = Bugzilla->params->{'timetrackinggroup'};
+    $self->{'is_timetracker'} = ($tt_group && $self->in_group($tt_group)) ? 1 : 0;
+  }
+  return $self->{'is_timetracker'};
 }
 
 sub can_tag_comments {
-    my $self = shift;
+  my $self = shift;
 
-    if (!defined $self->{'can_tag_comments'}) {
-        my $group = Bugzilla->params->{'comment_taggers_group'};
-        $self->{'can_tag_comments'} =
-            ($group && $self->in_group($group)) ? 1 : 0;
-    }
-    return $self->{'can_tag_comments'};
+  if (!defined $self->{'can_tag_comments'}) {
+    my $group = Bugzilla->params->{'comment_taggers_group'};
+    $self->{'can_tag_comments'} = ($group && $self->in_group($group)) ? 1 : 0;
+  }
+  return $self->{'can_tag_comments'};
 }
 
 sub get_userlist {
-    my $self = shift;
-
-    return $self->{'userlist'} if defined $self->{'userlist'};
-
-    my $dbh = Bugzilla->dbh;
-    my $query  = "SELECT DISTINCT login_name, realname,";
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        $query .= " COUNT(group_id) ";
-    } else {
-        $query .= " 1 ";
-    }
-    $query     .= "FROM profiles ";
-    if (Bugzilla->params->{'usevisibilitygroups'}) {
-        $query .= "LEFT JOIN user_group_map " .
-                  "ON user_group_map.user_id = userid AND isbless = 0 " .
-                  "AND group_id IN(" .
-                  join(', ', (-1, @{$self->visible_groups_inherited})) . ")";
-    }
-    $query    .= " WHERE is_enabled = 1 ";
-    $query    .= $dbh->sql_group_by('userid', 'login_name, realname');
-
-    my $sth = $dbh->prepare($query);
-    $sth->execute;
-
-    my @userlist;
-    while (my($login, $name, $visible) = $sth->fetchrow_array) {
-        push @userlist, {
-            login => $login,
-            identity => $name ? "$name <$login>" : $login,
-            visible => $visible,
-        };
-    }
-    @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist;
-
-    $self->{'userlist'} = \@userlist;
-    return $self->{'userlist'};
+  my $self = shift;
+
+  return $self->{'userlist'} if defined $self->{'userlist'};
+
+  my $dbh   = Bugzilla->dbh;
+  my $query = "SELECT DISTINCT login_name, realname,";
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+    $query .= " COUNT(group_id) ";
+  }
+  else {
+    $query .= " 1 ";
+  }
+  $query .= "FROM profiles ";
+  if (Bugzilla->params->{'usevisibilitygroups'}) {
+    $query
+      .= "LEFT JOIN user_group_map "
+      . "ON user_group_map.user_id = userid AND isbless = 0 "
+      . "AND group_id IN("
+      . join(', ', (-1, @{$self->visible_groups_inherited})) . ")";
+  }
+  $query .= " WHERE is_enabled = 1 ";
+  $query .= $dbh->sql_group_by('userid', 'login_name, realname');
+
+  my $sth = $dbh->prepare($query);
+  $sth->execute;
+
+  my @userlist;
+  while (my ($login, $name, $visible) = $sth->fetchrow_array) {
+    push @userlist,
+      {
+      login    => $login,
+      identity => $name ? "$name <$login>" : $login,
+      visible  => $visible,
+      };
+  }
+  @userlist = sort { lc $$a{'identity'} cmp lc $$b{'identity'} } @userlist;
+
+  $self->{'userlist'} = \@userlist;
+  return $self->{'userlist'};
 }
 
 sub create {
-    my ($class, $params) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    $dbh->bz_start_transaction();
-    $params->{nickname} = _generate_nickname($params->{realname}, $params->{login_name}, 0);
-    my $user = $class->SUPER::create($params);
-
-    # Turn on all email for the new user
-    require Bugzilla::BugMail;
-    my %relationships = Bugzilla::BugMail::relationships();
-    foreach my $rel (keys %relationships) {
-        foreach my $event (POS_EVENTS, NEG_EVENTS) {
-            # These "exceptions" define the default email preferences.
-            #
-            # We enable mail unless the change was made by the user, or it's
-            # just a CC list addition and the user is not the reporter.
-            next if ($event == EVT_CHANGED_BY_ME);
-            next if (($event == EVT_CC) && ($rel != REL_REPORTER));
-
-            $dbh->do('INSERT INTO email_setting (user_id, relationship, event)
-                      VALUES (?, ?, ?)', undef, ($user->id, $rel, $event));
-        }
-    }
-
-    foreach my $event (GLOBAL_EVENTS) {
-        $dbh->do('INSERT INTO email_setting (user_id, relationship, event)
-                  VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event));
-    }
+  my ($class, $params) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  $dbh->bz_start_transaction();
+  $params->{nickname}
+    = _generate_nickname($params->{realname}, $params->{login_name}, 0);
+  my $user = $class->SUPER::create($params);
+
+  # Turn on all email for the new user
+  require Bugzilla::BugMail;
+  my %relationships = Bugzilla::BugMail::relationships();
+  foreach my $rel (keys %relationships) {
+    foreach my $event (POS_EVENTS, NEG_EVENTS) {
+
+      # These "exceptions" define the default email preferences.
+      #
+      # We enable mail unless the change was made by the user, or it's
+      # just a CC list addition and the user is not the reporter.
+      next if ($event == EVT_CHANGED_BY_ME);
+      next if (($event == EVT_CC) && ($rel != REL_REPORTER));
+
+      $dbh->do(
+        'INSERT INTO email_setting (user_id, relationship, event)
+                      VALUES (?, ?, ?)', undef, ($user->id, $rel, $event)
+      );
+    }
+  }
+
+  foreach my $event (GLOBAL_EVENTS) {
+    $dbh->do(
+      'INSERT INTO email_setting (user_id, relationship, event)
+                  VALUES (?, ?, ?)', undef, ($user->id, REL_ANY, $event)
+    );
+  }
 
-    $user->derive_regexp_groups();
+  $user->derive_regexp_groups();
 
-    # Add the creation date to the profiles_activity table.
-    # $who is the user who created the new user account, i.e. either an
-    # admin or the new user himself.
-    my $who = Bugzilla->user->id || $user->id;
-    my $creation_date_fieldid = get_field_id('creation_ts');
+  # Add the creation date to the profiles_activity table.
+  # $who is the user who created the new user account, i.e. either an
+  # admin or the new user himself.
+  my $who = Bugzilla->user->id || $user->id;
+  my $creation_date_fieldid = get_field_id('creation_ts');
 
-    $dbh->do('INSERT INTO profiles_activity
+  $dbh->do(
+    'INSERT INTO profiles_activity
                           (userid, who, profiles_when, fieldid, newvalue)
-                   VALUES (?, ?, NOW(), ?, NOW())',
-                   undef, ($user->id, $who, $creation_date_fieldid));
+                   VALUES (?, ?, NOW(), ?, NOW())', undef,
+    ($user->id, $who, $creation_date_fieldid)
+  );
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 
-    # Return the newly created user account.
-    return $user;
+  # Return the newly created user account.
+  return $user;
 }
 
 sub check_required_create_fields {
-    my ($invocant, $params) = @_;
-    my $class = ref($invocant) || $invocant;
-    # ensure disabled users also have their email disabled
-    $params->{disable_mail} = 1 if
-        exists $params->{disabledtext}
-        && defined($params->{disabledtext})
-        && trim($params->{disabledtext}) ne '';
-    $class->SUPER::check_required_create_fields($params);
+  my ($invocant, $params) = @_;
+  my $class = ref($invocant) || $invocant;
+
+  # ensure disabled users also have their email disabled
+  $params->{disable_mail} = 1
+    if exists $params->{disabledtext}
+    && defined($params->{disabledtext})
+    && trim($params->{disabledtext}) ne '';
+  $class->SUPER::check_required_create_fields($params);
 }
 
 ###########################
@@ -2596,44 +2709,45 @@ sub check_required_create_fields {
 ###########################
 
 sub account_is_locked_out {
-    my $self = shift;
-    my $login_failures = scalar @{ $self->account_ip_login_failures };
-    return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0;
+  my $self           = shift;
+  my $login_failures = scalar @{$self->account_ip_login_failures};
+  return $login_failures >= MAX_LOGIN_ATTEMPTS ? 1 : 0;
 }
 
 sub note_login_failure {
-    my $self = shift;
-    my $ip_addr = remote_ip();
-    trick_taint($ip_addr);
-    Bugzilla->dbh->do("INSERT INTO login_failure (user_id, ip_addr, login_time)
-                       VALUES (?, ?, LOCALTIMESTAMP(0))",
-                      undef, $self->id, $ip_addr);
-    delete $self->{account_ip_login_failures};
+  my $self    = shift;
+  my $ip_addr = remote_ip();
+  trick_taint($ip_addr);
+  Bugzilla->dbh->do(
+    "INSERT INTO login_failure (user_id, ip_addr, login_time)
+                       VALUES (?, ?, LOCALTIMESTAMP(0))", undef, $self->id, $ip_addr
+  );
+  delete $self->{account_ip_login_failures};
 }
 
 sub clear_login_failures {
-    my $self = shift;
-    my $ip_addr = remote_ip();
-    trick_taint($ip_addr);
-    Bugzilla->dbh->do(
-        'DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?',
-        undef, $self->id, $ip_addr);
-    delete $self->{account_ip_login_failures};
+  my $self    = shift;
+  my $ip_addr = remote_ip();
+  trick_taint($ip_addr);
+  Bugzilla->dbh->do('DELETE FROM login_failure WHERE user_id = ? AND ip_addr = ?',
+    undef, $self->id, $ip_addr);
+  delete $self->{account_ip_login_failures};
 }
 
 sub account_ip_login_failures {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
-    my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-',
-                                   LOGIN_LOCKOUT_INTERVAL, 'MINUTE');
-    my $ip_addr = remote_ip();
-    trick_taint($ip_addr);
-    $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref(
-        "SELECT login_time, ip_addr, user_id FROM login_failure
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+  my $time = $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', LOGIN_LOCKOUT_INTERVAL,
+    'MINUTE');
+  my $ip_addr = remote_ip();
+  trick_taint($ip_addr);
+  $self->{account_ip_login_failures} ||= Bugzilla->dbh->selectall_arrayref(
+    "SELECT login_time, ip_addr, user_id FROM login_failure
           WHERE user_id = ? AND login_time > $time
                 AND ip_addr = ?
-       ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr);
-    return $self->{account_ip_login_failures};
+       ORDER BY login_time", {Slice => {}}, $self->id, $ip_addr
+  );
+  return $self->{account_ip_login_failures};
 }
 
 ###############
@@ -2641,116 +2755,126 @@ sub account_ip_login_failures {
 ###############
 
 sub is_available_username {
-    my ($username, $old_username) = @_;
+  my ($username, $old_username) = @_;
 
-    if(login_to_id($username) != 0) {
-        return 0;
-    }
-
-    my $dbh = Bugzilla->dbh;
-    # $username is safe because it is only used in SELECT placeholders.
-    trick_taint($username);
-    # Reject if the new login is part of an email change which is
-    # still in progress
-    #
-    # substring/locate stuff: bug 165221; this used to use regexes, but that
-    # was unsafe and required weird escaping; using substring to pull out
-    # the new/old email addresses and sql_position() to find the delimiter (':')
-    # is cleaner/safer
-    my $eventdata = $dbh->selectrow_array(
-        "SELECT eventdata
+  if (login_to_id($username) != 0) {
+    return 0;
+  }
+
+  my $dbh = Bugzilla->dbh;
+
+  # $username is safe because it is only used in SELECT placeholders.
+  trick_taint($username);
+
+  # Reject if the new login is part of an email change which is
+  # still in progress
+  #
+  # substring/locate stuff: bug 165221; this used to use regexes, but that
+  # was unsafe and required weird escaping; using substring to pull out
+  # the new/old email addresses and sql_position() to find the delimiter (':')
+  # is cleaner/safer
+  my $eventdata = $dbh->selectrow_array(
+    "SELECT eventdata
            FROM tokens
           WHERE (tokentype = 'emailold'
-                AND SUBSTRING(eventdata, 1, (" .
-                    $dbh->sql_position(q{':'}, 'eventdata') . "-  1)) = ?)
+                AND SUBSTRING(eventdata, 1, ("
+      . $dbh->sql_position(q{':'}, 'eventdata') . "-  1)) = ?)
              OR (tokentype = 'emailnew'
-                AND SUBSTRING(eventdata, (" .
-                    $dbh->sql_position(q{':'}, 'eventdata') . "+ 1), LENGTH(eventdata)) = ?)",
-         undef, ($username, $username));
-
-    if ($eventdata) {
-        # Allow thru owner of token
-        if($old_username && ($eventdata eq "$old_username:$username")) {
-            return 1;
-        }
-        return 0;
+                AND SUBSTRING(eventdata, ("
+      . $dbh->sql_position(q{':'}, 'eventdata')
+      . "+ 1), LENGTH(eventdata)) = ?)",
+    undef, ($username, $username)
+  );
+
+  if ($eventdata) {
+
+    # Allow thru owner of token
+    if ($old_username && ($eventdata eq "$old_username:$username")) {
+      return 1;
     }
+    return 0;
+  }
 
-    return 1;
+  return 1;
 }
 
 sub check_account_creation_enabled {
-    my $self = shift;
+  my $self = shift;
 
-    # If we're using e.g. LDAP for login, then we can't create a new account.
-    $self->authorizer->user_can_create_account
-      || ThrowUserError('auth_cant_create_account');
+  # If we're using e.g. LDAP for login, then we can't create a new account.
+  $self->authorizer->user_can_create_account
+    || ThrowUserError('auth_cant_create_account');
 
-    Bugzilla->params->{'createemailregexp'}
-      || ThrowUserError('account_creation_disabled');
+  Bugzilla->params->{'createemailregexp'}
+    || ThrowUserError('account_creation_disabled');
 }
 
 sub check_and_send_account_creation_confirmation {
-    my ($self, $login) = @_;
+  my ($self, $login) = @_;
 
-    $login = $self->check_login_name_for_creation($login);
-    my $creation_regexp = Bugzilla->params->{'createemailregexp'};
+  $login = $self->check_login_name_for_creation($login);
+  my $creation_regexp = Bugzilla->params->{'createemailregexp'};
 
-    if ($login !~ /$creation_regexp/i) {
-        ThrowUserError('account_creation_restricted');
-    }
+  if ($login !~ /$creation_regexp/i) {
+    ThrowUserError('account_creation_restricted');
+  }
 
-    # BMO - add a hook to allow extra validation prior to account creation.
-    Bugzilla::Hook::process("user_verify_login", { login => $login });
+  # BMO - add a hook to allow extra validation prior to account creation.
+  Bugzilla::Hook::process("user_verify_login", {login => $login});
 
-    # Create and send a token for this new account.
-    require Bugzilla::Token;
-    Bugzilla::Token::issue_new_user_account_token($login);
+  # Create and send a token for this new account.
+  require Bugzilla::Token;
+  Bugzilla::Token::issue_new_user_account_token($login);
 }
 
 # This is used in a few performance-critical areas where we don't want to
 # do check() and pull all the user data from the database.
 sub login_to_id {
-    my ($login, $throw_error) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {};
-
-    # We cache lookups because this function showed up as taking up a
-    # significant amount of time in profiles of xt/search.t. However,
-    # for users that don't exist, we re-do the check every time, because
-    # otherwise we break is_available_username.
-    my $user_id;
-    if (defined $cache->{$login}) {
-        $user_id = $cache->{$login};
-    }
-    else {
-        # No need to validate $login -- it will be used by the following SELECT
-        # statement only, so it's safe to simply trick_taint.
-        trick_taint($login);
-        $user_id = $dbh->selectrow_array(
-            "SELECT userid FROM profiles
-              WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login);
-        $cache->{$login} = $user_id;
-    }
-
-    if ($user_id) {
-        return $user_id;
-    } elsif ($throw_error) {
-        ThrowUserError('invalid_username', { name => $login });
-    } else {
-        return 0;
-    }
+  my ($login, $throw_error) = @_;
+  my $dbh = Bugzilla->dbh;
+  my $cache = Bugzilla->request_cache->{user_login_to_id} ||= {};
+
+  # We cache lookups because this function showed up as taking up a
+  # significant amount of time in profiles of xt/search.t. However,
+  # for users that don't exist, we re-do the check every time, because
+  # otherwise we break is_available_username.
+  my $user_id;
+  if (defined $cache->{$login}) {
+    $user_id = $cache->{$login};
+  }
+  else {
+    # No need to validate $login -- it will be used by the following SELECT
+    # statement only, so it's safe to simply trick_taint.
+    trick_taint($login);
+    $user_id = $dbh->selectrow_array(
+      "SELECT userid FROM profiles
+              WHERE " . $dbh->sql_istrcmp('login_name', '?'), undef, $login
+    );
+    $cache->{$login} = $user_id;
+  }
+
+  if ($user_id) {
+    return $user_id;
+  }
+  elsif ($throw_error) {
+    ThrowUserError('invalid_username', {name => $login});
+  }
+  else {
+    return 0;
+  }
 }
 
 sub user_id_to_login {
-    my $user_id = shift;
-    my $dbh = Bugzilla->dbh;
+  my $user_id = shift;
+  my $dbh     = Bugzilla->dbh;
 
-    return '' unless ($user_id && detaint_natural($user_id));
+  return '' unless ($user_id && detaint_natural($user_id));
 
-    my $login = $dbh->selectrow_array('SELECT login_name FROM profiles
-                                       WHERE userid = ?', undef, $user_id);
-    return $login || '';
+  my $login = $dbh->selectrow_array(
+    'SELECT login_name FROM profiles
+                                       WHERE userid = ?', undef, $user_id
+  );
+  return $login || '';
 }
 
 1;
diff --git a/Bugzilla/User/APIKey.pm b/Bugzilla/User/APIKey.pm
index c1a4ed572..a30c6718f 100644
--- a/Bugzilla/User/APIKey.pm
+++ b/Bugzilla/User/APIKey.pm
@@ -21,58 +21,60 @@ use Bugzilla::Error;
 # Overriden Constants that are used as methods
 #####################################################################
 
-use constant DB_TABLE       => 'user_api_keys';
-use constant DB_COLUMNS     => qw(
-    id
-    user_id
-    api_key
-    app_id
-    description
-    revoked
-    last_used
-    last_used_ip
+use constant DB_TABLE   => 'user_api_keys';
+use constant DB_COLUMNS => qw(
+  id
+  user_id
+  api_key
+  app_id
+  description
+  revoked
+  last_used
+  last_used_ip
 );
 
 use constant UPDATE_COLUMNS => qw(description revoked last_used last_used_ip);
 use constant VALIDATORS     => {
-    api_key     => \&_check_api_key,
-    app_id      => \&_check_app_id,
-    description => \&_check_description,
-    revoked     => \&Bugzilla::Object::check_boolean,
+  api_key     => \&_check_api_key,
+  app_id      => \&_check_app_id,
+  description => \&_check_description,
+  revoked     => \&Bugzilla::Object::check_boolean,
 };
-use constant LIST_ORDER     => 'id';
-use constant NAME_FIELD     => 'api_key';
+use constant LIST_ORDER => 'id';
+use constant NAME_FIELD => 'api_key';
 
 # 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
+};
 
 # Accessors
-sub id            { return $_[0]->{id}          }
-sub user_id       { return $_[0]->{user_id}     }
-sub api_key       { return $_[0]->{api_key}     }
-sub app_id        { return $_[0]->{app_id}      }
-sub description   { return $_[0]->{description} }
-sub revoked       { return $_[0]->{revoked}     }
-sub last_used     { return $_[0]->{last_used}   }
-sub last_used_ip  { return $_[0]->{last_used_ip}   }
+sub id           { return $_[0]->{id} }
+sub user_id      { return $_[0]->{user_id} }
+sub api_key      { return $_[0]->{api_key} }
+sub app_id       { return $_[0]->{app_id} }
+sub description  { return $_[0]->{description} }
+sub revoked      { return $_[0]->{revoked} }
+sub last_used    { return $_[0]->{last_used} }
+sub last_used_ip { return $_[0]->{last_used_ip} }
 
 # Helpers
 sub user {
-    my $self = shift;
-    $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1});
-    return $self->{user};
+  my $self = shift;
+  $self->{user} //= Bugzilla::User->new({name => $self->user_id, cache => 1});
+  return $self->{user};
 }
 
 sub update_last_used {
-    my $self = shift;
-    my $timestamp = shift
-        || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-    $self->set('last_used', $timestamp);
-    $self->set('last_used_ip', remote_ip());
-    $self->update;
+  my $self = shift;
+  my $timestamp
+    = shift || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+  $self->set('last_used',    $timestamp);
+  $self->set('last_used_ip', remote_ip());
+  $self->update;
 }
 
 # Setters
@@ -80,20 +82,22 @@ sub set_description { $_[0]->set('description', $_[1]); }
 sub set_revoked     { $_[0]->set('revoked',     $_[1]); }
 
 # Validators
-sub _check_api_key     { return generate_random_password(40); }
-sub _check_description { return trim($_[1]) || '';   }
+sub _check_api_key { return generate_random_password(40); }
+sub _check_description { return trim($_[1]) || ''; }
+
 sub _check_app_id {
-    my ($invocant, $app_id) = @_;
+  my ($invocant, $app_id) = @_;
 
-    ThrowCodeError("invalid_app_id", { app_id => $app_id }) unless $app_id =~ /^[[:xdigit:]]+$/;
+  ThrowCodeError("invalid_app_id", {app_id => $app_id})
+    unless $app_id =~ /^[[:xdigit:]]+$/;
 
-    return $app_id;
+  return $app_id;
 }
 
 sub create_special {
-    my ($class, @args) = @_;
-    local VALIDATORS->{api_key} = sub { return $_[1] };
-    return $class->create(@args);
+  my ($class, @args) = @_;
+  local VALIDATORS->{api_key} = sub { return $_[1] };
+  return $class->create(@args);
 }
 1;
 
diff --git a/Bugzilla/User/Session.pm b/Bugzilla/User/Session.pm
index 56e1cd07a..652a866c1 100644
--- a/Bugzilla/User/Session.pm
+++ b/Bugzilla/User/Session.pm
@@ -17,14 +17,14 @@ use base qw(Bugzilla::Object);
 # Overriden Constants that are used as methods
 #####################################################################
 
-use constant DB_TABLE       => 'logincookies';
-use constant DB_COLUMNS     => qw(
-    cookie
-    userid
-    lastused
-    ipaddr
-    id
-    restrict_ipaddr
+use constant DB_TABLE   => 'logincookies';
+use constant DB_COLUMNS => qw(
+  cookie
+  userid
+  lastused
+  ipaddr
+  id
+  restrict_ipaddr
 );
 
 use constant UPDATE_COLUMNS => qw();
@@ -33,17 +33,19 @@ use constant LIST_ORDER     => 'lastused DESC';
 use constant NAME_FIELD     => 'cookie';
 
 # 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
+};
 
 # Accessors
-sub id              { return $_[0]->{id}              }
-sub userid          { return $_[0]->{userid}          }
-sub cookie          { return $_[0]->{cookie}          }
-sub lastused        { return $_[0]->{lastused}        }
-sub ipaddr          { return $_[0]->{ipaddr}          }
+sub id              { return $_[0]->{id} }
+sub userid          { return $_[0]->{userid} }
+sub cookie          { return $_[0]->{cookie} }
+sub lastused        { return $_[0]->{lastused} }
+sub ipaddr          { return $_[0]->{ipaddr} }
 sub restrict_ipaddr { return $_[0]->{restrict_ipaddr} }
 
 1;
diff --git a/Bugzilla/User/Setting.pm b/Bugzilla/User/Setting.pm
index ac53fbb32..e08f3bd8c 100644
--- a/Bugzilla/User/Setting.pm
+++ b/Bugzilla/User/Setting.pm
@@ -13,12 +13,13 @@ use strict;
 use warnings;
 
 use base qw(Exporter);
+
 # Module stuff
 @Bugzilla::User::Setting::EXPORT = qw(
-    get_all_settings
-    get_defaults
-    add_setting
-    clear_settings_cache
+  get_all_settings
+  get_defaults
+  add_setting
+  clear_settings_cache
 );
 
 use Bugzilla::Error;
@@ -30,89 +31,86 @@ use Module::Runtime qw(require_module);
 ###############################
 
 sub new {
-    my $invocant = shift;
-    my $setting_name = shift;
-    my $user_id = shift;
-
-    my $class = ref($invocant) || $invocant;
-    my $subclass = '';
-
-    # Create a ref to an empty hash and bless it
-    my $self = {};
-
-    my $dbh = Bugzilla->dbh;
-
-    # Confirm that the $setting_name is properly formed;
-    # if not, throw a code error.
-    #
-    # NOTE: due to the way that setting names are used in templates,
-    # they must conform to to the limitations set for HTML NAMEs and IDs.
-    #
-    if ( !($setting_name =~ /^[a-zA-Z][-.:\w]*$/) ) {
-      ThrowCodeError("setting_name_invalid", { name => $setting_name });
-    }
-
-    # If there were only two parameters passed in, then we need
-    # to retrieve the information for this setting ourselves.
-    if (scalar @_ == 0) {
-
-        my ($default, $is_enabled, $value, $category);
-        ($default, $is_enabled, $value, $subclass, $category) =
-          $dbh->selectrow_array(
-             q{SELECT default_value, is_enabled, setting_value, subclass, category
+  my $invocant     = shift;
+  my $setting_name = shift;
+  my $user_id      = shift;
+
+  my $class = ref($invocant) || $invocant;
+  my $subclass = '';
+
+  # Create a ref to an empty hash and bless it
+  my $self = {};
+
+  my $dbh = Bugzilla->dbh;
+
+  # Confirm that the $setting_name is properly formed;
+  # if not, throw a code error.
+  #
+  # NOTE: due to the way that setting names are used in templates,
+  # they must conform to to the limitations set for HTML NAMEs and IDs.
+  #
+  if (!($setting_name =~ /^[a-zA-Z][-.:\w]*$/)) {
+    ThrowCodeError("setting_name_invalid", {name => $setting_name});
+  }
+
+  # If there were only two parameters passed in, then we need
+  # to retrieve the information for this setting ourselves.
+  if (scalar @_ == 0) {
+
+    my ($default, $is_enabled, $value, $category);
+    ($default, $is_enabled, $value, $subclass, $category) = $dbh->selectrow_array(
+      q{SELECT default_value, is_enabled, setting_value, subclass, category
                  FROM setting
             LEFT JOIN profile_setting
                    ON setting.name = profile_setting.setting_name
                 WHERE name = ?
-                  AND profile_setting.user_id = ?},
-             undef,
-             $setting_name, $user_id);
-
-        # if not defined, then grab the default value
-        if (! defined $value) {
-            ($default, $is_enabled, $subclass, $category) =
-              $dbh->selectrow_array(
-                 q{SELECT default_value, is_enabled, subclass, category
+                  AND profile_setting.user_id = ?}, undef, $setting_name, $user_id
+    );
+
+    # if not defined, then grab the default value
+    if (!defined $value) {
+      ($default, $is_enabled, $subclass, $category) = $dbh->selectrow_array(
+        q{SELECT default_value, is_enabled, subclass, category
                    FROM setting
-                   WHERE name = ?},
-              undef,
-              $setting_name);
-        }
-
-        $self->{'is_enabled'}    = $is_enabled;
-        $self->{'default_value'} = $default;
-        $self->{'category'}      = $category;
-
-        # IF the setting is enabled, AND the user has chosen a setting
-        # THEN return that value
-        # ELSE return the site default, and note that it is the default.
-        if ( ($is_enabled) && (defined $value) ) {
-            $self->{'value'} = $value;
-        } else {
-            $self->{'value'} = $default;
-            $self->{'isdefault'} = 1;
-        }
-    }
-    else {
-        # If the values were passed in, simply assign them and return.
-        $self->{'is_enabled'}    = shift;
-        $self->{'default_value'} = shift;
-        $self->{'value'}         = shift;
-        $self->{'is_default'}    = shift;
-        $subclass                = shift;
-        $self->{'category'}      = shift;
+                   WHERE name = ?}, undef, $setting_name
+      );
     }
-    if ($subclass) {
-        eval { require_module( $class . '::' . $subclass ) }
-            || ThrowCodeError( 'setting_subclass_invalid', { 'subclass' => $subclass } );
-        $class = $class . '::' . $subclass;
-    }
-    bless($self, $class);
 
-    $self->{'_setting_name'} = $setting_name;
-    $self->{'_user_id'}      = $user_id;
+    $self->{'is_enabled'}    = $is_enabled;
+    $self->{'default_value'} = $default;
+    $self->{'category'}      = $category;
 
-    return $self;
+    # IF the setting is enabled, AND the user has chosen a setting
+    # THEN return that value
+    # ELSE return the site default, and note that it is the default.
+    if (($is_enabled) && (defined $value)) {
+      $self->{'value'} = $value;
+    }
+    else {
+      $self->{'value'}     = $default;
+      $self->{'isdefault'} = 1;
+    }
+  }
+  else {
+    # If the values were passed in, simply assign them and return.
+    $self->{'is_enabled'}    = shift;
+    $self->{'default_value'} = shift;
+    $self->{'value'}         = shift;
+    $self->{'is_default'}    = shift;
+    $subclass                = shift;
+    $self->{'category'}      = shift;
+  }
+  if ($subclass) {
+    eval { require_module($class . '::' . $subclass) }
+      || ThrowCodeError('setting_subclass_invalid', {'subclass' => $subclass});
+    $class = $class . '::' . $subclass;
+  }
+  bless($self, $class);
+
+  $self->{'_setting_name'} = $setting_name;
+  $self->{'_user_id'}      = $user_id;
+
+  return $self;
 }
 
 ###############################
@@ -120,205 +118,220 @@ sub new {
 ###############################
 
 sub add_setting {
-    my ($params) = @_;
-    my ($name, $options, $default, $subclass, $force_check, $silently, $category)
-        = @$params{qw( name options default subclass force_check silently category )};
-    my $dbh = Bugzilla->dbh;
-
-    # Categories were added later, so we need to check if the old
-    # setting has the correct category if provided
-    my $exists = _setting_exists($name);
-    if ($exists && $category) {
-        my $old_category = $dbh->selectrow_arrayref(
-            "SELECT category FROM setting WHERE name = ?", undef, $name);
-        if ($old_category ne $category) {
-            $dbh->do('UPDATE setting SET category = ? WHERE name = ?',
-                     undef, $category, $name);
-            Bugzilla->memcached->clear_config();
-        }
+  my ($params) = @_;
+  my ($name, $options, $default, $subclass, $force_check, $silently, $category)
+    = @$params{qw( name options default subclass force_check silently category )};
+  my $dbh = Bugzilla->dbh;
+
+  # Categories were added later, so we need to check if the old
+  # setting has the correct category if provided
+  my $exists = _setting_exists($name);
+  if ($exists && $category) {
+    my $old_category
+      = $dbh->selectrow_arrayref("SELECT category FROM setting WHERE name = ?",
+      undef, $name);
+    if ($old_category ne $category) {
+      $dbh->do('UPDATE setting SET category = ? WHERE name = ?',
+        undef, $category, $name);
+      Bugzilla->memcached->clear_config();
     }
+  }
 
-    return if ($exists && !$force_check);
-
-    ($name && $default)
-      ||  ThrowCodeError("setting_info_invalid");
-
-    if ($exists) {
-        # If this setting exists, we delete it and regenerate it.
-        $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name);
-        $dbh->do('DELETE FROM setting WHERE name = ?', undef, $name);
-        # Remove obsolete user preferences for this setting.
-        if (defined $options && scalar(@$options)) {
-            my $list = join(', ', map {$dbh->quote($_)} @$options);
-            $dbh->do("DELETE FROM profile_setting
-                      WHERE setting_name = ? AND setting_value NOT IN ($list)",
-                      undef, $name);
-        }
-    }
-    elsif (!$silently) {
-        print get_text('install_setting_new', { name => $name }) . "\n";
-    }
-    $dbh->do(q{INSERT INTO setting (name, default_value, is_enabled, subclass, category)
-                    VALUES (?, ?, 1, ?, ?)},
-             undef, ($name, $default, $subclass, $category));
+  return if ($exists && !$force_check);
+
+  ($name && $default) || ThrowCodeError("setting_info_invalid");
 
-    my $sth = $dbh->prepare(q{INSERT INTO setting_value (name, value, sortindex)
-                                    VALUES (?, ?, ?)});
+  if ($exists) {
 
-    my $sortindex = 5;
-    foreach my $key (@$options){
-        $sth->execute($name, $key, $sortindex);
-        $sortindex += 5;
+    # If this setting exists, we delete it and regenerate it.
+    $dbh->do('DELETE FROM setting_value WHERE name = ?', undef, $name);
+    $dbh->do('DELETE FROM setting WHERE name = ?',       undef, $name);
+
+    # Remove obsolete user preferences for this setting.
+    if (defined $options && scalar(@$options)) {
+      my $list = join(', ', map { $dbh->quote($_) } @$options);
+      $dbh->do(
+        "DELETE FROM profile_setting
+                      WHERE setting_name = ? AND setting_value NOT IN ($list)", undef,
+        $name
+      );
     }
+  }
+  elsif (!$silently) {
+    print get_text('install_setting_new', {name => $name}) . "\n";
+  }
+  $dbh->do(
+    q{INSERT INTO setting (name, default_value, is_enabled, subclass, category)
+                    VALUES (?, ?, 1, ?, ?)}, undef,
+    ($name, $default, $subclass, $category)
+  );
+
+  my $sth = $dbh->prepare(
+    q{INSERT INTO setting_value (name, value, sortindex)
+                                    VALUES (?, ?, ?)}
+  );
+
+  my $sortindex = 5;
+  foreach my $key (@$options) {
+    $sth->execute($name, $key, $sortindex);
+    $sortindex += 5;
+  }
 }
 
 sub get_all_settings {
-    my ($user_id) = @_;
-    my $settings = {};
-    my $dbh = Bugzilla->dbh;
-
-    my $cache_key = "user_settings.$user_id";
-    my $rows = Bugzilla->memcached->get_config({ key => $cache_key });
-    if (!$rows) {
-        $rows = $dbh->selectall_arrayref(
-            q{SELECT name, default_value, is_enabled, setting_value, subclass, category
+  my ($user_id) = @_;
+  my $settings  = {};
+  my $dbh       = Bugzilla->dbh;
+
+  my $cache_key = "user_settings.$user_id";
+  my $rows = Bugzilla->memcached->get_config({key => $cache_key});
+  if (!$rows) {
+    $rows = $dbh->selectall_arrayref(
+      q{SELECT name, default_value, is_enabled, setting_value, subclass, category
                 FROM setting
            LEFT JOIN profile_setting
                      ON setting.name = profile_setting.setting_name
-                     AND profile_setting.user_id = ?}, undef, ($user_id));
-        Bugzilla->memcached->set_config({ key => $cache_key, data => $rows });
-    }
+                     AND profile_setting.user_id = ?}, undef, ($user_id)
+    );
+    Bugzilla->memcached->set_config({key => $cache_key, data => $rows});
+  }
 
-    foreach my $row (@$rows) {
-        my ($name, $default_value, $is_enabled, $value, $subclass, $category) = @$row;
+  foreach my $row (@$rows) {
+    my ($name, $default_value, $is_enabled, $value, $subclass, $category) = @$row;
 
-        my $is_default;
+    my $is_default;
 
-        if ( ($is_enabled) && (defined $value) ) {
-            $is_default = 0;
-        } else {
-            $value = $default_value;
-            $is_default = 1;
-        }
-
-        $settings->{$name} = new Bugzilla::User::Setting(
-           $name, $user_id, $is_enabled,
-           $default_value, $value, $is_default,
-           $subclass, $category);
+    if (($is_enabled) && (defined $value)) {
+      $is_default = 0;
     }
+    else {
+      $value      = $default_value;
+      $is_default = 1;
+    }
+
+    $settings->{$name}
+      = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value,
+      $value, $is_default, $subclass, $category);
+  }
 
-    return $settings;
+  return $settings;
 }
 
 sub clear_settings_cache {
-    my ($user_id) = @_;
-    Bugzilla->memcached->clear_config({ key => "user_settings.$user_id" });
+  my ($user_id) = @_;
+  Bugzilla->memcached->clear_config({key => "user_settings.$user_id"});
 }
 
 sub get_defaults {
-    my ($user_id) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $default_settings = {};
+  my ($user_id)        = @_;
+  my $dbh              = Bugzilla->dbh;
+  my $default_settings = {};
 
-    $user_id ||= 0;
+  $user_id ||= 0;
 
-    my $rows = $dbh->selectall_arrayref(q{SELECT name, default_value, is_enabled, subclass, category
-                                            FROM setting});
+  my $rows = $dbh->selectall_arrayref(
+    q{SELECT name, default_value, is_enabled, subclass, category
+                                            FROM setting}
+  );
 
-    foreach my $row (@$rows) {
-        my ($name, $default_value, $is_enabled, $subclass, $category) = @$row;
+  foreach my $row (@$rows) {
+    my ($name, $default_value, $is_enabled, $subclass, $category) = @$row;
 
-        $default_settings->{$name} = new Bugzilla::User::Setting(
-            $name, $user_id, $is_enabled, $default_value, $default_value, 1,
-            $subclass, $category);
-    }
+    $default_settings->{$name}
+      = new Bugzilla::User::Setting($name, $user_id, $is_enabled, $default_value,
+      $default_value, 1, $subclass, $category);
+  }
 
-    return $default_settings;
+  return $default_settings;
 }
 
 sub set_default {
-    my ($setting_name, $default_value, $is_enabled) = @_;
-    my $dbh = Bugzilla->dbh;
+  my ($setting_name, $default_value, $is_enabled) = @_;
+  my $dbh = Bugzilla->dbh;
 
-    my $sth = $dbh->prepare(q{UPDATE setting
+  my $sth = $dbh->prepare(
+    q{UPDATE setting
                                  SET default_value = ?, is_enabled = ?
-                               WHERE name = ?});
-    $sth->execute($default_value, $is_enabled, $setting_name);
+                               WHERE name = ?}
+  );
+  $sth->execute($default_value, $is_enabled, $setting_name);
 }
 
 sub _setting_exists {
-    my ($setting_name) = @_;
-    my $dbh = Bugzilla->dbh;
-    return $dbh->selectrow_arrayref(
-        "SELECT 1 FROM setting WHERE name = ?", undef, $setting_name) || 0;
+  my ($setting_name) = @_;
+  my $dbh = Bugzilla->dbh;
+  return $dbh->selectrow_arrayref("SELECT 1 FROM setting WHERE name = ?",
+    undef, $setting_name)
+    || 0;
 }
 
 
 sub legal_values {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'legal_values'} if defined $self->{'legal_values'};
+  return $self->{'legal_values'} if defined $self->{'legal_values'};
 
-    my $dbh = Bugzilla->dbh;
-    $self->{'legal_values'} = $dbh->selectcol_arrayref(
-              q{SELECT value
+  my $dbh = Bugzilla->dbh;
+  $self->{'legal_values'} = $dbh->selectcol_arrayref(
+    q{SELECT value
                   FROM setting_value
                  WHERE name = ?
-              ORDER BY sortindex},
-        undef, $self->{'_setting_name'});
+              ORDER BY sortindex}, undef, $self->{'_setting_name'}
+  );
 
-    return $self->{'legal_values'};
+  return $self->{'legal_values'};
 }
 
 sub validate_value {
-    my $self = shift;
-
-    if (grep(/^$_[0]$/, @{$self->legal_values()})) {
-        trick_taint($_[0]);
-    }
-    else {
-        ThrowCodeError('setting_value_invalid',
-                       {'name'  => $self->{'_setting_name'},
-                        'value' => $_[0]});
-    }
+  my $self = shift;
+
+  if (grep(/^$_[0]$/, @{$self->legal_values()})) {
+    trick_taint($_[0]);
+  }
+  else {
+    ThrowCodeError('setting_value_invalid',
+      {'name' => $self->{'_setting_name'}, 'value' => $_[0]});
+  }
 }
 
 sub reset_to_default {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    my $dbh = Bugzilla->dbh;
-    my $sth = $dbh->do(q{ DELETE
+  my $dbh = Bugzilla->dbh;
+  my $sth = $dbh->do(
+    q{ DELETE
                             FROM profile_setting
                            WHERE setting_name = ?
-                             AND user_id = ?},
-                       undef, $self->{'_setting_name'}, $self->{'_user_id'});
-      $self->{'value'}       = $self->{'default_value'};
-      $self->{'is_default'}  = 1;
+                             AND user_id = ?}, undef, $self->{'_setting_name'},
+    $self->{'_user_id'}
+  );
+  $self->{'value'}      = $self->{'default_value'};
+  $self->{'is_default'} = 1;
 }
 
 sub set {
-    my ($self, $value) = @_;
-    my $dbh = Bugzilla->dbh;
-    my $query;
+  my ($self, $value) = @_;
+  my $dbh = Bugzilla->dbh;
+  my $query;
 
-    if ($self->{'is_default'}) {
-        $query = q{INSERT INTO profile_setting
+  if ($self->{'is_default'}) {
+    $query = q{INSERT INTO profile_setting
                    (setting_value, setting_name, user_id)
                    VALUES (?,?,?)};
-    } else {
-        $query = q{UPDATE profile_setting
+  }
+  else {
+    $query = q{UPDATE profile_setting
                       SET setting_value = ?
                     WHERE setting_name = ?
                       AND user_id = ?};
-    }
-    $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'});
+  }
+  $dbh->do($query, undef, $value, $self->{'_setting_name'}, $self->{'_user_id'});
 
-    $self->{'value'}       = $value;
-    $self->{'is_default'}  = 0;
+  $self->{'value'}      = $value;
+  $self->{'is_default'} = 0;
 }
 
 
-
 1;
 
 __END__
diff --git a/Bugzilla/User/Setting/Lang.pm b/Bugzilla/User/Setting/Lang.pm
index a3032b9fc..5f49bc6e4 100644
--- a/Bugzilla/User/Setting/Lang.pm
+++ b/Bugzilla/User/Setting/Lang.pm
@@ -16,11 +16,11 @@ use base qw(Bugzilla::User::Setting);
 use Bugzilla::Constants;
 
 sub legal_values {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'legal_values'} if defined $self->{'legal_values'};
+  return $self->{'legal_values'} if defined $self->{'legal_values'};
 
-    return $self->{'legal_values'} = Bugzilla->languages;
+  return $self->{'legal_values'} = Bugzilla->languages;
 }
 
 1;
diff --git a/Bugzilla/User/Setting/Skin.pm b/Bugzilla/User/Setting/Skin.pm
index f0ea502ef..f4d82007b 100644
--- a/Bugzilla/User/Setting/Skin.pm
+++ b/Bugzilla/User/Setting/Skin.pm
@@ -21,28 +21,31 @@ use File::Basename;
 use constant BUILTIN_SKIN_NAMES => ['standard'];
 
 sub legal_values {
-    my ($self) = @_;
-
-    return $self->{'legal_values'} if defined $self->{'legal_values'};
-
-    my $dirbase = bz_locations()->{'skinsdir'} . '/contrib';
-    # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the
-    # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES.
-    my @legal_values = @{(BUILTIN_SKIN_NAMES)};
-
-    foreach my $direntry (glob(catdir($dirbase, '*'))) {
-        if (-d $direntry) {
-            # Stylesheet set
-            next if basename($direntry) =~ /^cvs$/i;
-            push(@legal_values, basename($direntry));
-        }
-        elsif ($direntry =~ /\.css$/) {
-            # Single-file stylesheet
-            push(@legal_values, basename($direntry));
-        }
+  my ($self) = @_;
+
+  return $self->{'legal_values'} if defined $self->{'legal_values'};
+
+  my $dirbase = bz_locations()->{'skinsdir'} . '/contrib';
+
+  # Avoid modification of the list BUILTIN_SKIN_NAMES points to by copying the
+  # list over instead of simply writing $legal_values = BUILTIN_SKIN_NAMES.
+  my @legal_values = @{(BUILTIN_SKIN_NAMES)};
+
+  foreach my $direntry (glob(catdir($dirbase, '*'))) {
+    if (-d $direntry) {
+
+      # Stylesheet set
+      next if basename($direntry) =~ /^cvs$/i;
+      push(@legal_values, basename($direntry));
+    }
+    elsif ($direntry =~ /\.css$/) {
+
+      # Single-file stylesheet
+      push(@legal_values, basename($direntry));
     }
+  }
 
-    return $self->{'legal_values'} = \@legal_values;
+  return $self->{'legal_values'} = \@legal_values;
 }
 
 1;
diff --git a/Bugzilla/User/Setting/Timezone.pm b/Bugzilla/User/Setting/Timezone.pm
index a9515259e..6e20f1fc2 100644
--- a/Bugzilla/User/Setting/Timezone.pm
+++ b/Bugzilla/User/Setting/Timezone.pm
@@ -18,19 +18,21 @@ use base qw(Bugzilla::User::Setting);
 use Bugzilla::Constants;
 
 sub legal_values {
-    my ($self) = @_;
+  my ($self) = @_;
 
-    return $self->{'legal_values'} if defined $self->{'legal_values'};
+  return $self->{'legal_values'} if defined $self->{'legal_values'};
 
-    my @timezones = DateTime::TimeZone->all_names;
-    # Remove old formats, such as CST6CDT, EST, EST5EDT.
-    @timezones = grep { $_ =~ m#.+/.+#} @timezones;
-    # Append 'local' to the list, which will use the timezone
-    # given by the server.
-    push(@timezones, 'local');
-    push(@timezones, 'UTC');
+  my @timezones = DateTime::TimeZone->all_names;
 
-    return $self->{'legal_values'} = \@timezones;
+  # Remove old formats, such as CST6CDT, EST, EST5EDT.
+  @timezones = grep { $_ =~ m#.+/.+# } @timezones;
+
+  # Append 'local' to the list, which will use the timezone
+  # given by the server.
+  push(@timezones, 'local');
+  push(@timezones, 'UTC');
+
+  return $self->{'legal_values'} = \@timezones;
 }
 
 1;
diff --git a/Bugzilla/UserAgent.pm b/Bugzilla/UserAgent.pm
index bd31a2a13..ec75a6131 100644
--- a/Bugzilla/UserAgent.pm
+++ b/Bugzilla/UserAgent.pm
@@ -20,177 +20,202 @@ use List::MoreUtils qw(natatime);
 use constant DEFAULT_VALUE => 'Other';
 
 use constant PLATFORMS_MAP => (
-    # PowerPC
-    qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"],
-    # AMD64, Intel x86_64
-    qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*amd64.*\)/ => ["AMD64", "x86_64", "PC"],
-    qr/\(.*x86_64.*\)/ => ["AMD64", "x86_64", "PC"],
-    qr/\(.*Intel Mac OS X.*\)/ => ["x86_64"],
-    # Intel IA64
-    qr/\(.*IA64.*\)/ => ["IA64", "PC"],
-    # Intel x86
-    qr/\(.*Intel.*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"],
-    # Versions of Windows that only run on Intel x86
-    qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*Win(?:dows |)16.*\)/ => ["IA32", "x86", "PC"],
-    # Sparc
-    qr/\(.*sparc.*\)/ => ["Sparc", "Sun"],
-    qr/\(.*sun4.*\)/ => ["Sparc", "Sun"],
-    # Alpha
-    qr/\(.*AXP.*\)/i => ["Alpha", "DEC"],
-    qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"],
-    qr/\(.*[ _]Alpha\)/i => ["Alpha", "DEC"],
-    # MIPS
-    qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
-    qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"],
-    # 68k
-    qr/\(.*68K.*\)/ => ["68k", "Macintosh"],
-    qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"],
-    # HP
-    qr/\(.*9000.*\)/ => ["PA-RISC", "HP"],
-    # ARM
-    qr/\(.*(?:iPad|iPhone).*\)/ => ["ARM"],
-    qr/\(.*ARM.*\)/ => ["ARM", "PocketPC"],
-    # PocketPC intentionally before PowerPC
-    qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"],
-    # PowerPC
-    qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"],
-    qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"],
-    # Stereotypical and broken
-    qr/\(.*Windows CE.*\)/ => ["ARM", "PocketPC"],
-    qr/\(.*Macintosh.*\)/ => ["68k", "Macintosh"],
-    qr/\(.*Mac OS [89].*\)/ => ["68k", "Macintosh"],
-    qr/\(.*WOW64.*\)/ => ["x86_64"],
-    qr/\(.*Win64.*\)/ => ["IA64"],
-    qr/\(Win.*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32", "x86", "PC"],
-    qr/\(.*OSF.*\)/ => ["Alpha", "DEC"],
-    qr/\(.*HP-?UX.*\)/i => ["PA-RISC", "HP"],
-    qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
-    qr/\(.*(SunOS|Solaris).*\)/ => ["Sparc", "Sun"],
-    # Braindead old browsers who didn't follow convention:
-    qr/Amiga/ => ["68k", "Macintosh"],
-    qr/WinMosaic/ => ["IA32", "x86", "PC"],
+
+  # PowerPC
+  qr/\(.*PowerPC.*\)/i => ["PowerPC", "Macintosh"],
+
+  # AMD64, Intel x86_64
+  qr/\(.*[ix0-9]86 (?:on |\()x86_64.*\)/ => ["IA32",  "x86",    "PC"],
+  qr/\(.*amd64.*\)/                      => ["AMD64", "x86_64", "PC"],
+  qr/\(.*x86_64.*\)/                     => ["AMD64", "x86_64", "PC"],
+  qr/\(.*Intel Mac OS X.*\)/             => ["x86_64"],
+
+  # Intel IA64
+  qr/\(.*IA64.*\)/ => ["IA64", "PC"],
+
+  # Intel x86
+  qr/\(.*Intel.*\)/     => ["IA32", "x86", "PC"],
+  qr/\(.*[ix0-9]86.*\)/ => ["IA32", "x86", "PC"],
+
+  # Versions of Windows that only run on Intel x86
+  qr/\(.*Win(?:dows |)[39M].*\)/ => ["IA32", "x86", "PC"],
+  qr/\(.*Win(?:dows |)16.*\)/    => ["IA32", "x86", "PC"],
+
+  # Sparc
+  qr/\(.*sparc.*\)/ => ["Sparc", "Sun"],
+  qr/\(.*sun4.*\)/  => ["Sparc", "Sun"],
+
+  # Alpha
+  qr/\(.*AXP.*\)/i      => ["Alpha", "DEC"],
+  qr/\(.*[ _]Alpha.\D/i => ["Alpha", "DEC"],
+  qr/\(.*[ _]Alpha\)/i  => ["Alpha", "DEC"],
+
+  # MIPS
+  qr/\(.*IRIX.*\)/i => ["MIPS", "SGI"],
+  qr/\(.*MIPS.*\)/i => ["MIPS", "SGI"],
+
+  # 68k
+  qr/\(.*68K.*\)/      => ["68k", "Macintosh"],
+  qr/\(.*680[x0]0.*\)/ => ["68k", "Macintosh"],
+
+  # HP
+  qr/\(.*9000.*\)/ => ["PA-RISC", "HP"],
+
+  # ARM
+  qr/\(.*(?:iPad|iPhone).*\)/ => ["ARM"],
+  qr/\(.*ARM.*\)/             => ["ARM", "PocketPC"],
+
+  # PocketPC intentionally before PowerPC
+  qr/\(.*Windows CE.*PPC.*\)/ => ["ARM", "PocketPC"],
+
+  # PowerPC
+  qr/\(.*PPC.*\)/ => ["PowerPC", "Macintosh"],
+  qr/\(.*AIX.*\)/ => ["PowerPC", "Macintosh"],
+
+  # Stereotypical and broken
+  qr/\(.*Windows CE.*\)/        => ["ARM",     "PocketPC"],
+  qr/\(.*Macintosh.*\)/         => ["68k",     "Macintosh"],
+  qr/\(.*Mac OS [89].*\)/       => ["68k",     "Macintosh"],
+  qr/\(.*WOW64.*\)/             => ["x86_64"],
+  qr/\(.*Win64.*\)/             => ["IA64"],
+  qr/\(Win.*\)/                 => ["IA32",    "x86", "PC"],
+  qr/\(.*Win(?:dows[ -])NT.*\)/ => ["IA32",    "x86", "PC"],
+  qr/\(.*OSF.*\)/               => ["Alpha",   "DEC"],
+  qr/\(.*HP-?UX.*\)/i           => ["PA-RISC", "HP"],
+  qr/\(.*IRIX.*\)/i             => ["MIPS",    "SGI"],
+  qr/\(.*(SunOS|Solaris).*\)/   => ["Sparc",   "Sun"],
+
+  # Braindead old browsers who didn't follow convention:
+  qr/Amiga/     => ["68k",  "Macintosh"],
+  qr/WinMosaic/ => ["IA32", "x86", "PC"],
 );
 
 use constant OS_MAP => (
-    # Sun
-    qr/\(.*Solaris.*\)/ => ["Solaris"],
-    qr/\(.*SunOS 5.11.*\)/ => [("OpenSolaris", "Opensolaris", "Solaris 11")],
-    qr/\(.*SunOS 5.10.*\)/ => ["Solaris 10"],
-    qr/\(.*SunOS 5.9.*\)/ => ["Solaris 9"],
-    qr/\(.*SunOS 5.8.*\)/ => ["Solaris 8"],
-    qr/\(.*SunOS 5.7.*\)/ => ["Solaris 7"],
-    qr/\(.*SunOS 5.6.*\)/ => ["Solaris 6"],
-    qr/\(.*SunOS 5.5.*\)/ => ["Solaris 5"],
-    qr/\(.*SunOS 5.*\)/ => ["Solaris"],
-    qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"],
-    qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"],
-    qr/\(.*SunOS.*\)/ => ["SunOS"],
-    # BSD
-    qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"],
-    qr/\(.*FreeBSD.*\)/ => ["FreeBSD"],
-    qr/\(.*OpenBSD.*\)/ => ["OpenBSD"],
-    qr/\(.*NetBSD.*\)/ => ["NetBSD"],
-    # Misc POSIX
-    qr/\(.*IRIX.*\)/ => ["IRIX"],
-    qr/\(.*OSF.*\)/ => ["OSF/1"],
-    qr/\(.*Linux.*\)/ => ["Linux"],
-    qr/\(.*BeOS.*\)/ => ["BeOS"],
-    qr/\(.*AIX.*\)/ => ["AIX"],
-    qr/\(.*OS\/2.*\)/ => ["OS/2"],
-    qr/\(.*QNX.*\)/ => ["Neutrino"],
-    qr/\(.*VMS.*\)/ => ["OpenVMS"],
-    qr/\(.*HP-?UX.*\)/ => ["HP-UX"],
-    qr/\(.*Android.*\)/ => ["Android"],
-    # Windows
-    qr/\(.*Windows XP.*\)/ => ["Windows XP"],
-    qr/\(.*Windows NT 10\.0.*\)/ => ["Windows 10"],
-    qr/\(.*Windows NT 6\.4.*\)/ => ["Windows 10"],
-    qr/\(.*Windows NT 6\.3.*\)/ => ["Windows 8.1"],
-    qr/\(.*Windows NT 6\.2.*\)/ => ["Windows 8"],
-    qr/\(.*Windows NT 6\.1.*\)/ => ["Windows 7"],
-    qr/\(.*Windows NT 6\.0.*\)/ => ["Windows Vista"],
-    qr/\(.*Windows NT 5\.2.*\)/ => ["Windows Server 2003"],
-    qr/\(.*Windows NT 5\.1.*\)/ => ["Windows XP"],
-    qr/\(.*Windows 2000.*\)/ => ["Windows 2000"],
-    qr/\(.*Windows NT 5.*\)/ => ["Windows 2000"],
-    qr/\(.*Win.*9[8x].*4\.9.*\)/ => ["Windows ME"],
-    qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"],
-    qr/\(.*Win(?:dows |)98.*\)/ => ["Windows 98"],
-    qr/\(.*Win(?:dows |)95.*\)/ => ["Windows 95"],
-    qr/\(.*Win(?:dows |)16.*\)/ => ["Windows 3.1"],
-    qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"],
-    qr/\(.*Windows.*NT.*\)/ => ["Windows NT"],
-    # OS X
-    qr/\(.*(?:iPad|iPhone).*OS 7.*\)/ => ["iOS 7"],
-    qr/\(.*(?:iPad|iPhone).*OS 6.*\)/ => ["iOS 6"],
-    qr/\(.*(?:iPad|iPhone).*OS 5.*\)/ => ["iOS 5"],
-    qr/\(.*(?:iPad|iPhone).*OS 4.*\)/ => ["iOS 4"],
-    qr/\(.*(?:iPad|iPhone).*OS 3.*\)/ => ["iOS 3"],
-    qr/\(.*(?:iPad|iPhone).*\)/ => ["iOS"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"],
-    qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"],
-    # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback
-    # support because some browsers refused to include the OS Version.
-    qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"],
-    # OS X 10.3 is the most likely default version of PowerPC Macs
-    # OS X 10.0 is more for configurations which didn't setup 10.x versions
-    qr/\(.*Mac OS X.*\)/ => [("Mac OS X 10.3", "Mac OS X 10.0", "Mac OS X")],
-    qr/\(.*Mac OS 9.*\)/ => [("Mac System 9.x", "Mac System 9.0")],
-    qr/\(.*Mac OS 8\.6.*\)/ => [("Mac System 8.6", "Mac System 8.5")],
-    qr/\(.*Mac OS 8\.5.*\)/ => ["Mac System 8.5"],
-    qr/\(.*Mac OS 8\.1.*\)/ => [("Mac System 8.1", "Mac System 8.0")],
-    qr/\(.*Mac OS 8\.0.*\)/ => ["Mac System 8.0"],
-    qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"],
-    qr/\(.*Mac OS 8.*\)/ => ["Mac System 8.6"],
-    qr/\(.*Darwin.*\)/ => [("Mac OS X 10.0", "Mac OS X")],
-    # Firefox OS
-    qr/\(Mobile;.*Gecko.*Firefox/ => ["Gonk (Firefox OS)"],
-    # Silly
-    qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"],
-    qr/\(.*Mac.*PPC.*\)/ => ["Mac System 9.x"],
-    qr/\(.*Mac.*68k.*\)/ => ["Mac System 8.0"],
-    # Evil
-    qr/Amiga/i => ["Other"],
-    qr/WinMosaic/ => ["Windows 95"],
-    qr/\(.*32bit.*\)/ => ["Windows 95"],
-    qr/\(.*16bit.*\)/ => ["Windows 3.1"],
-    qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"],
-    qr/\(.*PPC.*\)/ => ["Mac System 9.x"],
-    qr/\(.*68K.*\)/ => ["Mac System 8.0"],
+
+  # Sun
+  qr/\(.*Solaris.*\)/      => ["Solaris"],
+  qr/\(.*SunOS 5.11.*\)/   => [("OpenSolaris", "Opensolaris", "Solaris 11")],
+  qr/\(.*SunOS 5.10.*\)/   => ["Solaris 10"],
+  qr/\(.*SunOS 5.9.*\)/    => ["Solaris 9"],
+  qr/\(.*SunOS 5.8.*\)/    => ["Solaris 8"],
+  qr/\(.*SunOS 5.7.*\)/    => ["Solaris 7"],
+  qr/\(.*SunOS 5.6.*\)/    => ["Solaris 6"],
+  qr/\(.*SunOS 5.5.*\)/    => ["Solaris 5"],
+  qr/\(.*SunOS 5.*\)/      => ["Solaris"],
+  qr/\(.*SunOS.*sun4u.*\)/ => ["Solaris"],
+  qr/\(.*SunOS.*i86pc.*\)/ => ["Solaris"],
+  qr/\(.*SunOS.*\)/        => ["SunOS"],
+
+  # BSD
+  qr/\(.*BSD\/(?:OS|386).*\)/ => ["BSDI"],
+  qr/\(.*FreeBSD.*\)/         => ["FreeBSD"],
+  qr/\(.*OpenBSD.*\)/         => ["OpenBSD"],
+  qr/\(.*NetBSD.*\)/          => ["NetBSD"],
+
+  # Misc POSIX
+  qr/\(.*IRIX.*\)/    => ["IRIX"],
+  qr/\(.*OSF.*\)/     => ["OSF/1"],
+  qr/\(.*Linux.*\)/   => ["Linux"],
+  qr/\(.*BeOS.*\)/    => ["BeOS"],
+  qr/\(.*AIX.*\)/     => ["AIX"],
+  qr/\(.*OS\/2.*\)/   => ["OS/2"],
+  qr/\(.*QNX.*\)/     => ["Neutrino"],
+  qr/\(.*VMS.*\)/     => ["OpenVMS"],
+  qr/\(.*HP-?UX.*\)/  => ["HP-UX"],
+  qr/\(.*Android.*\)/ => ["Android"],
+
+  # Windows
+  qr/\(.*Windows XP.*\)/         => ["Windows XP"],
+  qr/\(.*Windows NT 10\.0.*\)/   => ["Windows 10"],
+  qr/\(.*Windows NT 6\.4.*\)/    => ["Windows 10"],
+  qr/\(.*Windows NT 6\.3.*\)/    => ["Windows 8.1"],
+  qr/\(.*Windows NT 6\.2.*\)/    => ["Windows 8"],
+  qr/\(.*Windows NT 6\.1.*\)/    => ["Windows 7"],
+  qr/\(.*Windows NT 6\.0.*\)/    => ["Windows Vista"],
+  qr/\(.*Windows NT 5\.2.*\)/    => ["Windows Server 2003"],
+  qr/\(.*Windows NT 5\.1.*\)/    => ["Windows XP"],
+  qr/\(.*Windows 2000.*\)/       => ["Windows 2000"],
+  qr/\(.*Windows NT 5.*\)/       => ["Windows 2000"],
+  qr/\(.*Win.*9[8x].*4\.9.*\)/   => ["Windows ME"],
+  qr/\(.*Win(?:dows |)M[Ee].*\)/ => ["Windows ME"],
+  qr/\(.*Win(?:dows |)98.*\)/    => ["Windows 98"],
+  qr/\(.*Win(?:dows |)95.*\)/    => ["Windows 95"],
+  qr/\(.*Win(?:dows |)16.*\)/    => ["Windows 3.1"],
+  qr/\(.*Win(?:dows[ -]|)NT.*\)/ => ["Windows NT"],
+  qr/\(.*Windows.*NT.*\)/        => ["Windows NT"],
+
+  # OS X
+  qr/\(.*(?:iPad|iPhone).*OS 7.*\)/        => ["iOS 7"],
+  qr/\(.*(?:iPad|iPhone).*OS 6.*\)/        => ["iOS 6"],
+  qr/\(.*(?:iPad|iPhone).*OS 5.*\)/        => ["iOS 5"],
+  qr/\(.*(?:iPad|iPhone).*OS 4.*\)/        => ["iOS 4"],
+  qr/\(.*(?:iPad|iPhone).*OS 3.*\)/        => ["iOS 3"],
+  qr/\(.*(?:iPad|iPhone).*\)/              => ["iOS"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.6.*\)/ => ["Mac OS X 10.6"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.5.*\)/ => ["Mac OS X 10.5"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.4.*\)/ => ["Mac OS X 10.4"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.3.*\)/ => ["Mac OS X 10.3"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.2.*\)/ => ["Mac OS X 10.2"],
+  qr/\(.*Mac OS X (?:|Mach-O |\()10.1.*\)/ => ["Mac OS X 10.1"],
+
+  # Unfortunately, OS X 10.4 was the first to support Intel. This is fallback
+  # support because some browsers refused to include the OS Version.
+  qr/\(.*Intel.*Mac OS X.*\)/ => ["Mac OS X 10.4"],
+
+  # OS X 10.3 is the most likely default version of PowerPC Macs
+  # OS X 10.0 is more for configurations which didn't setup 10.x versions
+  qr/\(.*Mac OS X.*\)/     => [("Mac OS X 10.3",  "Mac OS X 10.0", "Mac OS X")],
+  qr/\(.*Mac OS 9.*\)/     => [("Mac System 9.x", "Mac System 9.0")],
+  qr/\(.*Mac OS 8\.6.*\)/  => [("Mac System 8.6", "Mac System 8.5")],
+  qr/\(.*Mac OS 8\.5.*\)/  => ["Mac System 8.5"],
+  qr/\(.*Mac OS 8\.1.*\)/  => [("Mac System 8.1", "Mac System 8.0")],
+  qr/\(.*Mac OS 8\.0.*\)/  => ["Mac System 8.0"],
+  qr/\(.*Mac OS 8[^.].*\)/ => ["Mac System 8.0"],
+  qr/\(.*Mac OS 8.*\)/     => ["Mac System 8.6"],
+  qr/\(.*Darwin.*\)/       => [("Mac OS X 10.0",  "Mac OS X")],
+
+  # Firefox OS
+  qr/\(Mobile;.*Gecko.*Firefox/ => ["Gonk (Firefox OS)"],
+
+  # Silly
+  qr/\(.*Mac.*PowerPC.*\)/ => ["Mac System 9.x"],
+  qr/\(.*Mac.*PPC.*\)/     => ["Mac System 9.x"],
+  qr/\(.*Mac.*68k.*\)/     => ["Mac System 8.0"],
+
+  # Evil
+  qr/Amiga/i          => ["Other"],
+  qr/WinMosaic/       => ["Windows 95"],
+  qr/\(.*32bit.*\)/   => ["Windows 95"],
+  qr/\(.*16bit.*\)/   => ["Windows 3.1"],
+  qr/\(.*PowerPC.*\)/ => ["Mac System 9.x"],
+  qr/\(.*PPC.*\)/     => ["Mac System 9.x"],
+  qr/\(.*68K.*\)/     => ["Mac System 8.0"],
 );
 
 sub detect_platform {
-    my $userAgent = shift || Bugzilla->cgi->user_agent || '';
-    my @detected;
-    my $iterator = natatime(2, PLATFORMS_MAP);
-    while (my($re, $ra) = $iterator->()) {
-        if ($userAgent =~ $re) {
-            push @detected, @$ra;
-        }
+  my $userAgent = shift || Bugzilla->cgi->user_agent || '';
+  my @detected;
+  my $iterator = natatime(2, PLATFORMS_MAP);
+  while (my ($re, $ra) = $iterator->()) {
+    if ($userAgent =~ $re) {
+      push @detected, @$ra;
     }
-    return _pick_valid_field_value('rep_platform', @detected);
+  }
+  return _pick_valid_field_value('rep_platform', @detected);
 }
 
 sub detect_op_sys {
-    my $userAgent = shift || Bugzilla->cgi->user_agent || '';
-    my @detected;
-    my $iterator = natatime(2, OS_MAP);
-    while (my($re, $ra) = $iterator->()) {
-        if ($userAgent =~ $re) {
-            push @detected, @$ra;
-        }
+  my $userAgent = shift || Bugzilla->cgi->user_agent || '';
+  my @detected;
+  my $iterator = natatime(2, OS_MAP);
+  while (my ($re, $ra) = $iterator->()) {
+    if ($userAgent =~ $re) {
+      push @detected, @$ra;
     }
-    push(@detected, "Windows") if grep(/^Windows /, @detected);
-    push(@detected, "Mac OS") if grep(/^Mac /, @detected);
-    return _pick_valid_field_value('op_sys', @detected);
+  }
+  push(@detected, "Windows") if grep(/^Windows /, @detected);
+  push(@detected, "Mac OS")  if grep(/^Mac /,     @detected);
+  return _pick_valid_field_value('op_sys', @detected);
 }
 
 # Takes the name of a field and a list of possible values for that field.
@@ -198,11 +223,11 @@ sub detect_op_sys {
 # field.
 # Returns 'Other' if none of the values match.
 sub _pick_valid_field_value {
-    my ($field, @values) = @_;
-    foreach my $value (@values) {
-        return $value if check_field($field, $value, undef, 1);
-    }
-    return DEFAULT_VALUE;
+  my ($field, @values) = @_;
+  foreach my $value (@values) {
+    return $value if check_field($field, $value, undef, 1);
+  }
+  return DEFAULT_VALUE;
 }
 
 1;
diff --git a/Bugzilla/Util.pm b/Bugzilla/Util.pm
index aa524b263..ab7e2189b 100644
--- a/Bugzilla/Util.pm
+++ b/Bugzilla/Util.pm
@@ -13,22 +13,22 @@ use warnings;
 
 use base qw(Exporter);
 @Bugzilla::Util::EXPORT = qw(trick_taint detaint_natural
-                             detaint_signed
-                             with_writable_database with_readonly_database
-                             html_quote url_quote xml_quote
-                             css_class_quote html_light_quote
-                             i_am_cgi i_am_webservice is_webserver_group
-                             correct_urlbase remote_ip
-                             validate_ip do_ssl_redirect_if_required use_attachbase
-                             diff_arrays on_main_db css_url_rewrite
-                             trim wrap_hard wrap_comment find_wrap_point
-                             format_time validate_date validate_time datetime_from time_ago
-                             file_mod_time is_7bit_clean
-                             bz_crypt generate_random_password
-                             validate_email_syntax clean_text
-                             get_text template_var disable_utf8
-                             enable_utf8 detect_encoding email_filter
-                             round extract_nicks);
+  detaint_signed
+  with_writable_database with_readonly_database
+  html_quote url_quote xml_quote
+  css_class_quote html_light_quote
+  i_am_cgi i_am_webservice is_webserver_group
+  correct_urlbase remote_ip
+  validate_ip do_ssl_redirect_if_required use_attachbase
+  diff_arrays on_main_db css_url_rewrite
+  trim wrap_hard wrap_comment find_wrap_point
+  format_time validate_date validate_time datetime_from time_ago
+  file_mod_time is_7bit_clean
+  bz_crypt generate_random_password
+  validate_email_syntax clean_text
+  get_text template_var disable_utf8
+  enable_utf8 detect_encoding email_filter
+  round extract_nicks);
 use Bugzilla::Logging;
 use Bugzilla::Constants;
 use Bugzilla::RNG qw(irand);
@@ -50,663 +50,704 @@ use Text::Wrap;
 use Try::Tiny;
 
 sub with_writable_database(&) {
-    my ($code) = @_;
-    my $dbh = Bugzilla->dbh_main;
-    local Bugzilla->request_cache->{dbh} = $dbh;
-    local Bugzilla->request_cache->{error_mode} = ERROR_MODE_DIE;
-    try {
-        $dbh->bz_start_transaction;
-        $code->();
-        $dbh->bz_commit_transaction;
-    } catch {
-        $dbh->bz_rollback_transaction;
-        # re-throw
-        die $_;
-    };
+  my ($code) = @_;
+  my $dbh = Bugzilla->dbh_main;
+  local Bugzilla->request_cache->{dbh}        = $dbh;
+  local Bugzilla->request_cache->{error_mode} = ERROR_MODE_DIE;
+  try {
+    $dbh->bz_start_transaction;
+    $code->();
+    $dbh->bz_commit_transaction;
+  }
+  catch {
+    $dbh->bz_rollback_transaction;
+
+    # re-throw
+    die $_;
+  };
 }
 
 sub with_readonly_database(&) {
-    my ($code) = @_;
-    local Bugzilla->request_cache->{dbh} = undef;
-    local Bugzilla->request_cache->{error_mode} = ERROR_MODE_DIE;
-    Bugzilla->switch_to_shadow_db();
-    $code->();
+  my ($code) = @_;
+  local Bugzilla->request_cache->{dbh}        = undef;
+  local Bugzilla->request_cache->{error_mode} = ERROR_MODE_DIE;
+  Bugzilla->switch_to_shadow_db();
+  $code->();
 }
 
 sub trick_taint {
-    untaint($_[0]);
+  untaint($_[0]);
 
-    return defined $_[0];
+  return defined $_[0];
 }
 
 sub detaint_natural {
-    my $match = $_[0] =~ /^(\d+)$/;
-    $_[0] = $match ? int($1) : undef;
-    return (defined($_[0]));
+  my $match = $_[0] =~ /^(\d+)$/;
+  $_[0] = $match ? int($1) : undef;
+  return (defined($_[0]));
 }
 
 sub detaint_signed {
-    my $match = $_[0] =~ /^([-+]?\d+)$/;
-    # The "int()" call removes any leading plus sign.
-    $_[0] = $match ? int($1) : undef;
-    return (defined($_[0]));
+  my $match = $_[0] =~ /^([-+]?\d+)$/;
+
+  # The "int()" call removes any leading plus sign.
+  $_[0] = $match ? int($1) : undef;
+  return (defined($_[0]));
 }
 
 my %html_quote = (
-    q{&} => '&',
-    q{<} => '<',
-    q{>} => '>',
-    q{"} => '"',
-    q{@} => '@', # Obscure '@'.
+  q{&} => '&',
+  q{<} => '<',
+  q{>} => '>',
+  q{"} => '"',
+  q{@} => '@',    # Obscure '@'.
 );
 
 # Bug 120030: Override html filter to obscure the '@' in user
 #             visible strings.
 # Bug 319331: Handle BiDi disruptions.
 sub html_quote {
-    my $var = shift;
-    no warnings 'utf8';
-    $var =~ s/([&<>"@])/$html_quote{$1}/g;
-
-    state $use_utf8 = Bugzilla->params->{'utf8'};
-
-    if ($use_utf8) {
-        # Remove control characters if the encoding is utf8.
-        # Other multibyte encodings may be using this range; so ignore if not utf8.
-        $var =~ s/(?![\t\r\n])[[:cntrl:]]//g;
-
-        # Remove the following characters because they're
-        # influencing BiDi:
-        # --------------------------------------------------------
-        # |Code  |Name                      |UTF-8 representation|
-        # |------|--------------------------|--------------------|
-        # |U+202a|Left-To-Right Embedding   |0xe2 0x80 0xaa      |
-        # |U+202b|Right-To-Left Embedding   |0xe2 0x80 0xab      |
-        # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac      |
-        # |U+202d|Left-To-Right Override    |0xe2 0x80 0xad      |
-        # |U+202e|Right-To-Left Override    |0xe2 0x80 0xae      |
-        # --------------------------------------------------------
-        #
-        # The following are characters influencing BiDi, too, but
-        # they can be spared from filtering because they don't
-        # influence more than one character right or left:
-        # --------------------------------------------------------
-        # |Code  |Name                      |UTF-8 representation|
-        # |------|--------------------------|--------------------|
-        # |U+200e|Left-To-Right Mark        |0xe2 0x80 0x8e      |
-        # |U+200f|Right-To-Left Mark        |0xe2 0x80 0x8f      |
-        # --------------------------------------------------------
-        $var =~ tr/\x{202a}-\x{202e}//d;
-    }
-    return $var;
+  my $var = shift;
+  no warnings 'utf8';
+  $var =~ s/([&<>"@])/$html_quote{$1}/g;
+
+  state $use_utf8 = Bugzilla->params->{'utf8'};
+
+  if ($use_utf8) {
+
+    # Remove control characters if the encoding is utf8.
+    # Other multibyte encodings may be using this range; so ignore if not utf8.
+    $var =~ s/(?![\t\r\n])[[:cntrl:]]//g;
+
+    # Remove the following characters because they're
+    # influencing BiDi:
+    # --------------------------------------------------------
+    # |Code  |Name                      |UTF-8 representation|
+    # |------|--------------------------|--------------------|
+    # |U+202a|Left-To-Right Embedding   |0xe2 0x80 0xaa      |
+    # |U+202b|Right-To-Left Embedding   |0xe2 0x80 0xab      |
+    # |U+202c|Pop Directional Formatting|0xe2 0x80 0xac      |
+    # |U+202d|Left-To-Right Override    |0xe2 0x80 0xad      |
+    # |U+202e|Right-To-Left Override    |0xe2 0x80 0xae      |
+    # --------------------------------------------------------
+    #
+    # The following are characters influencing BiDi, too, but
+    # they can be spared from filtering because they don't
+    # influence more than one character right or left:
+    # --------------------------------------------------------
+    # |Code  |Name                      |UTF-8 representation|
+    # |------|--------------------------|--------------------|
+    # |U+200e|Left-To-Right Mark        |0xe2 0x80 0x8e      |
+    # |U+200f|Right-To-Left Mark        |0xe2 0x80 0x8f      |
+    # --------------------------------------------------------
+    $var =~ tr/\x{202a}-\x{202e}//d;
+  }
+  return $var;
 }
 
 sub html_light_quote {
-    my ($text) = @_;
-    # admin/table.html.tmpl calls |FILTER html_light| many times.
-    # There is no need to recreate the HTML::Scrubber object again and again.
-    my $scrubber = Bugzilla->process_cache->{html_scrubber};
-
-    # List of allowed HTML elements having no attributes.
-    my @allow = qw(b strong em i u p br abbr acronym ins del cite code var
-                   dfn samp kbd big small sub sup tt dd dt dl ul li ol
-                   fieldset legend);
-
-    if (!Bugzilla->feature('html_desc')) {
-        my $safe = join('|', @allow);
-        my $chr = chr(1);
-
-        # First, escape safe elements.
-        $text =~ s#<($safe)>#$chr$1$chr#go;
-        $text =~ s##$chr/$1$chr#go;
-        # Now filter < and >.
-        $text =~ s#<#<#g;
-        $text =~ s#>#>#g;
-        # Restore safe elements.
-        $text =~ s#$chr/($safe)$chr##go;
-        $text =~ s#$chr($safe)$chr#<$1>#go;
-        return $text;
-    }
-    elsif (!$scrubber) {
-        # We can be less restrictive. We can accept elements with attributes.
-        push(@allow, qw(a blockquote q span));
-
-        # Allowed protocols.
-        my $safe_protocols = join('|', SAFE_PROTOCOLS);
-        my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i;
-
-        # Deny all elements and attributes unless explicitly authorized.
-        my @default = (0 => {
-                             id    => 1,
-                             name  => 1,
-                             class => 1,
-                             '*'   => 0, # Reject all other attributes.
-                            }
-                       );
-
-        # Specific rules for allowed elements. If no specific rule is set
-        # for a given element, then the default is used.
-        my @rules = (a => {
-                           href  => $protocol_regexp,
-                           title => 1,
-                           id    => 1,
-                           name  => 1,
-                           class => 1,
-                           '*'   => 0, # Reject all other attributes.
-                          },
-                     blockquote => {
-                                    cite => $protocol_regexp,
-                                    id    => 1,
-                                    name  => 1,
-                                    class => 1,
-                                    '*'  => 0, # Reject all other attributes.
-                                   },
-                     'q' => {
-                             cite => $protocol_regexp,
-                             id    => 1,
-                             name  => 1,
-                             class => 1,
-                             '*'  => 0, # Reject all other attributes.
-                          },
-                    );
-
-        Bugzilla->process_cache->{html_scrubber} = $scrubber =
-          HTML::Scrubber->new(default => \@default,
-                              allow   => \@allow,
-                              rules   => \@rules,
-                              comment => 0,
-                              process => 0);
-    }
-    return $scrubber->scrub($text);
+  my ($text) = @_;
+
+  # admin/table.html.tmpl calls |FILTER html_light| many times.
+  # There is no need to recreate the HTML::Scrubber object again and again.
+  my $scrubber = Bugzilla->process_cache->{html_scrubber};
+
+  # List of allowed HTML elements having no attributes.
+  my @allow = qw(b strong em i u p br abbr acronym ins del cite code var
+    dfn samp kbd big small sub sup tt dd dt dl ul li ol
+    fieldset legend);
+
+  if (!Bugzilla->feature('html_desc')) {
+    my $safe = join('|', @allow);
+    my $chr = chr(1);
+
+    # First, escape safe elements.
+    $text =~ s#<($safe)>#$chr$1$chr#go;
+    $text =~ s##$chr/$1$chr#go;
+
+    # Now filter < and >.
+    $text =~ s#<#<#g;
+    $text =~ s#>#>#g;
+
+    # Restore safe elements.
+    $text =~ s#$chr/($safe)$chr##go;
+    $text =~ s#$chr($safe)$chr#<$1>#go;
+    return $text;
+  }
+  elsif (!$scrubber) {
+
+    # We can be less restrictive. We can accept elements with attributes.
+    push(@allow, qw(a blockquote q span));
+
+    # Allowed protocols.
+    my $safe_protocols = join('|', SAFE_PROTOCOLS);
+    my $protocol_regexp = qr{(^(?:$safe_protocols):|^[^:]+$)}i;
+
+    # Deny all elements and attributes unless explicitly authorized.
+    my @default = (
+      0 => {
+        id    => 1,
+        name  => 1,
+        class => 1,
+        '*'   => 0,    # Reject all other attributes.
+      }
+    );
+
+    # Specific rules for allowed elements. If no specific rule is set
+    # for a given element, then the default is used.
+    my @rules = (
+      a => {
+        href  => $protocol_regexp,
+        title => 1,
+        id    => 1,
+        name  => 1,
+        class => 1,
+        '*'   => 0,                  # Reject all other attributes.
+      },
+      blockquote => {
+        cite  => $protocol_regexp,
+        id    => 1,
+        name  => 1,
+        class => 1,
+        '*'   => 0,                  # Reject all other attributes.
+      },
+      'q' => {
+        cite  => $protocol_regexp,
+        id    => 1,
+        name  => 1,
+        class => 1,
+        '*'   => 0,                  # Reject all other attributes.
+      },
+    );
+
+    Bugzilla->process_cache->{html_scrubber} = $scrubber = HTML::Scrubber->new(
+      default => \@default,
+      allow   => \@allow,
+      rules   => \@rules,
+      comment => 0,
+      process => 0
+    );
+  }
+  return $scrubber->scrub($text);
 }
 
 sub email_filter {
-    my ($toencode) = @_;
-    if (!Bugzilla->user->id) {
-        my @emails = Email::Address->parse($toencode);
-        if (scalar @emails) {
-            my @hosts = map { quotemeta($_->host) } @emails;
-            my $hosts_re = join('|', @hosts);
-            $toencode =~ s/\@(?:$hosts_re)//g;
-            return $toencode;
-        }
+  my ($toencode) = @_;
+  if (!Bugzilla->user->id) {
+    my @emails = Email::Address->parse($toencode);
+    if (scalar @emails) {
+      my @hosts = map { quotemeta($_->host) } @emails;
+      my $hosts_re = join('|', @hosts);
+      $toencode =~ s/\@(?:$hosts_re)//g;
+      return $toencode;
     }
-    return $toencode;
+  }
+  return $toencode;
 }
 
 # This originally came from CGI.pm, by Lincoln D. Stein
 sub url_quote {
-    my ($toencode) = (@_);
-    utf8::encode($toencode) # The below regex works only on bytes
-        if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode);
-    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
-    return $toencode;
+  my ($toencode) = (@_);
+  utf8::encode($toencode)    # The below regex works only on bytes
+    if Bugzilla->params->{'utf8'} && utf8::is_utf8($toencode);
+  $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg;
+  return $toencode;
 }
 
 sub css_class_quote {
-    my ($toencode) = (@_);
-    $toencode =~ s#[ /]#_#g;
-    $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
-    return $toencode;
+  my ($toencode) = (@_);
+  $toencode =~ s#[ /]#_#g;
+  $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("&#x%x;",ord($1))/eg;
+  return $toencode;
 }
 
 sub xml_quote {
-    my ($var) = (@_);
-    $var =~ s/\&/\&/g;
-    $var =~ s//\>/g;
-    $var =~ s/\"/\"/g;
-    $var =~ s/\'/\'/g;
-
-    # the following nukes characters disallowed by the XML 1.0
-    # spec, Production 2.2. 1.0 declares that only the following
-    # are valid:
-    # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF])
-    $var =~ s/([\x{0001}-\x{0008}]|
+  my ($var) = (@_);
+  $var =~ s/\&/\&/g;
+  $var =~ s//\>/g;
+  $var =~ s/\"/\"/g;
+  $var =~ s/\'/\'/g;
+
+  # the following nukes characters disallowed by the XML 1.0
+  # spec, Production 2.2. 1.0 declares that only the following
+  # are valid:
+  # (#x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF])
+  $var =~ s/([\x{0001}-\x{0008}]|
                [\x{000B}-\x{000C}]|
                [\x{000E}-\x{001F}]|
                [\x{D800}-\x{DFFF}]|
                [\x{FFFE}-\x{FFFF}])//gx;
-    return $var;
+  return $var;
 }
 
 sub i_am_cgi {
-    # I use SERVER_SOFTWARE because it's required to be
-    # defined for all requests in the CGI spec.
-    return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
+
+  # I use SERVER_SOFTWARE because it's required to be
+  # defined for all requests in the CGI spec.
+  return exists $ENV{'SERVER_SOFTWARE'} ? 1 : 0;
 }
 
 sub i_am_webservice {
-    my $usage_mode = Bugzilla->usage_mode;
-    return $usage_mode == USAGE_MODE_XMLRPC
-           || $usage_mode == USAGE_MODE_JSON
-           || $usage_mode == USAGE_MODE_REST;
+  my $usage_mode = Bugzilla->usage_mode;
+  return
+       $usage_mode == USAGE_MODE_XMLRPC
+    || $usage_mode == USAGE_MODE_JSON
+    || $usage_mode == USAGE_MODE_REST;
 }
 
 sub is_webserver_group {
-    my @effective_gids = split(/ /, $EGID);
+  my @effective_gids = split(/ /, $EGID);
 
-    state $web_server_gid;
-    if (!defined $web_server_gid) {
-        my $web_server_group = Bugzilla->localconfig->{webservergroup};
+  state $web_server_gid;
+  if (!defined $web_server_gid) {
+    my $web_server_group = Bugzilla->localconfig->{webservergroup};
 
-        if ($web_server_group eq '' || ON_WINDOWS) {
-            $web_server_gid = $effective_gids[0];
-        }
+    if ($web_server_group eq '' || ON_WINDOWS) {
+      $web_server_gid = $effective_gids[0];
+    }
 
-        elsif ($web_server_group =~ /^\d+$/) {
-            $web_server_gid = $web_server_group;
-        }
+    elsif ($web_server_group =~ /^\d+$/) {
+      $web_server_gid = $web_server_group;
+    }
 
-        else {
-            $web_server_gid = eval { getgrnam($web_server_group) };
-            $web_server_gid //= 0;
-        }
+    else {
+      $web_server_gid = eval { getgrnam($web_server_group) };
+      $web_server_gid //= 0;
     }
+  }
 
-    return any { $web_server_gid == $_ } @effective_gids;
+  return any { $web_server_gid == $_ } @effective_gids;
 }
 
 # This exists as a separate function from Bugzilla::CGI::redirect_to_https
 # because we don't want to create a CGI object during XML-RPC calls
 # (doing so can mess up XML-RPC).
 sub do_ssl_redirect_if_required {
-    return if !i_am_cgi();
-    my $uri = URI->new(Bugzilla->localconfig->{'urlbase'});
-    return if $uri->scheme ne 'https';
-
-    # If we're already running under SSL, never redirect.
-    return if $ENV{HTTPS} && $ENV{HTTPS} eq 'on';
-    DEBUG("Redirect to HTTPS because \$ENV{HTTPS}=$ENV{HTTPS}");
-    Bugzilla->cgi->redirect_to_https();
+  return if !i_am_cgi();
+  my $uri = URI->new(Bugzilla->localconfig->{'urlbase'});
+  return if $uri->scheme ne 'https';
+
+  # If we're already running under SSL, never redirect.
+  return if $ENV{HTTPS} && $ENV{HTTPS} eq 'on';
+  DEBUG("Redirect to HTTPS because \$ENV{HTTPS}=$ENV{HTTPS}");
+  Bugzilla->cgi->redirect_to_https();
 }
 
 # Returns the real remote address of the client,
 sub remote_ip {
-    my $remote_ip       = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
-    my @proxies         = split(/[\s,]+/, Bugzilla->localconfig->{inbound_proxies});
-    my @x_forwarded_for = split(/[\s,]+/, $ENV{HTTP_X_FORWARDED_FOR} // '');
-
-    return $remote_ip unless @x_forwarded_for;
-    return $x_forwarded_for[0] if @proxies && $proxies[0] eq '*';
-    return $remote_ip if none { $_ eq $remote_ip } @proxies;
-
-    foreach my $ip (reverse @x_forwarded_for) {
-        if (none { $_ eq $ip } @proxies) {
-            # Keep the original IP address if the remote IP is invalid.
-            return validate_ip($ip) || $remote_ip;
-        }
+  my $remote_ip = $ENV{'REMOTE_ADDR'} || '127.0.0.1';
+  my @proxies         = split(/[\s,]+/, Bugzilla->localconfig->{inbound_proxies});
+  my @x_forwarded_for = split(/[\s,]+/, $ENV{HTTP_X_FORWARDED_FOR} // '');
+
+  return $remote_ip unless @x_forwarded_for;
+  return $x_forwarded_for[0] if @proxies && $proxies[0] eq '*';
+  return $remote_ip if none { $_ eq $remote_ip } @proxies;
+
+  foreach my $ip (reverse @x_forwarded_for) {
+    if (none { $_ eq $ip } @proxies) {
+
+      # Keep the original IP address if the remote IP is invalid.
+      return validate_ip($ip) || $remote_ip;
     }
-    return $remote_ip;
+  }
+  return $remote_ip;
 }
 
 sub validate_ip {
-    my $ip = shift;
-    return is_ipv4($ip) || is_ipv6($ip);
+  my $ip = shift;
+  return is_ipv4($ip) || is_ipv6($ip);
 }
 
 # Copied from Data::Validate::IP::is_ipv4().
 sub is_ipv4 {
-    my $ip = shift;
-    return unless defined $ip;
+  my $ip = shift;
+  return unless defined $ip;
 
-    my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
-    return unless scalar(@octets) == 4;
+  my @octets = $ip =~ /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
+  return unless scalar(@octets) == 4;
 
-    foreach my $octet (@octets) {
-        return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/);
-    }
+  foreach my $octet (@octets) {
+    return unless ($octet >= 0 && $octet <= 255 && $octet !~ /^0\d{1,2}$/);
+  }
 
-    # The IP address is valid and can now be detainted.
-    return join('.', @octets);
+  # The IP address is valid and can now be detainted.
+  return join('.', @octets);
 }
 
 # Copied from Data::Validate::IP::is_ipv6().
 sub is_ipv6 {
-    my $ip = shift;
-    return unless defined $ip;
-
-    # If there is a :: then there must be only one :: and the length
-    # can be variable. Without it, the length must be 8 groups.
-    my @chunks = split(':', $ip);
-
-    # Need to check if the last chunk is an IPv4 address, if it is we
-    # pop it off and exempt it from the normal IPv6 checking and stick
-    # it back on at the end. If there is only one chunk and it's an IPv4
-    # address, then it isn't an IPv6 address.
-    my $ipv4;
-    my $expected_chunks = 8;
-    if (@chunks > 1 && is_ipv4($chunks[$#chunks])) {
-        $ipv4 = pop(@chunks);
-        $expected_chunks--;
-    }
+  my $ip = shift;
+  return unless defined $ip;
+
+  # If there is a :: then there must be only one :: and the length
+  # can be variable. Without it, the length must be 8 groups.
+  my @chunks = split(':', $ip);
+
+  # Need to check if the last chunk is an IPv4 address, if it is we
+  # pop it off and exempt it from the normal IPv6 checking and stick
+  # it back on at the end. If there is only one chunk and it's an IPv4
+  # address, then it isn't an IPv6 address.
+  my $ipv4;
+  my $expected_chunks = 8;
+  if (@chunks > 1 && is_ipv4($chunks[$#chunks])) {
+    $ipv4 = pop(@chunks);
+    $expected_chunks--;
+  }
+
+  my $empty = 0;
+
+  # Workaround to handle trailing :: being valid.
+  if ($ip =~ /[0-9a-f]{1,4}::$/) {
+    $empty++;
 
-    my $empty = 0;
-    # Workaround to handle trailing :: being valid.
-    if ($ip =~ /[0-9a-f]{1,4}::$/) {
-        $empty++;
     # Single trailing ':' is invalid.
-    } elsif ($ip =~ /:$/) {
-        return;
-    }
+  }
+  elsif ($ip =~ /:$/) {
+    return;
+  }
 
-    foreach my $chunk (@chunks) {
-        return unless $chunk =~ /^[0-9a-f]{0,4}$/i;
-        $empty++ if $chunk eq '';
-    }
-    # More than one :: block is bad, but if it starts with :: it will
-    # look like two, so we need an exception.
-    if ($empty == 2 && $ip =~ /^::/) {
-        # This is ok
-    } elsif ($empty > 1) {
-        return;
-    }
+  foreach my $chunk (@chunks) {
+    return unless $chunk =~ /^[0-9a-f]{0,4}$/i;
+    $empty++ if $chunk eq '';
+  }
+
+  # More than one :: block is bad, but if it starts with :: it will
+  # look like two, so we need an exception.
+  if ($empty == 2 && $ip =~ /^::/) {
+
+    # This is ok
+  }
+  elsif ($empty > 1) {
+    return;
+  }
+
+  push(@chunks, $ipv4) if $ipv4;
+
+  # Need 8 chunks, or we need an empty section that could be filled
+  # to represent the missing '0' sections.
+  return
+    unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty);
 
-    push(@chunks, $ipv4) if $ipv4;
-    # Need 8 chunks, or we need an empty section that could be filled
-    # to represent the missing '0' sections.
-    return unless (@chunks == $expected_chunks || @chunks < $expected_chunks && $empty);
+  my $ipv6 = join(':', @chunks);
 
-    my $ipv6 = join(':', @chunks);
-    # The IP address is valid and can now be detainted.
-    untaint($ipv6);
+  # The IP address is valid and can now be detainted.
+  untaint($ipv6);
 
-    # Need to handle the exception of trailing :: being valid.
-    return "${ipv6}::" if $ip =~ /::$/;
-    return $ipv6;
+  # Need to handle the exception of trailing :: being valid.
+  return "${ipv6}::" if $ip =~ /::$/;
+  return $ipv6;
 }
 
 sub use_attachbase {
-    my $attachbase = Bugzilla->localconfig->{'attachment_base'};
-    my $urlbase    = Bugzilla->localconfig->{'urlbase'};
-    return ($attachbase ne '' && $attachbase ne $urlbase);
+  my $attachbase = Bugzilla->localconfig->{'attachment_base'};
+  my $urlbase    = Bugzilla->localconfig->{'urlbase'};
+  return ($attachbase ne '' && $attachbase ne $urlbase);
 }
 
 sub diff_arrays {
-    my ($old_ref, $new_ref, $attrib) = @_;
-    $attrib ||= 'name';
-
-    my (%counts, %pos);
-    # We are going to alter the old array.
-    my @old = @$old_ref;
-    my $i = 0;
-
-    # $counts{foo}-- means old, $counts{foo}++ means new.
-    # If $counts{foo} becomes positive, then we are adding new items,
-    # else we simply cancel one old existing item. Remaining items
-    # in the old list have been removed.
-    foreach (@old) {
-        next unless defined $_;
-        my $value = blessed($_) ? $_->$attrib : $_;
-        $counts{$value}--;
-        push @{$pos{$value}}, $i++;
+  my ($old_ref, $new_ref, $attrib) = @_;
+  $attrib ||= 'name';
+
+  my (%counts, %pos);
+
+  # We are going to alter the old array.
+  my @old = @$old_ref;
+  my $i   = 0;
+
+  # $counts{foo}-- means old, $counts{foo}++ means new.
+  # If $counts{foo} becomes positive, then we are adding new items,
+  # else we simply cancel one old existing item. Remaining items
+  # in the old list have been removed.
+  foreach (@old) {
+    next unless defined $_;
+    my $value = blessed($_) ? $_->$attrib : $_;
+    $counts{$value}--;
+    push @{$pos{$value}}, $i++;
+  }
+  my @added;
+  foreach (@$new_ref) {
+    next unless defined $_;
+    my $value = blessed($_) ? $_->$attrib : $_;
+    if (++$counts{$value} > 0) {
+
+      # Ignore empty strings, but objects having an empty string
+      # as attribute are fine.
+      push(@added, $_) unless ($value eq '' && !blessed($_));
     }
-    my @added;
-    foreach (@$new_ref) {
-        next unless defined $_;
-        my $value = blessed($_) ? $_->$attrib : $_;
-        if (++$counts{$value} > 0) {
-            # Ignore empty strings, but objects having an empty string
-            # as attribute are fine.
-            push(@added, $_) unless ($value eq '' && !blessed($_));
-        }
-        else {
-            my $old_pos = shift @{$pos{$value}};
-            $old[$old_pos] = undef;
-        }
+    else {
+      my $old_pos = shift @{$pos{$value}};
+      $old[$old_pos] = undef;
     }
-    # Ignore canceled items as well as empty strings.
-    my @removed = grep { defined $_ && $_ ne '' } @old;
-    return (\@removed, \@added);
+  }
+
+  # Ignore canceled items as well as empty strings.
+  my @removed = grep { defined $_ && $_ ne '' } @old;
+  return (\@removed, \@added);
 }
 
 sub css_url_rewrite {
-    my ($content, $callback) = @_;
-    $content =~ s{(?($2)}eig;
-    return $content;
+  my ($content, $callback) = @_;
+  $content =~ s{(?($2)}eig;
+  return $content;
 }
 
 sub trim {
-    my ($str) = @_;
-    if ($str) {
-      $str =~ s/^\s+//g;
-      $str =~ s/\s+$//g;
-    }
-    return $str;
+  my ($str) = @_;
+  if ($str) {
+    $str =~ s/^\s+//g;
+    $str =~ s/\s+$//g;
+  }
+  return $str;
 }
 
 sub wrap_comment {
-    my ($comment, $cols) = @_;
-    my $wrappedcomment = "";
-
-    # Use 'local', as recommended by Text::Wrap's perldoc.
-    local $Text::Wrap::columns = $cols || COMMENT_COLS;
-    # Make words that are longer than COMMENT_COLS not wrap.
-    local $Text::Wrap::huge    = 'overflow';
-    # Don't mess with tabs.
-    local $Text::Wrap::unexpand = 0;
-
-    # If the line starts with ">", don't wrap it. Otherwise, wrap.
-    foreach my $line (split(/\r\n|\r|\n/, $comment)) {
-      if ($line =~ qr/^>/) {
-        $wrappedcomment .= ($line . "\n");
-      }
-      else {
-        # Due to a segfault in Text::Tabs::expand() when processing tabs with
-        # Unicode (see http://rt.perl.org/rt3/Public/Bug/Display.html?id=52104),
-        # we have to remove tabs before processing the comment. This restriction
-        # can go away when we require Perl 5.8.9 or newer.
-        $line =~ s/\t/    /g;
-        $wrappedcomment .= (wrap('', '', $line) . "\n");
-      }
+  my ($comment, $cols) = @_;
+  my $wrappedcomment = "";
+
+  # Use 'local', as recommended by Text::Wrap's perldoc.
+  local $Text::Wrap::columns = $cols || COMMENT_COLS;
+
+  # Make words that are longer than COMMENT_COLS not wrap.
+  local $Text::Wrap::huge = 'overflow';
+
+  # Don't mess with tabs.
+  local $Text::Wrap::unexpand = 0;
+
+  # If the line starts with ">", don't wrap it. Otherwise, wrap.
+  foreach my $line (split(/\r\n|\r|\n/, $comment)) {
+    if ($line =~ qr/^>/) {
+      $wrappedcomment .= ($line . "\n");
+    }
+    else {
+      # Due to a segfault in Text::Tabs::expand() when processing tabs with
+      # Unicode (see http://rt.perl.org/rt3/Public/Bug/Display.html?id=52104),
+      # we have to remove tabs before processing the comment. This restriction
+      # can go away when we require Perl 5.8.9 or newer.
+      $line =~ s/\t/    /g;
+      $wrappedcomment .= (wrap('', '', $line) . "\n");
     }
+  }
 
-    chomp($wrappedcomment); # Text::Wrap adds an extra newline at the end.
-    return $wrappedcomment;
+  chomp($wrappedcomment);    # Text::Wrap adds an extra newline at the end.
+  return $wrappedcomment;
 }
 
 sub find_wrap_point {
-    my ($string, $maxpos) = @_;
-    if (!$string) { return 0 }
-    if (length($string) < $maxpos) { return length($string) }
-    my $wrappoint = rindex($string, ",", $maxpos); # look for comma
-    if ($wrappoint <= 0) {  # can't find comma
-        $wrappoint = rindex($string, " ", $maxpos); # look for space
-        if ($wrappoint <= 0) {  # can't find space
-            $wrappoint = rindex($string, "-", $maxpos); # look for hyphen
-            if ($wrappoint <= 0) {  # can't find hyphen
-                $wrappoint = $maxpos;  # just truncate it
-            } else {
-                $wrappoint++; # leave hyphen on the left side
-            }
-        }
+  my ($string, $maxpos) = @_;
+  if (!$string) { return 0 }
+  if (length($string) < $maxpos) { return length($string) }
+  my $wrappoint = rindex($string, ",", $maxpos);    # look for comma
+  if ($wrappoint <= 0) {                            # can't find comma
+    $wrappoint = rindex($string, " ", $maxpos);     # look for space
+    if ($wrappoint <= 0) {                          # can't find space
+      $wrappoint = rindex($string, "-", $maxpos);    # look for hyphen
+      if ($wrappoint <= 0) {                         # can't find hyphen
+        $wrappoint = $maxpos;                        # just truncate it
+      }
+      else {
+        $wrappoint++;                                # leave hyphen on the left side
+      }
     }
-    return $wrappoint;
+  }
+  return $wrappoint;
 }
 
 sub wrap_hard {
-    my ($string, $columns) = @_;
-    local $Text::Wrap::columns = $columns;
-    local $Text::Wrap::unexpand = 0;
-    local $Text::Wrap::huge = 'wrap';
-
-    my $wrapped = wrap('', '', $string);
-    chomp($wrapped);
-    return $wrapped;
+  my ($string, $columns) = @_;
+  local $Text::Wrap::columns  = $columns;
+  local $Text::Wrap::unexpand = 0;
+  local $Text::Wrap::huge     = 'wrap';
+
+  my $wrapped = wrap('', '', $string);
+  chomp($wrapped);
+  return $wrapped;
 }
 
 sub format_time {
-    my ($date, $format, $timezone) = @_;
-
-    # If $format is not set, try to guess the correct date format.
-    if (!$format) {
-        if (!ref $date
-            && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/)
-        {
-            my $sec = $7;
-            if (defined $sec) {
-                $format = "%Y-%m-%d %T %Z";
-            } else {
-                $format = "%Y-%m-%d %R %Z";
-            }
-        } else {
-            # Default date format. See DateTime for other formats available.
-            $format = "%Y-%m-%d %R %Z";
-        }
-    }
-
-    my $dt = ref $date ? $date : datetime_from($date, $timezone);
-    $date = defined $dt ? $dt->strftime($format) : '';
-    return trim($date);
-}
+  my ($date, $format, $timezone) = @_;
 
-sub datetime_from {
-    my ($date, $timezone) = @_;
-
-    # In the database, this is the "0" date.
-    use Carp qw(cluck);
-    cluck("undefined date") unless defined $date;
-    return undef unless defined $date;
-    return undef if $date =~ /^0000/;
-
-    my @time;
-    # Most dates will be in this format, avoid strptime's generic parser
-    if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) {
-        @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef);
+  # If $format is not set, try to guess the correct date format.
+  if (!$format) {
+    if (!ref $date
+      && $date =~ /^(\d{4})[-\.](\d{2})[-\.](\d{2}) (\d{2}):(\d{2})(:(\d{2}))?$/)
+    {
+      my $sec = $7;
+      if (defined $sec) {
+        $format = "%Y-%m-%d %T %Z";
+      }
+      else {
+        $format = "%Y-%m-%d %R %Z";
+      }
     }
     else {
-        @time = strptime($date);
-    }
-
-    unless (scalar @time) {
-        # If an unknown timezone is passed (such as MSK, for Moskow),
-        # strptime() is unable to parse the date. We try again, but we first
-        # remove the timezone.
-        $date =~ s/\s+\S+$//;
-        @time = strptime($date);
-    }
-
-    return undef if !@time;
-
-    # strptime() counts years from 1900, except if they are older than 1901
-    # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84,
-    # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000.
-    $time[5] += 1900 if $time[5] < 1100;
-
-    my %args = (
-        year   => $time[5],
-        # Months start from 0 (January).
-        month  => $time[4] + 1,
-        day    => $time[3],
-        hour   => $time[2],
-        minute => $time[1],
-        # DateTime doesn't like fractional seconds.
-        # Also, sometimes seconds are undef.
-        second => defined($time[0]) ? int($time[0]) : undef,
-        # If a timezone was specified, use it. Otherwise, use the
-        # local timezone.
-        time_zone => Bugzilla->local_timezone->offset_as_string($time[6])
-                     || Bugzilla->local_timezone,
-    );
-
-    # If something wasn't specified in the date, it's best to just not
-    # pass it to DateTime at all. (This is important for doing datetime_from
-    # on the deadline field, which is usually just a date with no time.)
-    foreach my $arg (keys %args) {
-        delete $args{$arg} if !defined $args{$arg};
+      # Default date format. See DateTime for other formats available.
+      $format = "%Y-%m-%d %R %Z";
     }
+  }
 
-    my $dt = new DateTime(\%args);
+  my $dt = ref $date ? $date : datetime_from($date, $timezone);
+  $date = defined $dt ? $dt->strftime($format) : '';
+  return trim($date);
+}
 
-    # Now display the date using the given timezone,
-    # or the user's timezone if none is given.
-    $dt->set_time_zone($timezone || Bugzilla->user->timezone);
-    return $dt;
+sub datetime_from {
+  my ($date, $timezone) = @_;
+
+  # In the database, this is the "0" date.
+  use Carp qw(cluck);
+  cluck("undefined date") unless defined $date;
+  return undef unless defined $date;
+  return undef if $date =~ /^0000/;
+
+  my @time;
+
+  # Most dates will be in this format, avoid strptime's generic parser
+  if ($date =~ /^(\d{4})[\.-](\d{2})[\.-](\d{2})(?: (\d{2}):(\d{2}):(\d{2}))?$/) {
+    @time = ($6, $5, $4, $3, $2 - 1, $1 - 1900, undef);
+  }
+  else {
+    @time = strptime($date);
+  }
+
+  unless (scalar @time) {
+
+    # If an unknown timezone is passed (such as MSK, for Moskow),
+    # strptime() is unable to parse the date. We try again, but we first
+    # remove the timezone.
+    $date =~ s/\s+\S+$//;
+    @time = strptime($date);
+  }
+
+  return undef if !@time;
+
+  # strptime() counts years from 1900, except if they are older than 1901
+  # in which case it returns the full year (so 1890 -> 1890, but 1984 -> 84,
+  # and 3790 -> 1890). We make a guess and assume that 1100 <= year < 3000.
+  $time[5] += 1900 if $time[5] < 1100;
+
+  my %args = (
+    year => $time[5],
+
+    # Months start from 0 (January).
+    month  => $time[4] + 1,
+    day    => $time[3],
+    hour   => $time[2],
+    minute => $time[1],
+
+    # DateTime doesn't like fractional seconds.
+    # Also, sometimes seconds are undef.
+    second => defined($time[0]) ? int($time[0]) : undef,
+
+    # If a timezone was specified, use it. Otherwise, use the
+    # local timezone.
+    time_zone => Bugzilla->local_timezone->offset_as_string($time[6])
+      || Bugzilla->local_timezone,
+  );
+
+  # If something wasn't specified in the date, it's best to just not
+  # pass it to DateTime at all. (This is important for doing datetime_from
+  # on the deadline field, which is usually just a date with no time.)
+  foreach my $arg (keys %args) {
+    delete $args{$arg} if !defined $args{$arg};
+  }
+
+  my $dt = new DateTime(\%args);
+
+  # Now display the date using the given timezone,
+  # or the user's timezone if none is given.
+  $dt->set_time_zone($timezone || Bugzilla->user->timezone);
+  return $dt;
 }
 
 sub time_ago {
-    my ($param) = @_;
-    # DateTime object or seconds
-    my $ss = ref($param) ? time() - $param->epoch : $param;
-    my $mm = round($ss / 60);
-    my $hh = round($mm / 60);
-    my $dd = round($hh / 24);
-    my $mo = round($dd / 30);
-    my $yy = round($mo / 12);
-
-    return 'just now'           if $ss < 10;
-    return $ss . ' seconds ago' if $ss < 45;
-    return 'a minute ago'       if $ss < 90;
-    return $mm . ' minutes ago' if $mm < 45;
-    return 'an hour ago'        if $mm < 90;
-    return $hh . ' hours ago'   if $hh < 24;
-    return 'a day ago'          if $hh < 36;
-    return $dd . ' days ago'    if $dd < 30;
-    return 'a month ago'        if $dd < 45;
-    return $mo . ' months ago'  if $mo < 12;
-    return 'a year ago'         if $mo < 18;
-    return $yy . ' years ago';
+  my ($param) = @_;
+
+  # DateTime object or seconds
+  my $ss = ref($param) ? time() - $param->epoch : $param;
+  my $mm = round($ss / 60);
+  my $hh = round($mm / 60);
+  my $dd = round($hh / 24);
+  my $mo = round($dd / 30);
+  my $yy = round($mo / 12);
+
+  return 'just now'           if $ss < 10;
+  return $ss . ' seconds ago' if $ss < 45;
+  return 'a minute ago'       if $ss < 90;
+  return $mm . ' minutes ago' if $mm < 45;
+  return 'an hour ago'        if $mm < 90;
+  return $hh . ' hours ago'   if $hh < 24;
+  return 'a day ago'          if $hh < 36;
+  return $dd . ' days ago'    if $dd < 30;
+  return 'a month ago'        if $dd < 45;
+  return $mo . ' months ago'  if $mo < 12;
+  return 'a year ago'         if $mo < 18;
+  return $yy . ' years ago';
 }
 
 sub file_mod_time {
-    my ($filename) = (@_);
-    my ($dev,$ino,$mode,$nlink,$uid,$gid,$rdev,$size,
-        $atime,$mtime,$ctime,$blksize,$blocks)
-        = stat($filename);
-    return $mtime;
+  my ($filename) = (@_);
+  my (
+    $dev,  $ino,   $mode,  $nlink, $uid,     $gid, $rdev,
+    $size, $atime, $mtime, $ctime, $blksize, $blocks
+  ) = stat($filename);
+  return $mtime;
 }
 
 sub bz_crypt {
-    my ($password, $salt) = @_;
-
-    my $algorithm;
-    if (!defined $salt) {
-        # If you don't use a salt, then people can create tables of
-        # hashes that map to particular passwords, and then break your
-        # hashing very easily if they have a large-enough table of common
-        # (or even uncommon) passwords. So we generate a unique salt for
-        # each password in the database, and then just prepend it to
-        # the hash.
-        $salt = generate_random_password(PASSWORD_SALT_LENGTH);
-        $algorithm = PASSWORD_DIGEST_ALGORITHM;
-    }
-
-    # We append the algorithm used to the string. This is good because then
-    # we can change the algorithm being used, in the future, without
-    # disrupting the validation of existing passwords. Also, this tells
-    # us if a password is using the old "crypt" method of hashing passwords,
-    # because the algorithm will be missing from the string.
-    if ($salt =~ /{([^}]+)}$/) {
-        $algorithm = $1;
-    }
-
-    # Wide characters cause crypt and Digest to die.
-    if (Bugzilla->params->{'utf8'}) {
-        utf8::encode($password) if utf8::is_utf8($password);
-    }
-
-    my $crypted_password;
-    if (!$algorithm) {
-        # Crypt the password.
-        $crypted_password = crypt($password, $salt);
-
-        # HACK: Perl has bug where returned crypted password is considered
-        # tainted. See http://rt.perl.org/rt3/Public/Bug/Display.html?id=59998
-        unless(tainted($password) || tainted($salt)) {
-            untaint($crypted_password);
-        }
-    }
-    else {
-        my $hasher = Digest->new($algorithm);
-        # We only want to use the first characters of the salt, no
-        # matter how long of a salt we may have been passed.
-        $salt = substr($salt, 0, PASSWORD_SALT_LENGTH);
-        $hasher->add($password, $salt);
-        $crypted_password = $salt . $hasher->b64digest . "{$algorithm}";
+  my ($password, $salt) = @_;
+
+  my $algorithm;
+  if (!defined $salt) {
+
+    # If you don't use a salt, then people can create tables of
+    # hashes that map to particular passwords, and then break your
+    # hashing very easily if they have a large-enough table of common
+    # (or even uncommon) passwords. So we generate a unique salt for
+    # each password in the database, and then just prepend it to
+    # the hash.
+    $salt      = generate_random_password(PASSWORD_SALT_LENGTH);
+    $algorithm = PASSWORD_DIGEST_ALGORITHM;
+  }
+
+  # We append the algorithm used to the string. This is good because then
+  # we can change the algorithm being used, in the future, without
+  # disrupting the validation of existing passwords. Also, this tells
+  # us if a password is using the old "crypt" method of hashing passwords,
+  # because the algorithm will be missing from the string.
+  if ($salt =~ /{([^}]+)}$/) {
+    $algorithm = $1;
+  }
+
+  # Wide characters cause crypt and Digest to die.
+  if (Bugzilla->params->{'utf8'}) {
+    utf8::encode($password) if utf8::is_utf8($password);
+  }
+
+  my $crypted_password;
+  if (!$algorithm) {
+
+    # Crypt the password.
+    $crypted_password = crypt($password, $salt);
+
+    # HACK: Perl has bug where returned crypted password is considered
+    # tainted. See http://rt.perl.org/rt3/Public/Bug/Display.html?id=59998
+    unless (tainted($password) || tainted($salt)) {
+      untaint($crypted_password);
     }
-
-    # Return the crypted password.
-    return $crypted_password;
+  }
+  else {
+    my $hasher = Digest->new($algorithm);
+
+    # We only want to use the first characters of the salt, no
+    # matter how long of a salt we may have been passed.
+    $salt = substr($salt, 0, PASSWORD_SALT_LENGTH);
+    $hasher->add($password, $salt);
+    $crypted_password = $salt . $hasher->b64digest . "{$algorithm}";
+  }
+
+  # Return the crypted password.
+  return $crypted_password;
 }
 
 # If you want to understand the security of strings generated by this
@@ -716,211 +757,218 @@ sub bz_crypt {
 # by the number of characters you generate, and that gets you the equivalent
 # strength of the string in bits.
 sub generate_random_password {
-    my $size = shift || 10; # default to 10 chars if nothing specified
-    return join("", map{ ('0'..'9','a'..'z','A'..'Z')[irand 62] } (1..$size));
+  my $size = shift || 10;    # default to 10 chars if nothing specified
+  return
+    join("", map { ('0' .. '9', 'a' .. 'z', 'A' .. 'Z')[irand 62] } (1 .. $size));
 }
 
 sub validate_email_syntax {
-    my ($addr) = @_;
-    my $match = Bugzilla->params->{'emailregexp'};
-    my $email = $addr . Bugzilla->params->{'emailsuffix'};
-    # This regexp follows RFC 2822 section 3.4.1.
-    my $addr_spec = $Email::Address::addr_spec;
-    # RFC 2822 section 2.1 specifies that email addresses must
-    # be made of US-ASCII characters only.
-    # Email::Address::addr_spec doesn't enforce this.
-    if ($addr =~ /$match/
-        && $email !~ /\P{ASCII}/
-        && $email =~ /^$addr_spec$/
-        && length($email) <= 127)
-    {
-        # We assume these checks to suffice to consider the address untainted.
-        untaint($_[0]);
-        return 1;
-    }
-    return 0;
+  my ($addr) = @_;
+  my $match  = Bugzilla->params->{'emailregexp'};
+  my $email  = $addr . Bugzilla->params->{'emailsuffix'};
+
+  # This regexp follows RFC 2822 section 3.4.1.
+  my $addr_spec = $Email::Address::addr_spec;
+
+  # RFC 2822 section 2.1 specifies that email addresses must
+  # be made of US-ASCII characters only.
+  # Email::Address::addr_spec doesn't enforce this.
+  if ( $addr =~ /$match/
+    && $email !~ /\P{ASCII}/
+    && $email =~ /^$addr_spec$/
+    && length($email) <= 127)
+  {
+    # We assume these checks to suffice to consider the address untainted.
+    untaint($_[0]);
+    return 1;
+  }
+  return 0;
 }
 
 sub validate_date {
-    my ($date) = @_;
-    my $date2;
-
-    # $ts is undefined if the parser fails.
-    my $ts = str2time($date);
-    if ($ts) {
-        $date2 = time2str("%Y-%m-%d", $ts);
-
-        $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
-        $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
-    }
-    my $ret = ($ts && $date eq $date2);
-    return $ret ? 1 : 0;
+  my ($date) = @_;
+  my $date2;
+
+  # $ts is undefined if the parser fails.
+  my $ts = str2time($date);
+  if ($ts) {
+    $date2 = time2str("%Y-%m-%d", $ts);
+
+    $date =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
+    $date2 =~ s/(\d+)-0*(\d+?)-0*(\d+?)/$1-$2-$3/;
+  }
+  my $ret = ($ts && $date eq $date2);
+  return $ret ? 1 : 0;
 }
 
 sub validate_time {
-    my ($time) = @_;
-    my $time2;
-
-    # $ts is undefined if the parser fails.
-    my $ts = str2time($time);
-    if ($ts) {
-        $time2 = time2str("%H:%M:%S", $ts);
-        if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) {
-            $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0);
-        }
+  my ($time) = @_;
+  my $time2;
+
+  # $ts is undefined if the parser fails.
+  my $ts = str2time($time);
+  if ($ts) {
+    $time2 = time2str("%H:%M:%S", $ts);
+    if ($time =~ /^(\d{1,2}):(\d\d)(?::(\d\d))?$/) {
+      $time = sprintf("%02d:%02d:%02d", $1, $2, $3 || 0);
     }
-    my $ret = ($ts && $time eq $time2);
-    return $ret ? 1 : 0;
+  }
+  my $ret = ($ts && $time eq $time2);
+  return $ret ? 1 : 0;
 }
 
 sub is_7bit_clean {
-    return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/;
+  return $_[0] !~ /[^\x20-\x7E\x0A\x0D]/;
 }
 
 sub clean_text {
-    my $dtext = shift;
-    if ($dtext) {
-        # change control characters into a space
-        $dtext =~ s/[\x00-\x1F\x7F]+/ /g;
-    }
-    return trim($dtext);
+  my $dtext = shift;
+  if ($dtext) {
+
+    # change control characters into a space
+    $dtext =~ s/[\x00-\x1F\x7F]+/ /g;
+  }
+  return trim($dtext);
 }
 
 sub on_main_db (&) {
-    my $code = shift;
-    my $original_dbh = Bugzilla->dbh;
-    Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main;
-    $code->();
-    Bugzilla->request_cache->{dbh} = $original_dbh;
+  my $code         = shift;
+  my $original_dbh = Bugzilla->dbh;
+  Bugzilla->request_cache->{dbh} = Bugzilla->dbh_main;
+  $code->();
+  Bugzilla->request_cache->{dbh} = $original_dbh;
 }
 
 sub get_text {
-    my ($name, $vars) = @_;
-    my $template = Bugzilla->template_inner;
-    $vars ||= {};
-    $vars->{'message'} = $name;
-    my $message;
-    if (!$template->process('global/message.txt.tmpl', $vars, \$message)) {
-        require Bugzilla::Error;
-        Bugzilla::Error::ThrowTemplateError($template->error());
-    }
-    # Remove the indenting that exists in messages.html.tmpl.
-    $message =~ s/^    //gm;
-    return $message;
+  my ($name, $vars) = @_;
+  my $template = Bugzilla->template_inner;
+  $vars ||= {};
+  $vars->{'message'} = $name;
+  my $message;
+  if (!$template->process('global/message.txt.tmpl', $vars, \$message)) {
+    require Bugzilla::Error;
+    Bugzilla::Error::ThrowTemplateError($template->error());
+  }
+
+  # Remove the indenting that exists in messages.html.tmpl.
+  $message =~ s/^    //gm;
+  return $message;
 }
 
 sub template_var {
-    my $name = shift;
-    my $request_cache = Bugzilla->request_cache;
-    my $cache = $request_cache->{util_template_var} ||= {};
-    my $lang = $request_cache->{template_current_lang}->[0] || '';
-    return $cache->{$lang}->{$name} if defined $cache->{$lang};
-
-    my $template = Bugzilla->template_inner($lang);
-    my %vars;
-    # Note: If we suddenly start needing a lot of template_var variables,
-    # they should move into their own template, not field-descs.
-    my $result = $template->process('global/field-descs.none.tmpl',
-                                    { vars => \%vars, in_template_var => 1 });
-    # Bugzilla::Error can't be "use"d in Bugzilla::Util.
-    if (!$result) {
-        require Bugzilla::Error;
-        Bugzilla::Error::ThrowTemplateError($template->error);
-    }
-    $cache->{$lang} = \%vars;
-    return $vars{$name};
+  my $name          = shift;
+  my $request_cache = Bugzilla->request_cache;
+  my $cache         = $request_cache->{util_template_var} ||= {};
+  my $lang          = $request_cache->{template_current_lang}->[0] || '';
+  return $cache->{$lang}->{$name} if defined $cache->{$lang};
+
+  my $template = Bugzilla->template_inner($lang);
+  my %vars;
+
+  # Note: If we suddenly start needing a lot of template_var variables,
+  # they should move into their own template, not field-descs.
+  my $result = $template->process('global/field-descs.none.tmpl',
+    {vars => \%vars, in_template_var => 1});
+
+  # Bugzilla::Error can't be "use"d in Bugzilla::Util.
+  if (!$result) {
+    require Bugzilla::Error;
+    Bugzilla::Error::ThrowTemplateError($template->error);
+  }
+  $cache->{$lang} = \%vars;
+  return $vars{$name};
 }
 
 sub display_value {
-    my ($field, $value) = @_;
-    return template_var('value_descs')->{$field}->{$value} // $value;
+  my ($field, $value) = @_;
+  return template_var('value_descs')->{$field}->{$value} // $value;
 }
 
 sub disable_utf8 {
-    if (Bugzilla->params->{'utf8'}) {
-        binmode STDOUT, ':bytes'; # Turn off UTF8 encoding.
-    }
+  if (Bugzilla->params->{'utf8'}) {
+    binmode STDOUT, ':bytes';    # Turn off UTF8 encoding.
+  }
 }
 
 sub enable_utf8 {
-    if (Bugzilla->params->{'utf8'}) {
-        binmode STDOUT, ':utf8'; # Turn on UTF8 encoding.
-    }
+  if (Bugzilla->params->{'utf8'}) {
+    binmode STDOUT, ':utf8';     # Turn on UTF8 encoding.
+  }
 }
 
 use constant UTF8_ACCIDENTAL => qw(shiftjis big5-eten euc-kr euc-jp);
 
 sub detect_encoding {
-    my $data = shift;
-
-    if (!Bugzilla->feature('detect_charset')) {
-        require Bugzilla::Error;
-        Bugzilla::Error::ThrowCodeError('feature_disabled',
-            { feature => 'detect_charset' });
-    }
-
-    require Encode::Detect::Detector;
-    import Encode::Detect::Detector 'detect';
-
-    my $encoding = detect($data);
-    $encoding = resolve_alias($encoding) if $encoding;
-
-    # Encode::Detect is bad at detecting certain charsets, but Encode::Guess
-    # is better at them. Here's the details:
-
-    # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect
-    # tends to accidentally mis-detect UTF-8 strings as being
-    # these encodings.)
-    if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) {
-        $encoding = undef;
-        my $decoder = guess_encoding($data, UTF8_ACCIDENTAL);
-        $encoding = $decoder->name if ref $decoder;
-    }
-
-    # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8,
-    # but Encode::Guess can usually tell which one it is.
-    if ($encoding && $encoding eq 'iso-8859-8') {
-        my $decoded_as = _guess_iso($data, 'iso-8859-8',
-            # These are ordered this way because it gives the most
-            # accurate results.
-            qw(iso-8859-7 iso-8859-2));
-        $encoding = $decoded_as if $decoded_as;
-    }
+  my $data = shift;
+
+  if (!Bugzilla->feature('detect_charset')) {
+    require Bugzilla::Error;
+    Bugzilla::Error::ThrowCodeError('feature_disabled',
+      {feature => 'detect_charset'});
+  }
+
+  require Encode::Detect::Detector;
+  import Encode::Detect::Detector 'detect';
+
+  my $encoding = detect($data);
+  $encoding = resolve_alias($encoding) if $encoding;
+
+  # Encode::Detect is bad at detecting certain charsets, but Encode::Guess
+  # is better at them. Here's the details:
+
+  # shiftjis, big5-eten, euc-kr, and euc-jp: (Encode::Detect
+  # tends to accidentally mis-detect UTF-8 strings as being
+  # these encodings.)
+  if ($encoding && grep($_ eq $encoding, UTF8_ACCIDENTAL)) {
+    $encoding = undef;
+    my $decoder = guess_encoding($data, UTF8_ACCIDENTAL);
+    $encoding = $decoder->name if ref $decoder;
+  }
+
+  # Encode::Detect sometimes mis-detects various ISO encodings as iso-8859-8,
+  # but Encode::Guess can usually tell which one it is.
+  if ($encoding && $encoding eq 'iso-8859-8') {
+    my $decoded_as = _guess_iso(
+      $data, 'iso-8859-8',
+
+      # These are ordered this way because it gives the most
+      # accurate results.
+      qw(iso-8859-7 iso-8859-2)
+    );
+    $encoding = $decoded_as if $decoded_as;
+  }
 
-    return $encoding;
+  return $encoding;
 }
 
 # A helper for detect_encoding.
 sub _guess_iso {
-    my ($data, $versus, @isos) = (shift, shift, shift);
-
-    my $encoding;
-    foreach my $iso (@isos) {
-        my $decoder = guess_encoding($data, ($iso, $versus));
-        if (ref $decoder) {
-            $encoding = $decoder->name if ref $decoder;
-            last;
-        }
+  my ($data, $versus, @isos) = (shift, shift, shift);
+
+  my $encoding;
+  foreach my $iso (@isos) {
+    my $decoder = guess_encoding($data, ($iso, $versus));
+    if (ref $decoder) {
+      $encoding = $decoder->name if ref $decoder;
+      last;
     }
-    return $encoding;
+  }
+  return $encoding;
 }
 
 # From Math::Round
 use constant ROUND_HALF => 0.50000000000008;
+
 sub round {
-    my @res = map {
-        $_ >= 0
-            ? floor($_ + ROUND_HALF)
-            : ceil($_ - ROUND_HALF);
-    } @_;
-    return (wantarray) ? @res : $res[0];
+  my @res = map { $_ >= 0 ? floor($_ + ROUND_HALF) : ceil($_ - ROUND_HALF); } @_;
+  return (wantarray) ? @res : $res[0];
 }
 
 sub extract_nicks {
-    my ($name) = @_;
-    return () unless defined $name;
-    my @nicks = (
-        $name =~ /
+  my ($name) = @_;
+  return () unless defined $name;
+  my @nicks = (
+    $name =~ /
             # This negative lookbehind lets us
             # match colons that are not followed by numbers.
             (? 'unspecified';
 
-use constant DB_TABLE => 'versions';
+use constant DB_TABLE   => 'versions';
 use constant NAME_FIELD => 'value';
+
 # This is "id" because it has to be filled in and id is probably the fastest.
 # We do a custom sort in new_from_list below.
 use constant LIST_ORDER => 'id';
 
 use constant DB_COLUMNS => qw(
-    id
-    value
-    product_id
-    isactive
+  id
+  value
+  product_id
+  isactive
 );
 
-use constant REQUIRED_FIELD_MAP => {
-    product_id => 'product',
-};
+use constant REQUIRED_FIELD_MAP => {product_id => 'product',};
 
 use constant UPDATE_COLUMNS => qw(
-    value
-    isactive
+  value
+  isactive
 );
 
 use constant VALIDATORS => {
-    product  => \&_check_product,
-    value    => \&_check_value,
-    isactive => \&Bugzilla::Object::check_boolean,
+  product  => \&_check_product,
+  value    => \&_check_value,
+  isactive => \&Bugzilla::Object::check_boolean,
 };
 
-use constant VALIDATOR_DEPENDENCIES => {
-    value => ['product'],
-};
+use constant VALIDATOR_DEPENDENCIES => {value => ['product'],};
 
 ################################
 # Methods
 ################################
 
 sub new {
-    my $class = shift;
-    my $param = shift;
-    my $dbh = Bugzilla->dbh;
-
-    my $product;
-    if (ref $param) {
-        $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 value = ?';
-        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) {
+    $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"});
     }
 
-    unshift @_, $param;
-    return $class->SUPER::new(@_);
+    my $condition = 'product_id = ? AND value = ?';
+    my @values = ($product->id, $name);
+    $param = {condition => $condition, values => \@values};
+  }
+
+  unshift @_, $param;
+  return $class->SUPER::new(@_);
 }
 
 sub new_from_list {
-    my $self = shift;
-    my $list = $self->SUPER::new_from_list(@_);
-    return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list];
+  my $self = shift;
+  my $list = $self->SUPER::new_from_list(@_);
+  return [sort { vers_cmp(lc($a->name), lc($b->name)) } @$list];
 }
 
 sub run_create_validators {
-    my $class  = shift;
-    my $params = $class->SUPER::run_create_validators(@_);
-    my $product = delete $params->{product};
-    $params->{product_id} = $product->id;
-    return $params;
+  my $class   = shift;
+  my $params  = $class->SUPER::run_create_validators(@_);
+  my $product = delete $params->{product};
+  $params->{product_id} = $product->id;
+  return $params;
 }
 
 sub bug_count {
-    my $self = shift;
-    my $dbh = Bugzilla->dbh;
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
 
-    if (!defined $self->{'bug_count'}) {
-        $self->{'bug_count'} = $dbh->selectrow_array(qq{
+  if (!defined $self->{'bug_count'}) {
+    $self->{'bug_count'} = $dbh->selectrow_array(
+      qq{
             SELECT COUNT(*) FROM bugs
             WHERE product_id = ? AND version = ?}, undef,
-            ($self->product_id, $self->name)) || 0;
-    }
-    return $self->{'bug_count'};
+      ($self->product_id, $self->name)
+    ) || 0;
+  }
+  return $self->{'bug_count'};
 }
 
 sub update {
-    my $self = shift;
-    my ($changes, $old_self) = $self->SUPER::update(@_);
-
-    if (exists $changes->{value}) {
-        my $dbh = Bugzilla->dbh;
-        $dbh->do('UPDATE bugs SET version = ?
-                  WHERE version = ? AND product_id = ?',
-                  undef, ($self->name, $old_self->name, $self->product_id));
-    }
-    return $changes;
-}
+  my $self = shift;
+  my ($changes, $old_self) = $self->SUPER::update(@_);
 
-sub remove_from_db {
-    my $self = shift;
+  if (exists $changes->{value}) {
     my $dbh = Bugzilla->dbh;
+    $dbh->do(
+      'UPDATE bugs SET version = ?
+                  WHERE version = ? AND product_id = ?', undef,
+      ($self->name, $old_self->name, $self->product_id)
+    );
+  }
+  return $changes;
+}
 
-    # The version cannot be removed if there are bugs
-    # associated with it.
-    if ($self->bug_count) {
-        ThrowUserError("version_has_bugs", { nb => $self->bug_count });
-    }
-    $self->SUPER::remove_from_db();
+sub remove_from_db {
+  my $self = shift;
+  my $dbh  = Bugzilla->dbh;
+
+  # The version cannot be removed if there are bugs
+  # associated with it.
+  if ($self->bug_count) {
+    ThrowUserError("version_has_bugs", {nb => $self->bug_count});
+  }
+  $self->SUPER::remove_from_db();
 }
 
 ###############################
@@ -148,45 +145,48 @@ sub remove_from_db {
 ###############################
 
 sub product_id { return $_[0]->{'product_id'}; }
-sub is_active  { return $_[0]->{'isactive'};   }
+sub is_active  { return $_[0]->{'isactive'}; }
 
 sub product {
-    my $self = shift;
+  my $self = shift;
 
-    require Bugzilla::Product;
-    $self->{'product'} ||= Bugzilla::Product->new({ id => $self->product_id, cache => 1 });
-    return $self->{'product'};
+  require Bugzilla::Product;
+  $self->{'product'}
+    ||= Bugzilla::Product->new({id => $self->product_id, cache => 1});
+  return $self->{'product'};
 }
 
 ################################
 # Validators
 ################################
 
-sub set_name      { $_[0]->set('value', $_[1]);    }
+sub set_name      { $_[0]->set('value',    $_[1]); }
 sub set_is_active { $_[0]->set('isactive', $_[1]); }
 
 sub _check_value {
-    my ($invocant, $name, undef, $params) = @_;
-    my $product = blessed($invocant) ? $invocant->product : $params->{product};
-
-    $name = trim($name);
-    $name || ThrowUserError('version_blank_name');
-    # Remove unprintable characters
-    $name = clean_text($name);
-
-    my $version = new Bugzilla::Version({ product => $product, name => $name });
-    if ($version && (!ref $invocant || $version->id != $invocant->id)) {
-        ThrowUserError('version_already_exists', { name    => $version->name,
-                                                   product => $product->name });
-    }
-    return $name;
+  my ($invocant, $name, undef, $params) = @_;
+  my $product = blessed($invocant) ? $invocant->product : $params->{product};
+
+  $name = trim($name);
+  $name || ThrowUserError('version_blank_name');
+
+  # Remove unprintable characters
+  $name = clean_text($name);
+
+  my $version = new Bugzilla::Version({product => $product, name => $name});
+  if ($version && (!ref $invocant || $version->id != $invocant->id)) {
+    ThrowUserError('version_already_exists',
+      {name => $version->name, product => $product->name});
+  }
+  return $name;
 }
 
 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);
 }
 
 ###############################
@@ -196,44 +196,52 @@ sub _check_product {
 # This is taken straight from Sort::Versions 1.5, which is not included
 # with perl by default.
 sub vers_cmp {
-    my ($a, $b) = @_;
-
-    # Remove leading zeroes - Bug 344661
-    $a =~ s/^0*(\d.+)/$1/;
-    $b =~ s/^0*(\d.+)/$1/;
-
-    my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g);
-    my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g);
-
-    my ($A, $B);
-    while (@A and @B) {
-        $A = shift @A;
-        $B = shift @B;
-        if ($A eq '-' and $B eq '-') {
-            next;
-        } elsif ( $A eq '-' ) {
-            return -1;
-        } elsif ( $B eq '-') {
-            return 1;
-        } elsif ($A eq '.' and $B eq '.') {
-            next;
-        } elsif ( $A eq '.' ) {
-            return -1;
-        } elsif ( $B eq '.' ) {
-            return 1;
-        } elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
-            if ($A =~ /^0/ || $B =~ /^0/) {
-                return $A cmp $B if $A cmp $B;
-            } else {
-                return $A <=> $B if $A <=> $B;
-            }
-        } else {
-            $A = uc $A;
-            $B = uc $B;
-            return $A cmp $B if $A cmp $B;
-        }
+  my ($a, $b) = @_;
+
+  # Remove leading zeroes - Bug 344661
+  $a =~ s/^0*(\d.+)/$1/;
+  $b =~ s/^0*(\d.+)/$1/;
+
+  my @A = ($a =~ /([-.]|\d+|[^-.\d]+)/g);
+  my @B = ($b =~ /([-.]|\d+|[^-.\d]+)/g);
+
+  my ($A, $B);
+  while (@A and @B) {
+    $A = shift @A;
+    $B = shift @B;
+    if ($A eq '-' and $B eq '-') {
+      next;
+    }
+    elsif ($A eq '-') {
+      return -1;
+    }
+    elsif ($B eq '-') {
+      return 1;
+    }
+    elsif ($A eq '.' and $B eq '.') {
+      next;
+    }
+    elsif ($A eq '.') {
+      return -1;
+    }
+    elsif ($B eq '.') {
+      return 1;
+    }
+    elsif ($A =~ /^\d+$/ and $B =~ /^\d+$/) {
+      if ($A =~ /^0/ || $B =~ /^0/) {
+        return $A cmp $B if $A cmp $B;
+      }
+      else {
+        return $A <=> $B if $A <=> $B;
+      }
+    }
+    else {
+      $A = uc $A;
+      $B = uc $B;
+      return $A cmp $B if $A cmp $B;
     }
-    return @A <=> @B;
+  }
+  return @A <=> @B;
 }
 
 1;
diff --git a/Bugzilla/WebService.pm b/Bugzilla/WebService.pm
index 6ff821a38..da0fbfd32 100644
--- a/Bugzilla/WebService.pm
+++ b/Bugzilla/WebService.pm
@@ -17,11 +17,12 @@ use Bugzilla::WebService::Server;
 
 # Used by the JSON-RPC server to convert incoming date fields apprpriately.
 use constant DATE_FIELDS => {};
+
 # Used by the JSON-RPC server to convert incoming base64 fields appropriately.
 use constant BASE64_FIELDS => {};
 
 # For some methods, we shouldn't call Bugzilla->login before we call them
-use constant LOGIN_EXEMPT => { };
+use constant LOGIN_EXEMPT => {};
 
 # Used to allow methods to be called in the JSON-RPC WebService via GET.
 # Methods that can modify data MUST not be listed here.
@@ -32,8 +33,8 @@ use constant READ_ONLY => ();
 use constant PUBLIC_METHODS => ();
 
 sub login_exempt {
-    my ($class, $method) = @_;
-    return $class->LOGIN_EXEMPT->{$method};
+  my ($class, $method) = @_;
+  return $class->LOGIN_EXEMPT->{$method};
 }
 
 1;
diff --git a/Bugzilla/WebService/Bug.pm b/Bugzilla/WebService/Bug.pm
index 61a95e07d..5b6c31063 100644
--- a/Bugzilla/WebService/Bug.pm
+++ b/Bugzilla/WebService/Bug.pm
@@ -19,7 +19,8 @@ use Bugzilla::Constants;
 use Bugzilla::Error;
 use Bugzilla::Field;
 use Bugzilla::WebService::Constants;
-use Bugzilla::WebService::Util qw(extract_flags filter filter_wants validate translate);
+use Bugzilla::WebService::Util
+  qw(extract_flags filter filter_wants validate translate);
 use Bugzilla::Bug;
 use Bugzilla::BugMail;
 use Bugzilla::Util qw(trick_taint trim detaint_natural remote_ip);
@@ -45,71 +46,68 @@ use Type::Utils;
 use constant PRODUCT_SPECIFIC_FIELDS => qw(version target_milestone component);
 
 sub DATE_FIELDS {
-    my $fields = {
-        comments => ['new_since'],
-        create   => [],
-        history  => ['new_since'],
-        search   => ['last_change_time', 'creation_time'],
-        update   => []
-    };
-
-    # Add date related custom fields
-    foreach my $field (Bugzilla->active_custom_fields) {
-        next unless ($field->type == FIELD_TYPE_DATETIME
-                     || $field->type == FIELD_TYPE_DATE);
-        push(@{ $fields->{create} }, $field->name);
-        push(@{ $fields->{update} }, $field->name);
-    }
-
-    return $fields;
+  my $fields = {
+    comments => ['new_since'],
+    create   => [],
+    history  => ['new_since'],
+    search   => ['last_change_time', 'creation_time'],
+    update   => []
+  };
+
+  # Add date related custom fields
+  foreach my $field (Bugzilla->active_custom_fields) {
+    next
+      unless ($field->type == FIELD_TYPE_DATETIME
+      || $field->type == FIELD_TYPE_DATE);
+    push(@{$fields->{create}}, $field->name);
+    push(@{$fields->{update}}, $field->name);
+  }
+
+  return $fields;
 }
 
-use constant BASE64_FIELDS => {
-    add_attachment => ['data'],
-};
+use constant BASE64_FIELDS => {add_attachment => ['data'],};
 
 use constant READ_ONLY => qw(
-    attachments
-    comments
-    fields
-    get
-    history
-    legal_values
-    search
+  attachments
+  comments
+  fields
+  get
+  history
+  legal_values
+  search
 );
 
 use constant PUBLIC_METHODS => qw(
-    add_attachment
-    add_comment
-    attachments
-    comments
-    create
-    fields
-    get
-    history
-    legal_values
-    possible_duplicates
-    render_comment
-    search
-    search_comment_tags
-    update
-    update_attachment
-    update_comment_tags
-    update_see_also
+  add_attachment
+  add_comment
+  attachments
+  comments
+  create
+  fields
+  get
+  history
+  legal_values
+  possible_duplicates
+  render_comment
+  search
+  search_comment_tags
+  update
+  update_attachment
+  update_comment_tags
+  update_see_also
 );
 
-use constant ATTACHMENT_MAPPED_SETTERS => {
-    file_name => 'filename',
-    summary   => 'description',
-};
+use constant ATTACHMENT_MAPPED_SETTERS =>
+  {file_name => 'filename', summary => 'description',};
 
 use constant ATTACHMENT_MAPPED_RETURNS => {
-    description => 'summary',
-    ispatch     => 'is_patch',
-    isprivate   => 'is_private',
-    isobsolete  => 'is_obsolete',
-    filename    => 'file_name',
-    mimetype    => 'content_type',
+  description => 'summary',
+  ispatch     => 'is_patch',
+  isprivate   => 'is_private',
+  isobsolete  => 'is_obsolete',
+  filename    => 'file_name',
+  mimetype    => 'content_type',
 };
 
 ######################################################
@@ -119,6 +117,7 @@ use constant ATTACHMENT_MAPPED_RETURNS => {
 BEGIN {
   # In 3.0, get was called get_bugs
   *get_bugs = \&get;
+
   # Before 3.4rc1, "history" was get_history.
   *get_history = \&history;
 }
@@ -128,1216 +127,1240 @@ BEGIN {
 ###########
 
 sub fields {
-    my ($self, $params) = validate(@_, 'ids', 'names');
+  my ($self, $params) = validate(@_, 'ids', 'names');
 
-    Bugzilla->switch_to_shadow_db();
+  Bugzilla->switch_to_shadow_db();
 
-    my @fields;
-    if (defined $params->{ids}) {
-        my $ids = $params->{ids};
-        foreach my $id (@$ids) {
-            my $loop_field = Bugzilla::Field->check({ id => $id });
-            push(@fields, $loop_field);
-        }
+  my @fields;
+  if (defined $params->{ids}) {
+    my $ids = $params->{ids};
+    foreach my $id (@$ids) {
+      my $loop_field = Bugzilla::Field->check({id => $id});
+      push(@fields, $loop_field);
     }
-
-    if (defined $params->{names}) {
-        my $names = $params->{names};
-        foreach my $field_name (@$names) {
-            my $loop_field = Bugzilla::Field->check($field_name);
-            # Don't push in duplicate fields if we also asked for this field
-            # in "ids".
-            if (!grep($_->id == $loop_field->id, @fields)) {
-                push(@fields, $loop_field);
-            }
-        }
+  }
+
+  if (defined $params->{names}) {
+    my $names = $params->{names};
+    foreach my $field_name (@$names) {
+      my $loop_field = Bugzilla::Field->check($field_name);
+
+      # Don't push in duplicate fields if we also asked for this field
+      # in "ids".
+      if (!grep($_->id == $loop_field->id, @fields)) {
+        push(@fields, $loop_field);
+      }
     }
-
-    if (!defined $params->{ids} and !defined $params->{names}) {
-        @fields = @{ Bugzilla->fields({ obsolete => 0 }) };
+  }
+
+  if (!defined $params->{ids} and !defined $params->{names}) {
+    @fields = @{Bugzilla->fields({obsolete => 0})};
+  }
+
+  my @fields_out;
+  foreach my $field (@fields) {
+    my $visibility_field
+      = $field->visibility_field ? $field->visibility_field->name : undef;
+    my $vis_values = $field->visibility_values;
+    my $value_field = $field->value_field ? $field->value_field->name : undef;
+
+    my (@values, $has_values);
+    if ( ($field->is_select and $field->name ne 'product')
+      or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)
+      or $field->name eq 'keywords')
+    {
+      $has_values = 1;
+      @values = @{$self->_legal_field_values({field => $field})};
     }
 
-    my @fields_out;
-    foreach my $field (@fields) {
-        my $visibility_field = $field->visibility_field
-                               ? $field->visibility_field->name : undef;
-        my $vis_values = $field->visibility_values;
-        my $value_field = $field->value_field
-                          ? $field->value_field->name : undef;
-
-        my (@values, $has_values);
-        if ( ($field->is_select and $field->name ne 'product')
-             or grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)
-             or $field->name eq 'keywords')
-        {
-             $has_values = 1;
-             @values = @{ $self->_legal_field_values({ field => $field }) };
-        }
-
-        if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) {
-             $value_field = 'product';
-        }
+    if (grep($_ eq $field->name, PRODUCT_SPECIFIC_FIELDS)) {
+      $value_field = 'product';
+    }
 
-        my %field_data = (
-           id                => $self->type('int', $field->id),
-           type              => $self->type('int', $field->type),
-           is_custom         => $self->type('boolean', $field->custom),
-           name              => $self->type('string', $field->name),
-           display_name      => $self->type('string', $field->description),
-           is_mandatory      => $self->type('boolean', $field->is_mandatory),
-           is_on_bug_entry   => $self->type('boolean', $field->enter_bug),
-           visibility_field  => $self->type('string', $visibility_field),
-           visibility_values =>
-              [ map { $self->type('string', $_->name) } @$vis_values ],
-        );
-        if ($has_values) {
-           $field_data{value_field} = $self->type('string', $value_field);
-           $field_data{values}      = \@values;
-        };
-        push(@fields_out, filter $params, \%field_data);
+    my %field_data = (
+      id               => $self->type('int',     $field->id),
+      type             => $self->type('int',     $field->type),
+      is_custom        => $self->type('boolean', $field->custom),
+      name             => $self->type('string',  $field->name),
+      display_name     => $self->type('string',  $field->description),
+      is_mandatory     => $self->type('boolean', $field->is_mandatory),
+      is_on_bug_entry  => $self->type('boolean', $field->enter_bug),
+      visibility_field => $self->type('string',  $visibility_field),
+      visibility_values => [map { $self->type('string', $_->name) } @$vis_values],
+    );
+    if ($has_values) {
+      $field_data{value_field} = $self->type('string', $value_field);
+      $field_data{values} = \@values;
     }
+    push(@fields_out, filter $params, \%field_data);
+  }
 
-    return { fields => \@fields_out };
+  return {fields => \@fields_out};
 }
 
 sub _legal_field_values {
-    my ($self, $params) = @_;
-    my $field = $params->{field};
-    my $field_name = $field->name;
-    my $user = Bugzilla->user;
-
-    my @result;
-    if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) {
-        my @list;
-        if ($field_name eq 'version') {
-            @list = Bugzilla::Version->get_all;
-        }
-        elsif ($field_name eq 'component') {
-            @list = Bugzilla::Component->get_all;
-        }
-        else {
-            @list = Bugzilla::Milestone->get_all;
-        }
+  my ($self, $params) = @_;
+  my $field      = $params->{field};
+  my $field_name = $field->name;
+  my $user       = Bugzilla->user;
+
+  my @result;
+  if (grep($_ eq $field_name, PRODUCT_SPECIFIC_FIELDS)) {
+    my @list;
+    if ($field_name eq 'version') {
+      @list = Bugzilla::Version->get_all;
+    }
+    elsif ($field_name eq 'component') {
+      @list = Bugzilla::Component->get_all;
+    }
+    else {
+      @list = Bugzilla::Milestone->get_all;
+    }
 
-        foreach my $value (@list) {
-            my $sortkey = $field_name eq 'target_milestone'
-                          ? $value->sortkey : 0;
-            # XXX This is very slow for large numbers of values.
-            my $product_name = $value->product->name;
-            if ($user->can_see_product($product_name)) {
-                push(@result, {
-                    name     => $self->type('string', $value->name),
-                    sort_key => $self->type('int', $sortkey),
-                    sortkey  => $self->type('int', $sortkey), # deprecated
-                    visibility_values => [$self->type('string', $product_name)],
-                });
-            }
-        }
+    foreach my $value (@list) {
+      my $sortkey = $field_name eq 'target_milestone' ? $value->sortkey : 0;
+
+      # XXX This is very slow for large numbers of values.
+      my $product_name = $value->product->name;
+      if ($user->can_see_product($product_name)) {
+        push(
+          @result,
+          {
+            name              => $self->type('string',  $value->name),
+            sort_key          => $self->type('int',     $sortkey),
+            sortkey           => $self->type('int',     $sortkey),         # deprecated
+            visibility_values => [$self->type('string', $product_name)],
+          }
+        );
+      }
     }
+  }
+
+  elsif ($field_name eq 'bug_status') {
+    my @status_all = Bugzilla::Status->get_all;
+    foreach my $status (@status_all) {
+      my @can_change_to;
+      foreach my $change_to (@{$status->can_change_to}) {
+
+        # There's no need to note that a status can transition
+        # to itself.
+        next if $change_to->id == $status->id;
+        my %change_to_hash = (
+          name => $self->type('string', $change_to->name),
+          comment_required =>
+            $self->type('boolean', $change_to->comment_required_on_change_from($status)),
+        );
+        push(@can_change_to, \%change_to_hash);
+      }
 
-    elsif ($field_name eq 'bug_status') {
-        my @status_all = Bugzilla::Status->get_all;
-        foreach my $status (@status_all) {
-            my @can_change_to;
-            foreach my $change_to (@{ $status->can_change_to }) {
-                # There's no need to note that a status can transition
-                # to itself.
-                next if $change_to->id == $status->id;
-                my %change_to_hash = (
-                    name => $self->type('string', $change_to->name),
-                    comment_required => $self->type('boolean',
-                        $change_to->comment_required_on_change_from($status)),
-                );
-                push(@can_change_to, \%change_to_hash);
-            }
-
-            push (@result, {
-               name     => $self->type('string', $status->name),
-               is_open  => $self->type('boolean', $status->is_open),
-               sort_key => $self->type('int', $status->sortkey),
-               sortkey  => $self->type('int', $status->sortkey), # deprecated
-               can_change_to => \@can_change_to,
-               visibility_values => [],
-            });
+      push(
+        @result,
+        {
+          name              => $self->type('string',  $status->name),
+          is_open           => $self->type('boolean', $status->is_open),
+          sort_key          => $self->type('int',     $status->sortkey),
+          sortkey           => $self->type('int',     $status->sortkey),    # deprecated
+          can_change_to     => \@can_change_to,
+          visibility_values => [],
         }
+      );
     }
-
-    elsif ($field_name eq 'keywords') {
-        my @legal_keywords = Bugzilla::Keyword->get_all;
-        foreach my $value (@legal_keywords) {
-            next unless $value->is_active;
-            push (@result, {
-               name     => $self->type('string', $value->name),
-               description => $self->type('string', $value->description),
-            });
+  }
+
+  elsif ($field_name eq 'keywords') {
+    my @legal_keywords = Bugzilla::Keyword->get_all;
+    foreach my $value (@legal_keywords) {
+      next unless $value->is_active;
+      push(
+        @result,
+        {
+          name        => $self->type('string', $value->name),
+          description => $self->type('string', $value->description),
         }
+      );
     }
-    else {
-        my @values = Bugzilla::Field::Choice->type($field)->get_all();
-        foreach my $value (@values) {
-            my $vis_val = $value->visibility_value;
-            push(@result, {
-                name     => $self->type('string', $value->name),
-                sort_key => $self->type('int'   , $value->sortkey),
-                sortkey  => $self->type('int'   , $value->sortkey), # deprecated
-                visibility_values => [
-                    defined $vis_val ? $self->type('string', $vis_val->name)
-                                     : ()
-                ],
-            });
+  }
+  else {
+    my @values = Bugzilla::Field::Choice->type($field)->get_all();
+    foreach my $value (@values) {
+      my $vis_val = $value->visibility_value;
+      push(
+        @result,
+        {
+          name     => $self->type('string', $value->name),
+          sort_key => $self->type('int',    $value->sortkey),
+          sortkey  => $self->type('int',    $value->sortkey),    # deprecated
+          visibility_values =>
+            [defined $vis_val ? $self->type('string', $vis_val->name) : ()],
         }
+      );
     }
+  }
 
-    return \@result;
+  return \@result;
 }
 
 sub comments {
-    my ($self, $params) = validate(@_, 'ids', 'comment_ids');
+  my ($self, $params) = validate(@_, 'ids', 'comment_ids');
 
-    if (!(defined $params->{ids} || defined $params->{comment_ids})) {
-        ThrowCodeError('params_required',
-                       { function => 'Bug.comments',
-                         params   => ['ids', 'comment_ids'] });
-    }
+  if (!(defined $params->{ids} || defined $params->{comment_ids})) {
+    ThrowCodeError('params_required',
+      {function => 'Bug.comments', params => ['ids', 'comment_ids']});
+  }
 
-    my $bug_ids = $params->{ids} || [];
-    my $comment_ids = $params->{comment_ids} || [];
+  my $bug_ids     = $params->{ids}         || [];
+  my $comment_ids = $params->{comment_ids} || [];
 
-    my $dbh  = Bugzilla->switch_to_shadow_db();
-    my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->switch_to_shadow_db();
+  my $user = Bugzilla->user;
 
-    my %bugs;
-    foreach my $bug_id (@$bug_ids) {
-        my $bug = Bugzilla::Bug->check($bug_id);
-        # We want the API to always return comments in the same order.
+  my %bugs;
+  foreach my $bug_id (@$bug_ids) {
+    my $bug = Bugzilla::Bug->check($bug_id);
 
-        my $comments = $bug->comments({ order => 'oldest_to_newest',
-                                        after => $params->{new_since} });
-        my @result;
-        foreach my $comment (@$comments) {
-            next if $comment->is_private && !$user->is_insider;
-            push(@result, $self->_translate_comment($comment, $params));
-        }
-        $bugs{$bug->id}{'comments'} = \@result;
-    }
+    # We want the API to always return comments in the same order.
 
-    my %comments;
-    if (scalar @$comment_ids) {
-        my @ids = map { trim($_) } @$comment_ids;
-        my $comment_data = Bugzilla::Comment->new_from_list(\@ids);
-
-        # See if we were passed any invalid comment ids.
-        my %got_ids = map { $_->id => 1 } @$comment_data;
-        foreach my $comment_id (@ids) {
-            if (!$got_ids{$comment_id}) {
-                ThrowUserError('comment_id_invalid', { id => $comment_id });
-            }
-        }
+    my $comments
+      = $bug->comments({order => 'oldest_to_newest', after => $params->{new_since}
+      });
+    my @result;
+    foreach my $comment (@$comments) {
+      next if $comment->is_private && !$user->is_insider;
+      push(@result, $self->_translate_comment($comment, $params));
+    }
+    $bugs{$bug->id}{'comments'} = \@result;
+  }
+
+  my %comments;
+  if (scalar @$comment_ids) {
+    my @ids = map { trim($_) } @$comment_ids;
+    my $comment_data = Bugzilla::Comment->new_from_list(\@ids);
+
+    # See if we were passed any invalid comment ids.
+    my %got_ids = map { $_->id => 1 } @$comment_data;
+    foreach my $comment_id (@ids) {
+      if (!$got_ids{$comment_id}) {
+        ThrowUserError('comment_id_invalid', {id => $comment_id});
+      }
+    }
 
-        # Now make sure that we can see all the associated bugs.
-        my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
-        Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);
+    # Now make sure that we can see all the associated bugs.
+    my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data;
+    Bugzilla::Bug->check($_) foreach (keys %got_bug_ids);
 
-        foreach my $comment (@$comment_data) {
-            if ($comment->is_private && !$user->is_insider) {
-                ThrowUserError('comment_is_private', { id => $comment->id });
-            }
-            $comments{$comment->id} =
-                $self->_translate_comment($comment, $params);
-        }
+    foreach my $comment (@$comment_data) {
+      if ($comment->is_private && !$user->is_insider) {
+        ThrowUserError('comment_is_private', {id => $comment->id});
+      }
+      $comments{$comment->id} = $self->_translate_comment($comment, $params);
     }
+  }
 
-    return { bugs => \%bugs, comments => \%comments };
+  return {bugs => \%bugs, comments => \%comments};
 }
 
 sub render_comment {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    unless (defined $params->{text}) {
-        ThrowCodeError('params_required',
-                       { function => 'Bug.render_comment',
-                         params   => ['text'] });
-    }
+  unless (defined $params->{text}) {
+    ThrowCodeError('params_required',
+      {function => 'Bug.render_comment', params => ['text']});
+  }
 
-    Bugzilla->switch_to_shadow_db();
-    my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
+  Bugzilla->switch_to_shadow_db();
+  my $bug = $params->{id} ? Bugzilla::Bug->check($params->{id}) : undef;
 
-    my $html = Bugzilla::Template::quoteUrls($params->{text}, $bug);
+  my $html = Bugzilla::Template::quoteUrls($params->{text}, $bug);
 
-    return { html => $html };
+  return {html => $html};
 }
 
 # Helper for Bug.comments
 sub _translate_comment {
-    my ($self, $comment, $filters, $types, $prefix) = @_;
-    my $attach_id = $comment->is_about_attachment ? $comment->extra_data
-                                                  : undef;
-
-    my $comment_hash = {
-        id         => $self->type('int', $comment->id),
-        bug_id     => $self->type('int', $comment->bug_id),
-        creator    => $self->type('email', $comment->author->login),
-        author     => $self->type('email', $comment->author->login),
-        time       => $self->type('dateTime', $comment->creation_ts),
-        creation_time => $self->type('dateTime', $comment->creation_ts),
-        is_private => $self->type('boolean', $comment->is_private),
-        text       => $self->type('string', $comment->body_full),
-        attachment_id => $self->type('int', $attach_id),
-        count      => $self->type('int', $comment->count),
-    };
-
-    # Don't load comment tags unless enabled
-    if (Bugzilla->params->{'comment_taggers_group'}) {
-        $comment_hash->{tags} = [
-            map { $self->type('string', $_) }
-            @{ $comment->tags }
-        ];
-    }
-
-    return filter($filters, $comment_hash, $types, $prefix);
+  my ($self, $comment, $filters, $types, $prefix) = @_;
+  my $attach_id = $comment->is_about_attachment ? $comment->extra_data : undef;
+
+  my $comment_hash = {
+    id            => $self->type('int',      $comment->id),
+    bug_id        => $self->type('int',      $comment->bug_id),
+    creator       => $self->type('email',    $comment->author->login),
+    author        => $self->type('email',    $comment->author->login),
+    time          => $self->type('dateTime', $comment->creation_ts),
+    creation_time => $self->type('dateTime', $comment->creation_ts),
+    is_private    => $self->type('boolean',  $comment->is_private),
+    text          => $self->type('string',   $comment->body_full),
+    attachment_id => $self->type('int',      $attach_id),
+    count         => $self->type('int',      $comment->count),
+  };
+
+  # Don't load comment tags unless enabled
+  if (Bugzilla->params->{'comment_taggers_group'}) {
+    $comment_hash->{tags} = [map { $self->type('string', $_) } @{$comment->tags}];
+  }
+
+  return filter($filters, $comment_hash, $types, $prefix);
 }
 
 sub get {
-    my ($self, $params) = validate(@_, 'ids');
-
-    unless (Bugzilla->user->id) {
-        Bugzilla->check_rate_limit("get_bug", remote_ip());
+  my ($self, $params) = validate(@_, 'ids');
+
+  unless (Bugzilla->user->id) {
+    Bugzilla->check_rate_limit("get_bug", remote_ip());
+  }
+  Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+
+  my $ids = $params->{ids};
+  (defined $ids && scalar @$ids)
+    || ThrowCodeError('param_required', {param => 'ids'});
+
+  my (@bugs, @faults, @hashes);
+
+  # Cache permissions for bugs. This highly reduces the number of calls to the DB.
+  # visible_bugs() is only able to handle bug IDs, so we have to skip aliases.
+  my @int = grep { $_ =~ /^\d+$/ } @$ids;
+  Bugzilla->user->visible_bugs(\@int);
+
+  foreach my $bug_id (@$ids) {
+    my $bug;
+    if ($params->{permissive}) {
+      eval { $bug = Bugzilla::Bug->check($bug_id); };
+      if ($@) {
+        push(@faults,
+          {id => $bug_id, faultString => $@->faultstring, faultCode => $@->faultcode,});
+        undef $@;
+        next;
+      }
     }
-    Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
-
-    my $ids = $params->{ids};
-    (defined $ids && scalar @$ids)
-        || ThrowCodeError('param_required', { param => 'ids' });
-
-    my (@bugs, @faults, @hashes);
-
-    # Cache permissions for bugs. This highly reduces the number of calls to the DB.
-    # visible_bugs() is only able to handle bug IDs, so we have to skip aliases.
-    my @int = grep { $_ =~ /^\d+$/ } @$ids;
-    Bugzilla->user->visible_bugs(\@int);
-
-    foreach my $bug_id (@$ids) {
-        my $bug;
-        if ($params->{permissive}) {
-            eval { $bug = Bugzilla::Bug->check($bug_id); };
-            if ($@) {
-                push(@faults, {id => $bug_id,
-                               faultString => $@->faultstring,
-                               faultCode => $@->faultcode,
-                              }
-                    );
-                undef $@;
-                next;
-            }
-        }
-        else {
-            $bug = Bugzilla::Bug->check($bug_id);
-        }
-        push(@bugs, $bug);
-        push(@hashes, $self->_bug_to_hash($bug, $params));
+    else {
+      $bug = Bugzilla::Bug->check($bug_id);
     }
+    push(@bugs, $bug);
+    push(@hashes, $self->_bug_to_hash($bug, $params));
+  }
 
-    # Set the ETag before inserting the update tokens
-    # since the tokens will always be unique even if
-    # the data has not changed.
-    $self->bz_etag(\@hashes);
+  # Set the ETag before inserting the update tokens
+  # since the tokens will always be unique even if
+  # the data has not changed.
+  $self->bz_etag(\@hashes);
 
-    $self->_add_update_tokens($params, \@bugs, \@hashes);
+  $self->_add_update_tokens($params, \@bugs, \@hashes);
 
-    if (Bugzilla->user->id) {
-        foreach my $bug (@bugs) {
-            Bugzilla->log_user_request($bug->id, undef, 'bug-get');
-        }
+  if (Bugzilla->user->id) {
+    foreach my $bug (@bugs) {
+      Bugzilla->log_user_request($bug->id, undef, 'bug-get');
     }
-    return { bugs => \@hashes, faults => \@faults };
+  }
+  return {bugs => \@hashes, faults => \@faults};
 }
 
 # this is a function that gets bug activity for list of bug ids
 # it can be called as the following:
 # $call = $rpc->call( 'Bug.history', { ids => [1,2] });
 sub history {
-    my ($self, $params) = validate(@_, 'ids');
-
-    Bugzilla->switch_to_shadow_db();
-
-    my $ids = $params->{ids};
-    defined $ids || ThrowCodeError('param_required', { param => 'ids' });
-
-    my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
-    $api_name{'bug_group'} = 'groups';
-
-    my @return;
-    foreach my $bug_id (@$ids) {
-        my %item;
-        my $bug = Bugzilla::Bug->check($bug_id);
-        $bug_id = $bug->id;
-        $item{id} = $self->type('int', $bug_id);
-
-        my ($activity) = Bugzilla::Bug::GetBugActivity($bug_id, undef, $params->{new_since});
-
-        my @history;
-        foreach my $changeset (@$activity) {
-            my %bug_history;
-            $bug_history{when} = $self->type('dateTime', $changeset->{when});
-            $bug_history{who}  = $self->type('email', $changeset->{who});
-            $bug_history{changes} = [];
-            foreach my $change (@{ $changeset->{changes} }) {
-                my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname};
-                my $attach_id = delete $change->{attachid};
-                if ($attach_id) {
-                    $change->{attachment_id} = $self->type('int', $attach_id);
-                }
-                $change->{removed} = $self->type('string', $change->{removed});
-                $change->{added}   = $self->type('string', $change->{added});
-                $change->{field_name} = $self->type('string', $api_field);
-                delete $change->{fieldname};
-                push (@{$bug_history{changes}}, $change);
-            }
-
-            push (@history, \%bug_history);
-        }
-
-        $item{history} = \@history;
-
-        # alias is returned in case users passes a mixture of ids and aliases
-        # then they get to know which bug activity relates to which value
-        # they passed
-        if (Bugzilla->params->{'usebugaliases'}) {
-            $item{alias} = $self->type('string', $bug->alias);
+  my ($self, $params) = validate(@_, 'ids');
+
+  Bugzilla->switch_to_shadow_db();
+
+  my $ids = $params->{ids};
+  defined $ids || ThrowCodeError('param_required', {param => 'ids'});
+
+  my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()};
+  $api_name{'bug_group'} = 'groups';
+
+  my @return;
+  foreach my $bug_id (@$ids) {
+    my %item;
+    my $bug = Bugzilla::Bug->check($bug_id);
+    $bug_id = $bug->id;
+    $item{id} = $self->type('int', $bug_id);
+
+    my ($activity)
+      = Bugzilla::Bug::GetBugActivity($bug_id, undef, $params->{new_since});
+
+    my @history;
+    foreach my $changeset (@$activity) {
+      my %bug_history;
+      $bug_history{when} = $self->type('dateTime', $changeset->{when});
+      $bug_history{who}  = $self->type('email',    $changeset->{who});
+      $bug_history{changes} = [];
+      foreach my $change (@{$changeset->{changes}}) {
+        my $api_field = $api_name{$change->{fieldname}} || $change->{fieldname};
+        my $attach_id = delete $change->{attachid};
+        if ($attach_id) {
+          $change->{attachment_id} = $self->type('int', $attach_id);
         }
-        else {
-            # For API reasons, we always want the value to appear, we just
-            # don't want it to have a value if aliases are turned off.
-            $item{alias} = undef;
-        }
-
-        push(@return, \%item);
-    }
-
-    return { bugs => \@return };
-}
-
-sub search {
-    my ($self, $params) = @_;
-    my $user = Bugzilla->user;
-    my $dbh  = Bugzilla->dbh;
-
-    Bugzilla->switch_to_shadow_db();
-
-    my $match_params = dclone($params);
-    delete $match_params->{include_fields};
-    delete $match_params->{exclude_fields};
-
-    # Determine whether this is a quicksearch query
-    if (exists $match_params->{quicksearch}) {
-        my $quicksearch = quicksearch($match_params->{'quicksearch'});
-        my $cgi = Bugzilla::CGI->new($quicksearch);
-        $match_params = $cgi->Vars;
+        $change->{removed}    = $self->type('string', $change->{removed});
+        $change->{added}      = $self->type('string', $change->{added});
+        $change->{field_name} = $self->type('string', $api_field);
+        delete $change->{fieldname};
+        push(@{$bug_history{changes}}, $change);
+      }
+
+      push(@history, \%bug_history);
     }
 
-    if ( defined($match_params->{offset}) and !defined($match_params->{limit}) ) {
-        ThrowCodeError('param_required',
-                       { param => 'limit', function => 'Bug.search()' });
-    }
+    $item{history} = \@history;
 
-    my $max_results = Bugzilla->params->{max_search_results};
-    unless (defined $match_params->{limit} && $match_params->{limit} == 0) {
-        if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) {
-            $match_params->{limit} = $max_results;
-        }
+    # alias is returned in case users passes a mixture of ids and aliases
+    # then they get to know which bug activity relates to which value
+    # they passed
+    if (Bugzilla->params->{'usebugaliases'}) {
+      $item{alias} = $self->type('string', $bug->alias);
     }
     else {
-        delete $match_params->{limit};
-        delete $match_params->{offset};
+      # For API reasons, we always want the value to appear, we just
+      # don't want it to have a value if aliases are turned off.
+      $item{alias} = undef;
     }
 
-    $match_params = Bugzilla::Bug::map_fields($match_params);
-
-    my %options = ( fields => ['bug_id'] );
+    push(@return, \%item);
+  }
 
-    # Find the highest custom field id
-    my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
-    my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+  return {bugs => \@return};
+}
 
-    # Do special search types for certain fields.
-    if (my $change_when = delete $match_params->{'delta_ts'}) {
-        $match_params->{"f${last_field_id}"} = 'delta_ts';
-        $match_params->{"o${last_field_id}"} = 'greaterthaneq';
-        $match_params->{"v${last_field_id}"} = $change_when;
-        $last_field_id++;
+sub search {
+  my ($self, $params) = @_;
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
+
+  Bugzilla->switch_to_shadow_db();
+
+  my $match_params = dclone($params);
+  delete $match_params->{include_fields};
+  delete $match_params->{exclude_fields};
+
+  # Determine whether this is a quicksearch query
+  if (exists $match_params->{quicksearch}) {
+    my $quicksearch = quicksearch($match_params->{'quicksearch'});
+    my $cgi         = Bugzilla::CGI->new($quicksearch);
+    $match_params = $cgi->Vars;
+  }
+
+  if (defined($match_params->{offset}) and !defined($match_params->{limit})) {
+    ThrowCodeError('param_required',
+      {param => 'limit', function => 'Bug.search()'});
+  }
+
+  my $max_results = Bugzilla->params->{max_search_results};
+  unless (defined $match_params->{limit} && $match_params->{limit} == 0) {
+    if (!defined $match_params->{limit} || $match_params->{limit} > $max_results) {
+      $match_params->{limit} = $max_results;
     }
-    if (my $creation_when = delete $match_params->{'creation_ts'}) {
-        $match_params->{"f${last_field_id}"} = 'creation_ts';
-        $match_params->{"o${last_field_id}"} = 'greaterthaneq';
-        $match_params->{"v${last_field_id}"} = $creation_when;
-        $last_field_id++;
+  }
+  else {
+    delete $match_params->{limit};
+    delete $match_params->{offset};
+  }
+
+  $match_params = Bugzilla::Bug::map_fields($match_params);
+
+  my %options = (fields => ['bug_id']);
+
+  # Find the highest custom field id
+  my @field_ids = grep(/^f(\d+)$/, keys %$match_params);
+  my $last_field_id = @field_ids ? max @field_ids + 1 : 1;
+
+  # Do special search types for certain fields.
+  if (my $change_when = delete $match_params->{'delta_ts'}) {
+    $match_params->{"f${last_field_id}"} = 'delta_ts';
+    $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+    $match_params->{"v${last_field_id}"} = $change_when;
+    $last_field_id++;
+  }
+  if (my $creation_when = delete $match_params->{'creation_ts'}) {
+    $match_params->{"f${last_field_id}"} = 'creation_ts';
+    $match_params->{"o${last_field_id}"} = 'greaterthaneq';
+    $match_params->{"v${last_field_id}"} = $creation_when;
+    $last_field_id++;
+  }
+
+  # Some fields require a search type such as short desc, keywords, etc.
+  foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) {
+    if (defined $match_params->{$param}
+      && !defined $match_params->{$param . '_type'})
+    {
+      $match_params->{$param . '_type'} = 'allwordssubstr';
     }
-
-    # Some fields require a search type such as short desc, keywords, etc.
-    foreach my $param (qw(short_desc longdesc status_whiteboard bug_file_loc)) {
-        if (defined $match_params->{$param} && !defined $match_params->{$param . '_type'}) {
-            $match_params->{$param . '_type'} = 'allwordssubstr';
-        }
+  }
+  if (defined $match_params->{'keywords'}
+    && !defined $match_params->{'keywords_type'})
+  {
+    $match_params->{'keywords_type'} = 'allwords';
+  }
+
+  # Backwards compatibility with old method regarding role search
+  $match_params->{'reporter'} = delete $match_params->{'creator'}
+    if $match_params->{'creator'};
+  foreach my $role (qw(assigned_to reporter qa_contact commenter cc)) {
+    next if !exists $match_params->{$role};
+    my $value = delete $match_params->{$role};
+    $match_params->{"f${last_field_id}"} = $role;
+    $match_params->{"o${last_field_id}"} = "anywordssubstr";
+    $match_params->{"v${last_field_id}"}
+      = ref $value ? join(" ", @{$value}) : $value;
+    $last_field_id++;
+  }
+
+  # If no other parameters have been passed other than limit and offset
+  # then we throw error if system is configured to do so.
+  if ( !grep(!/^(limit|offset)$/, keys %$match_params)
+    && !Bugzilla->params->{search_allow_no_criteria})
+  {
+    ThrowUserError('buglist_parameters_required');
+  }
+
+  # Allow the use of order shortcuts similar to web UI
+  if ($match_params->{order}) {
+
+    # Convert the value of the "order" form field into a list of columns
+    # by which to sort the results.
+    my %order_types = (
+      "Bug Number" => ["bug_id"],
+      "Importance" => ["priority", "bug_severity"],
+      "Assignee"   => ["assigned_to", "bug_status", "priority", "bug_id"],
+      "Last Changed" =>
+        ["changeddate", "bug_status", "priority", "assigned_to", "bug_id"],
+    );
+    if ($order_types{$match_params->{order}}) {
+      $options{order} = $order_types{$match_params->{order}};
     }
-    if (defined $match_params->{'keywords'} && !defined $match_params->{'keywords_type'}) {
-        $match_params->{'keywords_type'} = 'allwords';
+    else {
+      $options{order} = [split(/\s*,\s*/, $match_params->{order})];
     }
+  }
 
-    # Backwards compatibility with old method regarding role search
-    $match_params->{'reporter'} = delete $match_params->{'creator'} if $match_params->{'creator'};
-    foreach my $role (qw(assigned_to reporter qa_contact commenter cc)) {
-        next if !exists $match_params->{$role};
-        my $value = delete $match_params->{$role};
-        $match_params->{"f${last_field_id}"} = $role;
-        $match_params->{"o${last_field_id}"} = "anywordssubstr";
-        $match_params->{"v${last_field_id}"} = ref $value ? join(" ", @{$value}) : $value;
-        $last_field_id++;
-    }
+  $options{params} = $match_params;
 
-    # If no other parameters have been passed other than limit and offset
-    # then we throw error if system is configured to do so.
-    if (!grep(!/^(limit|offset)$/, keys %$match_params)
-        && !Bugzilla->params->{search_allow_no_criteria})
-    {
-        ThrowUserError('buglist_parameters_required');
-    }
+  my $search = new Bugzilla::Search(%options);
+  my ($data) = $search->data;
 
-    # Allow the use of order shortcuts similar to web UI
-    if ($match_params->{order}) {
-        # Convert the value of the "order" form field into a list of columns
-        # by which to sort the results.
-        my %order_types = (
-            "Bug Number"   => [ "bug_id" ],
-            "Importance"   => [ "priority", "bug_severity" ],
-            "Assignee"     => [ "assigned_to", "bug_status", "priority", "bug_id" ],
-            "Last Changed" => [ "changeddate", "bug_status", "priority",
-                                "assigned_to", "bug_id" ],
-        );
-        if ($order_types{$match_params->{order}}) {
-            $options{order} = $order_types{$match_params->{order}};
-        }
-        else {
-            $options{order} = [ split(/\s*,\s*/, $match_params->{order}) ];
-        }
+  # BMO if the caller only wants the count, that's all we need to return
+  if ($params->{count_only}) {
+    if (Bugzilla->usage_mode == USAGE_MODE_XMLRPC) {
+      return $data;
     }
-
-    $options{params} = $match_params;
-
-    my $search = new Bugzilla::Search(%options);
-    my ($data) = $search->data;
-
-    # BMO if the caller only wants the count, that's all we need to return
-    if ($params->{count_only}) {
-        if (Bugzilla->usage_mode == USAGE_MODE_XMLRPC) {
-            return $data;
-        }
-        else {
-            return { bug_count => $data };
-        }
+    else {
+      return {bug_count => $data};
     }
+  }
 
-    if (!scalar @$data) {
-        return { bugs => [] };
-    }
+  if (!scalar @$data) {
+    return {bugs => []};
+  }
 
-    # Search.pm won't return bugs that the user shouldn't see so no filtering is needed.
-    my @bug_ids = map { $_->[0] } @$data;
-    my %bug_objects = map { $_->id => $_ } @{ Bugzilla::Bug->new_from_list(\@bug_ids) };
-    my @bugs = map { $bug_objects{$_} } @bug_ids;
-    @bugs = map { $self->_bug_to_hash($_, $params) } @bugs;
+# Search.pm won't return bugs that the user shouldn't see so no filtering is needed.
+  my @bug_ids = map { $_->[0] } @$data;
+  my %bug_objects
+    = map { $_->id => $_ } @{Bugzilla::Bug->new_from_list(\@bug_ids)};
+  my @bugs = map { $bug_objects{$_} } @bug_ids;
+  @bugs = map { $self->_bug_to_hash($_, $params) } @bugs;
 
-    # BzAPI
-    Bugzilla->request_cache->{bzapi_search_bugs} = [ map { $bug_objects{$_} } @bug_ids ];
+  # BzAPI
+  Bugzilla->request_cache->{bzapi_search_bugs}
+    = [map { $bug_objects{$_} } @bug_ids];
 
-    return { bugs => \@bugs };
+  return {bugs => \@bugs};
 }
 
 sub possible_duplicates {
-    my ($self, $params) = validate(@_, 'product');
-    my $user = Bugzilla->user;
-
-    Bugzilla->switch_to_shadow_db();
-
-    state $params_type = Dict [
-        id                 => Optional [Int],
-        product            => Optional [ ArrayRef [Str] ],
-        limit              => Optional [Int],
-        summary            => Optional [Str],
-        include_fields     => Optional [ ArrayRef [Str] ],
-        Bugzilla_api_token => Optional [Str]
-    ];
-
-    ThrowCodeError( 'param_invalid', { function => 'Bug.possible_duplicates', param => 'A param' } )
-        if !$params_type->check($params);
-
-    my $summary;
-    if ($params->{id}) {
-        my $bug = Bugzilla::Bug->check({ id => $params->{id}, cache => 1 });
-        $summary = $bug->short_desc;
-    }
-    elsif ($params->{summary}) {
-        $summary = $params->{summary};
-    }
-    else {
-        ThrowCodeError('param_required',
-        { function => 'Bug.possible_duplicates', param => 'id or summary' });
-    }
-
-    my @products;
-    foreach my $name (@{ $params->{'product'} || [] }) {
-        my $object = $user->can_enter_product($name, THROW_ERROR);
-        push(@products, $object);
-    }
-
-    my $possible_dupes = Bugzilla::Bug->possible_duplicates(
-        {
-            summary  => $summary,
-            products => \@products,
-            limit    => $params->{limit}
-        }
-    );
+  my ($self, $params) = validate(@_, 'product');
+  my $user = Bugzilla->user;
+
+  Bugzilla->switch_to_shadow_db();
+
+  state $params_type = Dict [
+    id                 => Optional [Int],
+    product            => Optional [ArrayRef [Str]],
+    limit              => Optional [Int],
+    summary            => Optional [Str],
+    include_fields     => Optional [ArrayRef [Str]],
+    Bugzilla_api_token => Optional [Str]
+  ];
+
+  ThrowCodeError('param_invalid',
+    {function => 'Bug.possible_duplicates', param => 'A param'})
+    if !$params_type->check($params);
+
+  my $summary;
+  if ($params->{id}) {
+    my $bug = Bugzilla::Bug->check({id => $params->{id}, cache => 1});
+    $summary = $bug->short_desc;
+  }
+  elsif ($params->{summary}) {
+    $summary = $params->{summary};
+  }
+  else {
+    ThrowCodeError('param_required',
+      {function => 'Bug.possible_duplicates', param => 'id or summary'});
+  }
+
+  my @products;
+  foreach my $name (@{$params->{'product'} || []}) {
+    my $object = $user->can_enter_product($name, THROW_ERROR);
+    push(@products, $object);
+  }
+
+  my $possible_dupes
+    = Bugzilla::Bug->possible_duplicates({
+    summary => $summary, products => \@products, limit => $params->{limit}
+    });
 
-    # If a bug id was used, remove the bug with the same id from the list.
-    if ($params->{id}) {
-        @$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes;
-    }
+  # If a bug id was used, remove the bug with the same id from the list.
+  if ($params->{id}) {
+    @$possible_dupes = grep { $_->id != $params->{id} } @$possible_dupes;
+  }
 
-    my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
-    $self->_add_update_tokens($params, $possible_dupes, \@hashes);
-    return { bugs => \@hashes };
+  my @hashes = map { $self->_bug_to_hash($_, $params) } @$possible_dupes;
+  $self->_add_update_tokens($params, $possible_dupes, \@hashes);
+  return {bugs => \@hashes};
 }
 
 sub update {
-    my ($self, $params) = validate(@_, 'ids');
+  my ($self, $params) = validate(@_, 'ids');
 
-    # BMO: Don't allow updating of bugs if disabled
-    if (Bugzilla->params->{disable_bug_updates}) {
-        ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
-            'Bug updates are currently disabled.');
-    }
+  # BMO: Don't allow updating of bugs if disabled
+  if (Bugzilla->params->{disable_bug_updates}) {
+    ThrowErrorPage(
+      'bug/process/updates-disabled.html.tmpl',
+      'Bug updates are currently disabled.'
+    );
+  }
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    my $dbh = Bugzilla->dbh;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $dbh  = Bugzilla->dbh;
 
-    # We skip certain fields because their set_ methods actually use
-    # the external names instead of the internal names.
-    $params = Bugzilla::Bug::map_fields($params,
-        { summary => 1, platform => 1, severity => 1, url => 1 });
+  # We skip certain fields because their set_ methods actually use
+  # the external names instead of the internal names.
+  $params = Bugzilla::Bug::map_fields($params,
+    {summary => 1, platform => 1, severity => 1, url => 1});
 
-    my $ids = delete $params->{ids};
-    defined $ids || ThrowCodeError('param_required', { param => 'ids' });
+  my $ids = delete $params->{ids};
+  defined $ids || ThrowCodeError('param_required', {param => 'ids'});
 
-    my @bugs = map { Bugzilla::Bug->check($_) } @$ids;
+  my @bugs = map { Bugzilla::Bug->check($_) } @$ids;
 
-    my %values = %$params;
-    $values{other_bugs} = \@bugs;
+  my %values = %$params;
+  $values{other_bugs} = \@bugs;
 
-    if (exists $values{comment} and exists $values{comment}{comment}) {
-        $values{comment}{body} = delete $values{comment}{comment};
-    }
+  if (exists $values{comment} and exists $values{comment}{comment}) {
+    $values{comment}{body} = delete $values{comment}{comment};
+  }
 
-    # Prevent bugs that could be triggered by specifying fields that
-    # have valid "set_" functions in Bugzilla::Bug, but shouldn't be
-    # called using those field names.
-    delete $values{dependencies};
+  # Prevent bugs that could be triggered by specifying fields that
+  # have valid "set_" functions in Bugzilla::Bug, but shouldn't be
+  # called using those field names.
+  delete $values{dependencies};
 
-    my $flags = delete $values{flags};
+  my $flags = delete $values{flags};
 
-    foreach my $bug (@bugs) {
-        if (!$user->can_edit_product($bug->product_obj->id) ) {
-            ThrowUserError("product_edit_denied",
-                          { product => $bug->product });
-        }
-
-        $bug->set_all(\%values);
-        if ($flags) {
-            my ($old_flags, $new_flags) = extract_flags($flags, $bug);
-            $bug->set_flags($old_flags, $new_flags);
-        }
+  foreach my $bug (@bugs) {
+    if (!$user->can_edit_product($bug->product_obj->id)) {
+      ThrowUserError("product_edit_denied", {product => $bug->product});
     }
 
-    my %all_changes;
-    $dbh->bz_start_transaction();
-    foreach my $bug (@bugs) {
-        $all_changes{$bug->id} = $bug->update();
+    $bug->set_all(\%values);
+    if ($flags) {
+      my ($old_flags, $new_flags) = extract_flags($flags, $bug);
+      $bug->set_flags($old_flags, $new_flags);
     }
-    $dbh->bz_commit_transaction();
+  }
+
+  my %all_changes;
+  $dbh->bz_start_transaction();
+  foreach my $bug (@bugs) {
+    $all_changes{$bug->id} = $bug->update();
+  }
+  $dbh->bz_commit_transaction();
+
+  foreach my $bug (@bugs) {
+    $bug->send_changes($all_changes{$bug->id});
+  }
+
+  my %api_name = reverse %{Bugzilla::Bug::FIELD_MAP()};
+
+  # This doesn't normally belong in FIELD_MAP, but we do want to translate
+  # "bug_group" back into "groups".
+  $api_name{'bug_group'} = 'groups';
+
+  my @result;
+  foreach my $bug (@bugs) {
+    my %hash = (
+      id               => $self->type('int',      $bug->id),
+      last_change_time => $self->type('dateTime', $bug->delta_ts),
+      changes          => {},
+    );
 
-    foreach my $bug (@bugs) {
-        $bug->send_changes($all_changes{$bug->id});
+    # alias is returned in case users pass a mixture of ids and aliases,
+    # so that they can know which set of changes relates to which value
+    # they passed.
+    if (Bugzilla->params->{'usebugaliases'}) {
+      $hash{alias} = $self->type('string', $bug->alias);
+    }
+    else {
+      # For API reasons, we always want the alias field to appear, we
+      # just don't want it to have a value if aliases are turned off.
+      $hash{alias} = $self->type('string', '');
     }
 
-    my %api_name = reverse %{ Bugzilla::Bug::FIELD_MAP() };
-    # This doesn't normally belong in FIELD_MAP, but we do want to translate
-    # "bug_group" back into "groups".
-    $api_name{'bug_group'} = 'groups';
-
-    my @result;
-    foreach my $bug (@bugs) {
-        my %hash = (
-            id               => $self->type('int', $bug->id),
-            last_change_time => $self->type('dateTime', $bug->delta_ts),
-            changes          => {},
-        );
-
-        # alias is returned in case users pass a mixture of ids and aliases,
-        # so that they can know which set of changes relates to which value
-        # they passed.
-        if (Bugzilla->params->{'usebugaliases'}) {
-            $hash{alias} = $self->type('string', $bug->alias);
-        }
-        else {
-            # For API reasons, we always want the alias field to appear, we
-            # just don't want it to have a value if aliases are turned off.
-            $hash{alias} = $self->type('string', '');
-        }
-
-        my %changes = %{ $all_changes{$bug->id} };
-        foreach my $field (keys %changes) {
-            my $change = $changes{$field};
-            my $api_field = $api_name{$field} || $field;
-            # We normalize undef to an empty string, so that the API
-            # stays consistent for things like Deadline that can become
-            # empty.
-            $change->[0] = '' if !defined $change->[0];
-            $change->[1] = '' if !defined $change->[1];
-            $hash{changes}->{$api_field} = {
-                removed => $self->type('string', $change->[0]),
-                added   => $self->type('string', $change->[1])
-            };
-        }
-
-        push(@result, \%hash);
+    my %changes = %{$all_changes{$bug->id}};
+    foreach my $field (keys %changes) {
+      my $change = $changes{$field};
+      my $api_field = $api_name{$field} || $field;
+
+      # We normalize undef to an empty string, so that the API
+      # stays consistent for things like Deadline that can become
+      # empty.
+      $change->[0] = '' if !defined $change->[0];
+      $change->[1] = '' if !defined $change->[1];
+      $hash{changes}->{$api_field} = {
+        removed => $self->type('string', $change->[0]),
+        added   => $self->type('string', $change->[1])
+      };
     }
 
-    return { bugs => \@result };
+    push(@result, \%hash);
+  }
+
+  return {bugs => \@result};
 }
 
 sub create {
-    my ($self, $params) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    # BMO: Don't allow updating of bugs if disabled
-    if (Bugzilla->params->{disable_bug_updates}) {
-        ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
-            'Bug updates are currently disabled.');
-    }
+  my ($self, $params) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  # BMO: Don't allow updating of bugs if disabled
+  if (Bugzilla->params->{disable_bug_updates}) {
+    ThrowErrorPage(
+      'bug/process/updates-disabled.html.tmpl',
+      'Bug updates are currently disabled.'
+    );
+  }
 
-    Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    # Some fields cannot be sent to Bugzilla::Bug->create
-    foreach my $key (qw(login password token)) {
-        delete $params->{$key};
-    }
+  # Some fields cannot be sent to Bugzilla::Bug->create
+  foreach my $key (qw(login password token)) {
+    delete $params->{$key};
+  }
 
-    $params = Bugzilla::Bug::map_fields($params);
+  $params = Bugzilla::Bug::map_fields($params);
 
-    my $flags = delete $params->{flags};
+  my $flags = delete $params->{flags};
 
-    # We start a nested transaction in case flag setting fails
-    # we want the bug creation to roll back as well.
-    $dbh->bz_start_transaction();
+  # We start a nested transaction in case flag setting fails
+  # we want the bug creation to roll back as well.
+  $dbh->bz_start_transaction();
 
-    my $bug = Bugzilla::Bug->create($params);
+  my $bug = Bugzilla::Bug->create($params);
 
-    # Set bug flags
-    if ($flags) {
-        my ($flags, $new_flags) = extract_flags($flags, $bug);
-        $bug->set_flags($flags, $new_flags);
-        $bug->update($bug->creation_ts);
-    }
+  # Set bug flags
+  if ($flags) {
+    my ($flags, $new_flags) = extract_flags($flags, $bug);
+    $bug->set_flags($flags, $new_flags);
+    $bug->update($bug->creation_ts);
+  }
 
-    $dbh->bz_commit_transaction();
+  $dbh->bz_commit_transaction();
 
-    $bug->send_changes();
+  $bug->send_changes();
 
-    return { id => $self->type('int', $bug->bug_id) };
+  return {id => $self->type('int', $bug->bug_id)};
 }
 
 sub legal_values {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    Bugzilla->switch_to_shadow_db();
+  Bugzilla->switch_to_shadow_db();
 
-    defined $params->{field}
-        or ThrowCodeError('param_required', { param => 'field' });
+  defined $params->{field}
+    or ThrowCodeError('param_required', {param => 'field'});
 
-    my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}}
-                || $params->{field};
+  my $field = Bugzilla::Bug::FIELD_MAP->{$params->{field}} || $params->{field};
 
-    my @global_selects =
-        @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) };
+  my @global_selects = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})};
 
-    my $values;
-    if (grep($_->name eq $field, @global_selects)) {
-        # The field is a valid one.
-        trick_taint($field);
-        $values = get_legal_field_values($field);
-    }
-    elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) {
-        my $id = $params->{product_id};
-        defined $id || ThrowCodeError('param_required',
-            { function => 'Bug.legal_values', param => 'product_id' });
-        grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
-            || ThrowUserError('product_access_denied', { id => $id });
-
-        my $product = new Bugzilla::Product($id);
-        my @objects;
-        if ($field eq 'version') {
-            @objects = @{$product->versions};
-        }
-        elsif ($field eq 'target_milestone') {
-            @objects = @{$product->milestones};
-        }
-        elsif ($field eq 'component') {
-            @objects = @{$product->components};
-        }
+  my $values;
+  if (grep($_->name eq $field, @global_selects)) {
 
-        $values = [map { $_->name } @objects];
+    # The field is a valid one.
+    trick_taint($field);
+    $values = get_legal_field_values($field);
+  }
+  elsif (grep($_ eq $field, PRODUCT_SPECIFIC_FIELDS)) {
+    my $id = $params->{product_id};
+    defined $id
+      || ThrowCodeError('param_required',
+      {function => 'Bug.legal_values', param => 'product_id'});
+    grep($_->id eq $id, @{Bugzilla->user->get_accessible_products})
+      || ThrowUserError('product_access_denied', {id => $id});
+
+    my $product = new Bugzilla::Product($id);
+    my @objects;
+    if ($field eq 'version') {
+      @objects = @{$product->versions};
     }
-    else {
-        ThrowCodeError('invalid_field_name', { field => $params->{field} });
+    elsif ($field eq 'target_milestone') {
+      @objects = @{$product->milestones};
     }
-
-    my @result;
-    foreach my $val (@$values) {
-        push(@result, $self->type('string', $val));
+    elsif ($field eq 'component') {
+      @objects = @{$product->components};
     }
 
-    return { values => \@result };
+    $values = [map { $_->name } @objects];
+  }
+  else {
+    ThrowCodeError('invalid_field_name', {field => $params->{field}});
+  }
+
+  my @result;
+  foreach my $val (@$values) {
+    push(@result, $self->type('string', $val));
+  }
+
+  return {values => \@result};
 }
 
 sub add_attachment {
-    my ($self, $params) = validate(@_, 'ids');
-    my $dbh = Bugzilla->dbh;
+  my ($self, $params) = validate(@_, 'ids');
+  my $dbh = Bugzilla->dbh;
+
+  # BMO: Don't allow updating of bugs if disabled
+  if (Bugzilla->params->{disable_bug_updates}) {
+    ThrowErrorPage(
+      'bug/process/updates-disabled.html.tmpl',
+      'Bug updates are currently disabled.'
+    );
+  }
 
-    # BMO: Don't allow updating of bugs if disabled
-    if (Bugzilla->params->{disable_bug_updates}) {
-        ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
-            'Bug updates are currently disabled.');
-    }
+  Bugzilla->login(LOGIN_REQUIRED);
+  defined $params->{ids}  || ThrowCodeError('param_required', {param => 'ids'});
+  defined $params->{data} || ThrowCodeError('param_required', {param => 'data'});
 
-    Bugzilla->login(LOGIN_REQUIRED);
-    defined $params->{ids}
-        || ThrowCodeError('param_required', { param => 'ids' });
-    defined $params->{data}
-        || ThrowCodeError('param_required', { param => 'data' });
+  my @bugs = map { Bugzilla::Bug->check($_) } @{$params->{ids}};
+  foreach my $bug (@bugs) {
+    Bugzilla->user->can_edit_product($bug->product_id)
+      || ThrowUserError("product_edit_denied", {product => $bug->product});
+  }
+
+  my @created;
+  $dbh->bz_start_transaction();
+  my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+
+  my $flags = delete $params->{flags};
+
+  foreach my $bug (@bugs) {
+    my $attachment = Bugzilla::Attachment->create({
+      bug         => $bug,
+      creation_ts => $timestamp,
+      data        => $params->{data},
+      description => $params->{summary},
+      filename    => $params->{file_name},
+      mimetype    => $params->{content_type},
+      ispatch     => $params->{is_patch},
+      isprivate   => $params->{is_private},
+    });
 
-    my @bugs = map { Bugzilla::Bug->check($_) } @{ $params->{ids} };
-    foreach my $bug (@bugs) {
-        Bugzilla->user->can_edit_product($bug->product_id)
-          || ThrowUserError("product_edit_denied", {product => $bug->product});
+    if ($flags) {
+      my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment);
+      $attachment->set_flags($old_flags, $new_flags);
     }
 
-    my @created;
-    $dbh->bz_start_transaction();
-    my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-
-    my $flags = delete $params->{flags};
-
-    foreach my $bug (@bugs) {
-        my $attachment = Bugzilla::Attachment->create({
-            bug         => $bug,
-            creation_ts => $timestamp,
-            data        => $params->{data},
-            description => $params->{summary},
-            filename    => $params->{file_name},
-            mimetype    => $params->{content_type},
-            ispatch     => $params->{is_patch},
-            isprivate   => $params->{is_private},
-        });
-
-        if ($flags) {
-            my ($old_flags, $new_flags) = extract_flags($flags, $bug, $attachment);
-            $attachment->set_flags($old_flags, $new_flags);
-        }
-
-        $attachment->update($timestamp);
-        my $comment = $params->{comment} || '';
-        $attachment->bug->add_comment($comment,
-            { isprivate  => $attachment->isprivate,
-              type       => CMT_ATTACHMENT_CREATED,
-              extra_data => $attachment->id });
-        push(@created, $attachment);
-    }
-    $_->bug->update($timestamp) foreach @created;
-    $dbh->bz_commit_transaction();
+    $attachment->update($timestamp);
+    my $comment = $params->{comment} || '';
+    $attachment->bug->add_comment(
+      $comment,
+      {
+        isprivate  => $attachment->isprivate,
+        type       => CMT_ATTACHMENT_CREATED,
+        extra_data => $attachment->id
+      }
+    );
+    push(@created, $attachment);
+  }
+  $_->bug->update($timestamp) foreach @created;
+  $dbh->bz_commit_transaction();
 
-    $_->send_changes() foreach @bugs;
+  $_->send_changes() foreach @bugs;
 
-    my %attachments = map { $_->id => $self->_attachment_to_hash($_, $params) }
-                          @created;
+  my %attachments
+    = map { $_->id => $self->_attachment_to_hash($_, $params) } @created;
 
-    return { attachments => \%attachments };
+  return {attachments => \%attachments};
 }
 
 sub update_attachment {
-    my ($self, $params) = validate(@_, 'ids');
-
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    my $dbh = Bugzilla->dbh;
-
-    my $ids = delete $params->{ids};
-    defined $ids || ThrowCodeError('param_required', { param => 'ids' });
-
-    # Some fields cannot be sent to set_all
-    foreach my $key (qw(login password token)) {
-        delete $params->{$key};
-    }
-
-    $params = translate($params, ATTACHMENT_MAPPED_SETTERS);
-
-    # Get all the attachments, after verifying that they exist and are editable
-    my @attachments = ();
-    my %bugs = ();
-    foreach my $id (@$ids) {
-        my $attachment = Bugzilla::Attachment->new($id)
-          || ThrowUserError("invalid_attach_id", { attach_id => $id });
-        my $bug = $attachment->bug;
-        $attachment->_check_bug;
-
-        push @attachments, $attachment;
-        $bugs{$bug->id} = $bug;
+  my ($self, $params) = validate(@_, 'ids');
+
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $dbh  = Bugzilla->dbh;
+
+  my $ids = delete $params->{ids};
+  defined $ids || ThrowCodeError('param_required', {param => 'ids'});
+
+  # Some fields cannot be sent to set_all
+  foreach my $key (qw(login password token)) {
+    delete $params->{$key};
+  }
+
+  $params = translate($params, ATTACHMENT_MAPPED_SETTERS);
+
+  # Get all the attachments, after verifying that they exist and are editable
+  my @attachments = ();
+  my %bugs        = ();
+  foreach my $id (@$ids) {
+    my $attachment = Bugzilla::Attachment->new($id)
+      || ThrowUserError("invalid_attach_id", {attach_id => $id});
+    my $bug = $attachment->bug;
+    $attachment->_check_bug;
+
+    push @attachments, $attachment;
+    $bugs{$bug->id} = $bug;
+  }
+
+  my $flags   = delete $params->{flags};
+  my $comment = delete $params->{comment};
+
+  # Update the values
+  foreach my $attachment (@attachments) {
+    my ($update_flags, $new_flags)
+      = $flags ? extract_flags($flags, $attachment->bug, $attachment) : ([], []);
+    if ($attachment->validate_can_edit) {
+      $attachment->set_all($params);
+      $attachment->set_flags($update_flags, $new_flags) if $flags;
     }
-
-    my $flags = delete $params->{flags};
-    my $comment = delete $params->{comment};
-
-    # Update the values
-    foreach my $attachment (@attachments) {
-        my ($update_flags, $new_flags) = $flags
-            ? extract_flags($flags, $attachment->bug, $attachment)
-            : ([], []);
-        if ($attachment->validate_can_edit) {
-            $attachment->set_all($params);
-            $attachment->set_flags($update_flags, $new_flags) if $flags;
-        }
-        elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) {
-            # Requestees can set flags targetted to them, even if they cannot
-            # edit the attachment. Flag setters can edit their own flags too.
-            my %flag_list = map { $_->{id} => $_ } @$update_flags;
-            my $flag_objs = Bugzilla::Flag->new_from_list([ keys %flag_list ]);
-            my @editable_flags;
-            foreach my $flag_obj (@$flag_objs) {
-                if ($flag_obj->setter_id == $user->id
-                    || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
-                {
-                    push(@editable_flags, $flag_list{$flag_obj->id});
-                }
-            }
-            if (!scalar @editable_flags) {
-                ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id });
-            }
-            $attachment->set_flags(\@editable_flags, []);
-        }
-        else {
-            ThrowUserError("illegal_attachment_edit", { attach_id => $attachment->id });
+    elsif (scalar @$update_flags && !scalar(@$new_flags) && !scalar keys %$params) {
+
+      # Requestees can set flags targetted to them, even if they cannot
+      # edit the attachment. Flag setters can edit their own flags too.
+      my %flag_list = map { $_->{id} => $_ } @$update_flags;
+      my $flag_objs = Bugzilla::Flag->new_from_list([keys %flag_list]);
+      my @editable_flags;
+      foreach my $flag_obj (@$flag_objs) {
+        if ($flag_obj->setter_id == $user->id
+          || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
+        {
+          push(@editable_flags, $flag_list{$flag_obj->id});
         }
+      }
+      if (!scalar @editable_flags) {
+        ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id});
+      }
+      $attachment->set_flags(\@editable_flags, []);
     }
+    else {
+      ThrowUserError("illegal_attachment_edit", {attach_id => $attachment->id});
+    }
+  }
 
-    $dbh->bz_start_transaction();
+  $dbh->bz_start_transaction();
 
-    # Do the actual update and get information to return to user
-    my @result;
-    foreach my $attachment (@attachments) {
-        my $changes = $attachment->update();
-
-        if ($comment = trim($comment)) {
-            $attachment->bug->add_comment($comment,
-                { isprivate  => $attachment->isprivate,
-                  type       => CMT_ATTACHMENT_UPDATED,
-                  extra_data => $attachment->id });
-        }
+  # Do the actual update and get information to return to user
+  my @result;
+  foreach my $attachment (@attachments) {
+    my $changes = $attachment->update();
 
-        $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
+    if ($comment = trim($comment)) {
+      $attachment->bug->add_comment(
+        $comment,
+        {
+          isprivate  => $attachment->isprivate,
+          type       => CMT_ATTACHMENT_UPDATED,
+          extra_data => $attachment->id
+        }
+      );
+    }
 
-        my %hash = (
-            id               => $self->type('int', $attachment->id),
-            last_change_time => $self->type('dateTime', $attachment->modification_time),
-            changes          => {},
-        );
+    $changes = translate($changes, ATTACHMENT_MAPPED_RETURNS);
 
-        foreach my $field (keys %$changes) {
-            my $change = $changes->{$field};
+    my %hash = (
+      id               => $self->type('int',      $attachment->id),
+      last_change_time => $self->type('dateTime', $attachment->modification_time),
+      changes          => {},
+    );
 
-            # We normalize undef to an empty string, so that the API
-            # stays consistent for things like Deadline that can become
-            # empty.
-            $hash{changes}->{$field} = {
-                removed => $self->type('string', $change->[0] // ''),
-                added   => $self->type('string', $change->[1] // '')
-            };
-        }
+    foreach my $field (keys %$changes) {
+      my $change = $changes->{$field};
 
-        push(@result, \%hash);
+      # We normalize undef to an empty string, so that the API
+      # stays consistent for things like Deadline that can become
+      # empty.
+      $hash{changes}->{$field} = {
+        removed => $self->type('string', $change->[0] // ''),
+        added   => $self->type('string', $change->[1] // '')
+      };
     }
 
-    $dbh->bz_commit_transaction();
+    push(@result, \%hash);
+  }
 
-    # Email users about the change
-    foreach my $bug (values %bugs) {
-        $bug->update();
-        $bug->send_changes();
-    }
+  $dbh->bz_commit_transaction();
 
-    # Return the information to the user
-    return { attachments => \@result };
+  # Email users about the change
+  foreach my $bug (values %bugs) {
+    $bug->update();
+    $bug->send_changes();
+  }
+
+  # Return the information to the user
+  return {attachments => \@result};
 }
 
 sub add_comment {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    # BMO: Don't allow updating of bugs if disabled
-    if (Bugzilla->params->{disable_bug_updates}) {
-        ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
-            'Bug updates are currently disabled.');
-    }
+  # BMO: Don't allow updating of bugs if disabled
+  if (Bugzilla->params->{disable_bug_updates}) {
+    ThrowErrorPage(
+      'bug/process/updates-disabled.html.tmpl',
+      'Bug updates are currently disabled.'
+    );
+  }
 
-    #The user must login in order add a comment
-    Bugzilla->login(LOGIN_REQUIRED);
+  #The user must login in order add a comment
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    # Check parameters
-    defined $params->{id}
-        || ThrowCodeError('param_required', { param => 'id' });
-    my $comment = $params->{comment};
-    (defined $comment && trim($comment) ne '')
-        || ThrowCodeError('param_required', { param => 'comment' });
+  # Check parameters
+  defined $params->{id} || ThrowCodeError('param_required', {param => 'id'});
+  my $comment = $params->{comment};
+  (defined $comment && trim($comment) ne '')
+    || ThrowCodeError('param_required', {param => 'comment'});
 
-    my $bug = Bugzilla::Bug->check($params->{id});
+  my $bug = Bugzilla::Bug->check($params->{id});
 
-    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});
 
-    # Backwards-compatibility for versions before 3.6
-    if (defined $params->{private}) {
-        $params->{is_private} = delete $params->{private};
-    }
-    # Append comment
-    $bug->add_comment($comment, { isprivate => $params->{is_private},
-                                  work_time => $params->{work_time} });
+  # Backwards-compatibility for versions before 3.6
+  if (defined $params->{private}) {
+    $params->{is_private} = delete $params->{private};
+  }
 
-    # Add comment tags
-    $bug->set_all({ comment_tags => $params->{comment_tags} })
-        if defined $params->{comment_tags};
+  # Append comment
+  $bug->add_comment($comment,
+    {isprivate => $params->{is_private}, work_time => $params->{work_time}});
 
-    # Capture the call to bug->update (which creates the new comment) in
-    # a transaction so we're sure to get the correct comment_id.
+  # Add comment tags
+  $bug->set_all({comment_tags => $params->{comment_tags}})
+    if defined $params->{comment_tags};
 
-    my $dbh = Bugzilla->dbh;
-    $dbh->bz_start_transaction();
+  # Capture the call to bug->update (which creates the new comment) in
+  # a transaction so we're sure to get the correct comment_id.
 
-    $bug->update();
+  my $dbh = Bugzilla->dbh;
+  $dbh->bz_start_transaction();
 
-    my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id');
+  $bug->update();
 
-    $dbh->bz_commit_transaction();
+  my $new_comment_id = $dbh->bz_last_key('longdescs', 'comment_id');
 
-    # Send mail.
-    Bugzilla::BugMail::Send($bug->bug_id, { changer => Bugzilla->user });
+  $dbh->bz_commit_transaction();
 
-    return { id => $self->type('int', $new_comment_id) };
+  # Send mail.
+  Bugzilla::BugMail::Send($bug->bug_id, {changer => Bugzilla->user});
+
+  return {id => $self->type('int', $new_comment_id)};
 }
 
 sub update_see_also {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    # BMO: Don't allow updating of bugs if disabled
-    if (Bugzilla->params->{disable_bug_updates}) {
-        ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
-            'Bug updates are currently disabled.');
+  # BMO: Don't allow updating of bugs if disabled
+  if (Bugzilla->params->{disable_bug_updates}) {
+    ThrowErrorPage(
+      'bug/process/updates-disabled.html.tmpl',
+      'Bug updates are currently disabled.'
+    );
+  }
+
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+
+  # Check parameters
+  $params->{ids} || ThrowCodeError('param_required', {param => 'id'});
+  my ($add, $remove) = @$params{qw(add remove)};
+  ($add || $remove)
+    or ThrowCodeError('params_required', {params => ['add', 'remove']});
+
+  my @bugs;
+  foreach my $id (@{$params->{ids}}) {
+    my $bug = Bugzilla::Bug->check($id);
+    $user->can_edit_product($bug->product_id)
+      || ThrowUserError("product_edit_denied", {product => $bug->product});
+    push(@bugs, $bug);
+    if ($remove) {
+      $bug->remove_see_also($_) foreach @$remove;
     }
-
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-
-    # Check parameters
-    $params->{ids}
-        || ThrowCodeError('param_required', { param => 'id' });
-    my ($add, $remove) = @$params{qw(add remove)};
-    ($add || $remove)
-        or ThrowCodeError('params_required', { params => ['add', 'remove'] });
-
-    my @bugs;
-    foreach my $id (@{ $params->{ids} }) {
-        my $bug = Bugzilla::Bug->check($id);
-        $user->can_edit_product($bug->product_id)
-            || ThrowUserError("product_edit_denied",
-                              { product => $bug->product });
-        push(@bugs, $bug);
-        if ($remove) {
-            $bug->remove_see_also($_) foreach @$remove;
-        }
-        if ($add) {
-            $bug->add_see_also($_) foreach @$add;
-        }
+    if ($add) {
+      $bug->add_see_also($_) foreach @$add;
     }
-
-    my %changes;
-    foreach my $bug (@bugs) {
-        my $change = $bug->update();
-        if (my $see_also = $change->{see_also}) {
-            $changes{$bug->id}->{see_also} = {
-                removed => [split(', ', $see_also->[0])],
-                added   => [split(', ', $see_also->[1])],
-            };
-        }
-        else {
-            # We still want a changes entry, for API consistency.
-            $changes{$bug->id}->{see_also} = { added => [], removed => [] };
-        }
-
-        Bugzilla::BugMail::Send($bug->id, { changer => $user });
+  }
+
+  my %changes;
+  foreach my $bug (@bugs) {
+    my $change = $bug->update();
+    if (my $see_also = $change->{see_also}) {
+      $changes{$bug->id}->{see_also} = {
+        removed => [split(', ', $see_also->[0])],
+        added   => [split(', ', $see_also->[1])],
+      };
     }
+    else {
+      # We still want a changes entry, for API consistency.
+      $changes{$bug->id}->{see_also} = {added => [], removed => []};
+    }
+
+    Bugzilla::BugMail::Send($bug->id, {changer => $user});
+  }
 
-    return { changes => \%changes };
+  return {changes => \%changes};
 }
 
 sub attachments {
-    my ($self, $params) = validate(@_, 'ids', 'attachment_ids');
+  my ($self, $params) = validate(@_, 'ids', 'attachment_ids');
 
-    Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
+  Bugzilla->switch_to_shadow_db() unless Bugzilla->user->id;
 
-    if (!(defined $params->{ids}
-          or defined $params->{attachment_ids}))
-    {
-        ThrowCodeError('param_required',
-                       { function => 'Bug.attachments',
-                         params   => ['ids', 'attachment_ids'] });
-    }
+  if (!(defined $params->{ids} or defined $params->{attachment_ids})) {
+    ThrowCodeError('param_required',
+      {function => 'Bug.attachments', params => ['ids', 'attachment_ids']});
+  }
 
-    my $ids = $params->{ids} || [];
-    my $attach_ids = $params->{attachment_ids} || [];
+  my $ids        = $params->{ids}            || [];
+  my $attach_ids = $params->{attachment_ids} || [];
 
-    my %bugs;
-    foreach my $bug_id (@$ids) {
-        my $bug = Bugzilla::Bug->check($bug_id);
-        $bugs{$bug->id} = [];
-        foreach my $attach (@{$bug->attachments}) {
-            push @{$bugs{$bug->id}},
-                $self->_attachment_to_hash($attach, $params);
-        }
+  my %bugs;
+  foreach my $bug_id (@$ids) {
+    my $bug = Bugzilla::Bug->check($bug_id);
+    $bugs{$bug->id} = [];
+    foreach my $attach (@{$bug->attachments}) {
+      push @{$bugs{$bug->id}}, $self->_attachment_to_hash($attach, $params);
     }
-
-    my %attachments;
-    my @log_attachments;
-    foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) {
-        Bugzilla::Bug->check($attach->bug_id);
-        if ($attach->isprivate && !Bugzilla->user->is_insider) {
-            ThrowUserError('auth_failure', {action    => 'access',
-                                            object    => 'attachment',
-                                            attach_id => $attach->id});
-        }
-        push @log_attachments, $attach;
-
-        $attachments{$attach->id} =
-            $self->_attachment_to_hash($attach, $params);
+  }
+
+  my %attachments;
+  my @log_attachments;
+  foreach my $attach (@{Bugzilla::Attachment->new_from_list($attach_ids)}) {
+    Bugzilla::Bug->check($attach->bug_id);
+    if ($attach->isprivate && !Bugzilla->user->is_insider) {
+      ThrowUserError('auth_failure',
+        {action => 'access', object => 'attachment', attach_id => $attach->id});
     }
+    push @log_attachments, $attach;
 
-    if (Bugzilla->user->id) {
-        foreach my $attachment (@log_attachments) {
-            Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get");
-        }
+    $attachments{$attach->id} = $self->_attachment_to_hash($attach, $params);
+  }
+
+  if (Bugzilla->user->id) {
+    foreach my $attachment (@log_attachments) {
+      Bugzilla->log_user_request($attachment->bug_id, $attachment->id,
+        "attachment-get");
     }
+  }
 
-    return { bugs => \%bugs, attachments => \%attachments };
+  return {bugs => \%bugs, attachments => \%attachments};
 }
 
 sub flag_types {
-    my ($self, $params) = @_;
-    my $dbh  = Bugzilla->switch_to_shadow_db();
-    my $user = Bugzilla->user;
-
-    defined $params->{product}
-        || ThrowCodeError('param_required',
-                          { function => 'Bug.flag_types',
-                            param   => 'product' });
-
-    my $product   = delete $params->{product};
-    my $component = delete $params->{component};
-
-    $product = Bugzilla::Product->check({ name => $product, cache => 1 });
-    $component = Bugzilla::Component->check(
-        { name => $component, product => $product, cache => 1 }) if $component;
-
-    my $flag_params = { product_id => $product->id };
-    $flag_params->{component_id} = $component->id if $component;
-    my $matched_flag_types = Bugzilla::FlagType::match($flag_params);
-
-    my $flag_types = { bug => [], attachment => [] };
-    foreach my $flag_type (@$matched_flag_types) {
-        push(@{ $flag_types->{bug} }, $self->_flagtype_to_hash($flag_type, $product))
-            if $flag_type->target_type eq 'bug';
-        push(@{ $flag_types->{attachment} }, $self->_flagtype_to_hash($flag_type, $product))
-            if $flag_type->target_type eq 'attachment';
-    }
-
-    return $flag_types;
+  my ($self, $params) = @_;
+  my $dbh  = Bugzilla->switch_to_shadow_db();
+  my $user = Bugzilla->user;
+
+  defined $params->{product}
+    || ThrowCodeError('param_required',
+    {function => 'Bug.flag_types', param => 'product'});
+
+  my $product   = delete $params->{product};
+  my $component = delete $params->{component};
+
+  $product = Bugzilla::Product->check({name => $product, cache => 1});
+  $component
+    = Bugzilla::Component->check(
+    {name => $component, product => $product, cache => 1})
+    if $component;
+
+  my $flag_params = {product_id => $product->id};
+  $flag_params->{component_id} = $component->id if $component;
+  my $matched_flag_types = Bugzilla::FlagType::match($flag_params);
+
+  my $flag_types = {bug => [], attachment => []};
+  foreach my $flag_type (@$matched_flag_types) {
+    push(@{$flag_types->{bug}}, $self->_flagtype_to_hash($flag_type, $product))
+      if $flag_type->target_type eq 'bug';
+    push(
+      @{$flag_types->{attachment}},
+      $self->_flagtype_to_hash($flag_type, $product)
+    ) if $flag_type->target_type eq 'attachment';
+  }
+
+  return $flag_types;
 }
 
 sub update_comment_tags {
-    my ($self, $params) = @_;
-
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->params->{'comment_taggers_group'}
-        || ThrowUserError("comment_tag_disabled");
-    $user->can_tag_comments
-        || ThrowUserError("auth_failure",
-                          { group  => Bugzilla->params->{'comment_taggers_group'},
-                            action => "update",
-                            object => "comment_tags" });
-
-    my $comment_id  = $params->{comment_id}
-        // ThrowCodeError('param_required',
-                          { function => 'Bug.update_comment_tags',
-                            param    => 'comment_id' });
-
-    my $comment = Bugzilla::Comment->new($comment_id)
-        || return [];
-    $comment->bug->check_is_visible();
-    if ($comment->is_private && !$user->is_insider) {
-        ThrowUserError('comment_is_private', { id => $comment_id });
-    }
+  my ($self, $params) = @_;
 
-    my $dbh = Bugzilla->dbh;
-    $dbh->bz_start_transaction();
-    foreach my $tag (@{ $params->{add} || [] }) {
-        $comment->add_tag($tag) if defined $tag;
-    }
-    foreach my $tag (@{ $params->{remove} || [] }) {
-        $comment->remove_tag($tag) if defined $tag;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->params->{'comment_taggers_group'}
+    || ThrowUserError("comment_tag_disabled");
+  $user->can_tag_comments || ThrowUserError(
+    "auth_failure",
+    {
+      group  => Bugzilla->params->{'comment_taggers_group'},
+      action => "update",
+      object => "comment_tags"
     }
-    $comment->update();
-    $dbh->bz_commit_transaction();
-
-    return $comment->tags;
+  );
+
+  my $comment_id = $params->{comment_id} // ThrowCodeError('param_required',
+    {function => 'Bug.update_comment_tags', param => 'comment_id'});
+
+  my $comment = Bugzilla::Comment->new($comment_id) || return [];
+  $comment->bug->check_is_visible();
+  if ($comment->is_private && !$user->is_insider) {
+    ThrowUserError('comment_is_private', {id => $comment_id});
+  }
+
+  my $dbh = Bugzilla->dbh;
+  $dbh->bz_start_transaction();
+  foreach my $tag (@{$params->{add} || []}) {
+    $comment->add_tag($tag) if defined $tag;
+  }
+  foreach my $tag (@{$params->{remove} || []}) {
+    $comment->remove_tag($tag) if defined $tag;
+  }
+  $comment->update();
+  $dbh->bz_commit_transaction();
+
+  return $comment->tags;
 }
 
 sub search_comment_tags {
-    my ($self, $params) = @_;
-
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->params->{'comment_taggers_group'}
-        || ThrowUserError("comment_tag_disabled");
-    Bugzilla->user->can_tag_comments
-        || ThrowUserError("auth_failure", { group  => Bugzilla->params->{'comment_taggers_group'},
-                                            action => "search",
-                                            object => "comment_tags"});
-
-    my $query = $params->{query};
-    $query
-        // ThrowCodeError('param_required', { param => 'query' });
-    my $limit = $params->{limit} || 7;
-    detaint_natural($limit)
-        || ThrowCodeError('param_must_be_numeric', { param    => 'limit',
-                                                     function => 'Bug.search_comment_tags' });
-
-
-    my $tags = Bugzilla::Comment::TagWeights->match({
-        WHERE => {
-            'tag LIKE ?' => "\%$query\%",
-        },
-        LIMIT => $limit,
+  my ($self, $params) = @_;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->params->{'comment_taggers_group'}
+    || ThrowUserError("comment_tag_disabled");
+  Bugzilla->user->can_tag_comments || ThrowUserError(
+    "auth_failure",
+    {
+      group  => Bugzilla->params->{'comment_taggers_group'},
+      action => "search",
+      object => "comment_tags"
+    }
+  );
+
+  my $query = $params->{query};
+  $query // ThrowCodeError('param_required', {param => 'query'});
+  my $limit = $params->{limit} || 7;
+  detaint_natural($limit)
+    || ThrowCodeError('param_must_be_numeric',
+    {param => 'limit', function => 'Bug.search_comment_tags'});
+
+
+  my $tags
+    = Bugzilla::Comment::TagWeights->match({
+    WHERE => {'tag LIKE ?' => "\%$query\%",}, LIMIT => $limit,
     });
-    return [ map { $_->tag } @$tags ];
+  return [map { $_->tag } @$tags];
 }
 
 ##############################
@@ -1350,304 +1373,320 @@ sub search_comment_tags {
 # return them directly.
 
 sub _bug_to_hash {
-    my ($self, $bug, $params) = @_;
-
-    # All the basic bug attributes are here, in alphabetical order.
-    # A bug attribute is "basic" if it doesn't require an additional
-    # database call to get the info.
-    my %item = %{ filter $params, {
-        alias            => $self->type('string', $bug->alias),
-        id               => $self->type('int', $bug->bug_id),
-        is_confirmed     => $self->type('boolean', $bug->everconfirmed),
-        op_sys           => $self->type('string', $bug->op_sys),
-        platform         => $self->type('string', $bug->rep_platform),
-        priority         => $self->type('string', $bug->priority),
-        resolution       => $self->type('string', $bug->resolution),
-        severity         => $self->type('string', $bug->bug_severity),
-        status           => $self->type('string', $bug->bug_status),
-        summary          => $self->type('string', $bug->short_desc),
-        target_milestone => $self->type('string', $bug->target_milestone),
-        url              => $self->type('string', $bug->bug_file_loc),
-        version          => $self->type('string', $bug->version),
-        whiteboard       => $self->type('string', $bug->status_whiteboard),
-    } };
-
-    state $voting_enabled //= $bug->can('votes') ? 1 : 0;
-    if ($voting_enabled && filter_wants $params, 'votes') {
-        $item{votes} = $self->type('int', $bug->votes);
-    }
+  my ($self, $bug, $params) = @_;
 
-    # First we handle any fields that require extra work (such as date parsing
-    # or SQL calls).
-    if (filter_wants $params, 'assigned_to') {
-        $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login);
-        $item{'assigned_to_detail'} = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
-    }
-    if (filter_wants $params, 'blocks') {
-        my @blocks = map { $self->type('int', $_) } @{ $bug->blocked };
-        $item{'blocks'} = \@blocks;
-    }
-    if (filter_wants $params, 'classification') {
-        $item{classification} = $self->type('string', $bug->classification);
-    }
-    if (filter_wants $params, 'component') {
-        $item{component} = $self->type('string', $bug->component);
-    }
-    if (filter_wants $params, 'cc') {
-        my @cc = map { $self->type('email', $_) } @{ $bug->cc || [] };
-        $item{'cc'} = \@cc;
-        $item{'cc_detail'} = [ map { $self->_user_to_hash($_, $params, undef, 'cc') } @{ $bug->cc_users } ];
-    }
-    if (filter_wants $params, 'creation_time') {
-        $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts);
-    }
-    if (filter_wants $params, 'creator') {
-        $item{'creator'} = $self->type('email', $bug->reporter->login);
-        $item{'creator_detail'} = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
-    }
-    if (filter_wants $params, 'depends_on') {
-        my @depends_on = map { $self->type('int', $_) } @{ $bug->dependson };
-        $item{'depends_on'} = \@depends_on;
-    }
-    if (filter_wants $params, 'dupe_of') {
-        $item{'dupe_of'} = $self->type('int', $bug->dup_id);
-    }
-    if (filter_wants $params, 'duplicates') {
-        $item{'duplicates'} = [ map { $self->type('int', $_->id) } @{ $bug->duplicates } ];
-    }
-    if (filter_wants $params, 'groups') {
-        my @groups = map { $self->type('string', $_->name) }
-                     @{ $bug->groups_in };
-        $item{'groups'} = \@groups;
-    }
-    if (filter_wants $params, 'is_open') {
-        $item{'is_open'} = $self->type('boolean', $bug->status->is_open);
-    }
-    if (filter_wants $params, 'keywords') {
-        my @keywords = map { $self->type('string', $_->name) }
-                       @{ $bug->keyword_objects };
-        $item{'keywords'} = \@keywords;
-    }
-    if (filter_wants $params, 'last_change_time') {
-        $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts);
+  # All the basic bug attributes are here, in alphabetical order.
+  # A bug attribute is "basic" if it doesn't require an additional
+  # database call to get the info.
+  my %item = %{filter $params,
+    {
+      alias            => $self->type('string',  $bug->alias),
+      id               => $self->type('int',     $bug->bug_id),
+      is_confirmed     => $self->type('boolean', $bug->everconfirmed),
+      op_sys           => $self->type('string',  $bug->op_sys),
+      platform         => $self->type('string',  $bug->rep_platform),
+      priority         => $self->type('string',  $bug->priority),
+      resolution       => $self->type('string',  $bug->resolution),
+      severity         => $self->type('string',  $bug->bug_severity),
+      status           => $self->type('string',  $bug->bug_status),
+      summary          => $self->type('string',  $bug->short_desc),
+      target_milestone => $self->type('string',  $bug->target_milestone),
+      url              => $self->type('string',  $bug->bug_file_loc),
+      version          => $self->type('string',  $bug->version),
+      whiteboard       => $self->type('string',  $bug->status_whiteboard),
     }
-    if (filter_wants $params, 'product') {
-        $item{product} = $self->type('string', $bug->product);
+  };
+
+  state $voting_enabled //= $bug->can('votes') ? 1 : 0;
+  if ($voting_enabled && filter_wants $params, 'votes') {
+    $item{votes} = $self->type('int', $bug->votes);
+  }
+
+  # First we handle any fields that require extra work (such as date parsing
+  # or SQL calls).
+  if (filter_wants $params, 'assigned_to') {
+    $item{'assigned_to'} = $self->type('email', $bug->assigned_to->login);
+    $item{'assigned_to_detail'}
+      = $self->_user_to_hash($bug->assigned_to, $params, undef, 'assigned_to');
+  }
+  if (filter_wants $params, 'blocks') {
+    my @blocks = map { $self->type('int', $_) } @{$bug->blocked};
+    $item{'blocks'} = \@blocks;
+  }
+  if (filter_wants $params, 'classification') {
+    $item{classification} = $self->type('string', $bug->classification);
+  }
+  if (filter_wants $params, 'component') {
+    $item{component} = $self->type('string', $bug->component);
+  }
+  if (filter_wants $params, 'cc') {
+    my @cc = map { $self->type('email', $_) } @{$bug->cc || []};
+    $item{'cc'} = \@cc;
+    $item{'cc_detail'}
+      = [map { $self->_user_to_hash($_, $params, undef, 'cc') } @{$bug->cc_users}];
+  }
+  if (filter_wants $params, 'creation_time') {
+    $item{'creation_time'} = $self->type('dateTime', $bug->creation_ts);
+  }
+  if (filter_wants $params, 'creator') {
+    $item{'creator'} = $self->type('email', $bug->reporter->login);
+    $item{'creator_detail'}
+      = $self->_user_to_hash($bug->reporter, $params, undef, 'creator');
+  }
+  if (filter_wants $params, 'depends_on') {
+    my @depends_on = map { $self->type('int', $_) } @{$bug->dependson};
+    $item{'depends_on'} = \@depends_on;
+  }
+  if (filter_wants $params, 'dupe_of') {
+    $item{'dupe_of'} = $self->type('int', $bug->dup_id);
+  }
+  if (filter_wants $params, 'duplicates') {
+    $item{'duplicates'} = [map { $self->type('int', $_->id) } @{$bug->duplicates}];
+  }
+  if (filter_wants $params, 'groups') {
+    my @groups = map { $self->type('string', $_->name) } @{$bug->groups_in};
+    $item{'groups'} = \@groups;
+  }
+  if (filter_wants $params, 'is_open') {
+    $item{'is_open'} = $self->type('boolean', $bug->status->is_open);
+  }
+  if (filter_wants $params, 'keywords') {
+    my @keywords = map { $self->type('string', $_->name) } @{$bug->keyword_objects};
+    $item{'keywords'} = \@keywords;
+  }
+  if (filter_wants $params, 'last_change_time') {
+    $item{'last_change_time'} = $self->type('dateTime', $bug->delta_ts);
+  }
+  if (filter_wants $params, 'product') {
+    $item{product} = $self->type('string', $bug->product);
+  }
+  if (filter_wants $params, 'qa_contact') {
+    my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
+    $item{'qa_contact'} = $self->type('email', $qa_login);
+    if ($bug->qa_contact) {
+      $item{'qa_contact_detail'}
+        = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
     }
-    if (filter_wants $params, 'qa_contact') {
-        my $qa_login = $bug->qa_contact ? $bug->qa_contact->login : '';
-        $item{'qa_contact'} = $self->type('email', $qa_login);
-        if ($bug->qa_contact) {
-            $item{'qa_contact_detail'} = $self->_user_to_hash($bug->qa_contact, $params, undef, 'qa_contact');
-        }
+  }
+  if (filter_wants $params, 'see_also') {
+    my @see_also = map { $self->type('string', $_->name) } @{$bug->see_also};
+    $item{'see_also'} = \@see_also;
+  }
+  if (filter_wants $params, 'flags') {
+    $item{'flags'} = [map { $self->_flag_to_hash($_) } @{$bug->flags}];
+  }
+
+  # And now custom fields
+  my @custom_fields = Bugzilla->active_custom_fields({
+    product   => $bug->product_obj,
+    component => $bug->component_obj,
+    bug_id    => $bug->id
+  });
+  foreach my $field (@custom_fields) {
+    my $name = $field->name;
+    next if !filter_wants($params, $name, ['default', 'custom']);
+    if ($field->type == FIELD_TYPE_BUG_ID) {
+      $item{$name} = $self->type('int', $bug->$name);
     }
-    if (filter_wants $params, 'see_also') {
-        my @see_also = map { $self->type('string', $_->name) }
-                       @{ $bug->see_also };
-        $item{'see_also'} = \@see_also;
+    elsif ($field->type == FIELD_TYPE_DATETIME || $field->type == FIELD_TYPE_DATE) {
+      my $value = $bug->$name;
+      $item{$name} = defined($value) ? $self->type('dateTime', $value) : undef;
     }
-    if (filter_wants $params, 'flags') {
-        $item{'flags'} = [ map { $self->_flag_to_hash($_) } @{$bug->flags} ];
+    elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
+      my @values = map { $self->type('string', $_) } @{$bug->$name};
+      $item{$name} = \@values;
     }
-
-    # And now custom fields
-    my @custom_fields = Bugzilla->active_custom_fields({
-        product => $bug->product_obj, component => $bug->component_obj, bug_id => $bug->id });
-    foreach my $field (@custom_fields) {
-        my $name = $field->name;
-        next if !filter_wants($params, $name, ['default', 'custom']);
-        if ($field->type == FIELD_TYPE_BUG_ID) {
-            $item{$name} = $self->type('int', $bug->$name);
-        }
-        elsif ($field->type == FIELD_TYPE_DATETIME
-               || $field->type == FIELD_TYPE_DATE)
-        {
-            my $value = $bug->$name;
-            $item{$name} = defined($value) ? $self->type('dateTime', $value) : undef;
-        }
-        elsif ($field->type == FIELD_TYPE_MULTI_SELECT) {
-            my @values = map { $self->type('string', $_) } @{ $bug->$name };
-            $item{$name} = \@values;
-        }
-        else {
-            $item{$name} = $self->type('string', $bug->$name);
-        }
-    }
-
-    # Timetracking fields are only sent if the user can see them.
-    if (Bugzilla->user->is_timetracker) {
-        if (filter_wants $params, 'estimated_time') {
-            $item{'estimated_time'} = $self->type('double', $bug->estimated_time);
-        }
-        if (filter_wants $params, 'remaining_time') {
-            $item{'remaining_time'} = $self->type('double', $bug->remaining_time);
-        }
-        if (filter_wants $params, 'deadline') {
-            # No need to format $bug->deadline specially, because Bugzilla::Bug
-            # already does it for us.
-            $item{'deadline'} = $self->type('string', $bug->deadline);
-        }
-        if (filter_wants $params, 'actual_time') {
-            $item{'actual_time'} = $self->type('double', $bug->actual_time);
-        }
+    else {
+      $item{$name} = $self->type('string', $bug->$name);
     }
+  }
 
-    # The "accessible" bits go here because they have long names and it
-    # makes the code look nicer to separate them out.
-    if (filter_wants $params, 'is_cc_accessible') {
-        $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible);
+  # Timetracking fields are only sent if the user can see them.
+  if (Bugzilla->user->is_timetracker) {
+    if (filter_wants $params, 'estimated_time') {
+      $item{'estimated_time'} = $self->type('double', $bug->estimated_time);
     }
-    if (filter_wants $params, 'is_creator_accessible') {
-        $item{'is_creator_accessible'} = $self->type('boolean', $bug->reporter_accessible);
+    if (filter_wants $params, 'remaining_time') {
+      $item{'remaining_time'} = $self->type('double', $bug->remaining_time);
     }
+    if (filter_wants $params, 'deadline') {
 
-    # BMO - support for special mentors field
-    if (filter_wants $params, 'mentors') {
-        $item{'mentors'}
-          = [ map { $self->type('email', $_->login) } @{ $bug->mentors || [] } ];
-        $item{'mentors_detail'}
-          = [ map { $self->_user_to_hash($_, $params, undef, 'mentors') } @{ $bug->mentors } ];
+      # No need to format $bug->deadline specially, because Bugzilla::Bug
+      # already does it for us.
+      $item{'deadline'} = $self->type('string', $bug->deadline);
     }
-
-    if (filter_wants $params, 'comment_count') {
-        $item{'comment_count'} = $self->type('int', $bug->comment_count);
+    if (filter_wants $params, 'actual_time') {
+      $item{'actual_time'} = $self->type('double', $bug->actual_time);
     }
-
-    return \%item;
+  }
+
+  # The "accessible" bits go here because they have long names and it
+  # makes the code look nicer to separate them out.
+  if (filter_wants $params, 'is_cc_accessible') {
+    $item{'is_cc_accessible'} = $self->type('boolean', $bug->cclist_accessible);
+  }
+  if (filter_wants $params, 'is_creator_accessible') {
+    $item{'is_creator_accessible'}
+      = $self->type('boolean', $bug->reporter_accessible);
+  }
+
+  # BMO - support for special mentors field
+  if (filter_wants $params, 'mentors') {
+    $item{'mentors'}
+      = [map { $self->type('email', $_->login) } @{$bug->mentors || []}];
+    $item{'mentors_detail'}
+      = [map { $self->_user_to_hash($_, $params, undef, 'mentors') }
+        @{$bug->mentors}];
+  }
+
+  if (filter_wants $params, 'comment_count') {
+    $item{'comment_count'} = $self->type('int', $bug->comment_count);
+  }
+
+  return \%item;
 }
 
 sub _user_to_hash {
-    my ($self, $user, $filters, $types, $prefix) = @_;
-    my $item = filter $filters, {
-        id        => $self->type('int', $user->id),
-        real_name => $self->type('string', $user->name),
-        name      => $self->type('email', $user->login),
-        email     => $self->type('email', $user->email),
-    }, $types, $prefix;
-    return $item;
+  my ($self, $user, $filters, $types, $prefix) = @_;
+  my $item = filter $filters,
+    {
+    id        => $self->type('int',    $user->id),
+    real_name => $self->type('string', $user->name),
+    name      => $self->type('email',  $user->login),
+    email     => $self->type('email',  $user->email),
+    },
+    $types, $prefix;
+  return $item;
 }
 
 sub _attachment_to_hash {
-    my ($self, $attach, $filters, $types, $prefix) = @_;
-
-    my $item = filter $filters, {
-        creation_time    => $self->type('dateTime', $attach->attached),
-        last_change_time => $self->type('dateTime', $attach->modification_time),
-        id               => $self->type('int', $attach->id),
-        bug_id           => $self->type('int', $attach->bug_id),
-        file_name        => $self->type('string', $attach->filename),
-        summary          => $self->type('string', $attach->description),
-        description      => $self->type('string', $attach->description),
-        content_type     => $self->type('string', $attach->contenttype),
-        is_private       => $self->type('int', $attach->isprivate),
-        is_obsolete      => $self->type('int', $attach->isobsolete),
-        is_patch         => $self->type('int', $attach->ispatch),
-    }, $types, $prefix;
-
-    # creator/attacher require an extra lookup, so we only send them if
-    # the filter wants them.
-    foreach my $field (qw(creator attacher)) {
-        if (filter_wants $filters, $field, $types, $prefix) {
-            $item->{$field} = $self->type('email', $attach->attacher->login);
-        }
-    }
+  my ($self, $attach, $filters, $types, $prefix) = @_;
 
-    if (filter_wants $filters, 'data', $types, $prefix) {
-        $item->{'data'} = $self->type('base64', $attach->data);
+  my $item = filter $filters,
+    {
+    creation_time    => $self->type('dateTime', $attach->attached),
+    last_change_time => $self->type('dateTime', $attach->modification_time),
+    id               => $self->type('int',      $attach->id),
+    bug_id           => $self->type('int',      $attach->bug_id),
+    file_name        => $self->type('string',   $attach->filename),
+    summary          => $self->type('string',   $attach->description),
+    description      => $self->type('string',   $attach->description),
+    content_type     => $self->type('string',   $attach->contenttype),
+    is_private       => $self->type('int',      $attach->isprivate),
+    is_obsolete      => $self->type('int',      $attach->isobsolete),
+    is_patch         => $self->type('int',      $attach->ispatch),
+    },
+    $types, $prefix;
+
+  # creator/attacher require an extra lookup, so we only send them if
+  # the filter wants them.
+  foreach my $field (qw(creator attacher)) {
+    if (filter_wants $filters, $field, $types, $prefix) {
+      $item->{$field} = $self->type('email', $attach->attacher->login);
     }
+  }
 
-    if (filter_wants $filters, 'size', $types, $prefix) {
-        $item->{'size'} = $self->type('int', $attach->datasize);
-    }
+  if (filter_wants $filters, 'data', $types, $prefix) {
+    $item->{'data'} = $self->type('base64', $attach->data);
+  }
 
-    if (filter_wants $filters, 'flags', $types, $prefix) {
-        $item->{'flags'} = [ map { $self->_flag_to_hash($_) } @{$attach->flags} ];
-    }
+  if (filter_wants $filters, 'size', $types, $prefix) {
+    $item->{'size'} = $self->type('int', $attach->datasize);
+  }
+
+  if (filter_wants $filters, 'flags', $types, $prefix) {
+    $item->{'flags'} = [map { $self->_flag_to_hash($_) } @{$attach->flags}];
+  }
 
-    return $item;
+  return $item;
 }
 
 sub _flag_to_hash {
-    my ($self, $flag) = @_;
-
-    my $item = {
-        id                => $self->type('int', $flag->id),
-        name              => $self->type('string', $flag->name),
-        type_id           => $self->type('int', $flag->type_id),
-        creation_date     => $self->type('dateTime', $flag->creation_date),
-        modification_date => $self->type('dateTime', $flag->modification_date),
-        status            => $self->type('string', $flag->status)
-    };
-
-    foreach my $field (qw(setter requestee)) {
-        my $field_id = $field . "_id";
-        $item->{$field} = $self->type('email', $flag->$field->login)
-            if $flag->$field_id;
-    }
-
-    return $item;
+  my ($self, $flag) = @_;
+
+  my $item = {
+    id                => $self->type('int',      $flag->id),
+    name              => $self->type('string',   $flag->name),
+    type_id           => $self->type('int',      $flag->type_id),
+    creation_date     => $self->type('dateTime', $flag->creation_date),
+    modification_date => $self->type('dateTime', $flag->modification_date),
+    status            => $self->type('string',   $flag->status)
+  };
+
+  foreach my $field (qw(setter requestee)) {
+    my $field_id = $field . "_id";
+    $item->{$field} = $self->type('email', $flag->$field->login)
+      if $flag->$field_id;
+  }
+
+  return $item;
 }
 
 sub _flagtype_to_hash {
-    my ($self, $flagtype, $product) = @_;
-    my $user = Bugzilla->user;
-
-    my @values = ('X');
-    push(@values, '?') if ($flagtype->is_requestable && $user->can_request_flag($flagtype));
-    push(@values, '+', '-') if $user->can_set_flag($flagtype);
-
-    my $item = {
-        id          => $self->type('int'    , $flagtype->id),
-        name        => $self->type('string' , $flagtype->name),
-        description => $self->type('string' , $flagtype->description),
-        type        => $self->type('string' , $flagtype->target_type),
-        values      => \@values,
-        is_active   => $self->type('boolean', $flagtype->is_active),
-        is_requesteeble  => $self->type('boolean', $flagtype->is_requesteeble),
-        is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable)
-    };
-
-    if ($product) {
-        my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id);
-        my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id);
-        # if we have both inclusions and exclusions, the exclusions are redundant
-        $exclusions = [] if @$inclusions && @$exclusions;
-        # no need to return anything if there's just "any component"
-        $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne '';
-        $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne '';
-    }
-
-    return $item;
+  my ($self, $flagtype, $product) = @_;
+  my $user = Bugzilla->user;
+
+  my @values = ('X');
+  push(@values, '?')
+    if ($flagtype->is_requestable && $user->can_request_flag($flagtype));
+  push(@values, '+', '-') if $user->can_set_flag($flagtype);
+
+  my $item = {
+    id               => $self->type('int',     $flagtype->id),
+    name             => $self->type('string',  $flagtype->name),
+    description      => $self->type('string',  $flagtype->description),
+    type             => $self->type('string',  $flagtype->target_type),
+    values           => \@values,
+    is_active        => $self->type('boolean', $flagtype->is_active),
+    is_requesteeble  => $self->type('boolean', $flagtype->is_requesteeble),
+    is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable)
+  };
+
+  if ($product) {
+    my $inclusions
+      = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $product->id);
+    my $exclusions
+      = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $product->id);
+
+    # if we have both inclusions and exclusions, the exclusions are redundant
+    $exclusions = [] if @$inclusions && @$exclusions;
+
+    # no need to return anything if there's just "any component"
+    $item->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne '';
+    $item->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne '';
+  }
+
+  return $item;
 }
 
 sub _flagtype_clusions_to_hash {
-    my ($self, $clusions, $product_id) = @_;
-    my $result = [];
-    foreach my $key (keys %$clusions) {
-        my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2);
-        if ($prod_id == 0 || $prod_id == $product_id) {
-            if ($comp_id) {
-                my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 });
-                push @$result, $component->name;
-            }
-            else {
-                return [ '' ];
-            }
-        }
+  my ($self, $clusions, $product_id) = @_;
+  my $result = [];
+  foreach my $key (keys %$clusions) {
+    my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2);
+    if ($prod_id == 0 || $prod_id == $product_id) {
+      if ($comp_id) {
+        my $component = Bugzilla::Component->new({id => $comp_id, cache => 1});
+        push @$result, $component->name;
+      }
+      else {
+        return [''];
+      }
     }
-    return $result;
+  }
+  return $result;
 }
 
 sub _add_update_tokens {
-    my ($self, $params, $bugs, $hashes) = @_;
+  my ($self, $params, $bugs, $hashes) = @_;
 
-    return if !Bugzilla->user->id;
-    return if !filter_wants($params, 'update_token');
+  return if !Bugzilla->user->id;
+  return if !filter_wants($params, 'update_token');
 
-    for(my $i = 0; $i < @$bugs; $i++) {
-        my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]);
-        $hashes->[$i]->{'update_token'} = $self->type('string', $token);
-    }
+  for (my $i = 0; $i < @$bugs; $i++) {
+    my $token = issue_hash_token([$bugs->[$i]->id, $bugs->[$i]->delta_ts]);
+    $hashes->[$i]->{'update_token'} = $self->type('string', $token);
+  }
 }
 
 1;
diff --git a/Bugzilla/WebService/BugUserLastVisit.pm b/Bugzilla/WebService/BugUserLastVisit.pm
index 5e4c0d2ba..9b4261bc3 100644
--- a/Bugzilla/WebService/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/BugUserLastVisit.pm
@@ -19,84 +19,84 @@ use Bugzilla::WebService::Util qw( validate filter );
 use Bugzilla::Constants;
 
 use constant PUBLIC_METHODS => qw(
-    get
-    update
+  get
+  update
 );
 
 sub update {
-    my ($self, $params) = validate(@_, 'ids');
-    my $user = Bugzilla->user;
-    my $dbh  = Bugzilla->dbh;
+  my ($self, $params) = validate(@_, 'ids');
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
 
-    $user->login(LOGIN_REQUIRED);
+  $user->login(LOGIN_REQUIRED);
 
-    my $ids = $params->{ids} // [];
-    ThrowCodeError('param_required', { param => 'ids' }) unless @$ids;
+  my $ids = $params->{ids} // [];
+  ThrowCodeError('param_required', {param => 'ids'}) unless @$ids;
 
-    # Cache permissions for bugs. This highly reduces the number of calls to the
-    # DB.  visible_bugs() is only able to handle bug IDs, so we have to skip
-    # aliases.
-    $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
+  # Cache permissions for bugs. This highly reduces the number of calls to the
+  # DB.  visible_bugs() is only able to handle bug IDs, so we have to skip
+  # aliases.
+  $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
 
-    $dbh->bz_start_transaction();
-    my @results;
-    my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
-    foreach my $bug_id (@$ids) {
-        my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 });
+  $dbh->bz_start_transaction();
+  my @results;
+  my $last_visit_ts = $dbh->selectrow_array('SELECT NOW()');
+  foreach my $bug_id (@$ids) {
+    my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1});
 
-        ThrowUserError('user_not_involved', { bug_id => $bug->id })
-            unless $user->is_involved_in_bug($bug);
+    ThrowUserError('user_not_involved', {bug_id => $bug->id})
+      unless $user->is_involved_in_bug($bug);
 
-        $bug->update_user_last_visit($user, $last_visit_ts);
+    $bug->update_user_last_visit($user, $last_visit_ts);
 
-        push(
-            @results,
-            $self->_bug_user_last_visit_to_hash(
-                $bug_id, $last_visit_ts, $params
-            ));
-    }
-    $dbh->bz_commit_transaction();
+    push(@results,
+      $self->_bug_user_last_visit_to_hash($bug_id, $last_visit_ts, $params));
+  }
+  $dbh->bz_commit_transaction();
 
-    return \@results;
+  return \@results;
 }
 
 sub get {
-    my ($self, $params) = validate(@_, 'ids');
-    my $user = Bugzilla->user;
-    my $ids  = $params->{ids};
-
-    $user->login(LOGIN_REQUIRED);
-
-    if ($ids) {
-        # Cache permissions for bugs. This highly reduces the number of calls to
-        # the DB.  visible_bugs() is only able to handle bug IDs, so we have to
-        # skip aliases.
-        $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
-    }
-
-    my @last_visits = @{ $user->last_visited };
-
-    if ($ids) {
-        # remove bugs that we are not interested in if ids is passed in.
-        my %id_set = map { ($_ => 1) } @$ids;
-        @last_visits = grep { $id_set{ $_->bug_id } } @last_visits;
-    }
-
-    return [
-        map {
-            $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts,
-                $params)
-        } @last_visits
-    ];
+  my ($self, $params) = validate(@_, 'ids');
+  my $user = Bugzilla->user;
+  my $ids  = $params->{ids};
+
+  $user->login(LOGIN_REQUIRED);
+
+  if ($ids) {
+
+    # Cache permissions for bugs. This highly reduces the number of calls to
+    # the DB.  visible_bugs() is only able to handle bug IDs, so we have to
+    # skip aliases.
+    $user->visible_bugs([grep /^[0-9]+$/, @$ids]);
+  }
+
+  my @last_visits = @{$user->last_visited};
+
+  if ($ids) {
+
+    # remove bugs that we are not interested in if ids is passed in.
+    my %id_set = map { ($_ => 1) } @$ids;
+    @last_visits = grep { $id_set{$_->bug_id} } @last_visits;
+  }
+
+  return [
+    map {
+      $self->_bug_user_last_visit_to_hash($_->bug_id, $_->last_visit_ts, $params)
+    } @last_visits
+  ];
 }
 
 sub _bug_user_last_visit_to_hash {
-    my ($self, $bug_id, $last_visit_ts, $params) = @_;
+  my ($self, $bug_id, $last_visit_ts, $params) = @_;
 
-    my %result = (id            => $self->type('int',      $bug_id),
-                  last_visit_ts => $self->type('dateTime', $last_visit_ts));
+  my %result = (
+    id            => $self->type('int',      $bug_id),
+    last_visit_ts => $self->type('dateTime', $last_visit_ts)
+  );
 
-    return filter($params, \%result);
+  return filter($params, \%result);
 }
 
 1;
diff --git a/Bugzilla/WebService/Bugzilla.pm b/Bugzilla/WebService/Bugzilla.pm
index 8e95028cb..f8f745a55 100644
--- a/Bugzilla/WebService/Bugzilla.pm
+++ b/Bugzilla/WebService/Bugzilla.pm
@@ -21,77 +21,76 @@ use Try::Tiny;
 use DateTime;
 
 # Basic info that is needed before logins
-use constant LOGIN_EXEMPT => {
-    timezone => 1,
-    version => 1,
-};
+use constant LOGIN_EXEMPT => {timezone => 1, version => 1,};
 
 use constant READ_ONLY => qw(
-    extensions
-    timezone
-    time
-    version
-    jobqueue_status
+  extensions
+  timezone
+  time
+  version
+  jobqueue_status
 );
 
 use constant PUBLIC_METHODS => qw(
-    extensions
-    time
-    timezone
-    version
-    jobqueue_status
+  extensions
+  time
+  timezone
+  version
+  jobqueue_status
 );
 
 sub version {
-    my $self = shift;
-    return { version => $self->type('string', BUGZILLA_VERSION) };
+  my $self = shift;
+  return {version => $self->type('string', BUGZILLA_VERSION)};
 }
 
 sub extensions {
-    my $self = shift;
-
-    my %retval;
-    foreach my $extension (@{ Bugzilla->extensions }) {
-        my $version = $extension->VERSION || 0;
-        my $name    = $extension->NAME;
-        $retval{$name}->{version} = $self->type('string', $version);
-    }
-    return { extensions => \%retval };
+  my $self = shift;
+
+  my %retval;
+  foreach my $extension (@{Bugzilla->extensions}) {
+    my $version = $extension->VERSION || 0;
+    my $name = $extension->NAME;
+    $retval{$name}->{version} = $self->type('string', $version);
+  }
+  return {extensions => \%retval};
 }
 
 sub timezone {
-    my $self = shift;
-    # All Webservices return times in UTC; Use UTC here for backwards compat.
-    return { timezone => $self->type('string', "+0000") };
+  my $self = shift;
+
+  # All Webservices return times in UTC; Use UTC here for backwards compat.
+  return {timezone => $self->type('string', "+0000")};
 }
 
 sub time {
-    my ($self) = @_;
-    # All Webservices return times in UTC; Use UTC here for backwards compat.
-    # Hardcode values where appropriate
-    my $dbh = Bugzilla->dbh;
-
-    my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
-    $db_time = datetime_from($db_time, 'UTC');
-    my $now_utc = DateTime->now();
-
-    return {
-        db_time       => $self->type('dateTime', $db_time),
-        web_time      => $self->type('dateTime', $now_utc),
-        web_time_utc  => $self->type('dateTime', $now_utc),
-        tz_name       => $self->type('string', 'UTC'),
-        tz_offset     => $self->type('string', '+0000'),
-        tz_short_name => $self->type('string', 'UTC'),
-    };
+  my ($self) = @_;
+
+  # All Webservices return times in UTC; Use UTC here for backwards compat.
+  # Hardcode values where appropriate
+  my $dbh = Bugzilla->dbh;
+
+  my $db_time = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+  $db_time = datetime_from($db_time, 'UTC');
+  my $now_utc = DateTime->now();
+
+  return {
+    db_time       => $self->type('dateTime', $db_time),
+    web_time      => $self->type('dateTime', $now_utc),
+    web_time_utc  => $self->type('dateTime', $now_utc),
+    tz_name       => $self->type('string',   'UTC'),
+    tz_offset     => $self->type('string',   '+0000'),
+    tz_short_name => $self->type('string',   'UTC'),
+  };
 }
 
 sub jobqueue_status {
-    my ( $self, $params ) = @_;
+  my ($self, $params) = @_;
 
-    Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    my $dbh = Bugzilla->dbh;
-    my $query = q{
+  my $dbh   = Bugzilla->dbh;
+  my $query = q{
         SELECT
             COUNT(*) AS total,
             COALESCE(
@@ -105,17 +104,18 @@ sub jobqueue_status {
                 ON f.funcid = j.funcid;
     };
 
-    my $status;
-    try {
-        $status = $dbh->selectrow_hashref($query);
-        $status->{errors} = 0 + $status->{errors};
-        $status->{total}  = 0 + $status->{total};
-    } catch {
-        ERROR($_);
-        ThrowCodeError('jobqueue_status_error');
-    };
-
-    return $status;
+  my $status;
+  try {
+    $status           = $dbh->selectrow_hashref($query);
+    $status->{errors} = 0 + $status->{errors};
+    $status->{total}  = 0 + $status->{total};
+  }
+  catch {
+    ERROR($_);
+    ThrowCodeError('jobqueue_status_error');
+  };
+
+  return $status;
 }
 
 1;
diff --git a/Bugzilla/WebService/Classification.pm b/Bugzilla/WebService/Classification.pm
index 32139ff3f..35e67ba08 100644
--- a/Bugzilla/WebService/Classification.pm
+++ b/Bugzilla/WebService/Classification.pm
@@ -18,65 +18,76 @@ use Bugzilla::Error;
 use Bugzilla::WebService::Util qw(filter validate params_to_objects);
 
 use constant READ_ONLY => qw(
-    get
+  get
 );
 
 use constant PUBLIC_METHODS => qw(
-    get
+  get
 );
 
 sub get {
-    my ($self, $params) = validate(@_, 'names', 'ids');
+  my ($self, $params) = validate(@_, 'names', 'ids');
 
-    defined $params->{names} || defined $params->{ids}
-        || ThrowCodeError('params_required', { function => 'Classification.get',
-                                               params => ['names', 'ids'] });
+  defined $params->{names}
+    || defined $params->{ids}
+    || ThrowCodeError('params_required',
+    {function => 'Classification.get', params => ['names', 'ids']});
 
-    my $user = Bugzilla->user;
+  my $user = Bugzilla->user;
 
-    Bugzilla->params->{'useclassification'}
-      || $user->in_group('editclassifications')
-      || ThrowUserError('auth_classification_not_enabled');
+  Bugzilla->params->{'useclassification'}
+    || $user->in_group('editclassifications')
+    || ThrowUserError('auth_classification_not_enabled');
 
-    Bugzilla->switch_to_shadow_db;
+  Bugzilla->switch_to_shadow_db;
 
-    my @classification_objs = @{ params_to_objects($params, 'Bugzilla::Classification') };
-    unless ($user->in_group('editclassifications')) {
-        my %selectable_class = map { $_->id => 1 } @{$user->get_selectable_classifications};
-        @classification_objs = grep { $selectable_class{$_->id} } @classification_objs;
-    }
+  my @classification_objs
+    = @{params_to_objects($params, 'Bugzilla::Classification')};
+  unless ($user->in_group('editclassifications')) {
+    my %selectable_class
+      = map { $_->id => 1 } @{$user->get_selectable_classifications};
+    @classification_objs = grep { $selectable_class{$_->id} } @classification_objs;
+  }
 
-    my @classifications = map { $self->_classification_to_hash($_, $params) } @classification_objs;
+  my @classifications
+    = map { $self->_classification_to_hash($_, $params) } @classification_objs;
 
-    return { classifications => \@classifications };
+  return {classifications => \@classifications};
 }
 
 sub _classification_to_hash {
-    my ($self, $classification, $params) = @_;
-
-    my $user = Bugzilla->user;
-    return unless (Bugzilla->params->{'useclassification'} || $user->in_group('editclassifications'));
-
-    my $products = $user->in_group('editclassifications') ?
-                     $classification->products : $user->get_selectable_products($classification->id);
-
-    return filter $params, {
-        id          => $self->type('int',    $classification->id),
-        name        => $self->type('string', $classification->name),
-        description => $self->type('string', $classification->description),
-        sort_key    => $self->type('int',    $classification->sortkey),
-        products    => [ map { $self->_product_to_hash($_, $params) } @$products ],
+  my ($self, $classification, $params) = @_;
+
+  my $user = Bugzilla->user;
+  return
+    unless (Bugzilla->params->{'useclassification'}
+    || $user->in_group('editclassifications'));
+
+  my $products
+    = $user->in_group('editclassifications')
+    ? $classification->products
+    : $user->get_selectable_products($classification->id);
+
+  return filter $params,
+    {
+    id          => $self->type('int',    $classification->id),
+    name        => $self->type('string', $classification->name),
+    description => $self->type('string', $classification->description),
+    sort_key    => $self->type('int',    $classification->sortkey),
+    products => [map { $self->_product_to_hash($_, $params) } @$products],
     };
 }
 
 sub _product_to_hash {
-    my ($self, $product, $params) = @_;
-
-    return filter $params, {
-       id          => $self->type('int', $product->id),
-       name        => $self->type('string', $product->name),
-       description => $self->type('string', $product->description),
-   }, undef, 'products';
+  my ($self, $product, $params) = @_;
+
+  return filter $params,
+    {
+    id          => $self->type('int',    $product->id),
+    name        => $self->type('string', $product->name),
+    description => $self->type('string', $product->description),
+    },
+    undef, 'products';
 }
 
 1;
diff --git a/Bugzilla/WebService/Constants.pm b/Bugzilla/WebService/Constants.pm
index 71435c13a..71fcccd6e 100644
--- a/Bugzilla/WebService/Constants.pm
+++ b/Bugzilla/WebService/Constants.pm
@@ -14,27 +14,27 @@ use warnings;
 use base qw(Exporter);
 
 our @EXPORT = qw(
-    WS_ERROR_CODE
+  WS_ERROR_CODE
 
-    STATUS_OK
-    STATUS_CREATED
-    STATUS_ACCEPTED
-    STATUS_NO_CONTENT
-    STATUS_MULTIPLE_CHOICES
-    STATUS_BAD_REQUEST
-    STATUS_NOT_FOUND
-    STATUS_GONE
-    REST_STATUS_CODE_MAP
+  STATUS_OK
+  STATUS_CREATED
+  STATUS_ACCEPTED
+  STATUS_NO_CONTENT
+  STATUS_MULTIPLE_CHOICES
+  STATUS_BAD_REQUEST
+  STATUS_NOT_FOUND
+  STATUS_GONE
+  REST_STATUS_CODE_MAP
 
-    ERROR_UNKNOWN_FATAL
-    ERROR_UNKNOWN_TRANSIENT
+  ERROR_UNKNOWN_FATAL
+  ERROR_UNKNOWN_TRANSIENT
 
-    XMLRPC_CONTENT_TYPE_WHITELIST
-    REST_CONTENT_TYPE_WHITELIST
+  XMLRPC_CONTENT_TYPE_WHITELIST
+  REST_CONTENT_TYPE_WHITELIST
 
-    WS_DISPATCH
+  WS_DISPATCH
 
-    API_AUTH_HEADERS
+  API_AUTH_HEADERS
 );
 
 # This maps the error names in global/*-error.html.tmpl to numbers.
@@ -56,161 +56,184 @@ our @EXPORT = qw(
 # comment that it was retired. Also, if an error changes its name, you'll
 # have to fix it here.
 use constant WS_ERROR_CODE => {
-    # Generic errors (Bugzilla::Object and others) are 50-99.
-    object_not_specified        => 50,
-    reassign_to_empty           => 50,
-    param_required              => 50,
-    params_required             => 50,
-    undefined_field             => 50,
-    object_does_not_exist       => 51,
-    param_must_be_numeric       => 52,
-    number_not_numeric          => 52,
-    param_invalid               => 53,
-    number_too_large            => 54,
-    number_too_small            => 55,
-    illegal_date                => 56,
-    # Bug errors usually occupy the 100-200 range.
-    improper_bug_id_field_value => 100,
-    bug_id_does_not_exist       => 101,
-    bug_access_denied           => 102,
-    bug_access_query            => 102,
-    # These all mean "invalid alias"
-    alias_too_long           => 103,
-    alias_in_use             => 103,
-    alias_is_numeric         => 103,
-    alias_has_comma_or_space => 103,
-    multiple_alias_not_allowed => 103,
-    # Misc. bug field errors
-    illegal_field => 104,
-    freetext_too_long => 104,
-    # Component errors
-    require_component       => 105,
-    component_name_too_long => 105,
-    # Invalid Product
-    no_products         => 106,
-    entry_access_denied => 106,
-    product_access_denied => 106,
-    product_disabled    => 106,
-    # Invalid Summary
-    require_summary => 107,
-    # Invalid field name
-    invalid_field_name => 108,
-    # Not authorized to edit the bug
-    product_edit_denied => 109,
-    # Comment-related errors
-    comment_is_private => 110,
-    comment_id_invalid => 111,
-    comment_too_long => 114,
-    comment_invalid_isprivate => 117,
-    # Comment tagging
-    comment_tag_disabled => 125,
-    comment_tag_invalid => 126,
-    comment_tag_too_long => 127,
-    comment_tag_too_short => 128,
-    # See Also errors
-    bug_url_invalid => 112,
-    bug_url_too_long => 112,
-    # Insidergroup Errors
-    user_not_insider => 113,
-    # Note: 114 is above in the Comment-related section.
-    # Bug update errors
-    illegal_change => 115,
-    # Dependency errors
-    dependency_loop_single => 116,
-    dependency_loop_multi  => 116,
-    # Note: 117 is above in the Comment-related section.
-    # Dup errors
-    dupe_loop_detected => 118,
-    dupe_id_required => 119,
-    # Bug-related group errors
-    group_invalid_removal => 120,
-    group_restriction_not_allowed => 120,
-    # Status/Resolution errors
-    missing_resolution => 121,
-    resolution_not_allowed => 122,
-    illegal_bug_status_transition => 123,
-    # Flag errors
-    flag_status_invalid => 129,
-    flag_update_denied => 130,
-    flag_type_requestee_disabled => 131,
-    flag_not_unique => 132,
-    flag_type_not_unique => 133,
-    flag_type_inactive => 134,
-
-    # Authentication errors are usually 300-400.
-    invalid_username_or_password => 300,
-    account_disabled             => 301,
-    auth_invalid_email           => 302,
-    extern_id_conflict           => -303,
-    auth_failure                 => 304,
-    password_insecure            => 305,
-    api_key_not_valid            => 306,
-    api_key_revoked              => 306,
-    auth_invalid_token           => 307,
-    invalid_cookies_or_token     => 307,
-
-    # Except, historically, AUTH_NODATA, which is 410.
-    login_required               => 410,
-
-    # User errors are 500-600.
-    account_exists        => 500,
-    illegal_email_address => 501,
-    auth_cant_create_account    => 501,
-    account_creation_disabled   => 501,
-    account_creation_restricted => 501,
-    # Error 502 password_too_short no longer exists.
-    # Error 503 password_too_long no longer exists.
-    invalid_username      => 504,
-    # This is from strict_isolation, but it also basically means
-    # "invalid user."
-    invalid_user_group    => 504,
-    user_access_by_id_denied    => 505,
-    user_access_by_match_denied => 505,
-
-    # Attachment errors are 600-700.
-    file_too_large         => 600,
-    invalid_content_type   => 601,
-    # Error 602 attachment_illegal_url no longer exists.
-    file_not_specified     => 603,
-    missing_attachment_description => 604,
-    # Error 605 attachment_url_disabled no longer exists.
-    zero_length_file       => 606,
-
-    # Product erros are 700-800
-    product_blank_name => 700,
-    product_name_too_long => 701,
-    product_name_already_in_use => 702,
-    product_name_diff_in_case => 702,
-    product_must_have_description => 703,
-    product_must_have_version => 704,
-    product_must_define_defaultmilestone => 705,
-
-    # Group errors are 800-900
-    empty_group_name => 800,
-    group_exists => 801,
-    empty_group_description => 802,
-    invalid_regexp => 803,
-    invalid_group_name => 804,
-    group_cannot_view => 805,
-
-    # Search errors are 1000-1100
-    buglist_parameters_required => 1000,
-
-    # BugUserLastVisited errors
-    user_not_involved => 1300,
-
-    # Job queue errors 1400-1500
-    jobqueue_status_error => 1400,
-
-    # Errors thrown by the WebService itself. The ones that are negative
-    # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
-    xmlrpc_invalid_value => -32600,
-    unknown_method       => -32601,
-    json_rpc_post_only   => 32610,
-    json_rpc_invalid_callback => 32611,
-    xmlrpc_illegal_content_type   => 32612,
-    json_rpc_illegal_content_type => 32613,
-    rest_invalid_resource         => 32614,
+
+  # Generic errors (Bugzilla::Object and others) are 50-99.
+  object_not_specified  => 50,
+  reassign_to_empty     => 50,
+  param_required        => 50,
+  params_required       => 50,
+  undefined_field       => 50,
+  object_does_not_exist => 51,
+  param_must_be_numeric => 52,
+  number_not_numeric    => 52,
+  param_invalid         => 53,
+  number_too_large      => 54,
+  number_too_small      => 55,
+  illegal_date          => 56,
+
+  # Bug errors usually occupy the 100-200 range.
+  improper_bug_id_field_value => 100,
+  bug_id_does_not_exist       => 101,
+  bug_access_denied           => 102,
+  bug_access_query            => 102,
+
+  # These all mean "invalid alias"
+  alias_too_long             => 103,
+  alias_in_use               => 103,
+  alias_is_numeric           => 103,
+  alias_has_comma_or_space   => 103,
+  multiple_alias_not_allowed => 103,
+
+  # Misc. bug field errors
+  illegal_field     => 104,
+  freetext_too_long => 104,
+
+  # Component errors
+  require_component       => 105,
+  component_name_too_long => 105,
+
+  # Invalid Product
+  no_products           => 106,
+  entry_access_denied   => 106,
+  product_access_denied => 106,
+  product_disabled      => 106,
+
+  # Invalid Summary
+  require_summary => 107,
+
+  # Invalid field name
+  invalid_field_name => 108,
+
+  # Not authorized to edit the bug
+  product_edit_denied => 109,
+
+  # Comment-related errors
+  comment_is_private        => 110,
+  comment_id_invalid        => 111,
+  comment_too_long          => 114,
+  comment_invalid_isprivate => 117,
+
+  # Comment tagging
+  comment_tag_disabled  => 125,
+  comment_tag_invalid   => 126,
+  comment_tag_too_long  => 127,
+  comment_tag_too_short => 128,
+
+  # See Also errors
+  bug_url_invalid  => 112,
+  bug_url_too_long => 112,
+
+  # Insidergroup Errors
+  user_not_insider => 113,
+
+  # Note: 114 is above in the Comment-related section.
+  # Bug update errors
+  illegal_change => 115,
+
+  # Dependency errors
+  dependency_loop_single => 116,
+  dependency_loop_multi  => 116,
+
+  # Note: 117 is above in the Comment-related section.
+  # Dup errors
+  dupe_loop_detected => 118,
+  dupe_id_required   => 119,
+
+  # Bug-related group errors
+  group_invalid_removal         => 120,
+  group_restriction_not_allowed => 120,
+
+  # Status/Resolution errors
+  missing_resolution            => 121,
+  resolution_not_allowed        => 122,
+  illegal_bug_status_transition => 123,
+
+  # Flag errors
+  flag_status_invalid          => 129,
+  flag_update_denied           => 130,
+  flag_type_requestee_disabled => 131,
+  flag_not_unique              => 132,
+  flag_type_not_unique         => 133,
+  flag_type_inactive           => 134,
+
+  # Authentication errors are usually 300-400.
+  invalid_username_or_password => 300,
+  account_disabled             => 301,
+  auth_invalid_email           => 302,
+  extern_id_conflict           => -303,
+  auth_failure                 => 304,
+  password_insecure            => 305,
+  api_key_not_valid            => 306,
+  api_key_revoked              => 306,
+  auth_invalid_token           => 307,
+  invalid_cookies_or_token     => 307,
+
+  # Except, historically, AUTH_NODATA, which is 410.
+  login_required => 410,
+
+  # User errors are 500-600.
+  account_exists              => 500,
+  illegal_email_address       => 501,
+  auth_cant_create_account    => 501,
+  account_creation_disabled   => 501,
+  account_creation_restricted => 501,
+
+  # Error 502 password_too_short no longer exists.
+  # Error 503 password_too_long no longer exists.
+  invalid_username => 504,
+
+  # This is from strict_isolation, but it also basically means
+  # "invalid user."
+  invalid_user_group          => 504,
+  user_access_by_id_denied    => 505,
+  user_access_by_match_denied => 505,
+
+  # Attachment errors are 600-700.
+  file_too_large       => 600,
+  invalid_content_type => 601,
+
+  # Error 602 attachment_illegal_url no longer exists.
+  file_not_specified             => 603,
+  missing_attachment_description => 604,
+
+  # Error 605 attachment_url_disabled no longer exists.
+  zero_length_file => 606,
+
+  # Product erros are 700-800
+  product_blank_name                   => 700,
+  product_name_too_long                => 701,
+  product_name_already_in_use          => 702,
+  product_name_diff_in_case            => 702,
+  product_must_have_description        => 703,
+  product_must_have_version            => 704,
+  product_must_define_defaultmilestone => 705,
+
+  # Group errors are 800-900
+  empty_group_name        => 800,
+  group_exists            => 801,
+  empty_group_description => 802,
+  invalid_regexp          => 803,
+  invalid_group_name      => 804,
+  group_cannot_view       => 805,
+
+  # Search errors are 1000-1100
+  buglist_parameters_required => 1000,
+
+  # BugUserLastVisited errors
+  user_not_involved => 1300,
+
+  # Job queue errors 1400-1500
+  jobqueue_status_error => 1400,
+
+  # Errors thrown by the WebService itself. The ones that are negative
+  # conform to http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
+  xmlrpc_invalid_value          => -32600,
+  unknown_method                => -32601,
+  json_rpc_post_only            => 32610,
+  json_rpc_invalid_callback     => 32611,
+  xmlrpc_illegal_content_type   => 32612,
+  json_rpc_illegal_content_type => 32613,
+  rest_invalid_resource         => 32614,
 };
 
 # RESTful webservices use the http status code
@@ -231,81 +254,82 @@ use constant STATUS_GONE             => 410;
 # http status code based on the error code or use the
 # default STATUS_BAD_REQUEST.
 sub REST_STATUS_CODE_MAP {
-    my $status_code_map = {
-        51       => STATUS_NOT_FOUND,
-        101      => STATUS_NOT_FOUND,
-        102      => STATUS_NOT_AUTHORIZED,
-        106      => STATUS_NOT_AUTHORIZED,
-        109      => STATUS_NOT_AUTHORIZED,
-        110      => STATUS_NOT_AUTHORIZED,
-        113      => STATUS_NOT_AUTHORIZED,
-        115      => STATUS_NOT_AUTHORIZED,
-        120      => STATUS_NOT_AUTHORIZED,
-        300      => STATUS_NOT_AUTHORIZED,
-        301      => STATUS_NOT_AUTHORIZED,
-        302      => STATUS_NOT_AUTHORIZED,
-        303      => STATUS_NOT_AUTHORIZED,
-        304      => STATUS_NOT_AUTHORIZED,
-        410      => STATUS_NOT_AUTHORIZED,
-        504      => STATUS_NOT_AUTHORIZED,
-        505      => STATUS_NOT_AUTHORIZED,
-        32614    => STATUS_NOT_FOUND,
-        _default => STATUS_BAD_REQUEST
-    };
-
-    Bugzilla::Hook::process('webservice_status_code_map',
-        { status_code_map => $status_code_map });
-
-    return $status_code_map;
-};
+  my $status_code_map = {
+    51       => STATUS_NOT_FOUND,
+    101      => STATUS_NOT_FOUND,
+    102      => STATUS_NOT_AUTHORIZED,
+    106      => STATUS_NOT_AUTHORIZED,
+    109      => STATUS_NOT_AUTHORIZED,
+    110      => STATUS_NOT_AUTHORIZED,
+    113      => STATUS_NOT_AUTHORIZED,
+    115      => STATUS_NOT_AUTHORIZED,
+    120      => STATUS_NOT_AUTHORIZED,
+    300      => STATUS_NOT_AUTHORIZED,
+    301      => STATUS_NOT_AUTHORIZED,
+    302      => STATUS_NOT_AUTHORIZED,
+    303      => STATUS_NOT_AUTHORIZED,
+    304      => STATUS_NOT_AUTHORIZED,
+    410      => STATUS_NOT_AUTHORIZED,
+    504      => STATUS_NOT_AUTHORIZED,
+    505      => STATUS_NOT_AUTHORIZED,
+    32614    => STATUS_NOT_FOUND,
+    _default => STATUS_BAD_REQUEST
+  };
+
+  Bugzilla::Hook::process('webservice_status_code_map',
+    {status_code_map => $status_code_map});
+
+  return $status_code_map;
+}
 
 # These are the fallback defaults for errors not in ERROR_CODE.
 use constant ERROR_UNKNOWN_FATAL     => -32000;
 use constant ERROR_UNKNOWN_TRANSIENT => 32000;
 
-use constant ERROR_GENERAL       => 999;
+use constant ERROR_GENERAL => 999;
 
 use constant XMLRPC_CONTENT_TYPE_WHITELIST => qw(
-    text/xml
-    application/xml
+  text/xml
+  application/xml
 );
 
 # The first content type specified is used as the default.
 use constant REST_CONTENT_TYPE_WHITELIST => qw(
-    application/json
-    application/javascript
-    text/javascript
-    text/html
+  application/json
+  application/javascript
+  text/javascript
+  text/html
 );
 
 sub WS_DISPATCH {
-    # We "require" here instead of "use" above to avoid a dependency loop.
-    require Bugzilla::Hook;
-    my %hook_dispatch;
-    Bugzilla::Hook::process('webservice', { dispatch => \%hook_dispatch });
-
-    my $dispatch = {
-        'Bugzilla'       => 'Bugzilla::WebService::Bugzilla',
-        'Bug'            => 'Bugzilla::WebService::Bug',
-        'Classification' => 'Bugzilla::WebService::Classification',
-        'User'           => 'Bugzilla::WebService::User',
-        'Product'        => 'Bugzilla::WebService::Product',
-        'Group'          => 'Bugzilla::WebService::Group',
-        'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
-        'Elastic'        => 'Bugzilla::WebService::Elastic',
-        %hook_dispatch
-    };
-    return $dispatch;
-};
+
+  # We "require" here instead of "use" above to avoid a dependency loop.
+  require Bugzilla::Hook;
+  my %hook_dispatch;
+  Bugzilla::Hook::process('webservice', {dispatch => \%hook_dispatch});
+
+  my $dispatch = {
+    'Bugzilla'         => 'Bugzilla::WebService::Bugzilla',
+    'Bug'              => 'Bugzilla::WebService::Bug',
+    'Classification'   => 'Bugzilla::WebService::Classification',
+    'User'             => 'Bugzilla::WebService::User',
+    'Product'          => 'Bugzilla::WebService::Product',
+    'Group'            => 'Bugzilla::WebService::Group',
+    'BugUserLastVisit' => 'Bugzilla::WebService::BugUserLastVisit',
+    'Elastic'          => 'Bugzilla::WebService::Elastic',
+    %hook_dispatch
+  };
+  return $dispatch;
+}
 
 # Custom HTTP headers that can be used for API authentication rather than
 # passing as URL parameters. This is useful if you do not want sensitive
 # information to show up in webserver log files.
 use constant API_AUTH_HEADERS => {
-    X_BUGZILLA_LOGIN    => 'Bugzilla_login',
-    X_BUGZILLA_PASSWORD => 'Bugzilla_password',
-    X_BUGZILLA_API_KEY  => 'Bugzilla_api_key',
-    X_BUGZILLA_TOKEN    => 'Bugzilla_token',
+  X_BUGZILLA_LOGIN    => 'Bugzilla_login',
+  X_BUGZILLA_PASSWORD => 'Bugzilla_password',
+  X_BUGZILLA_API_KEY  => 'Bugzilla_api_key',
+  X_BUGZILLA_TOKEN    => 'Bugzilla_token',
 };
 
 1;
diff --git a/Bugzilla/WebService/Elastic.pm b/Bugzilla/WebService/Elastic.pm
index 3a33a1dba..373f6db58 100644
--- a/Bugzilla/WebService/Elastic.pm
+++ b/Bugzilla/WebService/Elastic.pm
@@ -30,30 +30,28 @@ use Bugzilla::Error;
 use Bugzilla::WebService::Util qw(validate);
 use Bugzilla::Util qw(trim detaint_natural trick_taint);
 
-use constant READ_ONLY => qw( suggest_users );
+use constant READ_ONLY      => qw( suggest_users );
 use constant PUBLIC_METHODS => qw( suggest_users );
 
 sub suggest_users {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    Bugzilla->switch_to_shadow_db();
+  Bugzilla->switch_to_shadow_db();
 
-    ThrowCodeError('params_required', { function => 'Elastic.suggest_users', params => ['match'] })
-      unless defined $params->{match};
+  ThrowCodeError('params_required',
+    {function => 'Elastic.suggest_users', params => ['match']})
+    unless defined $params->{match};
 
-    ThrowUserError('user_access_by_match_denied')
-      unless Bugzilla->user->id;
+  ThrowUserError('user_access_by_match_denied') unless Bugzilla->user->id;
 
-    trick_taint($params->{match});
-    my $results = Bugzilla->elastic->suggest_users($params->{match} . "");
-    my @users = map {
-        {
-            real_name => $self->type(string => $_->{real_name}),
-            name      => $self->type(email  => $_->{name}),
-        }
-    } @$results;
+  trick_taint($params->{match});
+  my $results = Bugzilla->elastic->suggest_users($params->{match} . "");
+  my @users = map { {
+    real_name => $self->type(string => $_->{real_name}),
+    name      => $self->type(email  => $_->{name}),
+  } } @$results;
 
-    return { users => \@users };
+  return {users => \@users};
 }
 
-1;
\ No newline at end of file
+1;
diff --git a/Bugzilla/WebService/Group.pm b/Bugzilla/WebService/Group.pm
index b13003e08..4467883a4 100644
--- a/Bugzilla/WebService/Group.pm
+++ b/Bugzilla/WebService/Group.pm
@@ -17,207 +17,210 @@ use Bugzilla::Error;
 use Bugzilla::WebService::Util qw(validate translate params_to_objects);
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    update
+  create
+  get
+  update
 );
 
-use constant MAPPED_RETURNS => {
-    userregexp => 'user_regexp',
-    isactive => 'is_active'
-};
+use constant MAPPED_RETURNS =>
+  {userregexp => 'user_regexp', isactive => 'is_active'};
 
 sub create {
-    my ($self, $params) = @_;
-
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->user->in_group('creategroups')
-        || ThrowUserError("auth_failure", { group  => "creategroups",
-                                            action => "add",
-                                            object => "group"});
-    # Create group
-    my $group = Bugzilla::Group->create({
-        name               => $params->{name},
-        description        => $params->{description},
-        userregexp         => $params->{user_regexp},
-        isactive           => $params->{is_active},
-        isbuggroup         => 1,
-        icon_url           => $params->{icon_url}
-    });
-    return { id => $self->type('int', $group->id) };
+  my ($self, $params) = @_;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->user->in_group('creategroups')
+    || ThrowUserError("auth_failure",
+    {group => "creategroups", action => "add", object => "group"});
+
+  # Create group
+  my $group = Bugzilla::Group->create({
+    name        => $params->{name},
+    description => $params->{description},
+    userregexp  => $params->{user_regexp},
+    isactive    => $params->{is_active},
+    isbuggroup  => 1,
+    icon_url    => $params->{icon_url}
+  });
+  return {id => $self->type('int', $group->id)};
 }
 
 sub update {
-    my ($self, $params) = @_;
-
-    my $dbh = Bugzilla->dbh;
+  my ($self, $params) = @_;
+
+  my $dbh = Bugzilla->dbh;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->user->in_group('creategroups')
+    || ThrowUserError("auth_failure",
+    {group => "creategroups", action => "edit", object => "group"});
+
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'Group.update', params => ['ids', 'names']});
+
+  my $group_objects = params_to_objects($params, 'Bugzilla::Group');
+
+  my %values = %$params;
+
+  # We delete names and ids to keep only new values to set.
+  delete $values{names};
+  delete $values{ids};
+
+  $dbh->bz_start_transaction();
+  foreach my $group (@$group_objects) {
+    $group->set_all(\%values);
+  }
+
+  my %changes;
+  foreach my $group (@$group_objects) {
+    my $returned_changes = $group->update();
+    $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $group (@$group_objects) {
+    my %hash = (id => $group->id, changes => {},);
+    foreach my $field (keys %{$changes{$group->id}}) {
+      my $change = $changes{$group->id}->{$field};
+      $hash{changes}{$field} = {
+        removed => $self->type('string', $change->[0]),
+        added   => $self->type('string', $change->[1])
+      };
+    }
+    push(@result, \%hash);
+  }
 
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->user->in_group('creategroups')
-        || ThrowUserError("auth_failure", { group  => "creategroups",
-                                            action => "edit",
-                                            object => "group" });
+  return {groups => \@result};
+}
 
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'Group.update', params => ['ids', 'names'] });
+sub get {
+  my ($self, $params) = validate(@_, 'ids', 'names', 'type');
 
-    my $group_objects = params_to_objects($params, 'Bugzilla::Group');
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    my %values = %$params;
+  # Reject access if there is no sense in continuing.
+  my $user = Bugzilla->user;
+  my $all_groups
+    = $user->in_group('editusers') || $user->in_group('creategroups');
+  if (!$all_groups && !$user->can_bless) {
+    ThrowUserError('group_cannot_view');
+  }
 
-    # We delete names and ids to keep only new values to set.
-    delete $values{names};
-    delete $values{ids};
+  Bugzilla->switch_to_shadow_db();
 
-    $dbh->bz_start_transaction();
-    foreach my $group (@$group_objects) {
-        $group->set_all(\%values);
-    }
+  my $groups = [];
 
-    my %changes;
-    foreach my $group (@$group_objects) {
-        my $returned_changes = $group->update();
-        $changes{$group->id} = translate($returned_changes, MAPPED_RETURNS);
-    }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $group (@$group_objects) {
-        my %hash = (
-            id      => $group->id,
-            changes => {},
-        );
-        foreach my $field (keys %{ $changes{$group->id} }) {
-            my $change = $changes{$group->id}->{$field};
-            $hash{changes}{$field} = {
-                removed => $self->type('string', $change->[0]),
-                added   => $self->type('string', $change->[1])
-            };
-        }
-       push(@result, \%hash);
-    }
-
-    return { groups => \@result };
-}
-
-sub get {
-    my ($self, $params) = validate(@_, 'ids', 'names', 'type');
+  if (defined $params->{ids}) {
 
-    Bugzilla->login(LOGIN_REQUIRED);
+    # Get the groups by id
+    $groups = Bugzilla::Group->new_from_list($params->{ids});
+  }
 
-    # Reject access if there is no sense in continuing.
-    my $user = Bugzilla->user;
-    my $all_groups = $user->in_group('editusers') || $user->in_group('creategroups');
-    if (!$all_groups && !$user->can_bless) {
-        ThrowUserError('group_cannot_view');
-    }
+  if (defined $params->{names}) {
 
-    Bugzilla->switch_to_shadow_db();
+    # Get the groups by name. Check will throw an error if a bad name is given
+    foreach my $name (@{$params->{names}}) {
 
-    my $groups = [];
+      # Skip if we got this from params->{id}
+      next if grep { $_->name eq $name } @$groups;
 
-    if (defined $params->{ids}) {
-        # Get the groups by id
-        $groups = Bugzilla::Group->new_from_list($params->{ids});
+      push @$groups, Bugzilla::Group->check({name => $name});
     }
+  }
 
-    if (defined $params->{names}) {
-        # Get the groups by name. Check will throw an error if a bad name is given
-        foreach my $name (@{$params->{names}}) {
-            # Skip if we got this from params->{id}
-            next if grep { $_->name eq $name } @$groups;
-
-            push @$groups, Bugzilla::Group->check({ name => $name });
-        }
+  if (!defined $params->{ids} && !defined $params->{names}) {
+    if ($all_groups) {
+      @$groups = Bugzilla::Group->get_all;
     }
-
-    if (!defined $params->{ids} && !defined $params->{names}) {
-        if ($all_groups) {
-            @$groups = Bugzilla::Group->get_all;
-        }
-        else {
-            # Get only groups the user has bless groups too
-            $groups = $user->bless_groups;
-        }
+    else {
+      # Get only groups the user has bless groups too
+      $groups = $user->bless_groups;
     }
+  }
 
-    # Now create a result entry for each.
-    my @groups = map { $self->_group_to_hash($params, $_) } @$groups;
-    return { groups => \@groups };
+  # Now create a result entry for each.
+  my @groups = map { $self->_group_to_hash($params, $_) } @$groups;
+  return {groups => \@groups};
 }
 
 sub _group_to_hash {
-    my ($self, $params, $group) = @_;
-    my $user = Bugzilla->user;
-
-    my $field_data = {
-        id          => $self->type('int', $group->id),
-        name        => $self->type('string', $group->name),
-        description => $self->type('string', $group->description),
-    };
-
-    if ($user->in_group('creategroups')) {
-        $field_data->{is_active}    = $self->type('boolean', $group->is_active);
-        $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group);
-        $field_data->{user_regexp}  = $self->type('string', $group->user_regexp);
-    }
-
-    if ($params->{membership}) {
-        $field_data->{membership} = $self->_get_group_membership($group, $params);
-    }
-    return $field_data;
+  my ($self, $params, $group) = @_;
+  my $user = Bugzilla->user;
+
+  my $field_data = {
+    id          => $self->type('int',    $group->id),
+    name        => $self->type('string', $group->name),
+    description => $self->type('string', $group->description),
+  };
+
+  if ($user->in_group('creategroups')) {
+    $field_data->{is_active}    = $self->type('boolean', $group->is_active);
+    $field_data->{is_bug_group} = $self->type('boolean', $group->is_bug_group);
+    $field_data->{user_regexp}  = $self->type('string',  $group->user_regexp);
+  }
+
+  if ($params->{membership}) {
+    $field_data->{membership} = $self->_get_group_membership($group, $params);
+  }
+  return $field_data;
 }
 
 sub _get_group_membership {
-    my ($self, $group, $params) = @_;
-    my $user = Bugzilla->user;
+  my ($self, $group, $params) = @_;
+  my $user = Bugzilla->user;
 
-    my %users_only;
-    my $dbh = Bugzilla->dbh;
-    my $editusers = $user->in_group('editusers');
+  my %users_only;
+  my $dbh       = Bugzilla->dbh;
+  my $editusers = $user->in_group('editusers');
 
-    my $query = 'SELECT userid FROM profiles';
-    my $visibleGroups;
+  my $query = 'SELECT userid FROM profiles';
+  my $visibleGroups;
 
-    if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
-        # Show only users in visible groups.
-        $visibleGroups = $user->visible_groups_inherited;
+  if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) {
 
-        if (scalar @$visibleGroups) {
-            $query .= qq{, user_group_map AS ugm
+    # Show only users in visible groups.
+    $visibleGroups = $user->visible_groups_inherited;
+
+    if (scalar @$visibleGroups) {
+      $query .= qq{, user_group_map AS ugm
                          WHERE ugm.user_id = profiles.userid
                            AND ugm.isbless = 0
                            AND } . $dbh->sql_in('ugm.group_id', $visibleGroups);
-        }
-    } elsif ($editusers || $user->can_bless($group->id) || $user->in_group('creategroups')) {
-        $visibleGroups = 1;
-        $query .= qq{, user_group_map AS ugm
+    }
+  }
+  elsif ($editusers
+    || $user->can_bless($group->id)
+    || $user->in_group('creategroups'))
+  {
+    $visibleGroups = 1;
+    $query .= qq{, user_group_map AS ugm
                      WHERE ugm.user_id = profiles.userid
                        AND ugm.isbless = 0
                     };
-    }
-    if (!$visibleGroups) {
-        ThrowUserError('group_not_visible', { group => $group });
-    }
-
-    my $grouplist = Bugzilla::Group->flatten_group_membership($group->id);
-    $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist);
-
-    my $userids = $dbh->selectcol_arrayref($query);
-    my $user_objects = Bugzilla::User->new_from_list($userids);
-    my @users =
-        map {{
-            id                => $self->type('int', $_->id),
-            real_name         => $self->type('string', $_->name),
-            name              => $self->type('string', $_->login),
-            email             => $self->type('string', $_->email),
-            can_login         => $self->type('boolean', $_->is_enabled),
-            email_enabled     => $self->type('boolean', $_->email_enabled),
-            login_denied_text => $self->type('string', $_->disabledtext),
-        }} @$user_objects;
-
-    return \@users;
+  }
+  if (!$visibleGroups) {
+    ThrowUserError('group_not_visible', {group => $group});
+  }
+
+  my $grouplist = Bugzilla::Group->flatten_group_membership($group->id);
+  $query .= ' AND ' . $dbh->sql_in('ugm.group_id', $grouplist);
+
+  my $userids      = $dbh->selectcol_arrayref($query);
+  my $user_objects = Bugzilla::User->new_from_list($userids);
+  my @users        = map { {
+    id                => $self->type('int',     $_->id),
+    real_name         => $self->type('string',  $_->name),
+    name              => $self->type('string',  $_->login),
+    email             => $self->type('string',  $_->email),
+    can_login         => $self->type('boolean', $_->is_enabled),
+    email_enabled     => $self->type('boolean', $_->email_enabled),
+    login_denied_text => $self->type('string',  $_->disabledtext),
+  } } @$user_objects;
+
+  return \@users;
 }
 
 1;
diff --git a/Bugzilla/WebService/JSON.pm b/Bugzilla/WebService/JSON.pm
index 5c28b20f4..f670d1fd9 100644
--- a/Bugzilla/WebService/JSON.pm
+++ b/Bugzilla/WebService/JSON.pm
@@ -39,7 +39,7 @@ sub decode {
   }
 }
 
-sub _build_json  { JSON::MaybeXS->new }
+sub _build_json { JSON::MaybeXS->new }
 
 # delegation all the json options to the real json encoder.
 {
diff --git a/Bugzilla/WebService/Product.pm b/Bugzilla/WebService/Product.pm
index cdd8a0a92..0726c371d 100644
--- a/Bugzilla/WebService/Product.pm
+++ b/Bugzilla/WebService/Product.pm
@@ -20,24 +20,22 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Util qw(validate filter filter_wants);
 
 use constant READ_ONLY => qw(
-    get
-    get_accessible_products
-    get_enterable_products
-    get_selectable_products
+  get
+  get_accessible_products
+  get_enterable_products
+  get_selectable_products
 );
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    get_accessible_products
-    get_enterable_products
-    get_selectable_products
+  create
+  get
+  get_accessible_products
+  get_enterable_products
+  get_selectable_products
 );
 
-use constant FIELD_MAP => {
-    has_unconfirmed => 'allows_unconfirmed',
-    is_open         => 'isactive',
-};
+use constant FIELD_MAP =>
+  {has_unconfirmed => 'allows_unconfirmed', is_open => 'isactive',};
 
 ##################################################
 # Add aliases here for method name compatibility #
@@ -47,256 +45,240 @@ BEGIN { *get_products = \&get }
 
 # Get the ids of the products the user can search
 sub get_selectable_products {
-    Bugzilla->switch_to_shadow_db();
-    return {ids => [map {$_->id} @{Bugzilla->user->get_selectable_products}]};
+  Bugzilla->switch_to_shadow_db();
+  return {ids => [map { $_->id } @{Bugzilla->user->get_selectable_products}]};
 }
 
 # Get the ids of the products the user can enter bugs against
 sub get_enterable_products {
-    Bugzilla->switch_to_shadow_db();
-    return {ids => [map {$_->id} @{Bugzilla->user->get_enterable_products}]};
+  Bugzilla->switch_to_shadow_db();
+  return {ids => [map { $_->id } @{Bugzilla->user->get_enterable_products}]};
 }
 
 # Get the union of the products the user can search and enter bugs against.
 sub get_accessible_products {
-    Bugzilla->switch_to_shadow_db();
-    return {ids => [map {$_->id} @{Bugzilla->user->get_accessible_products}]};
+  Bugzilla->switch_to_shadow_db();
+  return {ids => [map { $_->id } @{Bugzilla->user->get_accessible_products}]};
 }
 
 # Get a list of actual products, based on list of ids or names
 our %FLAG_CACHE;
+
 sub get {
-    my ($self, $params) = validate(@_, 'ids', 'names', 'type');
-    my $user = Bugzilla->user;
-
-    Bugzilla->request_cache->{bz_etag_disable} = 1;
-
-    defined $params->{ids} || defined $params->{names} || defined $params->{type}
-        || ThrowCodeError("params_required", { function => "Product.get",
-                                               params => ['ids', 'names', 'type'] });
-
-    Bugzilla->switch_to_shadow_db();
-
-    my $products = [];
-    if (defined $params->{type}) {
-        my %product_hash;
-        foreach my $type (@{ $params->{type} }) {
-            my $result = [];
-            if ($type eq 'accessible') {
-                $result = $user->get_accessible_products();
-            }
-            elsif ($type eq 'enterable') {
-                $result = $user->get_enterable_products();
-            }
-            elsif ($type eq 'selectable') {
-                $result = $user->get_selectable_products();
-            }
-            else {
-                ThrowUserError('get_products_invalid_type',
-                               { type => $type });
-            }
-            map { $product_hash{$_->id} = $_ } @$result;
-        }
-        $products = [ values %product_hash ];
-    }
-    else {
-        $products = $user->get_accessible_products;
+  my ($self, $params) = validate(@_, 'ids', 'names', 'type');
+  my $user = Bugzilla->user;
+
+  Bugzilla->request_cache->{bz_etag_disable} = 1;
+
+       defined $params->{ids}
+    || defined $params->{names}
+    || defined $params->{type}
+    || ThrowCodeError("params_required",
+    {function => "Product.get", params => ['ids', 'names', 'type']});
+
+  Bugzilla->switch_to_shadow_db();
+
+  my $products = [];
+  if (defined $params->{type}) {
+    my %product_hash;
+    foreach my $type (@{$params->{type}}) {
+      my $result = [];
+      if ($type eq 'accessible') {
+        $result = $user->get_accessible_products();
+      }
+      elsif ($type eq 'enterable') {
+        $result = $user->get_enterable_products();
+      }
+      elsif ($type eq 'selectable') {
+        $result = $user->get_selectable_products();
+      }
+      else {
+        ThrowUserError('get_products_invalid_type', {type => $type});
+      }
+      map { $product_hash{$_->id} = $_ } @$result;
     }
+    $products = [values %product_hash];
+  }
+  else {
+    $products = $user->get_accessible_products;
+  }
 
-    my @requested_products;
+  my @requested_products;
 
-    if (defined $params->{ids}) {
-        # Create a hash with the ids the user wants
-        my %ids = map { $_ => 1 } @{$params->{ids}};
+  if (defined $params->{ids}) {
 
-        # Return the intersection of this, by grepping the ids from
-        # accessible products.
-        push(@requested_products,
-            grep { $ids{$_->id} } @$products);
-    }
+    # Create a hash with the ids the user wants
+    my %ids = map { $_ => 1 } @{$params->{ids}};
 
-    if (defined $params->{names}) {
-        # Create a hash with the names the user wants
-        my %names = map { lc($_) => 1 } @{$params->{names}};
-
-        # Return the intersection of this, by grepping the names from
-        # accessible products, union'ed with products found by ID to
-        # avoid duplicates
-        foreach my $product (grep { $names{lc $_->name} }
-                                  @$products) {
-            next if grep { $_->id == $product->id }
-                         @requested_products;
-            push @requested_products, $product;
-        }
-    }
+    # Return the intersection of this, by grepping the ids from
+    # accessible products.
+    push(@requested_products, grep { $ids{$_->id} } @$products);
+  }
 
-    # If we just requested a specific type of products without
-    # specifying ids or names, then return the entire list.
-    if (!defined $params->{ids} && !defined $params->{names}) {
-        @requested_products = @$products;
-    }
+  if (defined $params->{names}) {
+
+    # Create a hash with the names the user wants
+    my %names = map { lc($_) => 1 } @{$params->{names}};
 
-    # Now create a result entry for each.
-    local %FLAG_CACHE = ();
-    my @products = map { $self->_product_to_hash($params, $_) }
-                       @requested_products;
-    return { products => \@products };
+    # Return the intersection of this, by grepping the names from
+    # accessible products, union'ed with products found by ID to
+    # avoid duplicates
+    foreach my $product (grep { $names{lc $_->name} } @$products) {
+      next if grep { $_->id == $product->id } @requested_products;
+      push @requested_products, $product;
+    }
+  }
+
+  # If we just requested a specific type of products without
+  # specifying ids or names, then return the entire list.
+  if (!defined $params->{ids} && !defined $params->{names}) {
+    @requested_products = @$products;
+  }
+
+  # Now create a result entry for each.
+  local %FLAG_CACHE = ();
+  my @products = map { $self->_product_to_hash($params, $_) } @requested_products;
+  return {products => \@products};
 }
 
 sub create {
-    my ($self, $params) = @_;
-
-    Bugzilla->login(LOGIN_REQUIRED);
-    Bugzilla->user->in_group('editcomponents')
-        || ThrowUserError("auth_failure", { group  => "editcomponents",
-                                            action => "add",
-                                            object => "products"});
-    # Create product
-    my $args = {
-        name             => $params->{name},
-        description      => $params->{description},
-        version          => $params->{version},
-        defaultmilestone => $params->{default_milestone},
-        # create_series has no default value.
-        create_series    => defined $params->{create_series} ?
-                              $params->{create_series} : 1
-    };
-    foreach my $field (qw(has_unconfirmed is_open classification)) {
-        if (defined $params->{$field}) {
-            my $name = FIELD_MAP->{$field} || $field;
-            $args->{$name} = $params->{$field};
-        }
+  my ($self, $params) = @_;
+
+  Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->user->in_group('editcomponents')
+    || ThrowUserError("auth_failure",
+    {group => "editcomponents", action => "add", object => "products"});
+
+  # Create product
+  my $args = {
+    name             => $params->{name},
+    description      => $params->{description},
+    version          => $params->{version},
+    defaultmilestone => $params->{default_milestone},
+
+    # create_series has no default value.
+    create_series => defined $params->{create_series}
+    ? $params->{create_series}
+    : 1
+  };
+  foreach my $field (qw(has_unconfirmed is_open classification)) {
+    if (defined $params->{$field}) {
+      my $name = FIELD_MAP->{$field} || $field;
+      $args->{$name} = $params->{$field};
     }
-    my $product = Bugzilla::Product->create($args);
-    return { id => $self->type('int', $product->id) };
+  }
+  my $product = Bugzilla::Product->create($args);
+  return {id => $self->type('int', $product->id)};
 }
 
 sub _product_to_hash {
-    my ($self, $params, $product) = @_;
-
-    my $field_data = {
-        id          => $self->type('int', $product->id),
-        name        => $self->type('string', $product->name),
-        description => $self->type('string', $product->description),
-        is_active   => $self->type('boolean', $product->is_active),
-        default_milestone => $self->type('string', $product->default_milestone),
-        has_unconfirmed   => $self->type('boolean', $product->allows_unconfirmed),
-        classification => $self->type('string', $product->classification->name),
-    };
-    if (filter_wants($params, 'components')) {
-        $field_data->{components} = [map {
-            $self->_component_to_hash($_, $params)
-        } @{$product->components}];
-    }
-    if (filter_wants($params, 'versions')) {
-        $field_data->{versions} = [map {
-            $self->_version_to_hash($_, $params)
-        } @{$product->versions}];
-    }
-    if (filter_wants($params, 'milestones')) {
-        $field_data->{milestones} = [map {
-            $self->_milestone_to_hash($_, $params)
-        } @{$product->milestones}];
-    }
-    # BMO - add default hw/os
-    $field_data->{default_platform} = $self->type('string', $product->default_platform);
-    $field_data->{default_op_sys}   = $self->type('string', $product->default_op_sys);
-    # BMO - add default security group
-    $field_data->{default_security_group} = $self->type('string', $product->default_security_group);
-    return filter($params, $field_data);
+  my ($self, $params, $product) = @_;
+
+  my $field_data = {
+    id                => $self->type('int',     $product->id),
+    name              => $self->type('string',  $product->name),
+    description       => $self->type('string',  $product->description),
+    is_active         => $self->type('boolean', $product->is_active),
+    default_milestone => $self->type('string',  $product->default_milestone),
+    has_unconfirmed   => $self->type('boolean', $product->allows_unconfirmed),
+    classification    => $self->type('string',  $product->classification->name),
+  };
+  if (filter_wants($params, 'components')) {
+    $field_data->{components}
+      = [map { $self->_component_to_hash($_, $params) } @{$product->components}];
+  }
+  if (filter_wants($params, 'versions')) {
+    $field_data->{versions}
+      = [map { $self->_version_to_hash($_, $params) } @{$product->versions}];
+  }
+  if (filter_wants($params, 'milestones')) {
+    $field_data->{milestones}
+      = [map { $self->_milestone_to_hash($_, $params) } @{$product->milestones}];
+  }
+
+  # BMO - add default hw/os
+  $field_data->{default_platform}
+    = $self->type('string', $product->default_platform);
+  $field_data->{default_op_sys} = $self->type('string', $product->default_op_sys);
+
+  # BMO - add default security group
+  $field_data->{default_security_group}
+    = $self->type('string', $product->default_security_group);
+  return filter($params, $field_data);
 }
 
 sub _component_to_hash {
-    my ($self, $component, $params) = @_;
-    my $field_data = filter $params, {
-        id =>
-            $self->type('int', $component->id),
-        name =>
-            $self->type('string', $component->name),
-        description =>
-            $self->type('string', $component->description),
-        default_assigned_to =>
-            $self->type('email', $component->default_assignee->login),
-        default_qa_contact =>
-            $self->type('email', $component->default_qa_contact->login),
-        triage_owner =>
-            $self->type('email', $component->triage_owner->login),
-        sort_key =>  # sort_key is returned to match Bug.fields
-            0,
-        is_active =>
-            $self->type('boolean', $component->is_active),
-    }, undef, 'components';
-
-    if (filter_wants($params, 'flag_types', undef, 'components')) {
-        $field_data->{flag_types} = {
-            bug =>
-                [map {
-                    $FLAG_CACHE{ $_->id } //= $self->_flag_type_to_hash($_)
-                } @{$component->flag_types->{'bug'}}],
-            attachment =>
-                [map {
-                    $FLAG_CACHE{ $_->id } //= $self->_flag_type_to_hash($_)
-                } @{$component->flag_types->{'attachment'}}],
-        };
-    }
+  my ($self, $component, $params) = @_;
+  my $field_data = filter $params, {
+    id          => $self->type('int',    $component->id),
+    name        => $self->type('string', $component->name),
+    description => $self->type('string', $component->description),
+    default_assigned_to =>
+      $self->type('email', $component->default_assignee->login),
+    default_qa_contact =>
+      $self->type('email', $component->default_qa_contact->login),
+    triage_owner => $self->type('email', $component->triage_owner->login),
+    sort_key =>    # sort_key is returned to match Bug.fields
+      0,
+    is_active => $self->type('boolean', $component->is_active),
+    },
+    undef, 'components';
+
+  if (filter_wants($params, 'flag_types', undef, 'components')) {
+    $field_data->{flag_types} = {
+      bug => [
+        map { $FLAG_CACHE{$_->id} //= $self->_flag_type_to_hash($_) }
+          @{$component->flag_types->{'bug'}}
+      ],
+      attachment => [
+        map { $FLAG_CACHE{$_->id} //= $self->_flag_type_to_hash($_) }
+          @{$component->flag_types->{'attachment'}}
+      ],
+    };
+  }
 
-    return $field_data;
+  return $field_data;
 }
 
 sub _flag_type_to_hash {
-    my ($self, $flag_type) = @_;
-    return {
-        id =>
-            $self->type('int', $flag_type->id),
-        name =>
-            $self->type('string', $flag_type->name),
-        description =>
-            $self->type('string', $flag_type->description),
-        cc_list =>
-            $self->type('string', $flag_type->cc_list),
-        sort_key =>
-            $self->type('int', $flag_type->sortkey),
-        is_active =>
-            $self->type('boolean', $flag_type->is_active),
-        is_requestable =>
-            $self->type('boolean', $flag_type->is_requestable),
-        is_requesteeble =>
-            $self->type('boolean', $flag_type->is_requesteeble),
-        is_multiplicable =>
-            $self->type('boolean', $flag_type->is_multiplicable),
-        grant_group =>
-            $self->type('int', $flag_type->grant_group_id),
-        request_group =>
-            $self->type('int', $flag_type->request_group_id),
-    };
+  my ($self, $flag_type) = @_;
+  return {
+    id               => $self->type('int',     $flag_type->id),
+    name             => $self->type('string',  $flag_type->name),
+    description      => $self->type('string',  $flag_type->description),
+    cc_list          => $self->type('string',  $flag_type->cc_list),
+    sort_key         => $self->type('int',     $flag_type->sortkey),
+    is_active        => $self->type('boolean', $flag_type->is_active),
+    is_requestable   => $self->type('boolean', $flag_type->is_requestable),
+    is_requesteeble  => $self->type('boolean', $flag_type->is_requesteeble),
+    is_multiplicable => $self->type('boolean', $flag_type->is_multiplicable),
+    grant_group      => $self->type('int',     $flag_type->grant_group_id),
+    request_group    => $self->type('int',     $flag_type->request_group_id),
+  };
 }
 
 sub _version_to_hash {
-    my ($self, $version, $params) = @_;
-    return filter $params, {
-        id =>
-            $self->type('int', $version->id),
-        name =>
-            $self->type('string', $version->name),
-        sort_key =>  # sort_key is returened to match Bug.fields
-            0,
-        is_active =>
-            $self->type('boolean', $version->is_active),
-    }, undef, 'versions';
+  my ($self, $version, $params) = @_;
+  return filter $params, {
+    id   => $self->type('int',    $version->id),
+    name => $self->type('string', $version->name),
+    sort_key =>    # sort_key is returened to match Bug.fields
+      0,
+    is_active => $self->type('boolean', $version->is_active),
+    },
+    undef, 'versions';
 }
 
 sub _milestone_to_hash {
-    my ($self, $milestone, $params) = @_;
-    return filter $params, {
-        id =>
-            $self->type('int', $milestone->id),
-        name =>
-            $self->type('string', $milestone->name),
-        sort_key =>
-            $self->type('int', $milestone->sortkey),
-        is_active =>
-            $self->type('boolean', $milestone->is_active),
-    }, undef, 'milestones';
+  my ($self, $milestone, $params) = @_;
+  return filter $params,
+    {
+    id        => $self->type('int',     $milestone->id),
+    name      => $self->type('string',  $milestone->name),
+    sort_key  => $self->type('int',     $milestone->sortkey),
+    is_active => $self->type('boolean', $milestone->is_active),
+    },
+    undef, 'milestones';
 }
 
 1;
diff --git a/Bugzilla/WebService/Server.pm b/Bugzilla/WebService/Server.pm
index c4bd3e605..2aed48e22 100644
--- a/Bugzilla/WebService/Server.pm
+++ b/Bugzilla/WebService/Server.pm
@@ -22,88 +22,93 @@ use Module::Runtime qw(require_module);
 use Try::Tiny;
 
 sub handle_login {
-    my ($self, $class, $method, $full_method) = @_;
-    # Throw error if the supplied class does not exist or the method is private
-    ThrowCodeError('unknown_method', {method => $full_method}) if (!$class or $method =~ /^_/);
-
-    # We never want to create a new session unless the user is calling the
-    # login method.  Setting dont_persist_session makes
-    # Bugzilla::Auth::_handle_login_result() skip calling persist_login().
-    if ($full_method ne 'User.login') {
-        Bugzilla->request_cache->{dont_persist_session} = 1;
-    }
-
-    try {
-        require_module($class);
-    }
-    catch {
-        ThrowCodeError('unknown_method', {method => $full_method});
-        FATAL($_);
-    };
-    return if ($class->login_exempt($method)
-               and !defined Bugzilla->input_params->{Bugzilla_login});
-    Bugzilla->login();
-
-    Bugzilla::Hook::process(
-        'webservice_before_call',
-        { 'method'  => $method, full_method => $full_method });
+  my ($self, $class, $method, $full_method) = @_;
+
+  # Throw error if the supplied class does not exist or the method is private
+  ThrowCodeError('unknown_method', {method => $full_method})
+    if (!$class or $method =~ /^_/);
+
+  # We never want to create a new session unless the user is calling the
+  # login method.  Setting dont_persist_session makes
+  # Bugzilla::Auth::_handle_login_result() skip calling persist_login().
+  if ($full_method ne 'User.login') {
+    Bugzilla->request_cache->{dont_persist_session} = 1;
+  }
+
+  try {
+    require_module($class);
+  }
+  catch {
+    ThrowCodeError('unknown_method', {method => $full_method});
+    FATAL($_);
+  };
+  return
+    if ($class->login_exempt($method)
+    and !defined Bugzilla->input_params->{Bugzilla_login});
+  Bugzilla->login();
+
+  Bugzilla::Hook::process('webservice_before_call',
+    {'method' => $method, full_method => $full_method});
 }
 
 sub datetime_format_inbound {
-    my ($self, $time) = @_;
-
-    my $converted = datetime_from($time, Bugzilla->local_timezone);
-    if (!defined $converted) {
-        ThrowUserError('illegal_date', { date => $time });
-    }
-    $time = $converted->ymd() . ' ' . $converted->hms();
-    return $time
+  my ($self, $time) = @_;
+
+  my $converted = datetime_from($time, Bugzilla->local_timezone);
+  if (!defined $converted) {
+    ThrowUserError('illegal_date', {date => $time});
+  }
+  $time = $converted->ymd() . ' ' . $converted->hms();
+  return $time;
 }
 
 sub datetime_format_outbound {
-    my ($self, $date) = @_;
-
-    return undef if (!defined $date or $date eq '');
-
-    my $time = $date;
-    if (blessed($date)) {
-        # We expect this to mean we were sent a datetime object
-        $time->set_time_zone('UTC');
-    } else {
-        # We always send our time in UTC, for consistency.
-        # passed in value is likely a string, create a datetime object
-        $time = datetime_from($date, 'UTC');
-    }
-    return $time->iso8601();
+  my ($self, $date) = @_;
+
+  return undef if (!defined $date or $date eq '');
+
+  my $time = $date;
+  if (blessed($date)) {
+
+    # We expect this to mean we were sent a datetime object
+    $time->set_time_zone('UTC');
+  }
+  else {
+    # We always send our time in UTC, for consistency.
+    # passed in value is likely a string, create a datetime object
+    $time = datetime_from($date, 'UTC');
+  }
+  return $time->iso8601();
 }
 
 # ETag support
 sub bz_etag {
-    my ($self, $data) = @_;
-    my $cache = Bugzilla->request_cache;
-
-    if (Bugzilla->request_cache->{bz_etag_disable}) {
-      return undef;
+  my ($self, $data) = @_;
+  my $cache = Bugzilla->request_cache;
+
+  if (Bugzilla->request_cache->{bz_etag_disable}) {
+    return undef;
+  }
+  elsif (defined $data) {
+
+    # Serialize the data if passed a reference
+    local $Storable::canonical = 1;
+    $data = freeze($data) if ref $data;
+
+    # Wide characters cause md5_base64() to die.
+    utf8::encode($data) if utf8::is_utf8($data);
+
+    # Append content_type to the end of the data
+    # string as we want the etag to be unique to
+    # the content_type. We do not need this for
+    # XMLRPC as text/xml is always returned.
+    if (blessed($self) && $self->can('content_type')) {
+      $data .= $self->content_type if $self->content_type;
     }
-    elsif (defined $data) {
-        # Serialize the data if passed a reference
-        local $Storable::canonical = 1;
-        $data = freeze($data) if ref $data;
-
-        # Wide characters cause md5_base64() to die.
-        utf8::encode($data) if utf8::is_utf8($data);
-
-        # Append content_type to the end of the data
-        # string as we want the etag to be unique to
-        # the content_type. We do not need this for
-        # XMLRPC as text/xml is always returned.
-        if (blessed($self) && $self->can('content_type')) {
-            $data .= $self->content_type if $self->content_type;
-        }
-
-        $cache->{'bz_etag'} = md5_base64($data);
-    }
-    return $cache->{'bz_etag'};
+
+    $cache->{'bz_etag'} = md5_base64($data);
+  }
+  return $cache->{'bz_etag'};
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/JSONRPC.pm b/Bugzilla/WebService/Server/JSONRPC.pm
index 12a3143cc..ef00737ad 100644
--- a/Bugzilla/WebService/Server/JSONRPC.pm
+++ b/Bugzilla/WebService/Server/JSONRPC.pm
@@ -12,16 +12,17 @@ use strict;
 use warnings;
 
 use Bugzilla::WebService::Server;
-BEGIN {
-    our @ISA = qw(Bugzilla::WebService::Server);
 
-    if (eval { require JSON::RPC::Server::CGI }) {
-        unshift(@ISA, 'JSON::RPC::Server::CGI');
-    }
-    else {
-        require JSON::RPC::Legacy::Server::CGI;
-        unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI');
-    }
+BEGIN {
+  our @ISA = qw(Bugzilla::WebService::Server);
+
+  if (eval { require JSON::RPC::Server::CGI }) {
+    unshift(@ISA, 'JSON::RPC::Server::CGI');
+  }
+  else {
+    require JSON::RPC::Legacy::Server::CGI;
+    unshift(@ISA, 'JSON::RPC::Legacy::Server::CGI');
+  }
 }
 
 use Bugzilla::Error;
@@ -40,88 +41,92 @@ use Bugzilla::WebService::JSON;
 #####################################
 
 sub new {
-    my $class = shift;
-    my $self = $class->SUPER::new(@_);
-    Bugzilla->_json_server($self);
-    $self->dispatch(WS_DISPATCH);
-    $self->return_die_message(1);
-    return $self;
+  my $class = shift;
+  my $self  = $class->SUPER::new(@_);
+  Bugzilla->_json_server($self);
+  $self->dispatch(WS_DISPATCH);
+  $self->return_die_message(1);
+  return $self;
 }
 
 sub create_json_coder {
-    my $self = shift;
-    my $json = Bugzilla::WebService::JSON->new;
-    $json->allow_blessed(1);
-    $json->convert_blessed(1);
-    $json->allow_nonref(1);
-    # This may seem a little backwards, but what this really means is
-    # "don't convert our utf8 into byte strings, just leave it as a
-    # utf8 string."
-    $json->utf8(0) if Bugzilla->params->{'utf8'};
-    return $json;
+  my $self = shift;
+  my $json = Bugzilla::WebService::JSON->new;
+  $json->allow_blessed(1);
+  $json->convert_blessed(1);
+  $json->allow_nonref(1);
+
+  # This may seem a little backwards, but what this really means is
+  # "don't convert our utf8 into byte strings, just leave it as a
+  # utf8 string."
+  $json->utf8(0) if Bugzilla->params->{'utf8'};
+  return $json;
 }
 
 # Override the JSON::RPC method to return our CGI object instead of theirs.
 sub cgi { return Bugzilla->cgi; }
 
 sub response_header {
-    my $self = shift;
-    # The HTTP body needs to be bytes (not a utf8 string) for recent
-    # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this
-    # properly. $_[1] is the HTTP body content we're going to be sending.
-    if (utf8::is_utf8($_[1])) {
-        utf8::encode($_[1]);
-        # Since we're going to just be sending raw bytes, we need to
-        # set STDOUT to not expect utf8.
-        disable_utf8();
-    }
-    return $self->SUPER::response_header(@_);
+  my $self = shift;
+
+  # The HTTP body needs to be bytes (not a utf8 string) for recent
+  # versions of HTTP::Message, but JSON::RPC::Server doesn't handle this
+  # properly. $_[1] is the HTTP body content we're going to be sending.
+  if (utf8::is_utf8($_[1])) {
+    utf8::encode($_[1]);
+
+    # Since we're going to just be sending raw bytes, we need to
+    # set STDOUT to not expect utf8.
+    disable_utf8();
+  }
+  return $self->SUPER::response_header(@_);
 }
 
 sub response {
-    my ($self, $response) = @_;
-    my $cgi = $self->cgi;
-
-    # Implement JSONP.
-    if (my $callback = $self->_bz_callback) {
-        my $content = $response->content;
-        if (blessed $content) {
-          $content = $content->encode;
-        }
-        # Prepend the JSONP response with /**/ in order to protect
-        # against possible encoding attacks (e.g., affecting Flash).
-        $response->content("/**/$callback($content)");
-    }
-
-    # Use $cgi->header properly instead of just printing text directly.
-    # This fixes various problems, including sending Bugzilla's cookies
-    # properly.
-    my $headers = $response->headers;
-    my @header_args;
-    foreach my $name ($headers->header_field_names) {
-        my @values = $headers->header($name);
-        $name =~ s/-/_/g;
-        foreach my $value (@values) {
-            push(@header_args, "-$name", $value);
-        }
-    }
-
-    # ETag support
-    my $etag = $self->bz_etag;
-    if ($etag && $cgi->check_etag($etag)) {
-        push(@header_args, "-ETag", $etag);
-        print $cgi->header(-status => '304 Not Modified', @header_args);
-    }
-    else {
-        push(@header_args, "-ETag", $etag) if $etag;
-        print $cgi->header(-status => $response->code, @header_args);
-        my $content = $response->content;
-        if (blessed $content) {
-          $content = $content->encode;
-          utf8::encode($content);
-        }
-        print $content;
-    }
+  my ($self, $response) = @_;
+  my $cgi = $self->cgi;
+
+  # Implement JSONP.
+  if (my $callback = $self->_bz_callback) {
+    my $content = $response->content;
+    if (blessed $content) {
+      $content = $content->encode;
+    }
+
+    # Prepend the JSONP response with /**/ in order to protect
+    # against possible encoding attacks (e.g., affecting Flash).
+    $response->content("/**/$callback($content)");
+  }
+
+  # Use $cgi->header properly instead of just printing text directly.
+  # This fixes various problems, including sending Bugzilla's cookies
+  # properly.
+  my $headers = $response->headers;
+  my @header_args;
+  foreach my $name ($headers->header_field_names) {
+    my @values = $headers->header($name);
+    $name =~ s/-/_/g;
+    foreach my $value (@values) {
+      push(@header_args, "-$name", $value);
+    }
+  }
+
+  # ETag support
+  my $etag = $self->bz_etag;
+  if ($etag && $cgi->check_etag($etag)) {
+    push(@header_args, "-ETag", $etag);
+    print $cgi->header(-status => '304 Not Modified', @header_args);
+  }
+  else {
+    push(@header_args, "-ETag", $etag) if $etag;
+    print $cgi->header(-status => $response->code, @header_args);
+    my $content = $response->content;
+    if (blessed $content) {
+      $content = $content->encode;
+      utf8::encode($content);
+    }
+    print $content;
+  }
 }
 
 # The JSON-RPC 1.1 GET specification is not so great--you can't specify
@@ -133,70 +138,69 @@ sub response {
 # Base64 encoded, because that is ridiculous and obnoxious for JavaScript
 # clients.
 sub retrieve_json_from_get {
-    my $self = shift;
-    my $cgi = $self->cgi;
-
-    my %input;
-
-    # Both version and id must be set before any errors are thrown.
-    if ($cgi->param('version')) {
-        $self->version(scalar $cgi->param('version'));
-        $input{version} = $cgi->param('version');
-    }
-    else {
-        $self->version('1.0');
-    }
-
-    # The JSON-RPC 2.0 spec says that any request that omits an id doesn't
-    # want a response. However, in an HTTP GET situation, it's stupid to
-    # expect all clients to specify some id parameter just to get a response,
-    # so we don't require it.
-    my $id;
-    if (defined $cgi->param('id')) {
-        $id = $cgi->param('id');
-    }
-    # However, JSON::RPC does require that an id exist in most cases, in
-    # order to throw proper errors. We use the installation's urlbase as
-    # the id, in this case.
-    else {
-        $id = Bugzilla->localconfig->{urlbase};
-    }
-    # Setting _bz_request_id here is required in case we throw errors early,
-    # before _handle.
-    $self->{_bz_request_id} = $input{id} = $id;
-
-    # _bz_callback can throw an error, so we have to set it here, after we're
-    # ready to throw errors.
-    $self->_bz_callback(scalar $cgi->param('callback'));
-
-    if (!$cgi->param('method')) {
-        ThrowUserError('json_rpc_get_method_required');
-    }
-    $input{method} = $cgi->param('method');
-
-    my $params;
-    if (defined $cgi->param('params')) {
-        local $@;
-        $params = eval {
-            $self->json->decode(scalar $cgi->param('params'))
-        };
-        if ($@) {
-            ThrowUserError('json_rpc_invalid_params',
-                           { params => scalar $cgi->param('params'),
-                             err_msg  => $@ });
-        }
-    }
-    elsif (!$self->version or $self->version ne '1.1') {
-        $params = [];
-    }
-    else {
-        $params = {};
-    }
-
-    $input{params} = $params;
-
-    my $json = $self->json->encode(\%input);
-    return $json;
+  my $self = shift;
+  my $cgi  = $self->cgi;
+
+  my %input;
+
+  # Both version and id must be set before any errors are thrown.
+  if ($cgi->param('version')) {
+    $self->version(scalar $cgi->param('version'));
+    $input{version} = $cgi->param('version');
+  }
+  else {
+    $self->version('1.0');
+  }
+
+  # The JSON-RPC 2.0 spec says that any request that omits an id doesn't
+  # want a response. However, in an HTTP GET situation, it's stupid to
+  # expect all clients to specify some id parameter just to get a response,
+  # so we don't require it.
+  my $id;
+  if (defined $cgi->param('id')) {
+    $id = $cgi->param('id');
+  }
+
+  # However, JSON::RPC does require that an id exist in most cases, in
+  # order to throw proper errors. We use the installation's urlbase as
+  # the id, in this case.
+  else {
+    $id = Bugzilla->localconfig->{urlbase};
+  }
+
+  # Setting _bz_request_id here is required in case we throw errors early,
+  # before _handle.
+  $self->{_bz_request_id} = $input{id} = $id;
+
+  # _bz_callback can throw an error, so we have to set it here, after we're
+  # ready to throw errors.
+  $self->_bz_callback(scalar $cgi->param('callback'));
+
+  if (!$cgi->param('method')) {
+    ThrowUserError('json_rpc_get_method_required');
+  }
+  $input{method} = $cgi->param('method');
+
+  my $params;
+  if (defined $cgi->param('params')) {
+    local $@;
+    $params = eval { $self->json->decode(scalar $cgi->param('params')) };
+    if ($@) {
+      ThrowUserError('json_rpc_invalid_params',
+        {params => scalar $cgi->param('params'), err_msg => $@});
+    }
+  }
+  elsif (!$self->version or $self->version ne '1.1') {
+    $params = [];
+  }
+  else {
+    $params = {};
+  }
+
+  $input{params} = $params;
+
+  my $json = $self->json->encode(\%input);
+  return $json;
 }
 
 #######################################
@@ -204,72 +208,76 @@ sub retrieve_json_from_get {
 #######################################
 
 sub type {
-    my ($self, $type, $value) = @_;
-
-    # This is the only type that does something special with undef.
-    if ($type eq 'boolean') {
-        return $value ? JSON::true : JSON::false;
-    }
-
-    return JSON::null if !defined $value;
-
-    my $retval = $value;
-
-    if ($type eq 'int') {
-        $retval = int($value);
-    }
-    if ($type eq 'double') {
-        $retval = 0.0 + $value;
-    }
-    elsif ($type eq 'string') {
-        # Forces string context, so that JSON will make it a string.
-        $retval = "$value";
-    }
-    elsif ($type eq 'dateTime') {
-        # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T
-        $retval = $self->datetime_format_outbound($value);
-    }
-    elsif ($type eq 'base64') {
-        utf8::encode($value) if utf8::is_utf8($value);
-        $retval = encode_base64($value, '');
-    }
-    elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) {
-        $retval = email_filter($value);
-    }
-
-    return $retval;
+  my ($self, $type, $value) = @_;
+
+  # This is the only type that does something special with undef.
+  if ($type eq 'boolean') {
+    return $value ? JSON::true : JSON::false;
+  }
+
+  return JSON::null if !defined $value;
+
+  my $retval = $value;
+
+  if ($type eq 'int') {
+    $retval = int($value);
+  }
+  if ($type eq 'double') {
+    $retval = 0.0 + $value;
+  }
+  elsif ($type eq 'string') {
+
+    # Forces string context, so that JSON will make it a string.
+    $retval = "$value";
+  }
+  elsif ($type eq 'dateTime') {
+
+    # ISO-8601 "YYYYMMDDTHH:MM:SS" with a literal T
+    $retval = $self->datetime_format_outbound($value);
+  }
+  elsif ($type eq 'base64') {
+    utf8::encode($value) if utf8::is_utf8($value);
+    $retval = encode_base64($value, '');
+  }
+  elsif ($type eq 'email' && Bugzilla->params->{'webservice_email_filter'}) {
+    $retval = email_filter($value);
+  }
+
+  return $retval;
 }
 
 sub datetime_format_outbound {
-    my $self = shift;
-    # YUI expects ISO8601 in UTC time; including TZ specifier
-    return $self->SUPER::datetime_format_outbound(@_) . 'Z';
+  my $self = shift;
+
+  # YUI expects ISO8601 in UTC time; including TZ specifier
+  return $self->SUPER::datetime_format_outbound(@_) . 'Z';
 }
 
 sub handle_login {
-    my $self = shift;
-
-    # If we're being called using GET, we don't allow cookie-based or Env
-    # login, because GET requests can be done cross-domain, and we don't
-    # want private data showing up on another site unless the user
-    # explicitly gives that site their username and password. (This is
-    # particularly important for JSONP, which would allow a remote site
-    # to use private data without the user's knowledge, unless we had this
-    # protection in place.)
-    if ($self->request->method ne 'POST') {
-        # XXX There's no particularly good way for us to get a parameter
-        # to Bugzilla->login at this point, so we pass this information
-        # around using request_cache, which is a bit of a hack. The
-        # implementation of it is in Bugzilla::Auth::Login::Stack.
-        Bugzilla->request_cache->{auth_no_automatic_login} = 1;
-    }
-
-    my $path = $self->path_info;
-    my $class = $self->{dispatch_path}->{$path};
-    my $full_method = $self->_bz_method_name;
-    $full_method =~ /^\S+\.(\S+)/;
-    my $method = $1;
-    $self->SUPER::handle_login($class, $method, $full_method);
+  my $self = shift;
+
+  # If we're being called using GET, we don't allow cookie-based or Env
+  # login, because GET requests can be done cross-domain, and we don't
+  # want private data showing up on another site unless the user
+  # explicitly gives that site their username and password. (This is
+  # particularly important for JSONP, which would allow a remote site
+  # to use private data without the user's knowledge, unless we had this
+  # protection in place.)
+  if ($self->request->method ne 'POST') {
+
+    # XXX There's no particularly good way for us to get a parameter
+    # to Bugzilla->login at this point, so we pass this information
+    # around using request_cache, which is a bit of a hack. The
+    # implementation of it is in Bugzilla::Auth::Login::Stack.
+    Bugzilla->request_cache->{auth_no_automatic_login} = 1;
+  }
+
+  my $path        = $self->path_info;
+  my $class       = $self->{dispatch_path}->{$path};
+  my $full_method = $self->_bz_method_name;
+  $full_method =~ /^\S+\.(\S+)/;
+  my $method = $1;
+  $self->SUPER::handle_login($class, $method, $full_method);
 }
 
 ######################################
@@ -278,165 +286,165 @@ sub handle_login {
 
 # Store the ID of the current call, because Bugzilla::Error will need it.
 sub _handle {
-    my $self = shift;
-    my ($obj) = @_;
-    $self->{_bz_request_id} = $obj->{id};
+  my $self = shift;
+  my ($obj) = @_;
+  $self->{_bz_request_id} = $obj->{id};
 
-    my $result = $self->SUPER::_handle(@_);
+  my $result = $self->SUPER::_handle(@_);
 
-    # Set the ETag if not already set in the webservice methods.
-    my $etag = $self->bz_etag;
-    if (!$etag && ref $result) {
-        my $data = $self->json->decode($result)->{'result'};
-        $self->bz_etag($data);
-    }
+  # Set the ETag if not already set in the webservice methods.
+  my $etag = $self->bz_etag;
+  if (!$etag && ref $result) {
+    my $data = $self->json->decode($result)->{'result'};
+    $self->bz_etag($data);
+  }
 
-    return $result;
+  return $result;
 }
 
 # Make all error messages returned by JSON::RPC go into the 100000
 # range, and bring down all our errors into the normal range.
 sub _error {
-    my ($self, $id, $code) = (shift, shift, shift);
-    # All JSON::RPC errors are less than 1000.
-    if ($code < 1000) {
-        $code += 100000;
-    }
-    # Bugzilla::Error adds 100,000 to all *our* errors, so
-    # we know they came from us.
-    elsif ($code > 100000) {
-        $code -= 100000;
-    }
-
-    # We can't just set $_[1] because it's not always settable,
-    # in JSON::RPC::Server.
-    unshift(@_, $id, $code);
-    my $json = $self->SUPER::_error(@_);
-
-    # We want to always send the JSON-RPC 1.1 error format, although
-    # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter.
-    if (!$self->version or $self->version ne '1.1') {
-        my $object = $self->json->decode($json);
-        my $message = $object->{error};
-        # Just assure that future versions of JSON::RPC don't change the
-        # JSON-RPC 1.0 error format.
-        if (!ref $message) {
-            $object->{error} = {
-                code    => $code,
-                message => $message,
-            };
-            $json = $self->json->encode($object);
-        }
-    }
-    return $json;
+  my ($self, $id, $code) = (shift, shift, shift);
+
+  # All JSON::RPC errors are less than 1000.
+  if ($code < 1000) {
+    $code += 100000;
+  }
+
+  # Bugzilla::Error adds 100,000 to all *our* errors, so
+  # we know they came from us.
+  elsif ($code > 100000) {
+    $code -= 100000;
+  }
+
+  # We can't just set $_[1] because it's not always settable,
+  # in JSON::RPC::Server.
+  unshift(@_, $id, $code);
+  my $json = $self->SUPER::_error(@_);
+
+  # We want to always send the JSON-RPC 1.1 error format, although
+  # If we're not in JSON-RPC 1.1, we don't need the silly "name" parameter.
+  if (!$self->version or $self->version ne '1.1') {
+    my $object  = $self->json->decode($json);
+    my $message = $object->{error};
+
+    # Just assure that future versions of JSON::RPC don't change the
+    # JSON-RPC 1.0 error format.
+    if (!ref $message) {
+      $object->{error} = {code => $code, message => $message,};
+      $json = $self->json->encode($object);
+    }
+  }
+  return $json;
 }
 
 # This handles dispatching our calls to the appropriate class based on
 # the name of the method.
 sub _find_procedure {
-    my $self = shift;
+  my $self = shift;
 
-    my $method = shift;
-    $self->{_bz_method_name} = $method;
+  my $method = shift;
+  $self->{_bz_method_name} = $method;
 
-    # This tricks SUPER::_find_procedure into finding the right class.
-    $method =~ /^(\S+)\.(\S+)$/;
-    $self->path_info($1);
-    unshift(@_, $2);
+  # This tricks SUPER::_find_procedure into finding the right class.
+  $method =~ /^(\S+)\.(\S+)$/;
+  $self->path_info($1);
+  unshift(@_, $2);
 
-    return $self->SUPER::_find_procedure(@_);
+  return $self->SUPER::_find_procedure(@_);
 }
 
 # This is a hacky way to do something right before methods are called.
 # This is the last thing that JSON::RPC::Server::_handle calls right before
 # the method is actually called.
 sub _argument_type_check {
-    my $self = shift;
-    my $params = $self->SUPER::_argument_type_check(@_);
-
-    # JSON-RPC 1.0 requires all parameters to be passed as an array, so
-    # we just pull out the first item and assume it's an object.
-    my $params_is_array;
-    if (ref $params eq 'ARRAY') {
-        $params = $params->[0];
-        $params_is_array = 1;
-    }
-
-    taint_data($params);
-
-    # Now, convert dateTime fields on input.
-    $self->_bz_method_name =~ /^(\S+)\.(\S+)$/;
-    my ($class, $method) = ($1, $2);
-    my $pkg = $self->{dispatch_path}->{$class};
-    my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
-    foreach my $field (@date_fields) {
-        if (defined $params->{$field}) {
-            my $value = $params->{$field};
-            if (ref $value eq 'ARRAY') {
-                $params->{$field} =
-                    [ map { $self->datetime_format_inbound($_) } @$value ];
-            }
-            else {
-                $params->{$field} = $self->datetime_format_inbound($value);
-            }
-        }
-    }
-    my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] };
-    foreach my $field (@base64_fields) {
-        if (defined $params->{$field}) {
-            $params->{$field} = decode_base64($params->{$field});
-        }
-    }
-
-    # Update the params to allow for several convenience key/values
-    # use for authentication
-    fix_credentials($params, $self->cgi);
-
-    Bugzilla->input_params($params);
-
-    if ($self->request->method eq 'POST') {
-        # CSRF is possible via XMLHttpRequest when the Content-Type header
-        # is not application/json (for example: text/plain or
-        # application/x-www-form-urlencoded).
-        # application/json is the single official MIME type, per RFC 4627.
-        my $content_type = $self->cgi->content_type;
-        # The charset can be appended to the content type, so we use a regexp.
-        if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) {
-            ThrowUserError('json_rpc_illegal_content_type',
-                            { content_type => $content_type });
-        }
-    }
-    else {
-        # When being called using GET, we don't allow calling
-        # methods that can change data. This protects us against cross-site
-        # request forgeries.
-        if (!grep($_ eq $method, $pkg->READ_ONLY)) {
-            ThrowUserError('json_rpc_post_only',
-                           { method => $self->_bz_method_name });
-        }
-    }
-
-    # Only allowed methods to be used from our whitelist
-    if (none { $_ eq $method} $pkg->PUBLIC_METHODS) {
-        ThrowCodeError('unknown_method', { method => $self->_bz_method_name });
-    }
-
-    # This is the best time to do login checks.
-    $self->handle_login();
-
-    # Bugzilla::WebService packages call internal methods like
-    # $self->_some_private_method. So we have to inherit from
-    # that class as well as this Server class.
-    my $new_class = ref($self) . '::' . $pkg;
-    my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
-    eval "package $new_class;$isa_string;";
-    bless $self, $new_class;
-
-    if ($params_is_array) {
-        $params = [$params];
-    }
-
-    return $params;
+  my $self   = shift;
+  my $params = $self->SUPER::_argument_type_check(@_);
+
+  # JSON-RPC 1.0 requires all parameters to be passed as an array, so
+  # we just pull out the first item and assume it's an object.
+  my $params_is_array;
+  if (ref $params eq 'ARRAY') {
+    $params          = $params->[0];
+    $params_is_array = 1;
+  }
+
+  taint_data($params);
+
+  # Now, convert dateTime fields on input.
+  $self->_bz_method_name =~ /^(\S+)\.(\S+)$/;
+  my ($class, $method) = ($1, $2);
+  my $pkg = $self->{dispatch_path}->{$class};
+  my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []};
+  foreach my $field (@date_fields) {
+    if (defined $params->{$field}) {
+      my $value = $params->{$field};
+      if (ref $value eq 'ARRAY') {
+        $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value];
+      }
+      else {
+        $params->{$field} = $self->datetime_format_inbound($value);
+      }
+    }
+  }
+  my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []};
+  foreach my $field (@base64_fields) {
+    if (defined $params->{$field}) {
+      $params->{$field} = decode_base64($params->{$field});
+    }
+  }
+
+  # Update the params to allow for several convenience key/values
+  # use for authentication
+  fix_credentials($params, $self->cgi);
+
+  Bugzilla->input_params($params);
+
+  if ($self->request->method eq 'POST') {
+
+    # CSRF is possible via XMLHttpRequest when the Content-Type header
+    # is not application/json (for example: text/plain or
+    # application/x-www-form-urlencoded).
+    # application/json is the single official MIME type, per RFC 4627.
+    my $content_type = $self->cgi->content_type;
+
+    # The charset can be appended to the content type, so we use a regexp.
+    if ($content_type !~ m{^application/json(-rpc)?(;.*)?$}i) {
+      ThrowUserError('json_rpc_illegal_content_type',
+        {content_type => $content_type});
+    }
+  }
+  else {
+    # When being called using GET, we don't allow calling
+    # methods that can change data. This protects us against cross-site
+    # request forgeries.
+    if (!grep($_ eq $method, $pkg->READ_ONLY)) {
+      ThrowUserError('json_rpc_post_only', {method => $self->_bz_method_name});
+    }
+  }
+
+  # Only allowed methods to be used from our whitelist
+  if (none { $_ eq $method } $pkg->PUBLIC_METHODS) {
+    ThrowCodeError('unknown_method', {method => $self->_bz_method_name});
+  }
+
+  # This is the best time to do login checks.
+  $self->handle_login();
+
+  # Bugzilla::WebService packages call internal methods like
+  # $self->_some_private_method. So we have to inherit from
+  # that class as well as this Server class.
+  my $new_class  = ref($self) . '::' . $pkg;
+  my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
+  eval "package $new_class;$isa_string;";
+  bless $self, $new_class;
+
+  if ($params_is_array) {
+    $params = [$params];
+  }
+
+  return $params;
 }
 
 ##########################
@@ -445,22 +453,24 @@ sub _argument_type_check {
 
 # _bz_method_name is stored by _find_procedure for later use.
 sub _bz_method_name {
-    return $_[0]->{_bz_method_name};
+  return $_[0]->{_bz_method_name};
 }
 
 sub _bz_callback {
-    my ($self, $value) = @_;
-    if (defined $value) {
-        $value = trim($value);
-        # We don't use \w because we don't want to allow Unicode here.
-        if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) {
-            ThrowUserError('json_rpc_invalid_callback', { callback => $value });
-        }
-        $self->{_bz_callback} = $value;
-        # JSONP needs to be parsed by a JS parser, not by a JSON parser.
-        $self->content_type('text/javascript');
+  my ($self, $value) = @_;
+  if (defined $value) {
+    $value = trim($value);
+
+    # We don't use \w because we don't want to allow Unicode here.
+    if ($value !~ /^[A-Za-z0-9_\.\[\]]+$/) {
+      ThrowUserError('json_rpc_invalid_callback', {callback => $value});
     }
-    return $self->{_bz_callback};
+    $self->{_bz_callback} = $value;
+
+    # JSONP needs to be parsed by a JS parser, not by a JSON parser.
+    $self->content_type('text/javascript');
+  }
+  return $self->{_bz_callback};
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST.pm b/Bugzilla/WebService/Server/REST.pm
index 5d8367410..781960c68 100644
--- a/Bugzilla/WebService/Server/REST.pm
+++ b/Bugzilla/WebService/Server/REST.pm
@@ -41,146 +41,146 @@ use Module::Runtime qw(require_module);
 ###########################
 
 sub handle {
-    my ($self)  = @_;
-
-    # Determine how the data should be represented. We do this early so
-    # errors will also be returned with the proper content type.
-    # If no accept header was sent or the content types specified were not
-    # matched, we default to the first type in the whitelist.
-    $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST()));
-
-    # Using current path information, decide which class/method to
-    # use to serve the request. Throw error if no resource was found
-    # unless we were looking for OPTIONS
-    if (!$self->_find_resource($self->cgi->path_info)) {
-        if ($self->request->method eq 'OPTIONS'
-            && $self->bz_rest_options)
-        {
-            my $response = $self->response_header(STATUS_OK, "");
-            my $options_string = join(', ', @{ $self->bz_rest_options });
-            $response->header('Allow' => $options_string,
-                              'Access-Control-Allow-Methods' => $options_string);
-            return $self->response($response);
-        }
-
-        ThrowUserError("rest_invalid_resource",
-                       { path   => $self->cgi->path_info,
-                         method => $self->request->method });
+  my ($self) = @_;
+
+  # Determine how the data should be represented. We do this early so
+  # errors will also be returned with the proper content type.
+  # If no accept header was sent or the content types specified were not
+  # matched, we default to the first type in the whitelist.
+  $self->content_type($self->_best_content_type(REST_CONTENT_TYPE_WHITELIST()));
+
+  # Using current path information, decide which class/method to
+  # use to serve the request. Throw error if no resource was found
+  # unless we were looking for OPTIONS
+  if (!$self->_find_resource($self->cgi->path_info)) {
+    if ($self->request->method eq 'OPTIONS' && $self->bz_rest_options) {
+      my $response = $self->response_header(STATUS_OK, "");
+      my $options_string = join(', ', @{$self->bz_rest_options});
+      $response->header(
+        'Allow'                        => $options_string,
+        'Access-Control-Allow-Methods' => $options_string
+      );
+      return $self->response($response);
     }
 
-    # Dispatch to the proper module
-    my $class  = $self->bz_class_name;
-    my ($path) = $class =~ /::([^:]+)$/;
-    $self->path_info($path);
-    delete $self->{dispatch_path};
-    $self->dispatch({ $path => $class });
+    ThrowUserError("rest_invalid_resource",
+      {path => $self->cgi->path_info, method => $self->request->method});
+  }
 
-    my $params = $self->_retrieve_json_params;
+  # Dispatch to the proper module
+  my $class = $self->bz_class_name;
+  my ($path) = $class =~ /::([^:]+)$/;
+  $self->path_info($path);
+  delete $self->{dispatch_path};
+  $self->dispatch({$path => $class});
 
-    fix_credentials($params, $self->cgi);
+  my $params = $self->_retrieve_json_params;
 
-    # Fix includes/excludes for each call
-    rest_include_exclude($params);
+  fix_credentials($params, $self->cgi);
 
-    # Set callback name if content-type is 'application/javascript'
-    if ($params->{'callback'}
-        || $self->content_type eq 'application/javascript')
-    {
-        $self->_bz_callback($params->{'callback'} || 'callback');
-    }
+  # Fix includes/excludes for each call
+  rest_include_exclude($params);
 
-    Bugzilla->input_params($params);
+  # Set callback name if content-type is 'application/javascript'
+  if ($params->{'callback'} || $self->content_type eq 'application/javascript') {
+    $self->_bz_callback($params->{'callback'} || 'callback');
+  }
 
-    # Set the JSON version to 1.1 and the id to the current urlbase
-    # also set up the correct handler method
-    my $obj = {
-        version => '1.1',
-        id      => Bugzilla->localconfig->{urlbase},
-        method  => $self->bz_method_name,
-        params  => $params
-    };
+  Bugzilla->input_params($params);
 
-    # Execute the handler
-    my $result = $self->_handle($obj);
+  # Set the JSON version to 1.1 and the id to the current urlbase
+  # also set up the correct handler method
+  my $obj = {
+    version => '1.1',
+    id      => Bugzilla->localconfig->{urlbase},
+    method  => $self->bz_method_name,
+    params  => $params
+  };
 
-    if (!$self->error_response_header) {
-        return $self->response(
-            $self->response_header($self->bz_success_code || STATUS_OK, $result));
-    }
+  # Execute the handler
+  my $result = $self->_handle($obj);
 
-    $self->response($self->error_response_header);
+  if (!$self->error_response_header) {
+    return $self->response(
+      $self->response_header($self->bz_success_code || STATUS_OK, $result));
+  }
+
+  $self->response($self->error_response_header);
 }
 
 sub response {
-    my ($self, $response) = @_;
-
-    # If we have thrown an error, the 'error' key will exist
-    # otherwise we use 'result'. JSONRPC returns other data
-    # along with the result/error such as version and id which
-    # we will strip off for REST calls.
-    my $content = $response->content;
-
-    my $json_data = {};
-    if ($content) {
-        # Content is in bytes at this point and needs to be converted
-        # back to utf8 string.
-        enable_utf8();
-        utf8::decode($content) if !utf8::is_utf8($content);
-        $json_data = $self->json->decode($content);
-    }
-
-    my $result = {};
-    if (exists $json_data->{error}) {
-        $result = $json_data->{error};
-        $result->{error} = $self->type('boolean', 1);
-
-        $result->{documentation} = Bugzilla->params->{docs_urlbase} . "api/";
-        delete $result->{'name'}; # Remove JSONRPCError
-    }
-    elsif (exists $json_data->{result}) {
-        $result = $json_data->{result};
-    }
-
-    Bugzilla::Hook::process('webservice_rest_response',
-        { rpc => $self, result => \$result, response => $response });
-
-    # Access Control
-    my @allowed_headers = qw(accept content-type origin user-agent x-requested-with);
-    foreach my $header (keys %{ API_AUTH_HEADERS() }) {
-        # We want to lowercase and replace _ with -
-        my $translated_header = $header;
-        $translated_header =~ tr/A-Z_/a-z\-/;
-        push(@allowed_headers, $translated_header);
-    }
-    $response->header("Access-Control-Allow-Origin", "*");
-    $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers));
-
-    # ETag support
-    my $etag = $self->bz_etag;
-    $self->bz_etag($result) if !$etag;
-
-    # If accessing through web browser, then display in readable format
-    if ($self->content_type eq 'text/html') {
-        $result = $self->json->pretty->canonical->allow_nonref->encode($result);
-
-        my $template = Bugzilla->template;
-        $content = "";
-        $result->encode if blessed $result;
-        $template->process("rest.html.tmpl", { result => $result }, \$content)
-            || ThrowTemplateError($template->error());
-
-        $response->content_type('text/html');
-    }
-    else {
-        $content = $self->json->encode($result);
-    }
-
-    utf8::encode($content) if utf8::is_utf8($content);
-    disable_utf8();
-
-    $response->content($content);
-
-    $self->SUPER::response($response);
+  my ($self, $response) = @_;
+
+  # If we have thrown an error, the 'error' key will exist
+  # otherwise we use 'result'. JSONRPC returns other data
+  # along with the result/error such as version and id which
+  # we will strip off for REST calls.
+  my $content = $response->content;
+
+  my $json_data = {};
+  if ($content) {
+
+    # Content is in bytes at this point and needs to be converted
+    # back to utf8 string.
+    enable_utf8();
+    utf8::decode($content) if !utf8::is_utf8($content);
+    $json_data = $self->json->decode($content);
+  }
+
+  my $result = {};
+  if (exists $json_data->{error}) {
+    $result = $json_data->{error};
+    $result->{error} = $self->type('boolean', 1);
+
+    $result->{documentation} = Bugzilla->params->{docs_urlbase} . "api/";
+    delete $result->{'name'};    # Remove JSONRPCError
+  }
+  elsif (exists $json_data->{result}) {
+    $result = $json_data->{result};
+  }
+
+  Bugzilla::Hook::process('webservice_rest_response',
+    {rpc => $self, result => \$result, response => $response});
+
+  # Access Control
+  my @allowed_headers
+    = qw(accept content-type origin user-agent x-requested-with);
+  foreach my $header (keys %{API_AUTH_HEADERS()}) {
+
+    # We want to lowercase and replace _ with -
+    my $translated_header = $header;
+    $translated_header =~ tr/A-Z_/a-z\-/;
+    push(@allowed_headers, $translated_header);
+  }
+  $response->header("Access-Control-Allow-Origin", "*");
+  $response->header("Access-Control-Allow-Headers", join(', ', @allowed_headers));
+
+  # ETag support
+  my $etag = $self->bz_etag;
+  $self->bz_etag($result) if !$etag;
+
+  # If accessing through web browser, then display in readable format
+  if ($self->content_type eq 'text/html') {
+    $result = $self->json->pretty->canonical->allow_nonref->encode($result);
+
+    my $template = Bugzilla->template;
+    $content = "";
+    $result->encode if blessed $result;
+    $template->process("rest.html.tmpl", {result => $result}, \$content)
+      || ThrowTemplateError($template->error());
+
+    $response->content_type('text/html');
+  }
+  else {
+    $content = $self->json->encode($result);
+  }
+
+  utf8::encode($content) if utf8::is_utf8($content);
+  disable_utf8();
+
+  $response->content($content);
+
+  $self->SUPER::response($response);
 }
 
 #######################################
@@ -188,21 +188,21 @@ sub response {
 #######################################
 
 sub handle_login {
-    my $self = shift;
-    my $class = $self->bz_class_name;
-    my $method = $self->bz_method_name;
-    my $full_method = $class . "." . $method;
-    $full_method =~ s/^Bugzilla::WebService:://;
-
-    # We never want to create a new session unless the user is calling the
-    # login method.  Setting dont_persist_session makes
-    # Bugzilla::Auth::_handle_login_result() skip calling persist_login().
-    if ($full_method ne 'User.login') {
-        Bugzilla->request_cache->{dont_persist_session} = 1;
-    }
-
-    # Bypass JSONRPC::handle_login
-    Bugzilla::WebService::Server->handle_login($class, $method, $full_method);
+  my $self        = shift;
+  my $class       = $self->bz_class_name;
+  my $method      = $self->bz_method_name;
+  my $full_method = $class . "." . $method;
+  $full_method =~ s/^Bugzilla::WebService:://;
+
+  # We never want to create a new session unless the user is calling the
+  # login method.  Setting dont_persist_session makes
+  # Bugzilla::Auth::_handle_login_result() skip calling persist_login().
+  if ($full_method ne 'User.login') {
+    Bugzilla->request_cache->{dont_persist_session} = 1;
+  }
+
+  # Bypass JSONRPC::handle_login
+  Bugzilla::WebService::Server->handle_login($class, $method, $full_method);
 }
 
 ############################
@@ -212,79 +212,78 @@ sub handle_login {
 # We do not want to run Bugzilla::WebService::Server::JSONRPC->_find_prodedure
 # as it determines the method name differently.
 sub _find_procedure {
-    my $self = shift;
-    if ($self->isa('JSON::RPC::Server::CGI')) {
-        return JSON::RPC::Server::_find_procedure($self, @_);
-    }
-    else {
-        return JSON::RPC::Legacy::Server::_find_procedure($self, @_);
-    }
+  my $self = shift;
+  if ($self->isa('JSON::RPC::Server::CGI')) {
+    return JSON::RPC::Server::_find_procedure($self, @_);
+  }
+  else {
+    return JSON::RPC::Legacy::Server::_find_procedure($self, @_);
+  }
 }
 
 sub _argument_type_check {
-    my $self = shift;
-    my $params;
-
-    if ($self->isa('JSON::RPC::Server::CGI')) {
-        $params = JSON::RPC::Server::_argument_type_check($self, @_);
-    }
-    else {
-        $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_);
-    }
-
-    # JSON-RPC 1.0 requires all parameters to be passed as an array, so
-    # we just pull out the first item and assume it's an object.
-    my $params_is_array;
-    if (ref $params eq 'ARRAY') {
-        $params = $params->[0];
-        $params_is_array = 1;
-    }
-
-    taint_data($params);
-
-    # Now, convert dateTime fields on input.
-    my $method = $self->bz_method_name;
-    my $pkg = $self->{dispatch_path}->{$self->path_info};
-    my @date_fields = @{ $pkg->DATE_FIELDS->{$method} || [] };
-    foreach my $field (@date_fields) {
-        if (defined $params->{$field}) {
-            my $value = $params->{$field};
-            if (ref $value eq 'ARRAY') {
-                $params->{$field} =
-                    [ map { $self->datetime_format_inbound($_) } @$value ];
-            }
-            else {
-                $params->{$field} = $self->datetime_format_inbound($value);
-            }
-        }
+  my $self = shift;
+  my $params;
+
+  if ($self->isa('JSON::RPC::Server::CGI')) {
+    $params = JSON::RPC::Server::_argument_type_check($self, @_);
+  }
+  else {
+    $params = JSON::RPC::Legacy::Server::_argument_type_check($self, @_);
+  }
+
+  # JSON-RPC 1.0 requires all parameters to be passed as an array, so
+  # we just pull out the first item and assume it's an object.
+  my $params_is_array;
+  if (ref $params eq 'ARRAY') {
+    $params          = $params->[0];
+    $params_is_array = 1;
+  }
+
+  taint_data($params);
+
+  # Now, convert dateTime fields on input.
+  my $method      = $self->bz_method_name;
+  my $pkg         = $self->{dispatch_path}->{$self->path_info};
+  my @date_fields = @{$pkg->DATE_FIELDS->{$method} || []};
+  foreach my $field (@date_fields) {
+    if (defined $params->{$field}) {
+      my $value = $params->{$field};
+      if (ref $value eq 'ARRAY') {
+        $params->{$field} = [map { $self->datetime_format_inbound($_) } @$value];
+      }
+      else {
+        $params->{$field} = $self->datetime_format_inbound($value);
+      }
     }
-    my @base64_fields = @{ $pkg->BASE64_FIELDS->{$method} || [] };
-    foreach my $field (@base64_fields) {
-        if (defined $params->{$field}) {
-            $params->{$field} = decode_base64($params->{$field});
-        }
+  }
+  my @base64_fields = @{$pkg->BASE64_FIELDS->{$method} || []};
+  foreach my $field (@base64_fields) {
+    if (defined $params->{$field}) {
+      $params->{$field} = decode_base64($params->{$field});
     }
+  }
 
-    # This is the best time to do login checks.
-    $self->handle_login();
+  # This is the best time to do login checks.
+  $self->handle_login();
 
-    # Bugzilla::WebService packages call internal methods like
-    # $self->_some_private_method. So we have to inherit from
-    # that class as well as this Server class.
-    my $new_class = ref($self) . '::' . $pkg;
-    my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
-    eval "package $new_class;$isa_string;";
-    bless $self, $new_class;
+  # Bugzilla::WebService packages call internal methods like
+  # $self->_some_private_method. So we have to inherit from
+  # that class as well as this Server class.
+  my $new_class  = ref($self) . '::' . $pkg;
+  my $isa_string = 'our @ISA = qw(' . ref($self) . " $pkg)";
+  eval "package $new_class;$isa_string;";
+  bless $self, $new_class;
 
-    # Allow extensions to modify the params post login
-    Bugzilla::Hook::process('webservice_rest_request',
-                            { rpc => $self, params => $params });
+  # Allow extensions to modify the params post login
+  Bugzilla::Hook::process('webservice_rest_request',
+    {rpc => $self, params => $params});
 
-    if ($params_is_array) {
-        $params = [$params];
-    }
+  if ($params_is_array) {
+    $params = [$params];
+  }
 
-    return $params;
+  return $params;
 }
 
 ###################
@@ -292,46 +291,46 @@ sub _argument_type_check {
 ###################
 
 sub bz_method_name {
-    my ($self, $method) = @_;
-    $self->{_bz_method_name} = $method if $method;
-    return $self->{_bz_method_name};
+  my ($self, $method) = @_;
+  $self->{_bz_method_name} = $method if $method;
+  return $self->{_bz_method_name};
 }
 
 sub bz_class_name {
-    my ($self, $class) = @_;
-    $self->{_bz_class_name} = $class if $class;
-    return $self->{_bz_class_name};
+  my ($self, $class) = @_;
+  $self->{_bz_class_name} = $class if $class;
+  return $self->{_bz_class_name};
 }
 
 sub bz_success_code {
-    my ($self, $value) = @_;
-    $self->{_bz_success_code} = $value if $value;
-    return $self->{_bz_success_code};
+  my ($self, $value) = @_;
+  $self->{_bz_success_code} = $value if $value;
+  return $self->{_bz_success_code};
 }
 
 sub bz_rest_params {
-    my ($self, $params) = @_;
-    $self->{_bz_rest_params} = $params if $params;
-    return $self->{_bz_rest_params};
+  my ($self, $params) = @_;
+  $self->{_bz_rest_params} = $params if $params;
+  return $self->{_bz_rest_params};
 }
 
 sub bz_rest_options {
-    my ($self, $options) = @_;
-    $self->{_bz_rest_options} = $options if $options;
-    return [ sort { $a cmp $b } @{ $self->{_bz_rest_options} } ];
+  my ($self, $options) = @_;
+  $self->{_bz_rest_options} = $options if $options;
+  return [sort { $a cmp $b } @{$self->{_bz_rest_options}}];
 }
 
 sub rest_include_exclude {
-    my ($params) = @_;
+  my ($params) = @_;
 
-    if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) {
-        $params->{'include_fields'} = [ split(/[\s+,]/, $params->{'include_fields'}) ];
-    }
-    if (exists $params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
-        $params->{'exclude_fields'} = [ split(/[\s+,]/, $params->{'exclude_fields'}) ];
-    }
+  if (exists $params->{'include_fields'} && !ref $params->{'include_fields'}) {
+    $params->{'include_fields'} = [split(/[\s+,]/, $params->{'include_fields'})];
+  }
+  if (exists $params->{'exclude_fields'} && !ref $params->{'exclude_fields'}) {
+    $params->{'exclude_fields'} = [split(/[\s+,]/, $params->{'exclude_fields'})];
+  }
 
-    return $params;
+  return $params;
 }
 
 ##########################
@@ -339,187 +338,195 @@ sub rest_include_exclude {
 ##########################
 
 sub _retrieve_json_params {
-    my $self = shift;
-
-    # Make a copy of the current input_params rather than edit directly
-    my $params = {};
-    %{$params} = %{ Bugzilla->input_params };
-
-    # First add any parameters we were able to pull out of the path
-    # based on the resource regexp and combine with the normal URL
-    # parameters.
-    if (my $rest_params = $self->bz_rest_params) {
-        foreach my $param (keys %$rest_params) {
-            # If the param does not already exist or if the
-            # rest param is a single value, add it to the
-            # global params.
-            if (!exists $params->{$param} || !ref $rest_params->{$param}) {
-                $params->{$param} = $rest_params->{$param};
-            }
-            # If rest_param is a list then add any extra values to the list
-            elsif (ref $rest_params->{$param}) {
-                my @extra_values = ref $params->{$param}
-                                   ? @{ $params->{$param} }
-                                   : ($params->{$param});
-                $params->{$param}
-                    = [ uniq (@{ $rest_params->{$param} }, @extra_values) ];
-            }
-        }
+  my $self = shift;
+
+  # Make a copy of the current input_params rather than edit directly
+  my $params = {};
+  %{$params} = %{Bugzilla->input_params};
+
+  # First add any parameters we were able to pull out of the path
+  # based on the resource regexp and combine with the normal URL
+  # parameters.
+  if (my $rest_params = $self->bz_rest_params) {
+    foreach my $param (keys %$rest_params) {
+
+      # If the param does not already exist or if the
+      # rest param is a single value, add it to the
+      # global params.
+      if (!exists $params->{$param} || !ref $rest_params->{$param}) {
+        $params->{$param} = $rest_params->{$param};
+      }
+
+      # If rest_param is a list then add any extra values to the list
+      elsif (ref $rest_params->{$param}) {
+        my @extra_values
+          = ref $params->{$param} ? @{$params->{$param}} : ($params->{$param});
+        $params->{$param} = [uniq(@{$rest_params->{$param}}, @extra_values)];
+      }
+    }
+  }
+
+  # Any parameters passed in in the body of a non-GET request will override
+  # any parameters pull from the url path. Otherwise non-unique keys are
+  # combined.
+  if ($self->request->method ne 'GET') {
+    my $extra_params = {};
+
+    # We do this manually because CGI.pm doesn't understand JSON strings.
+    my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
+    if ($json) {
+      eval { $extra_params = $self->json->utf8(0)->decode($json); };
+      if ($@) {
+        ThrowUserError('json_rpc_invalid_params', {err_msg => $@});
+      }
     }
 
-    # Any parameters passed in in the body of a non-GET request will override
-    # any parameters pull from the url path. Otherwise non-unique keys are
-    # combined.
-    if ($self->request->method ne 'GET') {
-        my $extra_params = {};
-        # We do this manually because CGI.pm doesn't understand JSON strings.
-        my $json = delete $params->{'POSTDATA'} || delete $params->{'PUTDATA'};
-        if ($json) {
-            eval { $extra_params = $self->json->utf8(0)->decode($json); };
-            if ($@) {
-                ThrowUserError('json_rpc_invalid_params', { err_msg  => $@ });
-            }
-        }
-
-        # Allow parameters in the query string if request was non-GET.
-        # Note: parameters in query string body override any matching
-        # parameters in the request body.
-        foreach my $param ($self->cgi->url_param()) {
-            $extra_params->{$param} = $self->cgi->url_param($param);
-        }
-
-        %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params};
+    # Allow parameters in the query string if request was non-GET.
+    # Note: parameters in query string body override any matching
+    # parameters in the request body.
+    foreach my $param ($self->cgi->url_param()) {
+      $extra_params->{$param} = $self->cgi->url_param($param);
     }
 
-    return $params;
+    %{$params} = (%{$params}, %{$extra_params}) if %{$extra_params};
+  }
+
+  return $params;
 }
 
 sub preload {
-    require_module($_) for values %{ WS_DISPATCH() };
+  require_module($_) for values %{WS_DISPATCH()};
 }
 
 sub _find_resource {
-    my ($self, $path) = @_;
-
-    # Load in the WebService module from the dispatch map and then call
-    # $module->rest_resources to get the resources array ref.
-    my $resources = {};
-    foreach my $module (values %{ $self->{dispatch_path} }) {
-        next if !$module->can('rest_resources');
-        $resources->{$module} = $module->rest_resources;
-    }
-
-    Bugzilla::Hook::process('webservice_rest_resources',
-                            { rpc => $self, resources => $resources }) if Bugzilla::request_cache->{bzapi};
-
-    # Use the resources hash from each module loaded earlier to determine
-    # which handler to use based on a regex match of the CGI path.
-    # Also any matches found in the regex will be passed in later to the
-    # handler for possible use.
-    my $request_method = $self->request->method;
-
-    my (@matches, $handler_found, $handler_method, $handler_class);
-    foreach my $class (keys %{ $resources }) {
-        # The resource data for each module needs to be
-        # an array ref with an even number of elements
-        # to work correctly.
-        next if (ref $resources->{$class} ne 'ARRAY'
-                 || scalar @{ $resources->{$class} } % 2 != 0);
-
-        while (my $regex = shift @{ $resources->{$class} }) {
-            my $options_data = shift @{ $resources->{$class} };
-            next if ref $options_data ne 'HASH';
-
-            if (@matches = ($path =~ $regex)) {
-                # If a specific path is accompanied by a OPTIONS request
-                # method, the user is asking for a list of possible request
-                # methods for a specific path.
-                $self->bz_rest_options([ keys %{ $options_data } ]);
-
-                if ($options_data->{$request_method}) {
-                    my $resource_data = $options_data->{$request_method};
-                    $self->bz_class_name($class);
-
-                    # The method key/value can be a simple scalar method name
-                    # or a anonymous subroutine so we execute it here.
-                    my $method = ref $resource_data->{method} eq 'CODE'
-                                 ? $resource_data->{method}->($self)
-                                 : $resource_data->{method};
-                    $self->bz_method_name($method);
-
-                    # Pull out any parameters parsed from the URL path
-                    # and store them for use by the method.
-                    if ($resource_data->{params}) {
-                        $self->bz_rest_params($resource_data->{params}->(@matches));
-                    }
-
-                    # If a special success code is needed for this particular
-                    # method, then store it for later when generating response.
-                    if ($resource_data->{success_code}) {
-                        $self->bz_success_code($resource_data->{success_code});
-                    }
-                    $handler_found = 1;
-                }
-            }
-            last if $handler_found;
+  my ($self, $path) = @_;
+
+  # Load in the WebService module from the dispatch map and then call
+  # $module->rest_resources to get the resources array ref.
+  my $resources = {};
+  foreach my $module (values %{$self->{dispatch_path}}) {
+    next if !$module->can('rest_resources');
+    $resources->{$module} = $module->rest_resources;
+  }
+
+  Bugzilla::Hook::process('webservice_rest_resources',
+    {rpc => $self, resources => $resources})
+    if Bugzilla::request_cache->{bzapi};
+
+  # Use the resources hash from each module loaded earlier to determine
+  # which handler to use based on a regex match of the CGI path.
+  # Also any matches found in the regex will be passed in later to the
+  # handler for possible use.
+  my $request_method = $self->request->method;
+
+  my (@matches, $handler_found, $handler_method, $handler_class);
+  foreach my $class (keys %{$resources}) {
+
+    # The resource data for each module needs to be
+    # an array ref with an even number of elements
+    # to work correctly.
+    next
+      if (ref $resources->{$class} ne 'ARRAY'
+      || scalar @{$resources->{$class}} % 2 != 0);
+
+    while (my $regex = shift @{$resources->{$class}}) {
+      my $options_data = shift @{$resources->{$class}};
+      next if ref $options_data ne 'HASH';
+
+      if (@matches = ($path =~ $regex)) {
+
+        # If a specific path is accompanied by a OPTIONS request
+        # method, the user is asking for a list of possible request
+        # methods for a specific path.
+        $self->bz_rest_options([keys %{$options_data}]);
+
+        if ($options_data->{$request_method}) {
+          my $resource_data = $options_data->{$request_method};
+          $self->bz_class_name($class);
+
+          # The method key/value can be a simple scalar method name
+          # or a anonymous subroutine so we execute it here.
+          my $method
+            = ref $resource_data->{method} eq 'CODE'
+            ? $resource_data->{method}->($self)
+            : $resource_data->{method};
+          $self->bz_method_name($method);
+
+          # Pull out any parameters parsed from the URL path
+          # and store them for use by the method.
+          if ($resource_data->{params}) {
+            $self->bz_rest_params($resource_data->{params}->(@matches));
+          }
+
+          # If a special success code is needed for this particular
+          # method, then store it for later when generating response.
+          if ($resource_data->{success_code}) {
+            $self->bz_success_code($resource_data->{success_code});
+          }
+          $handler_found = 1;
         }
-        last if $handler_found;
+      }
+      last if $handler_found;
     }
+    last if $handler_found;
+  }
 
-    return $handler_found;
+  return $handler_found;
 }
 
 sub _best_content_type {
-    my ($self, @types) = @_;
-    return ($self->_simple_content_negotiation(@types))[0] || '*/*';
+  my ($self, @types) = @_;
+  return ($self->_simple_content_negotiation(@types))[0] || '*/*';
 }
 
 sub _simple_content_negotiation {
-    my ($self, @types) = @_;
-    my @accept_types = $self->_get_content_prefs();
-    # Return the types as-is if no accept header sent, since sorting will be a no-op.
-    if (!@accept_types) {
-        return @types;
-    }
-    my $score = sub { $self->_score_type(shift, @accept_types) };
-    return sort {$score->($b) <=> $score->($a)} @types;
+  my ($self, @types) = @_;
+  my @accept_types = $self->_get_content_prefs();
+
+ # Return the types as-is if no accept header sent, since sorting will be a no-op.
+  if (!@accept_types) {
+    return @types;
+  }
+  my $score = sub { $self->_score_type(shift, @accept_types) };
+  return sort { $score->($b) <=> $score->($a) } @types;
 }
 
 sub _score_type {
-    my ($self, $type, @accept_types) = @_;
-    my $score = scalar(@accept_types);
-    for my $accept_type (@accept_types) {
-        return $score if $type eq $accept_type;
-        $score--;
-    }
-    return 0;
+  my ($self, $type, @accept_types) = @_;
+  my $score = scalar(@accept_types);
+  for my $accept_type (@accept_types) {
+    return $score if $type eq $accept_type;
+    $score--;
+  }
+  return 0;
 }
 
 sub _get_content_prefs {
-    my $self = shift;
-    my $default_weight = 1;
-    my @prefs;
-
-    # Parse the Accept header, and save type name, score, and position.
-    my @accept_types = split /,/, $self->cgi->http('accept') || '';
-    my $order = 0;
-    for my $accept_type (@accept_types) {
-        my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
-        my ($name) = ($accept_type =~ m#(\S+/[^;]+)#);
-        next unless $name;
-        push @prefs, { name => $name, order => $order++};
-        if (defined $weight) {
-            $prefs[-1]->{score} = $weight;
-        } else {
-            $prefs[-1]->{score} = $default_weight;
-            $default_weight -= 0.001;
-        }
+  my $self           = shift;
+  my $default_weight = 1;
+  my @prefs;
+
+  # Parse the Accept header, and save type name, score, and position.
+  my @accept_types = split /,/, $self->cgi->http('accept') || '';
+  my $order = 0;
+  for my $accept_type (@accept_types) {
+    my ($weight) = ($accept_type =~ /q=(\d\.\d+|\d+)/);
+    my ($name)   = ($accept_type =~ m#(\S+/[^;]+)#);
+    next unless $name;
+    push @prefs, {name => $name, order => $order++};
+    if (defined $weight) {
+      $prefs[-1]->{score} = $weight;
+    }
+    else {
+      $prefs[-1]->{score} = $default_weight;
+      $default_weight -= 0.001;
     }
+  }
 
-    # Sort the types by score, subscore by order, and pull out just the name
-    @prefs = map {$_->{name}} sort {$b->{score} <=> $a->{score} ||
-                                    $a->{order} <=> $b->{order}} @prefs;
-    return @prefs;
+  # Sort the types by score, subscore by order, and pull out just the name
+  @prefs = map { $_->{name} }
+    sort { $b->{score} <=> $a->{score} || $a->{order} <=> $b->{order} } @prefs;
+  return @prefs;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bug.pm b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
index 26aec011c..34580368d 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bug.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bug.pm
@@ -15,177 +15,172 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Bug;
 
 BEGIN {
-    *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Bug::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/bug$}, {
-            GET  => {
-                method => 'search',
-            },
-            POST => {
-                method => 'create',
-                status_code => STATUS_CREATED
-            }
-        },
-        qr{^/bug/$}, {
-            GET => {
-                method => 'get'
-            }
-        },
-        qr{^/bug/possible_duplicates$}, {
-            GET => {
-                method => 'possible_duplicates'
-            }
-        },
-        qr{^/bug/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            }
-        },
-        qr{^/bug/([^/]+)/comment$}, {
-            GET  => {
-                method => 'comments',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            },
-            POST => {
-                method => 'add_comment',
-                params => sub {
-                    return { id => $_[0] };
-                },
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/bug/comment/(\d+)$}, {
-            GET => {
-                method => 'comments',
-                params => sub {
-                    return { comment_ids => [ $_[0] ] };
-                }
-            }
-        },
-        qr{^/bug/comment/tags/([^/]+)$}, {
-            GET => {
-                method => 'search_comment_tags',
-                params => sub {
-                    return { query => $_[0] };
-                },
-            },
-        },
-        qr{^/bug/comment/([^/]+)/tags$}, {
-            PUT => {
-                method => 'update_comment_tags',
-                params => sub {
-                    return { comment_id => $_[0] };
-                },
-            },
-        },
-        qr{^/bug/comment/render$}, {
-            POST => {
-                method => 'render_comment',
-            },
-        },
-        qr{^/bug/([^/]+)/history$}, {
-            GET => {
-                method => 'history',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                },
-            }
-        },
-        qr{^/bug/([^/]+)/attachment$}, {
-            GET  => {
-                method => 'attachments',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            },
-            POST => {
-                method => 'add_attachment',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                },
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/bug/attachment/([^/]+)$}, {
-            GET => {
-                method => 'attachments',
-                params => sub {
-                    return { attachment_ids => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update_attachment',
-                params => sub {
-                    return { ids => [ $_[0] ] };
-                }
-            }
-        },
-        qr{^/field/bug$}, {
-            GET => {
-                method => 'fields',
-            }
-        },
-        qr{^/field/bug/([^/]+)$}, {
-            GET => {
-                method => 'fields',
-                params => sub {
-                    my $value = $_[0];
-                    my $param = 'names';
-                    $param = 'ids' if $value =~ /^\d+$/;
-                    return { $param => [ $_[0] ] };
-                }
-            }
-        },
-        qr{^/field/bug/([^/]+)/values$}, {
-            GET => {
-                method => 'legal_values',
-                params => sub {
-                    return { field => $_[0] };
-                }
-            }
-        },
-        qr{^/field/bug/([^/]+)/([^/]+)/values$}, {
-            GET => {
-                method => 'legal_values',
-                params => sub {
-                    return { field      => $_[0],
-                             product_id => $_[1] };
-                }
-            }
-        },
-        qr{^/flag_types/([^/]+)/([^/]+)$}, {
-            GET => {
-                method => 'flag_types',
-                params => sub {
-                    return { product   => $_[0],
-                             component => $_[1] };
-                }
-            }
-        },
-        qr{^/flag_types/([^/]+)$}, {
-            GET => {
-                method => 'flag_types',
-                params => sub {
-                    return { product => $_[0] };
-                }
-            }
+  my $rest_resources = [
+    qr{^/bug$},
+    {
+      GET  => {method => 'search',},
+      POST => {method => 'create', status_code => STATUS_CREATED}
+    },
+    qr{^/bug/$},
+    {GET => {method => 'get'}},
+    qr{^/bug/possible_duplicates$},
+    {GET => {method => 'possible_duplicates'}},
+    qr{^/bug/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      }
+    },
+    qr{^/bug/([^/]+)/comment$},
+    {
+      GET => {
+        method => 'comments',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      },
+      POST => {
+        method => 'add_comment',
+        params => sub {
+          return {id => $_[0]};
+        },
+        success_code => STATUS_CREATED
+      }
+    },
+    qr{^/bug/comment/(\d+)$},
+    {
+      GET => {
+        method => 'comments',
+        params => sub {
+          return {comment_ids => [$_[0]]};
+        }
+      }
+    },
+    qr{^/bug/comment/tags/([^/]+)$},
+    {
+      GET => {
+        method => 'search_comment_tags',
+        params => sub {
+          return {query => $_[0]};
+        },
+      },
+    },
+    qr{^/bug/comment/([^/]+)/tags$},
+    {
+      PUT => {
+        method => 'update_comment_tags',
+        params => sub {
+          return {comment_id => $_[0]};
+        },
+      },
+    },
+    qr{^/bug/comment/render$},
+    {POST => {method => 'render_comment',},},
+    qr{^/bug/([^/]+)/history$},
+    {
+      GET => {
+        method => 'history',
+        params => sub {
+          return {ids => [$_[0]]};
+        },
+      }
+    },
+    qr{^/bug/([^/]+)/attachment$},
+    {
+      GET => {
+        method => 'attachments',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      },
+      POST => {
+        method => 'add_attachment',
+        params => sub {
+          return {ids => [$_[0]]};
+        },
+        success_code => STATUS_CREATED
+      }
+    },
+    qr{^/bug/attachment/([^/]+)$},
+    {
+      GET => {
+        method => 'attachments',
+        params => sub {
+          return {attachment_ids => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update_attachment',
+        params => sub {
+          return {ids => [$_[0]]};
+        }
+      }
+    },
+    qr{^/field/bug$},
+    {GET => {method => 'fields',}},
+    qr{^/field/bug/([^/]+)$},
+    {
+      GET => {
+        method => 'fields',
+        params => sub {
+          my $value = $_[0];
+          my $param = 'names';
+          $param = 'ids' if $value =~ /^\d+$/;
+          return {$param => [$_[0]]};
+        }
+      }
+    },
+    qr{^/field/bug/([^/]+)/values$},
+    {
+      GET => {
+        method => 'legal_values',
+        params => sub {
+          return {field => $_[0]};
+        }
+      }
+    },
+    qr{^/field/bug/([^/]+)/([^/]+)/values$},
+    {
+      GET => {
+        method => 'legal_values',
+        params => sub {
+          return {field => $_[0], product_id => $_[1]};
+        }
+      }
+    },
+    qr{^/flag_types/([^/]+)/([^/]+)$},
+    {
+      GET => {
+        method => 'flag_types',
+        params => sub {
+          return {product => $_[0], component => $_[1]};
+        }
+      }
+    },
+    qr{^/flag_types/([^/]+)$},
+    {
+      GET => {
+        method => 'flag_types',
+        params => sub {
+          return {product => $_[0]};
         }
-    ];
-    return $rest_resources;
+      }
+    }
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
index 12290e84e..72aa0d40f 100644
--- a/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm
@@ -12,36 +12,32 @@ use strict;
 use warnings;
 
 BEGIN {
-    *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources;
+  *Bugzilla::WebService::BugUserLastVisit::rest_resources = \&_rest_resources;
 }
 
 sub _rest_resources {
-    return [
-        # bug-id
-        qr{^/bug_user_last_visit/(\d+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    return { ids => $_[0] };
-                },
-            },
-            POST => {
-                method => 'update',
-                params => sub {
-                    return { ids => $_[0] };
-                },
-            },
+  return [
+    # bug-id
+    qr{^/bug_user_last_visit/(\d+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          return {ids => $_[0]};
         },
-        # no bug-id
-        qr{^/bug_user_last_visit$}, {
-            GET => {
-                method => 'get',
-            },
-            POST => {
-                method => 'update',
-            },
+      },
+      POST => {
+        method => 'update',
+        params => sub {
+          return {ids => $_[0]};
         },
-    ];
+      },
+    },
+
+    # no bug-id
+    qr{^/bug_user_last_visit$},
+    {GET => {method => 'get',}, POST => {method => 'update',},},
+  ];
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
index 646355cd3..28872f698 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm
@@ -15,48 +15,20 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Bugzilla;
 
 BEGIN {
-    *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Bugzilla::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/version$}, {
-            GET  => {
-                method => 'version'
-            }
-        },
-        qr{^/extensions$}, {
-            GET => {
-                method => 'extensions'
-            }
-        },
-        qr{^/timezone$}, {
-            GET => {
-                method => 'timezone'
-            }
-        },
-        qr{^/time$}, {
-            GET => {
-                method => 'time'
-            }
-        },
-        qr{^/last_audit_time$}, {
-            GET => {
-                method => 'last_audit_time'
-            }
-        },
-        qr{^/parameters$}, {
-            GET => {
-                method => 'parameters'
-            }
-        },
-        qr{^/jobqueue_status$}, {
-            GET => {
-                method => 'jobqueue_status'
-            }
-        }
-    ];
-    return $rest_resources;
+  my $rest_resources = [
+    qr{^/version$},         {GET => {method => 'version'}},
+    qr{^/extensions$},      {GET => {method => 'extensions'}},
+    qr{^/timezone$},        {GET => {method => 'timezone'}},
+    qr{^/time$},            {GET => {method => 'time'}},
+    qr{^/last_audit_time$}, {GET => {method => 'last_audit_time'}},
+    qr{^/parameters$},      {GET => {method => 'parameters'}},
+    qr{^/jobqueue_status$}, {GET => {method => 'jobqueue_status'}}
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Classification.pm b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
index f20278f55..88ba028ba 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Classification.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Classification.pm
@@ -15,22 +15,23 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Classification;
 
 BEGIN {
-    *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Classification::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/classification/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
+  my $rest_resources = [
+    qr{^/classification/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
         }
-    ];
-    return $rest_resources;
+      }
+    }
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Elastic.pm b/Bugzilla/WebService/Server/REST/Resources/Elastic.pm
index 2f7c1eaa4..367dd9134 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Elastic.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Elastic.pm
@@ -15,16 +15,13 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Elastic;
 
 BEGIN {
-    *Bugzilla::WebService::Elastic::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Elastic::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/elastic/suggest_users$}, {
-            GET  => { method => 'suggest_users' },
-        },
-    ];
-    return $rest_resources;
+  my $rest_resources
+    = [qr{^/elastic/suggest_users$}, {GET => {method => 'suggest_users'},},];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Group.pm b/Bugzilla/WebService/Server/REST/Resources/Group.pm
index 6e3d934eb..b6a1b9b34 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Group.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Group.pm
@@ -15,38 +15,35 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::Group;
 
 BEGIN {
-    *Bugzilla::WebService::Group::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Group::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/group$}, {
-            GET  => {
-                method => 'get'
-            },
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/group/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
+  my $rest_resources = [
+    qr{^/group$},
+    {
+      GET  => {method => 'get'},
+      POST => {method => 'create', success_code => STATUS_CREATED}
+    },
+    qr{^/group/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
         }
-    ];
-    return $rest_resources;
+      }
+    }
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/Product.pm b/Bugzilla/WebService/Server/REST/Resources/Product.pm
index 9ca6e3074..3222642c8 100644
--- a/Bugzilla/WebService/Server/REST/Resources/Product.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/Product.pm
@@ -17,53 +17,41 @@ use Bugzilla::WebService::Product;
 use Bugzilla::Error;
 
 BEGIN {
-    *Bugzilla::WebService::Product::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::Product::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/product_accessible$}, {
-            GET => {
-                method => 'get_accessible_products'
-            }
-        },
-        qr{^/product_enterable$}, {
-            GET => {
-                method => 'get_enterable_products'
-            }
-        },
-        qr{^/product_selectable$}, {
-            GET => {
-                method => 'get_selectable_products'
-            }
-        },
-        qr{^/product$}, {
-            GET  => {
-                method => 'get'
-            },
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/product/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
-        },
-    ];
-    return $rest_resources;
+  my $rest_resources = [
+    qr{^/product_accessible$},
+    {GET => {method => 'get_accessible_products'}},
+    qr{^/product_enterable$},
+    {GET => {method => 'get_enterable_products'}},
+    qr{^/product_selectable$},
+    {GET => {method => 'get_selectable_products'}},
+    qr{^/product$},
+    {
+      GET  => {method => 'get'},
+      POST => {method => 'create', success_code => STATUS_CREATED}
+    },
+    qr{^/product/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      }
+    },
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/REST/Resources/User.pm b/Bugzilla/WebService/Server/REST/Resources/User.pm
index 6185237fb..ab5f78bde 100644
--- a/Bugzilla/WebService/Server/REST/Resources/User.pm
+++ b/Bugzilla/WebService/Server/REST/Resources/User.pm
@@ -15,71 +15,54 @@ use Bugzilla::WebService::Constants;
 use Bugzilla::WebService::User;
 
 BEGIN {
-    *Bugzilla::WebService::User::rest_resources = \&_rest_resources;
-};
+  *Bugzilla::WebService::User::rest_resources = \&_rest_resources;
+}
 
 sub _rest_resources {
-    my $rest_resources = [
-        qr{^/user/suggest$}, {
-            GET => {
-                method => 'suggest',
-            },
-        },
-        qr{^/valid_login$}, {
-            GET => {
-                method => 'valid_login'
-            }
-        },
-        qr{^/login$}, {
-            GET => {
-                method => 'login'
-            }
-        },
-        qr{^/logout$}, {
-            GET => {
-                method => 'logout'
-            }
-        },
-        qr{^/user$}, {
-            GET  => {
-                method => 'get'
-            },
-            POST => {
-                method => 'create',
-                success_code => STATUS_CREATED
-            }
-        },
-        qr{^/user/([^/]+)$}, {
-            GET => {
-                method => 'get',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            },
-            PUT => {
-                method => 'update',
-                params => sub {
-                    my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
-                    return { $param => [ $_[0] ] };
-                }
-            }
-        },
-        qr{^/user/mfa/([^/]+)/enroll$}, {
-            GET => {
-                method => 'mfa_enroll',
-                params => sub {
-                    return { provider => $_[0] };
-                }
-            },
-        },
-        qr{^/whoami$}, {
-            GET => {
-                method => 'whoami'
-            }
+  my $rest_resources = [
+    qr{^/user/suggest$},
+    {GET => {method => 'suggest',},},
+    qr{^/valid_login$},
+    {GET => {method => 'valid_login'}},
+    qr{^/login$},
+    {GET => {method => 'login'}},
+    qr{^/logout$},
+    {GET => {method => 'logout'}},
+    qr{^/user$},
+    {
+      GET  => {method => 'get'},
+      POST => {method => 'create', success_code => STATUS_CREATED}
+    },
+    qr{^/user/([^/]+)$},
+    {
+      GET => {
+        method => 'get',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      },
+      PUT => {
+        method => 'update',
+        params => sub {
+          my $param = $_[0] =~ /^\d+$/ ? 'ids' : 'names';
+          return {$param => [$_[0]]};
+        }
+      }
+    },
+    qr{^/user/mfa/([^/]+)/enroll$},
+    {
+      GET => {
+        method => 'mfa_enroll',
+        params => sub {
+          return {provider => $_[0]};
         }
-    ];
-    return $rest_resources;
+      },
+    },
+    qr{^/whoami$},
+    {GET => {method => 'whoami'}}
+  ];
+  return $rest_resources;
 }
 
 1;
diff --git a/Bugzilla/WebService/Server/XMLRPC.pm b/Bugzilla/WebService/Server/XMLRPC.pm
index 5ad50e91c..ca2d119de 100644
--- a/Bugzilla/WebService/Server/XMLRPC.pm
+++ b/Bugzilla/WebService/Server/XMLRPC.pm
@@ -23,99 +23,102 @@ use Bugzilla::Util;
 use List::MoreUtils qw(none);
 
 BEGIN {
-    # Allow WebService methods to call XMLRPC::Lite's type method directly
-    *Bugzilla::WebService::type = sub {
-        my ($self, $type, $value) = @_;
-        if ($type eq 'dateTime') {
-            # This is the XML-RPC implementation,  see the README in Bugzilla/WebService/.
-            # Our "base" implementation is in Bugzilla::WebService::Server.
-            if (defined $value) {
-                $value = Bugzilla::WebService::Server->datetime_format_outbound($value);
-                $value =~ s/-//g;
-            }
-            else {
-                my ($pkg, $file, $line) = caller;
-                my $class = ref $self;
-                ERROR("$class->type($type, undef) called from $pkg ($file line $line)");
-            }
-        }
-        elsif ($type eq 'email') {
-            $type = 'string';
-            if (Bugzilla->params->{'webservice_email_filter'}) {
-                $value = email_filter($value);
-            }
-        }
-        return XMLRPC::Data->type($type)->value($value);
-    };
-
-    # Add support for ETags into XMLRPC WebServices
-    *Bugzilla::WebService::bz_etag = sub {
-        return Bugzilla::WebService::Server->bz_etag($_[1]);
-    };
+  # Allow WebService methods to call XMLRPC::Lite's type method directly
+  *Bugzilla::WebService::type = sub {
+    my ($self, $type, $value) = @_;
+    if ($type eq 'dateTime') {
+
+      # This is the XML-RPC implementation,  see the README in Bugzilla/WebService/.
+      # Our "base" implementation is in Bugzilla::WebService::Server.
+      if (defined $value) {
+        $value = Bugzilla::WebService::Server->datetime_format_outbound($value);
+        $value =~ s/-//g;
+      }
+      else {
+        my ($pkg, $file, $line) = caller;
+        my $class = ref $self;
+        ERROR("$class->type($type, undef) called from $pkg ($file line $line)");
+      }
+    }
+    elsif ($type eq 'email') {
+      $type = 'string';
+      if (Bugzilla->params->{'webservice_email_filter'}) {
+        $value = email_filter($value);
+      }
+    }
+    return XMLRPC::Data->type($type)->value($value);
+  };
+
+  # Add support for ETags into XMLRPC WebServices
+  *Bugzilla::WebService::bz_etag = sub {
+    return Bugzilla::WebService::Server->bz_etag($_[1]);
+  };
 }
 
 sub initialize {
-    my $self = shift;
-    my %retval = $self->SUPER::initialize(@_);
-    $retval{'serializer'}   = Bugzilla::XMLRPC::Serializer->new;
-    $retval{'deserializer'} = Bugzilla::XMLRPC::Deserializer->new;
-    $retval{'dispatch_with'} = WS_DISPATCH;
-    return %retval;
+  my $self   = shift;
+  my %retval = $self->SUPER::initialize(@_);
+  $retval{'serializer'}    = Bugzilla::XMLRPC::Serializer->new;
+  $retval{'deserializer'}  = Bugzilla::XMLRPC::Deserializer->new;
+  $retval{'dispatch_with'} = WS_DISPATCH;
+  return %retval;
 }
 
 sub make_response {
-    my $self = shift;
-    my $cgi = Bugzilla->cgi;
-
-    $self->SUPER::make_response(@_);
-
-    # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
-    # its cookies in Bugzilla::CGI, so we need to copy them over.
-    foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) {
-        $self->response->headers->push_header('Set-Cookie', $cookie);
-    }
-
-    # Copy across security related headers from Bugzilla::CGI
-    foreach my $header (split(/[\r\n]+/, $cgi->header)) {
-        my ($name, $value) = $header =~ /^([^:]+): (.*)/;
-        if (!$self->response->headers->header($name)) {
-           $self->response->headers->header($name => $value);
-        }
-    }
-
-    # ETag support
-    my $etag = $self->bz_etag;
-    if (!$etag) {
-        my $data = $self->response->as_string;
-        $etag = $self->bz_etag($data);
-    }
-
-    if ($etag && $cgi->check_etag($etag)) {
-        $self->response->headers->push_header('ETag', $etag);
-        $self->response->headers->push_header('status', '304 Not Modified');
-    }
-    elsif ($etag) {
-        $self->response->headers->push_header('ETag', $etag);
+  my $self = shift;
+  my $cgi  = Bugzilla->cgi;
+
+  $self->SUPER::make_response(@_);
+
+  # XMLRPC::Transport::HTTP::CGI doesn't know about Bugzilla carrying around
+  # its cookies in Bugzilla::CGI, so we need to copy them over.
+  foreach my $cookie (@{$cgi->{'Bugzilla_cookie_list'}}) {
+    $self->response->headers->push_header('Set-Cookie', $cookie);
+  }
+
+  # Copy across security related headers from Bugzilla::CGI
+  foreach my $header (split(/[\r\n]+/, $cgi->header)) {
+    my ($name, $value) = $header =~ /^([^:]+): (.*)/;
+    if (!$self->response->headers->header($name)) {
+      $self->response->headers->header($name => $value);
     }
+  }
+
+  # ETag support
+  my $etag = $self->bz_etag;
+  if (!$etag) {
+    my $data = $self->response->as_string;
+    $etag = $self->bz_etag($data);
+  }
+
+  if ($etag && $cgi->check_etag($etag)) {
+    $self->response->headers->push_header('ETag',   $etag);
+    $self->response->headers->push_header('status', '304 Not Modified');
+  }
+  elsif ($etag) {
+    $self->response->headers->push_header('ETag', $etag);
+  }
 }
 
 sub handle_login {
-    my ($self, $classes, $action, $uri, $method) = @_;
-    my $class = $classes->{$uri};
-    if (!$class) {
-        ThrowCodeError('unknown_method', { method => $method eq 'methodName' ? '' : '.' . $method });
-    }
-    my $full_method = $uri . "." . $method;
-    # Only allowed methods to be used from the module's whitelist
-    my $file = $class;
-    $file =~ s{::}{/}g;
-    $file .= ".pm";
-    require $file;
-    if (none { $_ eq $method } $class->PUBLIC_METHODS) {
-        ThrowCodeError('unknown_method', { method => $full_method });
-    }
-    $self->SUPER::handle_login($class, $method, $full_method);
-    return;
+  my ($self, $classes, $action, $uri, $method) = @_;
+  my $class = $classes->{$uri};
+  if (!$class) {
+    ThrowCodeError('unknown_method',
+      {method => $method eq 'methodName' ? '' : '.' . $method});
+  }
+  my $full_method = $uri . "." . $method;
+
+  # Only allowed methods to be used from the module's whitelist
+  my $file = $class;
+  $file =~ s{::}{/}g;
+  $file .= ".pm";
+  require $file;
+  if (none { $_ eq $method } $class->PUBLIC_METHODS) {
+    ThrowCodeError('unknown_method', {method => $full_method});
+  }
+  $self->SUPER::handle_login($class, $method, $full_method);
+  return;
 }
 
 1;
@@ -124,6 +127,7 @@ sub handle_login {
 # and also, in some cases, to more-usefully decode them.
 package Bugzilla::XMLRPC::Deserializer;
 use strict;
+
 # We can't use "use base" because XMLRPC::Serializer doesn't return
 # a true value.
 use XMLRPC::Lite;
@@ -135,100 +139,111 @@ use Bugzilla::WebService::Util qw(fix_credentials);
 use Scalar::Util qw(tainted);
 
 sub new {
-    my $self = shift->SUPER::new(@_);
-    # Initialise XML::Parser to not expand references to entities, to prevent DoS
-    require XML::Parser;
-    my $parser = XML::Parser->new( NoExpand => 1, Handlers => { Default => sub {} } );
-    $self->{_parser}->parser($parser, $parser);
-    return $self;
+  my $self = shift->SUPER::new(@_);
+
+  # Initialise XML::Parser to not expand references to entities, to prevent DoS
+  require XML::Parser;
+  my $parser = XML::Parser->new(
+    NoExpand => 1,
+    Handlers => {
+      Default => sub { }
+    }
+  );
+  $self->{_parser}->parser($parser, $parser);
+  return $self;
 }
 
 sub deserialize {
-    my $self = shift;
-
-    # Only allow certain content types to protect against CSRF attacks
-    my $content_type = lc($ENV{'CONTENT_TYPE'});
-    # Remove charset, etc, if provided
-    $content_type =~ s/^([^;]+);.*/$1/;
-    if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) {
-        ThrowUserError('xmlrpc_illegal_content_type',
-                       { content_type => $ENV{'CONTENT_TYPE'} });
-    }
+  my $self = shift;
 
-    my ($xml) = @_;
-    my $som = $self->SUPER::deserialize(@_);
-    if (tainted($xml)) {
-        $som->{_bz_do_taint} = 1;
-    }
-    bless $som, 'Bugzilla::XMLRPC::SOM';
-    my $params = $som->paramsin;
-    # This allows positional parameters for Testopia.
-    $params = {} if ref $params ne 'HASH';
+  # Only allow certain content types to protect against CSRF attacks
+  my $content_type = lc($ENV{'CONTENT_TYPE'});
+
+  # Remove charset, etc, if provided
+  $content_type =~ s/^([^;]+);.*/$1/;
+  if (!grep($_ eq $content_type, XMLRPC_CONTENT_TYPE_WHITELIST)) {
+    ThrowUserError('xmlrpc_illegal_content_type',
+      {content_type => $ENV{'CONTENT_TYPE'}});
+  }
+
+  my ($xml) = @_;
+  my $som = $self->SUPER::deserialize(@_);
+  if (tainted($xml)) {
+    $som->{_bz_do_taint} = 1;
+  }
+  bless $som, 'Bugzilla::XMLRPC::SOM';
+  my $params = $som->paramsin;
+
+  # This allows positional parameters for Testopia.
+  $params = {} if ref $params ne 'HASH';
 
-    # Update the params to allow for several convenience key/values
-    # use for authentication
-    fix_credentials($params);
+  # Update the params to allow for several convenience key/values
+  # use for authentication
+  fix_credentials($params);
 
-    Bugzilla->input_params($params);
+  Bugzilla->input_params($params);
 
-    return $som;
+  return $som;
 }
 
 # Some method arguments need to be converted in some way, when they are input.
 sub decode_value {
-    my $self = shift;
-    my ($type) = @{ $_[0] };
-    my $value = $self->SUPER::decode_value(@_);
-
-    # We only validate/convert certain types here.
-    return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/;
-
-    # Though the XML-RPC standard doesn't allow an empty ,
-    # ,or ,  we do, and we just say
-    # "that's undef".
-    if (grep($type eq $_, qw(int double dateTime))) {
-        return undef if $value eq '';
-    }
-
-    my $validator = $self->_validation_subs->{$type};
-    if (!$validator->($value)) {
-        ThrowUserError('xmlrpc_invalid_value',
-                       { type => $type, value => $value });
-    }
-
-    # We convert dateTimes to a DB-friendly date format.
-    if ($type eq 'dateTime.iso8601') {
-        if ($value !~ /T.*[\-+Z]/i) {
-           # The caller did not specify a timezone, so we assume UTC.
-           # pass 'Z' specifier to datetime_from to force it
-           $value = $value . 'Z';
-        }
-        $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value);
+  my $self   = shift;
+  my ($type) = @{$_[0]};
+  my $value  = $self->SUPER::decode_value(@_);
+
+  # We only validate/convert certain types here.
+  return $value if $type !~ /^(?:int|i4|boolean|double|dateTime\.iso8601)$/;
+
+  # Though the XML-RPC standard doesn't allow an empty ,
+  # ,or ,  we do, and we just say
+  # "that's undef".
+  if (grep($type eq $_, qw(int double dateTime))) {
+    return undef if $value eq '';
+  }
+
+  my $validator = $self->_validation_subs->{$type};
+  if (!$validator->($value)) {
+    ThrowUserError('xmlrpc_invalid_value', {type => $type, value => $value});
+  }
+
+  # We convert dateTimes to a DB-friendly date format.
+  if ($type eq 'dateTime.iso8601') {
+    if ($value !~ /T.*[\-+Z]/i) {
+
+      # The caller did not specify a timezone, so we assume UTC.
+      # pass 'Z' specifier to datetime_from to force it
+      $value = $value . 'Z';
     }
+    $value = Bugzilla::WebService::Server::XMLRPC->datetime_format_inbound($value);
+  }
 
-    return $value;
+  return $value;
 }
 
 sub _validation_subs {
-    my $self = shift;
-    return $self->{_validation_subs} if $self->{_validation_subs};
-    # The only place that XMLRPC::Lite stores any sort of validation
-    # regex is in XMLRPC::Serializer. We want to re-use those regexes here.
-    my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup;
-
-    # $lookup is a hash whose values are arrayrefs, and whose keys are the
-    # names of types. The second item of each arrayref is a subroutine
-    # that will do our validation for us.
-    my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup);
-    # Add a boolean validator
-    $validators{'boolean'} = sub {$_[0] =~ /^[01]$/};
-    # Some types have multiple names, or have a different name in
-    # XMLRPC::Serializer than their standard XML-RPC name.
-    $validators{'dateTime.iso8601'} = $validators{'dateTime'};
-    $validators{'i4'} = $validators{'int'};
-
-    $self->{_validation_subs} = \%validators;
-    return \%validators;
+  my $self = shift;
+  return $self->{_validation_subs} if $self->{_validation_subs};
+
+  # The only place that XMLRPC::Lite stores any sort of validation
+  # regex is in XMLRPC::Serializer. We want to re-use those regexes here.
+  my $lookup = Bugzilla::XMLRPC::Serializer->new->typelookup;
+
+  # $lookup is a hash whose values are arrayrefs, and whose keys are the
+  # names of types. The second item of each arrayref is a subroutine
+  # that will do our validation for us.
+  my %validators = map { $_ => $lookup->{$_}->[1] } (keys %$lookup);
+
+  # Add a boolean validator
+  $validators{'boolean'} = sub { $_[0] =~ /^[01]$/ };
+
+  # Some types have multiple names, or have a different name in
+  # XMLRPC::Serializer than their standard XML-RPC name.
+  $validators{'dateTime.iso8601'} = $validators{'dateTime'};
+  $validators{'i4'}               = $validators{'int'};
+
+  $self->{_validation_subs} = \%validators;
+  return \%validators;
 }
 
 1;
@@ -240,16 +255,16 @@ our @ISA = qw(XMLRPC::SOM);
 use Bugzilla::WebService::Util qw(taint_data);
 
 sub paramsin {
-    my $self = shift;
-    if (!$self->{bz_params_in}) {
-        my @params = $self->SUPER::paramsin(@_);
-        if ($self->{_bz_do_taint}) {
-            taint_data(@params);
-        }
-        $self->{bz_params_in} = \@params;
+  my $self = shift;
+  if (!$self->{bz_params_in}) {
+    my @params = $self->SUPER::paramsin(@_);
+    if ($self->{_bz_do_taint}) {
+      taint_data(@params);
     }
-    my $params = $self->{bz_params_in};
-    return wantarray ? @$params : $params->[0];
+    $self->{bz_params_in} = \@params;
+  }
+  my $params = $self->{bz_params_in};
+  return wantarray ? @$params : $params->[0];
 }
 
 1;
@@ -259,43 +274,46 @@ sub paramsin {
 package Bugzilla::XMLRPC::Serializer;
 use Scalar::Util qw(blessed);
 use strict;
+
 # We can't use "use base" because XMLRPC::Serializer doesn't return
 # a true value.
 use XMLRPC::Lite;
 our @ISA = qw(XMLRPC::Serializer);
 
 sub new {
-    my $class = shift;
-    my $self = $class->SUPER::new(@_);
-    # This fixes UTF-8.
-    $self->{'_typelookup'}->{'base64'} =
-        [10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/},
-        'as_base64'];
-    # This makes arrays work right even though we're a subclass.
-    # (See http://rt.cpan.org//Ticket/Display.html?id=34514)
-    $self->{'_encodingStyle'} = '';
-    return $self;
+  my $class = shift;
+  my $self  = $class->SUPER::new(@_);
+
+  # This fixes UTF-8.
+  $self->{'_typelookup'}->{'base64'} = [
+    10, sub { !utf8::is_utf8($_[0]) && $_[0] =~ /[^\x09\x0a\x0d\x20-\x7f]/ },
+    'as_base64'
+  ];
+
+  # This makes arrays work right even though we're a subclass.
+  # (See http://rt.cpan.org//Ticket/Display.html?id=34514)
+  $self->{'_encodingStyle'} = '';
+  return $self;
 }
 
 # Here the XMLRPC::Serializer is extended to use the XMLRPC nil extension.
 sub encode_object {
-    my $self = shift;
-    my @encoded = $self->SUPER::encode_object(@_);
+  my $self    = shift;
+  my @encoded = $self->SUPER::encode_object(@_);
 
-    return $encoded[0]->[0] eq 'nil'
-        ? ['value', {}, [@encoded]]
-        : @encoded;
+  return $encoded[0]->[0] eq 'nil' ? ['value', {}, [@encoded]] : @encoded;
 }
 
 # Removes undefined values so they do not produce invalid XMLRPC.
 sub envelope {
-    my $self = shift;
-    my ($type, $method, $data) = @_;
-    # If the type isn't a successful response we don't want to change the values.
-    if ($type eq 'response'){
-        $data = _strip_undefs($data);
-    }
-    return $self->SUPER::envelope($type, $method, $data);
+  my $self = shift;
+  my ($type, $method, $data) = @_;
+
+  # If the type isn't a successful response we don't want to change the values.
+  if ($type eq 'response') {
+    $data = _strip_undefs($data);
+  }
+  return $self->SUPER::envelope($type, $method, $data);
 }
 
 # In an XMLRPC response we have to handle hashes of arrays, hashes, scalars,
@@ -303,57 +321,57 @@ sub envelope {
 # The whole XMLRPC::Data object must be removed if its value key is undefined
 # so it cannot be recursed like the other hash type objects.
 sub _strip_undefs {
-    my ($initial) = @_;
-    if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) {
-        while (my ($key, $value) = each(%$initial)) {
-            if ( !defined $value
-                 || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
-            {
-                # If the value is undefined remove it from the hash.
-                delete $initial->{$key};
-            }
-            else {
-                $initial->{$key} = _strip_undefs($value);
-            }
-        }
+  my ($initial) = @_;
+  if (ref $initial eq "HASH" || (blessed $initial && $initial->isa("HASH"))) {
+    while (my ($key, $value) = each(%$initial)) {
+      if (!defined $value
+        || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value))
+      {
+        # If the value is undefined remove it from the hash.
+        delete $initial->{$key};
+      }
+      else {
+        $initial->{$key} = _strip_undefs($value);
+      }
     }
-    if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) {
-        for (my $count = 0; $count < scalar @{$initial}; $count++) {
-            my $value = $initial->[$count];
-            if ( !defined $value
-                 || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value) )
-            {
-                # If the value is undefined remove it from the array.
-                splice(@$initial, $count, 1);
-                $count--;
-            }
-            else {
-                $initial->[$count] = _strip_undefs($value);
-            }
-        }
+  }
+  if (ref $initial eq "ARRAY" || (blessed $initial && $initial->isa("ARRAY"))) {
+    for (my $count = 0; $count < scalar @{$initial}; $count++) {
+      my $value = $initial->[$count];
+      if (!defined $value
+        || (blessed $value && $value->isa('XMLRPC::Data') && !defined $value->value))
+      {
+        # If the value is undefined remove it from the array.
+        splice(@$initial, $count, 1);
+        $count--;
+      }
+      else {
+        $initial->[$count] = _strip_undefs($value);
+      }
     }
-    return $initial;
+  }
+  return $initial;
 }
 
 sub BEGIN {
-    no strict 'refs';
-    for my $type (qw(double i4 int dateTime)) {
-        my $method = 'as_' . $type;
-        *$method = sub {
-            my ($self, $value) = @_;
-            if (!defined($value)) {
-                return as_nil();
-            }
-            else {
-                my $super_method = "SUPER::$method";
-                return $self->$super_method($value);
-            }
-        }
-    }
+  no strict 'refs';
+  for my $type (qw(double i4 int dateTime)) {
+    my $method = 'as_' . $type;
+    *$method = sub {
+      my ($self, $value) = @_;
+      if (!defined($value)) {
+        return as_nil();
+      }
+      else {
+        my $super_method = "SUPER::$method";
+        return $self->$super_method($value);
+      }
+      }
+  }
 }
 
 sub as_nil {
-    return ['nil', {}];
+  return ['nil', {}];
 }
 
 1;
diff --git a/Bugzilla/WebService/User.pm b/Bugzilla/WebService/User.pm
index c569cf9d8..1e5127f8a 100644
--- a/Bugzilla/WebService/User.pm
+++ b/Bugzilla/WebService/User.pm
@@ -20,44 +20,38 @@ use Bugzilla::Group;
 use Bugzilla::User;
 use Bugzilla::Util qw(trim detaint_natural);
 use Bugzilla::WebService::Util qw(filter filter_wants validate
-                                  translate params_to_objects);
+  translate params_to_objects);
 use Bugzilla::Hook;
 
 use List::Util qw(first);
 use Taint::Util qw(untaint);
 
 # Don't need auth to login
-use constant LOGIN_EXEMPT => {
-    login => 1,
-    offer_account_by_email => 1,
-};
+use constant LOGIN_EXEMPT => {login => 1, offer_account_by_email => 1,};
 
 use constant READ_ONLY => qw(
-    get
-    suggest
+  get
+  suggest
 );
 
 use constant PUBLIC_METHODS => qw(
-    create
-    get
-    login
-    logout
-    offer_account_by_email
-    update
-    valid_login
-    whoami
+  create
+  get
+  login
+  logout
+  offer_account_by_email
+  update
+  valid_login
+  whoami
 );
 
-use constant MAPPED_FIELDS => {
-    email => 'login',
-    full_name => 'name',
-    login_denied_text => 'disabledtext',
-};
+use constant MAPPED_FIELDS =>
+  {email => 'login', full_name => 'name', login_denied_text => 'disabledtext',};
 
 use constant MAPPED_RETURNS => {
-    login_name => 'email',
-    realname => 'full_name',
-    disabledtext => 'login_denied_text',
+  login_name   => 'email',
+  realname     => 'full_name',
+  disabledtext => 'login_denied_text',
 };
 
 ##############
@@ -65,38 +59,38 @@ use constant MAPPED_RETURNS => {
 ##############
 
 sub login {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    # Check to see if we are already logged in
-    my $user = Bugzilla->user;
-    if ($user->id) {
-        return $self->_login_to_hash($user);
-    }
+  # Check to see if we are already logged in
+  my $user = Bugzilla->user;
+  if ($user->id) {
+    return $self->_login_to_hash($user);
+  }
 
-    # Username and password params are required
-    foreach my $param ("login", "password") {
-        (defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
-            || ThrowCodeError('param_required', { param => $param });
-    }
+  # Username and password params are required
+  foreach my $param ("login", "password") {
+    (defined $params->{$param} || defined $params->{'Bugzilla_' . $param})
+      || ThrowCodeError('param_required', {param => $param});
+  }
 
-    $user = Bugzilla->login();
-    return $self->_login_to_hash($user);
+  $user = Bugzilla->login();
+  return $self->_login_to_hash($user);
 }
 
 sub logout {
-    my $self = shift;
-    Bugzilla->logout;
+  my $self = shift;
+  Bugzilla->logout;
 }
 
 sub valid_login {
-    my ($self, $params) = @_;
-    defined $params->{login}
-        || ThrowCodeError('param_required', { param => 'login' });
-    Bugzilla->login();
-    if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
-        return $self->type('boolean', 1);
-    }
-    return $self->type('boolean', 0);
+  my ($self, $params) = @_;
+  defined $params->{login}
+    || ThrowCodeError('param_required', {param => 'login'});
+  Bugzilla->login();
+  if (Bugzilla->user->id && Bugzilla->user->login eq $params->{login}) {
+    return $self->type('boolean', 1);
+  }
+  return $self->type('boolean', 0);
 }
 
 #################
@@ -104,102 +98,100 @@ sub valid_login {
 #################
 
 sub offer_account_by_email {
-    my $self = shift;
-    my ($params) = @_;
-    my $email = trim($params->{email})
-        || ThrowCodeError('param_required', { param => 'email' });
-
-    Bugzilla->user->check_account_creation_enabled;
-    Bugzilla->user->check_and_send_account_creation_confirmation($email);
-    return undef;
+  my $self     = shift;
+  my ($params) = @_;
+  my $email    = trim($params->{email})
+    || ThrowCodeError('param_required', {param => 'email'});
+
+  Bugzilla->user->check_account_creation_enabled;
+  Bugzilla->user->check_and_send_account_creation_confirmation($email);
+  return undef;
 }
 
 sub create {
-    my $self = shift;
-    my ($params) = @_;
-
-    Bugzilla->user->in_group('editusers')
-        || ThrowUserError("auth_failure", { group  => "editusers",
-                                            action => "add",
-                                            object => "users"});
-
-    my $email = trim($params->{email})
-        || ThrowCodeError('param_required', { param => 'email' });
-    my $realname = trim($params->{full_name});
-    my $password = trim($params->{password}) || '*';
-
-    my $user = Bugzilla::User->create({
-        login_name    => $email,
-        realname      => $realname,
-        cryptpassword => $password
-    });
-
-    return { id => $self->type('int', $user->id) };
-}
+  my $self = shift;
+  my ($params) = @_;
 
-sub suggest {
-    my ($self, $params) = @_;
+  Bugzilla->user->in_group('editusers')
+    || ThrowUserError("auth_failure",
+    {group => "editusers", action => "add", object => "users"});
 
-    Bugzilla->switch_to_shadow_db();
+  my $email = trim($params->{email})
+    || ThrowCodeError('param_required', {param => 'email'});
+  my $realname = trim($params->{full_name});
+  my $password = trim($params->{password}) || '*';
 
-    ThrowCodeError('params_required', { function => 'User.suggest', params => ['match'] })
-      unless defined $params->{match};
-
-    ThrowUserError('user_access_by_match_denied')
-      unless Bugzilla->user->id;
-
-    untaint($params->{match});
-    my $s = $params->{match};
-    trim($s);
-    return { users => [] } if length($s) < 3;
+  my $user
+    = Bugzilla::User->create({
+    login_name => $email, realname => $realname, cryptpassword => $password
+    });
 
-    my $dbh = Bugzilla->dbh;
-    my @select = ('userid AS id', 'realname AS real_name', 'login_name AS name');
-    my $order  = 'last_seen_date DESC';
-    my $where;
-    state $have_mysql = $dbh->isa('Bugzilla::DB::Mysql');
+  return {id => $self->type('int', $user->id)};
+}
 
-    if ($s =~ /^[:@](.+)$/s) {
-        $where = $dbh->sql_prefix_match(nickname => $1);
+sub suggest {
+  my ($self, $params) = @_;
+
+  Bugzilla->switch_to_shadow_db();
+
+  ThrowCodeError('params_required',
+    {function => 'User.suggest', params => ['match']})
+    unless defined $params->{match};
+
+  ThrowUserError('user_access_by_match_denied') unless Bugzilla->user->id;
+
+  untaint($params->{match});
+  my $s = $params->{match};
+  trim($s);
+  return {users => []} if length($s) < 3;
+
+  my $dbh    = Bugzilla->dbh;
+  my @select = ('userid AS id', 'realname AS real_name', 'login_name AS name');
+  my $order  = 'last_seen_date DESC';
+  my $where;
+  state $have_mysql = $dbh->isa('Bugzilla::DB::Mysql');
+
+  if ($s =~ /^[:@](.+)$/s) {
+    $where = $dbh->sql_prefix_match(nickname => $1);
+  }
+  elsif ($s =~ /@/) {
+    $where = $dbh->sql_prefix_match(login_name => $s);
+  }
+  else {
+    if ($have_mysql && ($s =~ /[[:space:]]/ || $s =~ /[^[:ascii:]]/)) {
+      my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s);
+      push @select, "$match AS relevance";
+      $order = 'relevance DESC';
+      $where = $match;
     }
-    elsif ($s =~ /@/) {
-        $where = $dbh->sql_prefix_match(login_name => $s);
+    elsif ($have_mysql && $s =~ /^[[:upper:]]/) {
+      my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s);
+      $where = join ' OR ', $match, $dbh->sql_prefix_match(nickname => $s),
+        $dbh->sql_prefix_match(login_name => $s);
     }
     else {
-        if ($have_mysql && ( $s =~ /[[:space:]]/ || $s =~ /[^[:ascii:]]/ ) ) {
-            my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s);
-            push @select, "$match AS relevance";
-            $order = 'relevance DESC';
-            $where = $match;
-        }
-        elsif ($have_mysql && $s =~ /^[[:upper:]]/) {
-            my $match = sprintf 'MATCH(realname) AGAINST (%s) ', $dbh->quote($s);
-            $where = join ' OR ',
-                $match,
-                $dbh->sql_prefix_match( nickname => $s ),
-                $dbh->sql_prefix_match( login_name => $s );
-        }
-        else {
-            $where = join ' OR ', $dbh->sql_prefix_match( nickname => $s ), $dbh->sql_prefix_match( login_name => $s );
-        }
+      $where = join ' OR ', $dbh->sql_prefix_match(nickname => $s),
+        $dbh->sql_prefix_match(login_name => $s);
     }
-    $where = "($where) AND is_enabled = 1";
+  }
+  $where = "($where) AND is_enabled = 1";
 
-    my $sql = 'SELECT ' . join(', ', @select) . " FROM profiles WHERE $where ORDER BY $order LIMIT 25";
-    my $results = $dbh->selectall_arrayref($sql, { Slice => {} });
+  my $sql
+    = 'SELECT '
+    . join(', ', @select)
+    . " FROM profiles WHERE $where ORDER BY $order LIMIT 25";
+  my $results = $dbh->selectall_arrayref($sql, {Slice => {}});
 
-    my @users = map {
-        {
-            id        => $self->type(int    => $_->{id}),
-            real_name => $self->type(string => $_->{real_name}),
-            name      => $self->type(email  => $_->{name}),
-        }
-    } @$results;
+  my @users = map { {
+    id        => $self->type(int    => $_->{id}),
+    real_name => $self->type(string => $_->{real_name}),
+    name      => $self->type(email  => $_->{name}),
+  } } @$results;
 
-    Bugzilla::Hook::process('webservice_user_suggest',
-        { webservice => $self, params => $params, users => \@users });
+  Bugzilla::Hook::process('webservice_user_suggest',
+    {webservice => $self, params => $params, users => \@users});
 
-    return { users => \@users };
+  return {users => \@users};
 }
 
 # function to return user information by passing either user ids or
@@ -207,125 +199,132 @@ sub suggest {
 # $call = $rpc->call( 'User.get', { ids => [1,2,3],
 #         names => ['testusera@redhat.com', 'testuserb@redhat.com'] });
 sub get {
-    my ($self, $params) = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
+  my ($self, $params)
+    = validate(@_, 'names', 'ids', 'match', 'group_ids', 'groups');
 
-    Bugzilla->switch_to_shadow_db();
+  Bugzilla->switch_to_shadow_db();
 
-    defined($params->{names}) || defined($params->{ids})
-        || defined($params->{match})
-        || ThrowCodeError('params_required',
-               { function => 'User.get', params => ['ids', 'names', 'match'] });
+       defined($params->{names})
+    || defined($params->{ids})
+    || defined($params->{match})
+    || ThrowCodeError('params_required',
+    {function => 'User.get', params => ['ids', 'names', 'match']});
 
-    my @user_objects;
-    @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }
-                    if $params->{names};
+  my @user_objects;
+  @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}}
+    if $params->{names};
 
-    # start filtering to remove duplicate user ids
-    my %unique_users = map { $_->id => $_ } @user_objects;
-    @user_objects = values %unique_users;
+  # start filtering to remove duplicate user ids
+  my %unique_users = map { $_->id => $_ } @user_objects;
+  @user_objects = values %unique_users;
 
-    my @users;
+  my @users;
 
-    # If the user is not logged in: Return an error if they passed any user ids.
-    # Otherwise, return a limited amount of information based on login names.
-    if (!Bugzilla->user->id){
-        if ($params->{ids}){
-            ThrowUserError("user_access_by_id_denied");
-        }
-        if ($params->{match}) {
-            ThrowUserError('user_access_by_match_denied');
-        }
-        my $in_group = $self->_filter_users_by_group(
-            \@user_objects, $params);
-        @users = map { filter $params, {
-                     id        => $self->type('int', $_->id),
-                     real_name => $self->type('string', $_->name),
-                     name      => $self->type('email', $_->login),
-                 } } @$in_group;
-
-        return { users => \@users };
+  # If the user is not logged in: Return an error if they passed any user ids.
+  # Otherwise, return a limited amount of information based on login names.
+  if (!Bugzilla->user->id) {
+    if ($params->{ids}) {
+      ThrowUserError("user_access_by_id_denied");
     }
-
-    my $obj_by_ids;
-    $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
-
-    # obj_by_ids are only visible to the user if he can see
-    # the otheruser, for non visible otheruser throw an error
-    foreach my $obj (@$obj_by_ids) {
-        if (Bugzilla->user->can_see_user($obj)){
-            if (!$unique_users{$obj->id}) {
-                push (@user_objects, $obj);
-                $unique_users{$obj->id} = $obj;
-            }
+    if ($params->{match}) {
+      ThrowUserError('user_access_by_match_denied');
+    }
+    my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
+    @users = map {
+      filter $params,
+        {
+        id        => $self->type('int',    $_->id),
+        real_name => $self->type('string', $_->name),
+        name      => $self->type('email',  $_->login),
         }
-        else {
-            ThrowUserError('auth_failure', {reason => "not_visible",
-                                            action => "access",
-                                            object => "user",
-                                            userid => $obj->id});
+    } @$in_group;
+
+    return {users => \@users};
+  }
+
+  my $obj_by_ids;
+  $obj_by_ids = Bugzilla::User->new_from_list($params->{ids}) if $params->{ids};
+
+  # obj_by_ids are only visible to the user if he can see
+  # the otheruser, for non visible otheruser throw an error
+  foreach my $obj (@$obj_by_ids) {
+    if (Bugzilla->user->can_see_user($obj)) {
+      if (!$unique_users{$obj->id}) {
+        push(@user_objects, $obj);
+        $unique_users{$obj->id} = $obj;
+      }
+    }
+    else {
+      ThrowUserError(
+        'auth_failure',
+        {
+          reason => "not_visible",
+          action => "access",
+          object => "user",
+          userid => $obj->id
         }
+      );
     }
-
-    # User Matching
-    my $limit;
-    if ($params->{limit}) {
-        detaint_natural($params->{limit})
-            || ThrowCodeError('param_must_be_numeric',
-                              { function => 'User.match', param => 'limit' });
-        $limit = $limit ? min($params->{limit}, $limit) : $params->{limit};
+  }
+
+  # User Matching
+  my $limit;
+  if ($params->{limit}) {
+    detaint_natural($params->{limit})
+      || ThrowCodeError('param_must_be_numeric',
+      {function => 'User.match', param => 'limit'});
+    $limit = $limit ? min($params->{limit}, $limit) : $params->{limit};
+  }
+  my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1;
+  foreach my $match_string (@{$params->{'match'} || []}) {
+    my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled);
+    foreach my $user (@$matched) {
+      if (!$unique_users{$user->id}) {
+        push(@user_objects, $user);
+        $unique_users{$user->id} = $user;
+      }
     }
-    my $exclude_disabled = $params->{'include_disabled'} ? 0 : 1;
-    foreach my $match_string (@{ $params->{'match'} || [] }) {
-        my $matched = Bugzilla::User::match($match_string, $limit, $exclude_disabled);
-        foreach my $user (@$matched) {
-            if (!$unique_users{$user->id}) {
-                push(@user_objects, $user);
-                $unique_users{$user->id} = $user;
-            }
-        }
+  }
+
+  my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
+  foreach my $user (@$in_group) {
+    my $user_info = filter $params,
+      {
+      id        => $self->type('int',     $user->id),
+      real_name => $self->type('string',  $user->name),
+      name      => $self->type('email',   $user->login),
+      email     => $self->type('email',   $user->email),
+      can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
+      };
+
+    if (Bugzilla->user->in_group('editusers')) {
+      $user_info->{email_enabled}     = $self->type('boolean', $user->email_enabled);
+      $user_info->{login_denied_text} = $self->type('string',  $user->disabledtext);
     }
 
-    my $in_group = $self->_filter_users_by_group(\@user_objects, $params);
-    foreach my $user (@$in_group) {
-        my $user_info = filter $params, {
-            id        => $self->type('int', $user->id),
-            real_name => $self->type('string', $user->name),
-            name      => $self->type('email', $user->login),
-            email     => $self->type('email', $user->email),
-            can_login => $self->type('boolean', $user->is_enabled ? 1 : 0),
-        };
-
-        if (Bugzilla->user->in_group('editusers')) {
-            $user_info->{email_enabled}     = $self->type('boolean', $user->email_enabled);
-            $user_info->{login_denied_text} = $self->type('string', $user->disabledtext);
-        }
-
-        if (Bugzilla->user->id == $user->id) {
-            if (filter_wants($params, 'saved_searches')) {
-                $user_info->{saved_searches} = [
-                    map { $self->_query_to_hash($_) } @{ $user->queries }
-                ];
-            }
-        }
-
-        if (filter_wants($params, 'groups')) {
-            if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) {
-                $user_info->{groups} = [
-                    map { $self->_group_to_hash($_) } @{ $user->groups }
-                ];
-            }
-            else {
-                $user_info->{groups} = $self->_filter_bless_groups($user->groups);
-            }
-        }
+    if (Bugzilla->user->id == $user->id) {
+      if (filter_wants($params, 'saved_searches')) {
+        $user_info->{saved_searches}
+          = [map { $self->_query_to_hash($_) } @{$user->queries}];
+      }
+    }
 
-        push(@users, $user_info);
+    if (filter_wants($params, 'groups')) {
+      if (Bugzilla->user->id == $user->id || Bugzilla->user->in_group('editusers')) {
+        $user_info->{groups} = [map { $self->_group_to_hash($_) } @{$user->groups}];
+      }
+      else {
+        $user_info->{groups} = $self->_filter_bless_groups($user->groups);
+      }
     }
 
-    Bugzilla::Hook::process('webservice_user_get',
-        { webservice => $self, params => $params, users => \@users });
+    push(@users, $user_info);
+  }
 
-    return { users => \@users };
+  Bugzilla::Hook::process('webservice_user_get',
+    {webservice => $self, params => $params, users => \@users});
+
+  return {users => \@users};
 }
 
 ###############
@@ -333,145 +332,144 @@ sub get {
 ###############
 
 sub update {
-    my ($self, $params) = @_;
+  my ($self, $params) = @_;
 
-    my $dbh = Bugzilla->dbh;
+  my $dbh = Bugzilla->dbh;
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
 
-    # Reject access if there is no sense in continuing.
-    $user->in_group('editusers')
-        || ThrowUserError("auth_failure", {group  => "editusers",
-                                           action => "edit",
-                                           object => "users"});
+  # Reject access if there is no sense in continuing.
+  $user->in_group('editusers')
+    || ThrowUserError("auth_failure",
+    {group => "editusers", action => "edit", object => "users"});
 
-    defined($params->{names}) || defined($params->{ids})
-        || ThrowCodeError('params_required',
-               { function => 'User.update', params => ['ids', 'names'] });
+  defined($params->{names})
+    || defined($params->{ids})
+    || ThrowCodeError('params_required',
+    {function => 'User.update', params => ['ids', 'names']});
 
-    my $user_objects = params_to_objects($params, 'Bugzilla::User');
+  my $user_objects = params_to_objects($params, 'Bugzilla::User');
 
-    my $values = translate($params, MAPPED_FIELDS);
+  my $values = translate($params, MAPPED_FIELDS);
 
-    # We delete names and ids to keep only new values to set.
-    delete $values->{names};
-    delete $values->{ids};
+  # We delete names and ids to keep only new values to set.
+  delete $values->{names};
+  delete $values->{ids};
 
-    $dbh->bz_start_transaction();
-    foreach my $user (@$user_objects){
-        $user->set_all($values);
-    }
+  $dbh->bz_start_transaction();
+  foreach my $user (@$user_objects) {
+    $user->set_all($values);
+  }
 
-    my %changes;
-    foreach my $user (@$user_objects){
-        my $returned_changes = $user->update();
-        $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
-    }
-    $dbh->bz_commit_transaction();
-
-    my @result;
-    foreach my $user (@$user_objects) {
-        my %hash = (
-            id      => $user->id,
-            changes => {},
-        );
-
-        foreach my $field (keys %{ $changes{$user->id} }) {
-            my $change = $changes{$user->id}->{$field};
-            # We normalize undef to an empty string, so that the API
-            # stays consistent for things that can become empty.
-            $change->[0] = '' if !defined $change->[0];
-            $change->[1] = '' if !defined $change->[1];
-            # We also flatten arrays (used by groups and blessed_groups)
-            $change->[0] = join(',', @{$change->[0]}) if ref $change->[0];
-            $change->[1] = join(',', @{$change->[1]}) if ref $change->[1];
-
-            $hash{changes}{$field} = {
-                removed => $self->type('string', $change->[0]),
-                added   => $self->type('string', $change->[1])
-            };
-        }
+  my %changes;
+  foreach my $user (@$user_objects) {
+    my $returned_changes = $user->update();
+    $changes{$user->id} = translate($returned_changes, MAPPED_RETURNS);
+  }
+  $dbh->bz_commit_transaction();
+
+  my @result;
+  foreach my $user (@$user_objects) {
+    my %hash = (id => $user->id, changes => {},);
+
+    foreach my $field (keys %{$changes{$user->id}}) {
+      my $change = $changes{$user->id}->{$field};
+
+      # We normalize undef to an empty string, so that the API
+      # stays consistent for things that can become empty.
+      $change->[0] = '' if !defined $change->[0];
+      $change->[1] = '' if !defined $change->[1];
 
-        push(@result, \%hash);
+      # We also flatten arrays (used by groups and blessed_groups)
+      $change->[0] = join(',', @{$change->[0]}) if ref $change->[0];
+      $change->[1] = join(',', @{$change->[1]}) if ref $change->[1];
+
+      $hash{changes}{$field} = {
+        removed => $self->type('string', $change->[0]),
+        added   => $self->type('string', $change->[1])
+      };
     }
 
-    return { users => \@result };
+    push(@result, \%hash);
+  }
+
+  return {users => \@result};
 }
 
 sub _filter_users_by_group {
-    my ($self, $users, $params) = @_;
-    my ($group_ids, $group_names) = @$params{qw(group_ids groups)};
+  my ($self, $users, $params) = @_;
+  my ($group_ids, $group_names) = @$params{qw(group_ids groups)};
 
-    # If no groups are specified, we return all users.
-    return $users if (!$group_ids and !$group_names);
+  # If no groups are specified, we return all users.
+  return $users if (!$group_ids and !$group_names);
 
-    my $user = Bugzilla->user;
+  my $user = Bugzilla->user;
 
-    my @groups = map { Bugzilla::Group->check({ id => $_ }) }
-                     @{ $group_ids || [] };
+  my @groups = map { Bugzilla::Group->check({id => $_}) } @{$group_ids || []};
 
-    if ($group_names) {
-        foreach my $name (@$group_names) {
-            my $group = Bugzilla::Group->check({ name => $name, _error => 'invalid_group_name' });
-            $user->in_group($group) || ThrowUserError('invalid_group_name', { name => $name });
-            push(@groups, $group);
-        }
+  if ($group_names) {
+    foreach my $name (@$group_names) {
+      my $group
+        = Bugzilla::Group->check({name => $name, _error => 'invalid_group_name'});
+      $user->in_group($group)
+        || ThrowUserError('invalid_group_name', {name => $name});
+      push(@groups, $group);
     }
+  }
 
-    my @in_group = grep { $self->_user_in_any_group($_, \@groups) }
-                        @$users;
-    return \@in_group;
+  my @in_group = grep { $self->_user_in_any_group($_, \@groups) } @$users;
+  return \@in_group;
 }
 
 sub _user_in_any_group {
-    my ($self, $user, $groups) = @_;
-    foreach my $group (@$groups) {
-        return 1 if $user->in_group($group);
-    }
-    return 0;
+  my ($self, $user, $groups) = @_;
+  foreach my $group (@$groups) {
+    return 1 if $user->in_group($group);
+  }
+  return 0;
 }
 
 sub _filter_bless_groups {
-    my ($self, $groups) = @_;
-    my $user = Bugzilla->user;
+  my ($self, $groups) = @_;
+  my $user = Bugzilla->user;
 
-    my @filtered_groups;
-    foreach my $group (@$groups) {
-        next unless ($user->in_group('editusers') || $user->can_bless($group->id));
-        push(@filtered_groups, $self->_group_to_hash($group));
-    }
+  my @filtered_groups;
+  foreach my $group (@$groups) {
+    next unless ($user->in_group('editusers') || $user->can_bless($group->id));
+    push(@filtered_groups, $self->_group_to_hash($group));
+  }
 
-    return \@filtered_groups;
+  return \@filtered_groups;
 }
 
 sub _group_to_hash {
-    my ($self, $group) = @_;
-    my $item = {
-        id          => $self->type('int', $group->id),
-        name        => $self->type('string', $group->name),
-        description => $self->type('string', $group->description),
-    };
-    return $item;
+  my ($self, $group) = @_;
+  my $item = {
+    id          => $self->type('int',    $group->id),
+    name        => $self->type('string', $group->name),
+    description => $self->type('string', $group->description),
+  };
+  return $item;
 }
 
 sub _query_to_hash {
-    my ($self, $query) = @_;
-    my $item = {
-        id   => $self->type('int', $query->id),
-        name => $self->type('string', $query->name),
-        url  => $self->type('string', $query->url),
-    };
-
-    return $item;
+  my ($self, $query) = @_;
+  my $item = {
+    id   => $self->type('int',    $query->id),
+    name => $self->type('string', $query->name),
+    url  => $self->type('string', $query->url),
+  };
+
+  return $item;
 }
 
 sub _login_to_hash {
-    my ($self, $user) = @_;
-    my $item = { id => $self->type('int', $user->id) };
-    if (my $login_token = $user->{_login_token}) {
-        $item->{'token'} = $user->id . "-" . $login_token;
-    }
-    return $item;
+  my ($self, $user) = @_;
+  my $item = {id => $self->type('int', $user->id)};
+  if (my $login_token = $user->{_login_token}) {
+    $item->{'token'} = $user->id . "-" . $login_token;
+  }
+  return $item;
 }
 
 #
@@ -479,27 +477,27 @@ sub _login_to_hash {
 #
 
 sub mfa_enroll {
-    my ($self, $params) = @_;
-    my $provider_name = lc($params->{provider});
+  my ($self, $params) = @_;
+  my $provider_name = lc($params->{provider});
 
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    $user->set_mfa($provider_name);
-    my $provider = $user->mfa_provider // die "Unknown MFA provider\n";
-    return $provider->enroll_api();
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  $user->set_mfa($provider_name);
+  my $provider = $user->mfa_provider // die "Unknown MFA provider\n";
+  return $provider->enroll_api();
 }
 
 sub whoami {
-    my ( $self, $params ) = @_;
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    return filter(
-        $params,
-        {
-            id         => $self->type( 'int',     $user->id ),
-            real_name  => $self->type( 'string',  $user->name ),
-            name       => $self->type( 'email',   $user->login ),
-            mfa_status => $self->type( 'boolean', !!$user->mfa ),
-        }
-    );
+  my ($self, $params) = @_;
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  return filter(
+    $params,
+    {
+      id         => $self->type('int',     $user->id),
+      real_name  => $self->type('string',  $user->name),
+      name       => $self->type('email',   $user->login),
+      mfa_status => $self->type('boolean', !!$user->mfa),
+    }
+  );
 }
 
 1;
diff --git a/Bugzilla/WebService/Util.pm b/Bugzilla/WebService/Util.pm
index ce5586911..933d1b5f7 100644
--- a/Bugzilla/WebService/Util.pm
+++ b/Bugzilla/WebService/Util.pm
@@ -29,284 +29,292 @@ use base qw(Exporter);
 require Test::Taint if ${^TAINT};
 
 our @EXPORT_OK = qw(
-    extract_flags
-    filter
-    filter_wants
-    taint_data
-    validate
-    translate
-    params_to_objects
-    fix_credentials
+  extract_flags
+  filter
+  filter_wants
+  taint_data
+  validate
+  translate
+  params_to_objects
+  fix_credentials
 );
 
 sub extract_flags {
-    my ($flags, $bug, $attachment) = @_;
-    my (@new_flags, @old_flags);
+  my ($flags, $bug, $attachment) = @_;
+  my (@new_flags, @old_flags);
 
-    my $flag_types    = $attachment ? $attachment->flag_types : $bug->flag_types;
-    my $current_flags = $attachment ? $attachment->flags : $bug->flags;
+  my $flag_types    = $attachment ? $attachment->flag_types : $bug->flag_types;
+  my $current_flags = $attachment ? $attachment->flags      : $bug->flags;
 
-    # Copy the user provided $flags as we may call extract_flags more than
-    # once when editing multiple bugs or attachments.
-    my $flags_copy = dclone($flags);
+  # Copy the user provided $flags as we may call extract_flags more than
+  # once when editing multiple bugs or attachments.
+  my $flags_copy = dclone($flags);
 
-    foreach my $flag (@$flags_copy) {
-        my $id      = $flag->{id};
-        my $type_id = $flag->{type_id};
+  foreach my $flag (@$flags_copy) {
+    my $id      = $flag->{id};
+    my $type_id = $flag->{type_id};
 
-        my $new  = delete $flag->{new};
-        my $name = delete $flag->{name};
+    my $new  = delete $flag->{new};
+    my $name = delete $flag->{name};
 
-        if ($id) {
-            my $flag_obj = grep($id == $_->id, @$current_flags);
-            $flag_obj || ThrowUserError('object_does_not_exist',
-                                        { class => 'Bugzilla::Flag', id => $id });
-        }
-        elsif ($type_id) {
-            my $type_obj = grep($type_id == $_->id, @$flag_types);
-            $type_obj || ThrowUserError('object_does_not_exist',
-                                        { class => 'Bugzilla::FlagType', id => $type_id });
-            if (!$new) {
-                my @flag_matches = grep($type_id == $_->type->id, @$current_flags);
-                @flag_matches > 1 && ThrowUserError('flag_not_unique',
-                                                     { value => $type_id });
-                if (!@flag_matches) {
-                    delete $flag->{id};
-                }
-                else {
-                    delete $flag->{type_id};
-                    $flag->{id} = $flag_matches[0]->id;
-                }
-            }
+    if ($id) {
+      my $flag_obj = grep($id == $_->id, @$current_flags);
+      $flag_obj
+        || ThrowUserError('object_does_not_exist',
+        {class => 'Bugzilla::Flag', id => $id});
+    }
+    elsif ($type_id) {
+      my $type_obj = grep($type_id == $_->id, @$flag_types);
+      $type_obj
+        || ThrowUserError('object_does_not_exist',
+        {class => 'Bugzilla::FlagType', id => $type_id});
+      if (!$new) {
+        my @flag_matches = grep($type_id == $_->type->id, @$current_flags);
+        @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $type_id});
+        if (!@flag_matches) {
+          delete $flag->{id};
         }
-        elsif ($name) {
-            my @type_matches = grep($name eq $_->name, @$flag_types);
-            @type_matches > 1 && ThrowUserError('flag_type_not_unique',
-                                                { value => $name });
-            @type_matches || ThrowUserError('object_does_not_exist',
-                                            { class => 'Bugzilla::FlagType', name => $name });
-            if ($new) {
-                delete $flag->{id};
-                $flag->{type_id} = $type_matches[0]->id;
-            }
-            else {
-                my @flag_matches = grep($name eq $_->type->name, @$current_flags);
-                @flag_matches > 1 && ThrowUserError('flag_not_unique', { value => $name });
-                if (@flag_matches) {
-                    $flag->{id} = $flag_matches[0]->id;
-                }
-                else {
-                    delete $flag->{id};
-                    $flag->{type_id} = $type_matches[0]->id;
-                }
-            }
+        else {
+          delete $flag->{type_id};
+          $flag->{id} = $flag_matches[0]->id;
         }
-
-        if ($flag->{id}) {
-            push(@old_flags, $flag);
+      }
+    }
+    elsif ($name) {
+      my @type_matches = grep($name eq $_->name, @$flag_types);
+      @type_matches > 1 && ThrowUserError('flag_type_not_unique', {value => $name});
+      @type_matches
+        || ThrowUserError('object_does_not_exist',
+        {class => 'Bugzilla::FlagType', name => $name});
+      if ($new) {
+        delete $flag->{id};
+        $flag->{type_id} = $type_matches[0]->id;
+      }
+      else {
+        my @flag_matches = grep($name eq $_->type->name, @$current_flags);
+        @flag_matches > 1 && ThrowUserError('flag_not_unique', {value => $name});
+        if (@flag_matches) {
+          $flag->{id} = $flag_matches[0]->id;
         }
         else {
-            push(@new_flags, $flag);
+          delete $flag->{id};
+          $flag->{type_id} = $type_matches[0]->id;
         }
+      }
     }
 
-    return (\@old_flags, \@new_flags);
+    if ($flag->{id}) {
+      push(@old_flags, $flag);
+    }
+    else {
+      push(@new_flags, $flag);
+    }
+  }
+
+  return (\@old_flags, \@new_flags);
 }
 
 sub filter($$;$$) {
-    my ($params, $hash, $types, $prefix) = @_;
-    my %newhash = %$hash;
+  my ($params, $hash, $types, $prefix) = @_;
+  my %newhash = %$hash;
 
-    foreach my $key (keys %$hash) {
-        delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
-    }
+  foreach my $key (keys %$hash) {
+    delete $newhash{$key} if !filter_wants($params, $key, $types, $prefix);
+  }
 
-    return \%newhash;
+  return \%newhash;
 }
 
 sub filter_wants($$;$$) {
-    my ($params, $field, $types, $prefix) = @_;
-
-    # Since this is operation is resource intensive, we will cache the results
-    # This assumes that $params->{*_fields} doesn't change between calls
-    my $cache = Bugzilla->request_cache->{filter_wants} ||= {};
-    $field = "${prefix}.${field}" if $prefix;
-
-    if (exists $cache->{$field}) {
-        return $cache->{$field};
-    }
-
-    # Mimic old behavior if no types provided
-    my %field_types = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
-
-    my %include = map { $_ => 1 } @{ $params->{'include_fields'} || [] };
-    my %exclude = map { $_ => 1 } @{ $params->{'exclude_fields'} || [] };
-
-    my %include_types;
-    my %exclude_types;
-
-    # Only return default fields if nothing is specified
-    $include_types{default} = 1 if !%include;
-
-    # Look for any field types requested
-    foreach my $key (keys %include) {
-        next if $key !~ /^_(.*)$/;
-        $include_types{$1} = 1;
-        delete $include{$key};
-    }
-    foreach my $key (keys %exclude) {
-        next if $key !~ /^_(.*)$/;
-        $exclude_types{$1} = 1;
-        delete $exclude{$key};
-    }
-
-    # Explicit inclusion/exclusion
-    return $cache->{$field} = 0 if $exclude{$field};
-    return $cache->{$field} = 1 if $include{$field};
-
-    # If the user has asked to include all or exclude all
-    return $cache->{$field} = 0 if $exclude_types{'all'};
-    return $cache->{$field} = 1 if $include_types{'all'};
-
-    # If the user has not asked for any fields specifically or if the user has asked
-    # for one or more of the field's types (and not excluded them)
-    foreach my $type (keys %field_types) {
-        return $cache->{$field} = 0 if $exclude_types{$type};
-        return $cache->{$field} = 1 if $include_types{$type};
-    }
-
-    my $wants = 0;
-    if ($prefix) {
-        # Include the field if the parent is include (and this one is not excluded)
-        $wants = 1 if $include{$prefix};
-    }
-    else {
-        # We want to include this if one of the sub keys is included
-        my $key = $field . '.';
-        my $len = length($key);
-        $wants = 1 if grep { substr($_, 0, $len) eq $key  } keys %include;
-    }
-
-    return $cache->{$field} = $wants;
+  my ($params, $field, $types, $prefix) = @_;
+
+  # Since this is operation is resource intensive, we will cache the results
+  # This assumes that $params->{*_fields} doesn't change between calls
+  my $cache = Bugzilla->request_cache->{filter_wants} ||= {};
+  $field = "${prefix}.${field}" if $prefix;
+
+  if (exists $cache->{$field}) {
+    return $cache->{$field};
+  }
+
+  # Mimic old behavior if no types provided
+  my %field_types
+    = map { $_ => 1 } (ref $types ? @$types : ($types || 'default'));
+
+  my %include = map { $_ => 1 } @{$params->{'include_fields'} || []};
+  my %exclude = map { $_ => 1 } @{$params->{'exclude_fields'} || []};
+
+  my %include_types;
+  my %exclude_types;
+
+  # Only return default fields if nothing is specified
+  $include_types{default} = 1 if !%include;
+
+  # Look for any field types requested
+  foreach my $key (keys %include) {
+    next if $key !~ /^_(.*)$/;
+    $include_types{$1} = 1;
+    delete $include{$key};
+  }
+  foreach my $key (keys %exclude) {
+    next if $key !~ /^_(.*)$/;
+    $exclude_types{$1} = 1;
+    delete $exclude{$key};
+  }
+
+  # Explicit inclusion/exclusion
+  return $cache->{$field} = 0 if $exclude{$field};
+  return $cache->{$field} = 1 if $include{$field};
+
+  # If the user has asked to include all or exclude all
+  return $cache->{$field} = 0 if $exclude_types{'all'};
+  return $cache->{$field} = 1 if $include_types{'all'};
+
+  # If the user has not asked for any fields specifically or if the user has asked
+  # for one or more of the field's types (and not excluded them)
+  foreach my $type (keys %field_types) {
+    return $cache->{$field} = 0 if $exclude_types{$type};
+    return $cache->{$field} = 1 if $include_types{$type};
+  }
+
+  my $wants = 0;
+  if ($prefix) {
+
+    # Include the field if the parent is include (and this one is not excluded)
+    $wants = 1 if $include{$prefix};
+  }
+  else {
+    # We want to include this if one of the sub keys is included
+    my $key = $field . '.';
+    my $len = length($key);
+    $wants = 1 if grep { substr($_, 0, $len) eq $key } keys %include;
+  }
+
+  return $cache->{$field} = $wants;
 }
 
 sub taint_data {
-    my @params = @_;
-    return if !@params;
-    # Though this is a private function, it hasn't changed since 2004 and
-    # should be safe to use, and prevents us from having to write it ourselves
-    # or require another module to do it.
-    if (${^TAINT}) {
-        Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params);
-        Test::Taint::taint_deeply(\@params);
-    }
+  my @params = @_;
+  return if !@params;
+
+  # Though this is a private function, it hasn't changed since 2004 and
+  # should be safe to use, and prevents us from having to write it ourselves
+  # or require another module to do it.
+  if (${^TAINT}) {
+    Test::Taint::_deeply_traverse(\&_delete_bad_keys, \@params);
+    Test::Taint::taint_deeply(\@params);
+  }
 }
 
 sub _delete_bad_keys {
-    foreach my $item (@_) {
-        next if ref $item ne 'HASH';
-        foreach my $key (keys %$item) {
-            # Making something a hash key always untaints it, in Perl.
-            # However, we need to validate our argument names in some way.
-            # We know that all hash keys passed in to the WebService will
-            # match \w+, so we delete any key that doesn't match that.
-            if ($key !~ /^[\w\.\-]+$/) {
-                delete $item->{$key};
-            }
-        }
+  foreach my $item (@_) {
+    next if ref $item ne 'HASH';
+    foreach my $key (keys %$item) {
+
+      # Making something a hash key always untaints it, in Perl.
+      # However, we need to validate our argument names in some way.
+      # We know that all hash keys passed in to the WebService will
+      # match \w+, so we delete any key that doesn't match that.
+      if ($key !~ /^[\w\.\-]+$/) {
+        delete $item->{$key};
+      }
     }
-    return @_;
+  }
+  return @_;
 }
 
-sub validate  {
-    my ($self, $params, @keys) = @_;
-    my $cache_key = join('|', (caller(1))[3], sort @keys);
-    # Type->of() is the same as Type[], used here because it is easier
-    # to chain with plus_coercions.
-    state $array_of_nonrefs = ArrayRef->of(Maybe[Value])->plus_coercions(
-        Maybe[Value], q{ [ $_ ] },
-    );
-    state $type_cache = {};
-    my $params_type = $type_cache->{$cache_key} //= do {
-        my %fields = map { $_ => Optional[$array_of_nonrefs] } @keys;
-        Maybe[ Dict[%fields, slurpy Any] ];
-    };
-
-    # If $params is defined but not a reference, then we weren't
-    # sent any parameters at all, and we're getting @keys where
-    # $params should be.
-    return ($self, undef) if (defined $params and !ref $params);
-
-    # If @keys is not empty then we convert any named
-    # parameters that have scalar values to arrayrefs
-    # that match.
-    $params = $params_type->coerce($params);
-    if (my $type_error = $params_type->validate($params)) {
-        FATAL("validate() found type error: $type_error");
-        ThrowUserError('invalid_params', { type_error => $type_error } ) if $type_error;
-    }
-
-    return ($self, $params);
+sub validate {
+  my ($self, $params, @keys) = @_;
+  my $cache_key = join('|', (caller(1))[3], sort @keys);
+
+  # Type->of() is the same as Type[], used here because it is easier
+  # to chain with plus_coercions.
+  state $array_of_nonrefs
+    = ArrayRef->of(Maybe [Value])->plus_coercions(Maybe [Value], q{ [ $_ ] },);
+  state $type_cache = {};
+  my $params_type = $type_cache->{$cache_key} //= do {
+    my %fields = map { $_ => Optional [$array_of_nonrefs] } @keys;
+    Maybe [Dict [%fields, slurpy Any]];
+  };
+
+  # If $params is defined but not a reference, then we weren't
+  # sent any parameters at all, and we're getting @keys where
+  # $params should be.
+  return ($self, undef) if (defined $params and !ref $params);
+
+  # If @keys is not empty then we convert any named
+  # parameters that have scalar values to arrayrefs
+  # that match.
+  $params = $params_type->coerce($params);
+  if (my $type_error = $params_type->validate($params)) {
+    FATAL("validate() found type error: $type_error");
+    ThrowUserError('invalid_params', {type_error => $type_error}) if $type_error;
+  }
+
+  return ($self, $params);
 }
 
 sub translate {
-    my ($params, $mapped) = @_;
-    my %changes;
-    while (my ($key,$value) = each (%$params)) {
-        my $new_field = $mapped->{$key} || $key;
-        $changes{$new_field} = $value;
-    }
-    return \%changes;
+  my ($params, $mapped) = @_;
+  my %changes;
+  while (my ($key, $value) = each(%$params)) {
+    my $new_field = $mapped->{$key} || $key;
+    $changes{$new_field} = $value;
+  }
+  return \%changes;
 }
 
 sub params_to_objects {
-    my ($params, $class) = @_;
-    my (@objects, @objects_by_ids);
+  my ($params, $class) = @_;
+  my (@objects, @objects_by_ids);
 
-    @objects = map { $class->check($_) }
-        @{ $params->{names} } if $params->{names};
+  @objects = map { $class->check($_) } @{$params->{names}} if $params->{names};
 
-    @objects_by_ids = map { $class->check({ id => $_ }) }
-        @{ $params->{ids} } if $params->{ids};
+  @objects_by_ids = map { $class->check({id => $_}) } @{$params->{ids}}
+    if $params->{ids};
 
-    push(@objects, @objects_by_ids);
-    my %seen;
-    @objects = grep { !$seen{$_->id}++ } @objects;
-    return \@objects;
+  push(@objects, @objects_by_ids);
+  my %seen;
+  @objects = grep { !$seen{$_->id}++ } @objects;
+  return \@objects;
 }
 
 sub fix_credentials {
-    my ($params, $cgi) = @_;
-
-    # Allow user to pass in authentication details in X-Headers
-    # This allows callers to keep credentials out of GET request query-strings
-    if ($cgi) {
-        foreach my $field (keys %{ API_AUTH_HEADERS() }) {
-            next if exists $params->{API_AUTH_HEADERS->{$field}} || ($cgi->http($field) // '') eq '';
-            $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field));
-        }
+  my ($params, $cgi) = @_;
+
+  # Allow user to pass in authentication details in X-Headers
+  # This allows callers to keep credentials out of GET request query-strings
+  if ($cgi) {
+    foreach my $field (keys %{API_AUTH_HEADERS()}) {
+      next
+        if exists $params->{API_AUTH_HEADERS->{$field}}
+        || ($cgi->http($field) // '') eq '';
+      $params->{API_AUTH_HEADERS->{$field}} = uri_unescape($cgi->http($field));
     }
-
-    # Allow user to pass in login=foo&password=bar as a convenience
-    # even if not calling GET /login. We also do not delete them as
-    # GET /login requires "login" and "password".
-    if (exists $params->{'login'} && exists $params->{'password'}) {
-        $params->{'Bugzilla_login'}    = delete $params->{'login'};
-        $params->{'Bugzilla_password'} = delete $params->{'password'};
-    }
-    # Allow user to pass api_key=12345678 as a convenience which becomes
-    # "Bugzilla_api_key" which is what the auth code looks for.
-    if (exists $params->{api_key}) {
-        $params->{Bugzilla_api_key} = delete $params->{api_key};
-    }
-    # Allow user to pass token=12345678 as a convenience which becomes
-    # "Bugzilla_token" which is what the auth code looks for.
-    if (exists $params->{'token'}) {
-        $params->{'Bugzilla_token'} = delete $params->{'token'};
-    }
-
-    # Allow extensions to modify the credential data before login
-    Bugzilla::Hook::process('webservice_fix_credentials', { params => $params });
+  }
+
+  # Allow user to pass in login=foo&password=bar as a convenience
+  # even if not calling GET /login. We also do not delete them as
+  # GET /login requires "login" and "password".
+  if (exists $params->{'login'} && exists $params->{'password'}) {
+    $params->{'Bugzilla_login'}    = delete $params->{'login'};
+    $params->{'Bugzilla_password'} = delete $params->{'password'};
+  }
+
+  # Allow user to pass api_key=12345678 as a convenience which becomes
+  # "Bugzilla_api_key" which is what the auth code looks for.
+  if (exists $params->{api_key}) {
+    $params->{Bugzilla_api_key} = delete $params->{api_key};
+  }
+
+  # Allow user to pass token=12345678 as a convenience which becomes
+  # "Bugzilla_token" which is what the auth code looks for.
+  if (exists $params->{'token'}) {
+    $params->{'Bugzilla_token'} = delete $params->{'token'};
+  }
+
+  # Allow extensions to modify the credential data before login
+  Bugzilla::Hook::process('webservice_fix_credentials', {params => $params});
 }
 
 __END__
diff --git a/Bugzilla/Whine.pm b/Bugzilla/Whine.pm
index c4301b4f6..67fc6cc5d 100644
--- a/Bugzilla/Whine.pm
+++ b/Bugzilla/Whine.pm
@@ -27,11 +27,11 @@ use Bugzilla::Whine::Query;
 use constant DB_TABLE => 'whine_events';
 
 use constant DB_COLUMNS => qw(
-    id
-    owner_userid
-    subject
-    body
-    mailifnobugs
+  id
+  owner_userid
+  subject
+  body
+  mailifnobugs
 );
 
 use constant LIST_ORDER => 'id';
@@ -39,15 +39,15 @@ use constant LIST_ORDER => 'id';
 ####################
 # Simple Accessors #
 ####################
-sub subject         { return $_[0]->{'subject'};      }
-sub body            { return $_[0]->{'body'};         }
+sub subject         { return $_[0]->{'subject'}; }
+sub body            { return $_[0]->{'body'}; }
 sub mail_if_no_bugs { return $_[0]->{'mailifnobugs'}; }
 
 sub user {
-    my ($self) = @_;
-    return $self->{user} if defined $self->{user};
-    $self->{user} = new Bugzilla::User($self->{'owner_userid'});
-    return $self->{user};
+  my ($self) = @_;
+  return $self->{user} if defined $self->{user};
+  $self->{user} = new Bugzilla::User($self->{'owner_userid'});
+  return $self->{user};
 }
 
 1;
diff --git a/Bugzilla/Whine/Query.pm b/Bugzilla/Whine/Query.pm
index 6ea91cc51..29e1e0b51 100644
--- a/Bugzilla/Whine/Query.pm
+++ b/Bugzilla/Whine/Query.pm
@@ -23,12 +23,12 @@ use Bugzilla::Search::Saved;
 use constant DB_TABLE => 'whine_queries';
 
 use constant DB_COLUMNS => qw(
-    id
-    eventid
-    query_name
-    sortkey
-    onemailperbug
-    title
+  id
+  eventid
+  query_name
+  sortkey
+  onemailperbug
+  title
 );
 
 use constant NAME_FIELD => 'id';
@@ -37,11 +37,11 @@ use constant LIST_ORDER => 'sortkey';
 ####################
 # Simple Accessors #
 ####################
-sub eventid           { return $_[0]->{'eventid'};       }
-sub sortkey           { return $_[0]->{'sortkey'};       }
+sub eventid           { return $_[0]->{'eventid'}; }
+sub sortkey           { return $_[0]->{'sortkey'}; }
 sub one_email_per_bug { return $_[0]->{'onemailperbug'}; }
-sub title             { return $_[0]->{'title'};         }
-sub name              { return $_[0]->{'query_name'};    }
+sub title             { return $_[0]->{'title'}; }
+sub name              { return $_[0]->{'query_name'}; }
 
 
 1;
diff --git a/Bugzilla/Whine/Schedule.pm b/Bugzilla/Whine/Schedule.pm
index 017b744e5..e6e8e67d9 100644
--- a/Bugzilla/Whine/Schedule.pm
+++ b/Bugzilla/Whine/Schedule.pm
@@ -22,22 +22,22 @@ use Bugzilla::Constants;
 use constant DB_TABLE => 'whine_schedules';
 
 use constant DB_COLUMNS => qw(
-    id
-    eventid
-    run_day
-    run_time
-    run_next
-    mailto
-    mailto_type
+  id
+  eventid
+  run_day
+  run_time
+  run_next
+  mailto
+  mailto_type
 );
 
 use constant UPDATE_COLUMNS => qw(
-    eventid
-    run_day
-    run_time
-    run_next
-    mailto
-    mailto_type
+  eventid
+  run_day
+  run_time
+  run_next
+  mailto
+  mailto_type
 );
 use constant NAME_FIELD => 'id';
 use constant LIST_ORDER => 'id';
@@ -45,36 +45,38 @@ use constant LIST_ORDER => 'id';
 ####################
 # Simple Accessors #
 ####################
-sub eventid         { return $_[0]->{'eventid'};     }
-sub run_day         { return $_[0]->{'run_day'};     }
-sub run_time        { return $_[0]->{'run_time'};    }
+sub eventid         { return $_[0]->{'eventid'}; }
+sub run_day         { return $_[0]->{'run_day'}; }
+sub run_time        { return $_[0]->{'run_time'}; }
 sub mailto_is_group { return $_[0]->{'mailto_type'}; }
 
 sub mailto {
-    my $self = shift;
-
-    return $self->{mailto_object} if exists $self->{mailto_object};
-    my $id = $self->{'mailto'};
-
-    if ($self->mailto_is_group) {
-        $self->{mailto_object} = Bugzilla::Group->new($id);
-    } else {
-        $self->{mailto_object} = Bugzilla::User->new($id);
-    }
-    return $self->{mailto_object};
+  my $self = shift;
+
+  return $self->{mailto_object} if exists $self->{mailto_object};
+  my $id = $self->{'mailto'};
+
+  if ($self->mailto_is_group) {
+    $self->{mailto_object} = Bugzilla::Group->new($id);
+  }
+  else {
+    $self->{mailto_object} = Bugzilla::User->new($id);
+  }
+  return $self->{mailto_object};
 }
 
 sub mailto_users {
-    my $self = shift;
-    return $self->{mailto_users} if exists $self->{mailto_users};
-    my $object = $self->mailto;
-
-    if ($self->mailto_is_group) {
-        $self->{mailto_users} = $object->members_non_inherited if $object->is_active;
-    } else {
-        $self->{mailto_users} = $object;
-    }
-    return $self->{mailto_users};
+  my $self = shift;
+  return $self->{mailto_users} if exists $self->{mailto_users};
+  my $object = $self->mailto;
+
+  if ($self->mailto_is_group) {
+    $self->{mailto_users} = $object->members_non_inherited if $object->is_active;
+  }
+  else {
+    $self->{mailto_users} = $object;
+  }
+  return $self->{mailto_users};
 }
 
 1;
diff --git a/Log/Log4perl/Layout/Mozilla.pm b/Log/Log4perl/Layout/Mozilla.pm
index 4aedd9843..e523a3198 100644
--- a/Log/Log4perl/Layout/Mozilla.pm
+++ b/Log/Log4perl/Layout/Mozilla.pm
@@ -16,66 +16,64 @@ use constant LOGGING_FORMAT_VERSION => 2.0;
 
 extends 'Log::Log4perl::Layout';
 
-has 'name' => (
-    is      => 'ro',
-    default => 'Bugzilla',
-);
+has 'name' => (is => 'ro', default => 'Bugzilla',);
 
 has 'max_json_length' => (
-    is      => 'ro',
-    isa     => sub { die "must be at least 1024\n" if $_[0] < 1024 },
-    default => 4096,
+  is      => 'ro',
+  isa     => sub { die "must be at least 1024\n" if $_[0] < 1024 },
+  default => 4096,
 );
 
 sub BUILDARGS {
-    my ($class, $params) = @_;
+  my ($class, $params) = @_;
 
-    delete $params->{value};
-    foreach my $key (keys %$params) {
-        if (ref $params->{$key} eq 'HASH') {
-            $params->{$key} = $params->{$key}{value};
-        }
+  delete $params->{value};
+  foreach my $key (keys %$params) {
+    if (ref $params->{$key} eq 'HASH') {
+      $params->{$key} = $params->{$key}{value};
     }
-    return $params;
+  }
+  return $params;
 }
 
 sub render {
-    my ( $self, $msg, $category, $priority, $caller_level ) = @_;
-
-    state $HOSTNAME = hostname();
-    state $JSON     = JSON::MaybeXS->new(
-        indent          => 0,    # to prevent newlines (and save space)
-        ascii           => 1,    # to avoid encoding issues downstream
-        allow_unknown   => 1,    # encode null on bad value (instead of exception)
-        convert_blessed => 1,    # call TO_JSON on blessed ref, if it exists
-        allow_blessed   => 1,    # encode null on blessed ref that can't be converted
-    );
-
-    my $mdc = Log::Log4perl::MDC->get_context;
-    my $fields = $mdc->{fields} // {};
-    if ($mdc->{request_id}) {
-        $fields->{request_id} = $mdc->{request_id}
-    }
-    my %out = (
-        EnvVersion => LOGGING_FORMAT_VERSION,
-        Hostname   => $HOSTNAME,
-        Logger     => $self->name,
-        Pid        => $PID,
-        Severity   => $Log::Log4perl::Level::SYSLOG{$priority},
-        Timestamp  => time() * 1e9,
-        Type       => $category,
-        Fields     => { msg => $msg, %$fields },
-    );
-
-    my $json_text = $JSON->encode(\%out) . "\n";
-    if (length($json_text) > $self->max_json_length) {
-        my $scary_msg = sprintf 'DANGER! LOG MESSAGE TOO BIG %d > %d', length($json_text), $self->max_json_length;
-        $out{Fields}   = { remote_ip => $mdc->{remote_ip}, msg => $scary_msg };
-        $out{Severity} = 1; # alert
-        $json_text     = $JSON->encode(\%out) . "\n";
-    }
-
-    return $json_text;
+  my ($self, $msg, $category, $priority, $caller_level) = @_;
+
+  state $HOSTNAME = hostname();
+  state $JSON     = JSON::MaybeXS->new(
+    indent          => 0,    # to prevent newlines (and save space)
+    ascii           => 1,    # to avoid encoding issues downstream
+    allow_unknown   => 1,    # encode null on bad value (instead of exception)
+    convert_blessed => 1,    # call TO_JSON on blessed ref, if it exists
+    allow_blessed   => 1,    # encode null on blessed ref that can't be converted
+  );
+
+  my $mdc = Log::Log4perl::MDC->get_context;
+  my $fields = $mdc->{fields} // {};
+  if ($mdc->{request_id}) {
+    $fields->{request_id} = $mdc->{request_id};
+  }
+  my %out = (
+    EnvVersion => LOGGING_FORMAT_VERSION,
+    Hostname   => $HOSTNAME,
+    Logger     => $self->name,
+    Pid        => $PID,
+    Severity   => $Log::Log4perl::Level::SYSLOG{$priority},
+    Timestamp  => time() * 1e9,
+    Type       => $category,
+    Fields     => {msg => $msg, %$fields},
+  );
+
+  my $json_text = $JSON->encode(\%out) . "\n";
+  if (length($json_text) > $self->max_json_length) {
+    my $scary_msg = sprintf 'DANGER! LOG MESSAGE TOO BIG %d > %d',
+      length($json_text), $self->max_json_length;
+    $out{Fields}   = {remote_ip => $mdc->{remote_ip}, msg => $scary_msg};
+    $out{Severity} = 1;                                                     # alert
+    $json_text     = $JSON->encode(\%out) . "\n";
+  }
+
+  return $json_text;
 }
 
 1;
@@ -125,4 +123,4 @@ This is useful where some downstream system has a limit on the maximum size of
 a message.
 
 If the message is larger than this limit, the message will be replaced
-with a scary message at a severity level of ALERT.
\ No newline at end of file
+with a scary message at a severity level of ALERT.
diff --git a/admin.cgi b/admin.cgi
index 801b26e20..91e1aec1f 100755
--- a/admin.cgi
+++ b/admin.cgi
@@ -16,24 +16,26 @@ use Bugzilla;
 use Bugzilla::Constants;
 use Bugzilla::Error;
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $user = Bugzilla->login(LOGIN_REQUIRED);
+my $user     = Bugzilla->login(LOGIN_REQUIRED);
 
 print $cgi->header();
 
-$user->in_group('admin')
+     $user->in_group('admin')
   || $user->in_group('tweakparams')
   || $user->in_group('editusers')
   || $user->in_group('disableusers')
   || $user->can_bless
-  || (Bugzilla->params->{'useclassification'} && $user->in_group('editclassifications'))
+  || (Bugzilla->params->{'useclassification'}
+  && $user->in_group('editclassifications'))
   || $user->in_group('editcomponents')
   || scalar(@{$user->get_products_by_permission('editcomponents')})
   || $user->in_group('creategroups')
   || $user->in_group('editkeywords')
   || $user->in_group('bz_canusewhines')
-  || ThrowUserError('auth_failure', {action => 'access', object => 'administrative_pages'});
+  || ThrowUserError('auth_failure',
+  {action => 'access', object => 'administrative_pages'});
 
 $template->process('admin/admin.html.tmpl')
   || ThrowTemplateError($template->error());
diff --git a/attachment.cgi b/attachment.cgi
index e1009c18e..01ee9a9e2 100755
--- a/attachment.cgi
+++ b/attachment.cgi
@@ -39,9 +39,9 @@ use MIME::Base64 qw(decode_base64);
 # when preparing Bugzilla for mod_perl, this script used these
 # variables in so many subroutines that it was easier to just
 # make them globals.
-local our $cgi = Bugzilla->cgi;
-local our $template = Bugzilla->template;
-local our $vars = {};
+local our $cgi                              = Bugzilla->cgi;
+local our $template                         = Bugzilla->template;
+local our $vars                             = {};
 local $Bugzilla::CGI::ALLOW_UNSAFE_RESPONSE = 1;
 
 ################################################################################
@@ -59,64 +59,57 @@ my $format = $cgi->param('format') || '';
 
 # BMO: Don't allow updating of bugs if disabled
 if (Bugzilla->params->{disable_bug_updates} && $cgi->request_method eq 'POST') {
-    ThrowErrorPage('bug/process/updates-disabled.html.tmpl',
-        'Bug updates are currently disabled.');
+  ThrowErrorPage(
+    'bug/process/updates-disabled.html.tmpl',
+    'Bug updates are currently disabled.'
+  );
 }
 
 # You must use the appropriate urlbase param when doing anything
 # but viewing an attachment, or a raw diff.
 if ($action ne 'view'
-    && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw'))
+  && (($action !~ /^(?:interdiff|diff)$/) || $format ne 'raw'))
 {
-    do_ssl_redirect_if_required();
-    if ($cgi->url_is_attachment_base) {
-        $cgi->redirect_to_urlbase;
-    }
-    Bugzilla->login();
+  do_ssl_redirect_if_required();
+  if ($cgi->url_is_attachment_base) {
+    $cgi->redirect_to_urlbase;
+  }
+  Bugzilla->login();
 }
 
 # When viewing an attachment, do not request credentials if we are on
 # the alternate host. Let view() decide when to call Bugzilla->login.
-if ($action eq "view")
-{
-    view();
+if ($action eq "view") {
+  view();
 }
-elsif ($action eq "interdiff")
-{
-    interdiff();
+elsif ($action eq "interdiff") {
+  interdiff();
 }
-elsif ($action eq "diff")
-{
-    diff();
+elsif ($action eq "diff") {
+  diff();
 }
-elsif ($action eq "viewall")
-{
-    viewall();
+elsif ($action eq "viewall") {
+  viewall();
 }
-elsif ($action eq "enter")
-{
-    Bugzilla->login(LOGIN_REQUIRED);
-    enter();
+elsif ($action eq "enter") {
+  Bugzilla->login(LOGIN_REQUIRED);
+  enter();
 }
-elsif ($action eq "insert")
-{
-    Bugzilla->login(LOGIN_REQUIRED);
-    insert();
+elsif ($action eq "insert") {
+  Bugzilla->login(LOGIN_REQUIRED);
+  insert();
 }
-elsif ($action eq "edit")
-{
-    edit();
+elsif ($action eq "edit") {
+  edit();
 }
-elsif ($action eq "update")
-{
-    Bugzilla->login(LOGIN_REQUIRED);
-    update();
+elsif ($action eq "update") {
+  Bugzilla->login(LOGIN_REQUIRED);
+  update();
 }
 elsif ($action eq "delete") {
-    delete_attachment();
+  delete_attachment();
 }
-else
-{
+else {
   ThrowUserError('unknown_action', {action => $action});
 }
 
@@ -138,72 +131,73 @@ exit;
 # Returns an attachment object.
 
 sub validateID {
-    my($param, $dont_validate_access) = @_;
-    $param ||= 'id';
+  my ($param, $dont_validate_access) = @_;
+  $param ||= 'id';
 
-    # If we're not doing interdiffs, check if id wasn't specified and
-    # prompt them with a page that allows them to choose an attachment.
-    # Happens when calling plain attachment.cgi from the urlbar directly
-    if ($param eq 'id' && !$cgi->param('id')) {
-        print $cgi->header();
-        $template->process("attachment/choose.html.tmpl", $vars) ||
-            ThrowTemplateError($template->error());
-        exit;
-    }
+  # If we're not doing interdiffs, check if id wasn't specified and
+  # prompt them with a page that allows them to choose an attachment.
+  # Happens when calling plain attachment.cgi from the urlbar directly
+  if ($param eq 'id' && !$cgi->param('id')) {
+    print $cgi->header();
+    $template->process("attachment/choose.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
 
-    my $attach_id = $cgi->param($param);
+  my $attach_id = $cgi->param($param);
 
-    # Validate the specified attachment id. detaint kills $attach_id if
-    # non-natural, so use the original value from $cgi in our exception
-    # message here.
-    detaint_natural($attach_id)
-        || ThrowUserError("invalid_attach_id",
-                          { attach_id => scalar $cgi->param($param) });
+  # Validate the specified attachment id. detaint kills $attach_id if
+  # non-natural, so use the original value from $cgi in our exception
+  # message here.
+  detaint_natural($attach_id)
+    || ThrowUserError("invalid_attach_id",
+    {attach_id => scalar $cgi->param($param)});
 
-    # Make sure the attachment exists in the database.
-    my $attachment = new Bugzilla::Attachment({ id => $attach_id, cache => 1 })
-        || ThrowUserError("invalid_attach_id", { attach_id => $attach_id });
+  # Make sure the attachment exists in the database.
+  my $attachment = new Bugzilla::Attachment({id => $attach_id, cache => 1})
+    || ThrowUserError("invalid_attach_id", {attach_id => $attach_id});
 
-    return $attachment if ($dont_validate_access || check_can_access($attachment));
+  return $attachment if ($dont_validate_access || check_can_access($attachment));
 }
 
 # Make sure the current user has access to the specified attachment.
 sub check_can_access {
-    my $attachment = shift;
-    my $user = Bugzilla->user;
-
-    # Make sure the user is authorized to access this attachment's bug.
-    Bugzilla::Bug->check({ id => $attachment->bug_id, cache => 1 });
-    if ($attachment->isprivate && $user->id != $attachment->attacher->id
-        && !$user->is_insider)
-    {
-        ThrowUserError('auth_failure', {action => 'access',
-                                        object => 'attachment',
-                                        attach_id => $attachment->id});
-    }
-    return 1;
+  my $attachment = shift;
+  my $user       = Bugzilla->user;
+
+  # Make sure the user is authorized to access this attachment's bug.
+  Bugzilla::Bug->check({id => $attachment->bug_id, cache => 1});
+  if ( $attachment->isprivate
+    && $user->id != $attachment->attacher->id
+    && !$user->is_insider)
+  {
+    ThrowUserError('auth_failure',
+      {action => 'access', object => 'attachment', attach_id => $attachment->id});
+  }
+  return 1;
 }
 
 # Determines if the attachment is public -- that is, if users who are
 # not logged in have access to the attachment
 sub attachmentIsPublic {
-    my $attachment = shift;
+  my $attachment = shift;
 
-    return 0 if Bugzilla->params->{'requirelogin'};
-    return 0 if $attachment->isprivate;
+  return 0 if Bugzilla->params->{'requirelogin'};
+  return 0 if $attachment->isprivate;
 
-    my $anon_user = new Bugzilla::User;
-    return $anon_user->can_see_bug($attachment->bug_id);
+  my $anon_user = new Bugzilla::User;
+  return $anon_user->can_see_bug($attachment->bug_id);
 }
 
 # Validates format of a diff/interdiff. Takes a list as an parameter, which
 # defines the valid format values. Will throw an error if the format is not
 # in the list. Returns either the user selected or default format.
 sub validateFormat {
+
   # receives a list of legal formats; first item is a default
   my $format = $cgi->param('format') || $_[0];
   if (not grep($_ eq $format, @_)) {
-     ThrowUserError("invalid_format", { format  => $format, formats => \@_ });
+    ThrowUserError("invalid_format", {format => $format, formats => \@_});
   }
 
   return $format;
@@ -211,13 +205,12 @@ sub validateFormat {
 
 # Validates context of a diff/interdiff. Will throw an error if the context
 # is not number, "file" or "patch". Returns the validated, detainted context.
-sub validateContext
-{
+sub validateContext {
   my $context = $cgi->param('context') || "patch";
   if ($context ne "file" && $context ne "patch") {
-      my $orig_context = $context;
-      detaint_natural($context)
-        || ThrowUserError("invalid_context", { context => $orig_context });
+    my $orig_context = $context;
+    detaint_natural($context)
+      || ThrowUserError("invalid_context", {context => $orig_context});
   }
 
   return $context;
@@ -226,127 +219,143 @@ sub validateContext
 # Gets the attachment object(s) generated by validateID, while ensuring
 # attachbase and token authentication is used when required.
 sub get_attachment {
-    my @field_names = @_ ? @_ : qw(id);
+  my @field_names = @_ ? @_ : qw(id);
+
+  my %attachments;
 
-    my %attachments;
+  if (use_attachbase()) {
 
-    if (use_attachbase()) {
-        # Load each attachment, and ensure they are all from the same bug
-        my $bug_id = 0;
+    # Load each attachment, and ensure they are all from the same bug
+    my $bug_id = 0;
+    foreach my $field_name (@field_names) {
+      my $attachment = validateID($field_name, 1);
+      if (!$bug_id) {
+        $bug_id = $attachment->bug_id;
+      }
+      elsif ($attachment->bug_id != $bug_id) {
+        ThrowUserError('attachment_bug_id_mismatch');
+      }
+      $attachments{$field_name} = $attachment;
+    }
+    my @args = map { $_ . '=' . $attachments{$_}->id } @field_names;
+    my $cgi_params = $cgi->canonicalise_query(@field_names, 't', 'Bugzilla_login',
+      'Bugzilla_password');
+    push(@args, $cgi_params) if $cgi_params;
+    my $path = 'attachment.cgi?' . join('&', @args);
+
+    # Make sure the attachment is served from the correct server.
+    if ($cgi->url_is_attachment_base($bug_id)) {
+
+      # No need to validate the token for public attachments. We cannot request
+      # credentials as we are on the alternate host.
+      if (!all_attachments_are_public(\%attachments)) {
+        my $token = $cgi->param('t');
+        my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token);
+        my %token_data  = unpack_token_data($token_data);
+        my $valid_token = 1;
         foreach my $field_name (@field_names) {
-            my $attachment = validateID($field_name, 1);
-            if (!$bug_id) {
-                $bug_id = $attachment->bug_id;
-            } elsif ($attachment->bug_id != $bug_id) {
-                ThrowUserError('attachment_bug_id_mismatch');
-            }
-            $attachments{$field_name} = $attachment;
-        }
-        my @args = map { $_ . '=' . $attachments{$_}->id } @field_names;
-        my $cgi_params = $cgi->canonicalise_query(@field_names, 't',
-            'Bugzilla_login', 'Bugzilla_password');
-        push(@args, $cgi_params) if $cgi_params;
-        my $path = 'attachment.cgi?' . join('&', @args);
-
-        # Make sure the attachment is served from the correct server.
-        if ($cgi->url_is_attachment_base($bug_id)) {
-            # No need to validate the token for public attachments. We cannot request
-            # credentials as we are on the alternate host.
-            if (!all_attachments_are_public(\%attachments)) {
-                my $token = $cgi->param('t');
-                my ($userid, undef, $token_data) = Bugzilla::Token::GetTokenData($token);
-                my %token_data = unpack_token_data($token_data);
-                my $valid_token = 1;
-                foreach my $field_name (@field_names) {
-                    my $token_id = $token_data{$field_name};
-                    if (!$token_id
-                        || !detaint_natural($token_id)
-                        || $attachments{$field_name}->id != $token_id)
-                    {
-                        $valid_token = 0;
-                        last;
-                    }
-                }
-                unless ($userid && $valid_token) {
-                    # Not a valid token.
-                    print $cgi->redirect('-location' => Bugzilla->localconfig->{urlbase} . $path);
-                    exit;
-                }
-                # Change current user without creating cookies.
-                Bugzilla->set_user(new Bugzilla::User($userid));
-                # Tokens are single use only, delete it.
-                delete_token($token);
-            }
-        }
-        elsif ($cgi->url_is_attachment_base) {
-            # If we come here, this means that each bug has its own host
-            # for attachments, and that we are trying to view one attachment
-            # using another bug's host. That's not desired.
-            $cgi->redirect_to_urlbase;
+          my $token_id = $token_data{$field_name};
+          if ( !$token_id
+            || !detaint_natural($token_id)
+            || $attachments{$field_name}->id != $token_id)
+          {
+            $valid_token = 0;
+            last;
+          }
         }
-        else {
-            # We couldn't call Bugzilla->login earlier as we first had to
-            # make sure we were not going to request credentials on the
-            # alternate host.
-            Bugzilla->login();
-            my $attachbase = Bugzilla->localconfig->{'attachment_base'};
-            # Replace %bugid% by the ID of the bug the attachment
-            # belongs to, if present.
-            $attachbase =~ s/\%bugid\%/$bug_id/;
-            # To avoid leaking information we redirect using the attachment ID only
-            $path = 'attachment.cgi?' . join('&', map { 'id=' . $attachments{$_}->id } keys %attachments);
-            if (all_attachments_are_public(\%attachments)) {
-                # No need for a token; redirect to attachment base.
-                print $cgi->redirect(-location => $attachbase . $path);
-                exit;
-            } else {
-                # Make sure the user can view the attachment.
-                foreach my $field_name (@field_names) {
-                    check_can_access($attachments{$field_name});
-                }
-                # Create a token and redirect.
-                my $token = url_quote(issue_session_token(pack_token_data(\%attachments)));
-                print $cgi->redirect(-location => $attachbase . "$path&t=$token");
-                exit;
-            }
+        unless ($userid && $valid_token) {
+
+          # Not a valid token.
+          print $cgi->redirect('-location' => Bugzilla->localconfig->{urlbase} . $path);
+          exit;
         }
-    } else {
-        do_ssl_redirect_if_required();
-        # No alternate host is used. Request credentials if required.
-        Bugzilla->login();
+
+        # Change current user without creating cookies.
+        Bugzilla->set_user(new Bugzilla::User($userid));
+
+        # Tokens are single use only, delete it.
+        delete_token($token);
+      }
+    }
+    elsif ($cgi->url_is_attachment_base) {
+
+      # If we come here, this means that each bug has its own host
+      # for attachments, and that we are trying to view one attachment
+      # using another bug's host. That's not desired.
+      $cgi->redirect_to_urlbase;
+    }
+    else {
+      # We couldn't call Bugzilla->login earlier as we first had to
+      # make sure we were not going to request credentials on the
+      # alternate host.
+      Bugzilla->login();
+      my $attachbase = Bugzilla->localconfig->{'attachment_base'};
+
+      # Replace %bugid% by the ID of the bug the attachment
+      # belongs to, if present.
+      $attachbase =~ s/\%bugid\%/$bug_id/;
+
+      # To avoid leaking information we redirect using the attachment ID only
+      $path = 'attachment.cgi?'
+        . join('&', map { 'id=' . $attachments{$_}->id } keys %attachments);
+      if (all_attachments_are_public(\%attachments)) {
+
+        # No need for a token; redirect to attachment base.
+        print $cgi->redirect(-location => $attachbase . $path);
+        exit;
+      }
+      else {
+        # Make sure the user can view the attachment.
         foreach my $field_name (@field_names) {
-            $attachments{$field_name} = validateID($field_name);
+          check_can_access($attachments{$field_name});
         }
+
+        # Create a token and redirect.
+        my $token = url_quote(issue_session_token(pack_token_data(\%attachments)));
+        print $cgi->redirect(-location => $attachbase . "$path&t=$token");
+        exit;
+      }
+    }
+  }
+  else {
+    do_ssl_redirect_if_required();
+
+    # No alternate host is used. Request credentials if required.
+    Bugzilla->login();
+    foreach my $field_name (@field_names) {
+      $attachments{$field_name} = validateID($field_name);
     }
+  }
 
-    return wantarray
-        ? map { $attachments{$_} } @field_names
-        : $attachments{$field_names[0]};
+  return
+    wantarray
+    ? map { $attachments{$_} } @field_names
+    : $attachments{$field_names[0]};
 }
 
 sub all_attachments_are_public {
-    my $attachments = shift;
-    foreach my $field_name (keys %$attachments) {
-        if (!attachmentIsPublic($attachments->{$field_name})) {
-            return 0;
-        }
+  my $attachments = shift;
+  foreach my $field_name (keys %$attachments) {
+    if (!attachmentIsPublic($attachments->{$field_name})) {
+      return 0;
     }
-    return 1;
+  }
+  return 1;
 }
 
 sub pack_token_data {
-    my $attachments = shift;
-    return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments);
+  my $attachments = shift;
+  return join(' ', map { $_ . '=' . $attachments->{$_}->id } keys %$attachments);
 }
 
 sub unpack_token_data {
-    my @token_data = split(/ /, shift || '');
-    my %data;
-    foreach my $token (@token_data) {
-        my ($field_name, $attach_id) = split('=', $token);
-        $data{$field_name} = $attach_id;
-    }
-    return %data;
+  my @token_data = split(/ /, shift || '');
+  my %data;
+  foreach my $token (@token_data) {
+    my ($field_name, $attach_id) = split('=', $token);
+    $data{$field_name} = $attach_id;
+  }
+  return %data;
 }
 
 ################################################################################
@@ -355,152 +364,168 @@ sub unpack_token_data {
 
 # Display an attachment.
 sub view {
-    my $attachment = get_attachment();
-
-    # At this point, Bugzilla->login has been called if it had to.
-    my $contenttype = $attachment->contenttype;
-    my $filename    = basename($attachment->filename);
-    my $contenttype_override = 0;
-
-    # Bug 111522: allow overriding content-type manually in the posted form
-    # params.
-    if (defined $cgi->param('content_type')) {
-        $contenttype = $attachment->_check_content_type($cgi->param('content_type'));
-        $contenttype_override = 1;
-    }
+  my $attachment = get_attachment();
+
+  # At this point, Bugzilla->login has been called if it had to.
+  my $contenttype          = $attachment->contenttype;
+  my $filename             = basename($attachment->filename);
+  my $contenttype_override = 0;
+
+  # Bug 111522: allow overriding content-type manually in the posted form
+  # params.
+  if (defined $cgi->param('content_type')) {
+    $contenttype = $attachment->_check_content_type($cgi->param('content_type'));
+    $contenttype_override = 1;
+  }
 
-    # Return the appropriate HTTP response headers.
-    $attachment->datasize || ThrowUserError("attachment_removed");
+  # Return the appropriate HTTP response headers.
+  $attachment->datasize || ThrowUserError("attachment_removed");
 
-    # BMO add a hook for github url redirection
-    Bugzilla::Hook::process('attachment_view', { attachment => $attachment });
+  # BMO add a hook for github url redirection
+  Bugzilla::Hook::process('attachment_view', {attachment => $attachment});
 
-    my $do_redirect = 0;
-    Bugzilla::Hook::process('attachment_should_redirect_login', { do_redirect => \$do_redirect });
+  my $do_redirect = 0;
+  Bugzilla::Hook::process('attachment_should_redirect_login',
+    {do_redirect => \$do_redirect});
 
-    if ($do_redirect) {
-        my $uri = URI->new(Bugzilla->localconfig->{urlbase} . 'attachment.cgi');
-        $uri->query_param(id => $attachment->id);
-        $uri->query_param(content_type => $contenttype) if $contenttype_override;
+  if ($do_redirect) {
+    my $uri = URI->new(Bugzilla->localconfig->{urlbase} . 'attachment.cgi');
+    $uri->query_param(id => $attachment->id);
+    $uri->query_param(content_type => $contenttype) if $contenttype_override;
 
-        print $cgi->redirect('-location' => $uri);
-        exit 0;
-    }
+    print $cgi->redirect('-location' => $uri);
+    exit 0;
+  }
 
-    # Don't send a charset header with attachments--they might not be UTF-8.
-    # However, we do allow people to explicitly specify a charset if they
-    # want.
-    if ($contenttype !~ /\bcharset=/i) {
-        # In order to prevent Apache from adding a charset, we have to send a
-        # charset that's a single space.
-        $cgi->charset("");
-        if (Bugzilla->feature('detect_charset') && $contenttype =~ /^text\//) {
-            my $encoding = detect_encoding($attachment->data);
-            if ($encoding) {
-                $cgi->charset(find_encoding($encoding)->mime_name);
-            }
-        }
+  # Don't send a charset header with attachments--they might not be UTF-8.
+  # However, we do allow people to explicitly specify a charset if they
+  # want.
+  if ($contenttype !~ /\bcharset=/i) {
+
+    # In order to prevent Apache from adding a charset, we have to send a
+    # charset that's a single space.
+    $cgi->charset("");
+    if (Bugzilla->feature('detect_charset') && $contenttype =~ /^text\//) {
+      my $encoding = detect_encoding($attachment->data);
+      if ($encoding) {
+        $cgi->charset(find_encoding($encoding)->mime_name);
+      }
     }
-    Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get")
-      if Bugzilla->user->id;
-
-    my $disposition = Bugzilla->params->{'allow_attachment_display'} ? 'inline' : 'attachment';
-    my $filename_star = qq{UTF-8''} . url_escape( encode('UTF-8', $filename) );
+  }
+  Bugzilla->log_user_request($attachment->bug_id, $attachment->id,
+    "attachment-get")
+    if Bugzilla->user->id;
 
-    print $cgi->header(-type=> $contenttype,
-                       -content_disposition=> "$disposition; filename*=$filename_star",
-                       -content_length => $attachment->datasize);
-    disable_utf8();
-    print $attachment->data;
+  my $disposition
+    = Bugzilla->params->{'allow_attachment_display'} ? 'inline' : 'attachment';
+  my $filename_star = qq{UTF-8''} . url_escape(encode('UTF-8', $filename));
+
+  print $cgi->header(
+    -type                => $contenttype,
+    -content_disposition => "$disposition; filename*=$filename_star",
+    -content_length      => $attachment->datasize
+  );
+  disable_utf8();
+  print $attachment->data;
 }
 
 sub interdiff {
-    # Retrieve and validate parameters
-    my $format = validateFormat('html', 'raw');
-    my($old_attachment, $new_attachment);
-    if ($format eq 'raw') {
-        ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid');
-    } else {
-        $old_attachment = validateID('oldid');
-        $new_attachment = validateID('newid');
-    }
-    my $context = validateContext();
 
-    Bugzilla::Attachment::PatchReader::process_interdiff(
-        $old_attachment, $new_attachment, $format, $context);
+  # Retrieve and validate parameters
+  my $format = validateFormat('html', 'raw');
+  my ($old_attachment, $new_attachment);
+  if ($format eq 'raw') {
+    ($old_attachment, $new_attachment) = get_attachment('oldid', 'newid');
+  }
+  else {
+    $old_attachment = validateID('oldid');
+    $new_attachment = validateID('newid');
+  }
+  my $context = validateContext();
+
+  Bugzilla::Attachment::PatchReader::process_interdiff($old_attachment,
+    $new_attachment, $format, $context);
 }
 
 sub diff {
-    # Retrieve and validate parameters
-    my $format = validateFormat('html', 'raw');
-    my $attachment = $format eq 'raw' ? get_attachment() : validateID();
-    my $context = validateContext();
-
-    # If it is not a patch, view normally.
-    if (!$attachment->ispatch) {
-        view();
-        return;
-    }
 
-    Bugzilla::Attachment::PatchReader::process_diff($attachment, $format, $context);
+  # Retrieve and validate parameters
+  my $format = validateFormat('html', 'raw');
+  my $attachment = $format eq 'raw' ? get_attachment() : validateID();
+  my $context = validateContext();
+
+  # If it is not a patch, view normally.
+  if (!$attachment->ispatch) {
+    view();
+    return;
+  }
+
+  Bugzilla::Attachment::PatchReader::process_diff($attachment, $format, $context);
 }
 
 # Display all attachments for a given bug in a series of IFRAMEs within one
 # HTML page.
 sub viewall {
-    # Retrieve and validate parameters
-    my $bug = Bugzilla::Bug->check({ id => scalar $cgi->param('bugid'), cache => 1 });
 
-    my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug);
-    # Ignore deleted attachments.
-    @$attachments = grep { $_->datasize } @$attachments;
+  # Retrieve and validate parameters
+  my $bug = Bugzilla::Bug->check({id => scalar $cgi->param('bugid'), cache => 1});
 
-    if ($cgi->param('hide_obsolete')) {
-        @$attachments = grep { !$_->isobsolete } @$attachments;
-        $vars->{'hide_obsolete'} = 1;
-    }
+  my $attachments = Bugzilla::Attachment->get_attachments_by_bug($bug);
 
-    # Define the variables and functions that will be passed to the UI template.
-    $vars->{'bug'} = $bug;
-    $vars->{'attachments'} = $attachments;
+  # Ignore deleted attachments.
+  @$attachments = grep { $_->datasize } @$attachments;
 
-    print $cgi->header();
+  if ($cgi->param('hide_obsolete')) {
+    @$attachments = grep { !$_->isobsolete } @$attachments;
+    $vars->{'hide_obsolete'} = 1;
+  }
 
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("attachment/show-multiple.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  # Define the variables and functions that will be passed to the UI template.
+  $vars->{'bug'}         = $bug;
+  $vars->{'attachments'} = $attachments;
+
+  print $cgi->header();
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process("attachment/show-multiple.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 # Display a form for entering a new attachment.
 sub enter {
+
   # Retrieve and validate parameters
-  my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
+  my $bug   = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
   my $bugid = $bug->id;
   Bugzilla::Attachment->_check_bug($bug);
-  my $dbh = Bugzilla->dbh;
+  my $dbh  = Bugzilla->dbh;
   my $user = Bugzilla->user;
 
   # Retrieve the attachments the user can edit from the database and write
   # them into an array of hashes where each hash represents one attachment.
   my $canEdit = "";
   if (!$user->in_group('editbugs', $bug->product_id)) {
-      $canEdit = "AND submitter_id = " . $user->id;
+    $canEdit = "AND submitter_id = " . $user->id;
   }
-  my $attach_ids = $dbh->selectcol_arrayref("SELECT attach_id FROM attachments
+  my $attach_ids = $dbh->selectcol_arrayref(
+    "SELECT attach_id FROM attachments
                                              WHERE bug_id = ? AND isobsolete = 0 $canEdit
-                                             ORDER BY attach_id", undef, $bugid);
+                                             ORDER BY attach_id", undef, $bugid
+  );
 
   # Define the variables and functions that will be passed to the UI template.
-  $vars->{'bug'} = $bug;
+  $vars->{'bug'}         = $bug;
   $vars->{'attachments'} = Bugzilla::Attachment->new_from_list($attach_ids);
 
-  my $flag_types = Bugzilla::FlagType::match({'target_type'  => 'attachment',
-                                              'product_id'   => $bug->product_id,
-                                              'component_id' => $bug->component_id,
-                                              'is_active'    => 1});
+  my $flag_types = Bugzilla::FlagType::match({
+    'target_type'  => 'attachment',
+    'product_id'   => $bug->product_id,
+    'component_id' => $bug->component_id,
+    'is_active'    => 1
+  });
   $vars->{'flag_types'} = $flag_types;
-  $vars->{'any_flags_requesteeble'} =
-    grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
+  $vars->{'any_flags_requesteeble'}
+    = grep { $_->is_requestable && $_->is_requesteeble } @$flag_types;
   $vars->{'token'} = issue_session_token('create_attachment');
 
   print $cgi->header();
@@ -512,100 +537,114 @@ sub enter {
 
 # Insert a new attachment into the database.
 sub insert {
-    my $dbh = Bugzilla->dbh;
-    my $user = Bugzilla->user;
-
-    $dbh->bz_start_transaction;
+  my $dbh  = Bugzilla->dbh;
+  my $user = Bugzilla->user;
 
-    # Retrieve and validate parameters
-    my $bug = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
-    my $bugid = $bug->id;
-    my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
+  $dbh->bz_start_transaction;
 
-    # Detect if the user already used the same form to submit an attachment
-    my $token = trim($cgi->param('token'));
-    check_token_data($token, 'create_attachment', 'index.cgi');
+  # Retrieve and validate parameters
+  my $bug         = Bugzilla::Bug->check(scalar $cgi->param('bugid'));
+  my $bugid       = $bug->id;
+  my ($timestamp) = $dbh->selectrow_array("SELECT NOW()");
+
+  # Detect if the user already used the same form to submit an attachment
+  my $token = trim($cgi->param('token'));
+  check_token_data($token, 'create_attachment', 'index.cgi');
+
+  # Check attachments the user tries to mark as obsolete.
+  my @obsolete_attachments;
+  if ($cgi->param('obsolete')) {
+    my @obsolete = $cgi->param('obsolete');
+    @obsolete_attachments
+      = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
+  }
 
-    # Check attachments the user tries to mark as obsolete.
-    my @obsolete_attachments;
-    if ($cgi->param('obsolete')) {
-        my @obsolete = $cgi->param('obsolete');
-        @obsolete_attachments = Bugzilla::Attachment->validate_obsolete($bug, \@obsolete);
-    }
+  # Must be called before create() as it may alter $cgi->param('ispatch').
+  my $content_type = Bugzilla::Attachment::get_content_type();
 
-    # Must be called before create() as it may alter $cgi->param('ispatch').
-    my $content_type = Bugzilla::Attachment::get_content_type();
+  # Get the filehandle of the attachment.
+  my $data_fh     = $cgi->upload('data');
+  my $attach_text = $cgi->param('attach_text');
+  my $data_base64 = $cgi->param('data_base64');
+  my $data;
+  my $filename;
 
-    # Get the filehandle of the attachment.
-    my $data_fh = $cgi->upload('data');
-    my $attach_text = $cgi->param('attach_text');
-    my $data_base64 = $cgi->param('data_base64');
-    my $data;
-    my $filename;
+  if ($attach_text) {
 
-    if ($attach_text) {
-        # Convert to unix line-endings if pasting a patch
-        if (scalar($cgi->param('ispatch'))) {
-            $attach_text =~ s/[\012\015]{1,2}/\012/g;
-        }
-        $data = $attach_text;
-        $filename = "file_$bugid.txt";
-    } elsif ($data_base64) {
-        $data = decode_base64($data_base64);
-        $filename = $cgi->param('filename') || "file_$bugid";
-    } else {
-        $data = $filename = $data_fh;
+    # Convert to unix line-endings if pasting a patch
+    if (scalar($cgi->param('ispatch'))) {
+      $attach_text =~ s/[\012\015]{1,2}/\012/g;
     }
+    $data     = $attach_text;
+    $filename = "file_$bugid.txt";
+  }
+  elsif ($data_base64) {
+    $data = decode_base64($data_base64);
+    $filename = $cgi->param('filename') || "file_$bugid";
+  }
+  else {
+    $data = $filename = $data_fh;
+  }
 
-    my $attachment = Bugzilla::Attachment->create(
-        {bug           => $bug,
-         creation_ts   => $timestamp,
-         data          => $data,
-         description   => scalar $cgi->param('description'),
-         filename      => $filename,
-         ispatch       => scalar $cgi->param('ispatch'),
-         isprivate     => scalar $cgi->param('isprivate'),
-         mimetype      => $content_type,
-         });
-
-    # Delete the token used to create this attachment.
-    delete_token($token);
+  my $attachment = Bugzilla::Attachment->create({
+    bug         => $bug,
+    creation_ts => $timestamp,
+    data        => $data,
+    description => scalar $cgi->param('description'),
+    filename    => $filename,
+    ispatch     => scalar $cgi->param('ispatch'),
+    isprivate   => scalar $cgi->param('isprivate'),
+    mimetype    => $content_type,
+  });
+
+  # Delete the token used to create this attachment.
+  delete_token($token);
+
+  foreach my $obsolete_attachment (@obsolete_attachments) {
+    $obsolete_attachment->set_is_obsolete(1);
+    $obsolete_attachment->update($timestamp);
+  }
 
-    foreach my $obsolete_attachment (@obsolete_attachments) {
-        $obsolete_attachment->set_is_obsolete(1);
-        $obsolete_attachment->update($timestamp);
+  # BMO - allow pre-processing of attachment flags
+  Bugzilla::Hook::process('create_attachment_flags',
+    {bug => $bug, attachment => $attachment});
+  my ($flags, $new_flags)
+    = Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars,
+    SKIP_REQUESTEE_ON_ERROR);
+  $attachment->set_flags($flags, $new_flags);
+
+  # Insert a comment about the new attachment into the database.
+  my $comment = $cgi->param('comment');
+  $comment = '' unless defined $comment;
+  $bug->add_comment(
+    $comment,
+    {
+      isprivate  => $attachment->isprivate,
+      type       => CMT_ATTACHMENT_CREATED,
+      extra_data => $attachment->id
     }
-
-    # BMO - allow pre-processing of attachment flags
-    Bugzilla::Hook::process('create_attachment_flags', { bug => $bug, attachment => $attachment });
-    my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi(
-                                  $bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR);
-    $attachment->set_flags($flags, $new_flags);
-
-    # Insert a comment about the new attachment into the database.
-    my $comment = $cgi->param('comment');
-    $comment = '' unless defined $comment;
-    $bug->add_comment($comment, { isprivate => $attachment->isprivate,
-                                  type => CMT_ATTACHMENT_CREATED,
-                                  extra_data => $attachment->id });
+  );
 
   # Assign the bug to the user, if they are allowed to take it
   my $owner = "";
   if ($cgi->param('takebug') && $user->in_group('editbugs', $bug->product_id)) {
-      # When taking a bug, we have to follow the workflow.
-      my $bug_status = $cgi->param('bug_status') || '';
-      ($bug_status) = grep {$_->name eq $bug_status} @{$bug->status->can_change_to};
 
-      if ($bug_status && $bug_status->is_open
-          && ($bug_status->name ne 'UNCONFIRMED'
-              || $bug->product_obj->allows_unconfirmed))
-      {
-          $bug->set_bug_status($bug_status->name);
-          $bug->clear_resolution();
-      }
-      # Make sure the person we are taking the bug from gets mail.
-      $owner = $bug->assigned_to->login;
-      $bug->set_assigned_to($user);
+    # When taking a bug, we have to follow the workflow.
+    my $bug_status = $cgi->param('bug_status') || '';
+    ($bug_status) = grep { $_->name eq $bug_status } @{$bug->status->can_change_to};
+
+    if ( $bug_status
+      && $bug_status->is_open
+      && ($bug_status->name ne 'UNCONFIRMED' || $bug->product_obj->allows_unconfirmed)
+      )
+    {
+      $bug->set_bug_status($bug_status->name);
+      $bug->clear_resolution();
+    }
+
+    # Make sure the person we are taking the bug from gets mail.
+    $owner = $bug->assigned_to->login;
+    $bug->set_assigned_to($user);
   }
   $bug->update($timestamp);
 
@@ -617,13 +656,14 @@ sub insert {
 
   # Define the variables and functions that will be passed to the UI template.
   $vars->{'attachment'} = $attachment;
+
   # We cannot reuse the $bug object as delta_ts has eventually been updated
   # since the object was created.
-  $vars->{'bugs'} = [new Bugzilla::Bug($bugid)];
-  $vars->{'header_done'} = 1;
+  $vars->{'bugs'}              = [new Bugzilla::Bug($bugid)];
+  $vars->{'header_done'}       = 1;
   $vars->{'contenttypemethod'} = $cgi->param('contenttypemethod');
 
-  my $recipients =  { 'changer' => $user, 'owner' => $owner };
+  my $recipients = {'changer' => $user, 'owner' => $owner};
   $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bugid, $recipients);
 
   # BMO: add show_bug_format hook for experimental UI work
@@ -631,10 +671,11 @@ sub insert {
   Bugzilla::Hook::process('show_bug_format', $show_bug_format);
 
   if ($show_bug_format->{format} eq 'modal') {
-      $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bugid));
+    $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bugid));
   }
 
   print $cgi->header();
+
   # Generate and return the UI (HTML page) from the appropriate template.
   $template->process("attachment/created.html.tmpl", $vars)
     || ThrowTemplateError($template->error());
@@ -647,19 +688,21 @@ sub insert {
 sub edit {
   my $attachment = validateID();
 
-  my $bugattachments =
-      Bugzilla::Attachment->get_attachments_by_bug($attachment->bug);
+  my $bugattachments
+    = Bugzilla::Attachment->get_attachments_by_bug($attachment->bug);
+
+  my $any_flags_requesteeble = grep { $_->is_requestable && $_->is_requesteeble }
+    @{$attachment->flag_types};
 
-  my $any_flags_requesteeble =
-    grep { $_->is_requestable && $_->is_requesteeble } @{$attachment->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 } @{$attachment->flags};
   $vars->{'any_flags_requesteeble'} = $any_flags_requesteeble;
-  $vars->{'attachment'} = $attachment;
-  $vars->{'attachments'} = $bugattachments;
+  $vars->{'attachment'}             = $attachment;
+  $vars->{'attachments'}            = $bugattachments;
 
-  Bugzilla->log_user_request($attachment->bug_id, $attachment->id, "attachment-get")
+  Bugzilla->log_user_request($attachment->bug_id, $attachment->id,
+    "attachment-get")
     if Bugzilla->user->id;
   print $cgi->header();
 
@@ -672,220 +715,229 @@ sub edit {
 # (or the original attachment's submitter) can edit the attachment.
 # Users cannot edit the content of the attachment itself.
 sub update {
-    my $user = Bugzilla->user;
-    my $dbh = Bugzilla->dbh;
-
-    # Start a transaction in preparation for updating the attachment.
-    $dbh->bz_start_transaction();
-
-    # Retrieve and validate parameters
-    my $attachment = validateID();
-    my $bug = $attachment->bug;
-    $attachment->_check_bug;
-    my $can_edit = $attachment->validate_can_edit;
-
-    if ($can_edit) {
-        $attachment->set_description(scalar $cgi->param('description'));
-        $attachment->set_is_patch(scalar $cgi->param('ispatch'));
-        $attachment->set_content_type(scalar $cgi->param('contenttypeentry'));
-        $attachment->set_is_obsolete(scalar $cgi->param('isobsolete'));
-        $attachment->set_is_private(scalar $cgi->param('isprivate'));
-        $attachment->set_filename(scalar $cgi->param('filename'));
-
-        # Now make sure the attachment has not been edited since we loaded the page.
-        my $delta_ts = $cgi->param('delta_ts');
-        my $modification_time = $attachment->modification_time;
-
-        if ($delta_ts && $delta_ts ne $modification_time) {
-            datetime_from($delta_ts)
-              or ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts });
-            ($vars->{'operations'}) =
-              Bugzilla::Bug::GetBugActivity($bug->id, $attachment->id, $delta_ts);
-
-            # If the modification date changed but there is no entry in
-            # the activity table, this means someone commented only.
-            # In this case, there is no reason to midair.
-            if (scalar(@{$vars->{'operations'}})) {
-                $cgi->param('delta_ts', $modification_time);
-                # The token contains the old modification_time. We need a new one.
-                $cgi->param('token', issue_hash_token([$attachment->id, $modification_time]));
-
-                $vars->{'attachment'} = $attachment;
-
-                print $cgi->header();
-                # Warn the user about the mid-air collision and ask them what to do.
-                $template->process("attachment/midair.html.tmpl", $vars)
-                  || ThrowTemplateError($template->error());
-                exit;
-            }
-        }
-    }
+  my $user = Bugzilla->user;
+  my $dbh  = Bugzilla->dbh;
+
+  # Start a transaction in preparation for updating the attachment.
+  $dbh->bz_start_transaction();
+
+  # Retrieve and validate parameters
+  my $attachment = validateID();
+  my $bug        = $attachment->bug;
+  $attachment->_check_bug;
+  my $can_edit = $attachment->validate_can_edit;
+
+  if ($can_edit) {
+    $attachment->set_description(scalar $cgi->param('description'));
+    $attachment->set_is_patch(scalar $cgi->param('ispatch'));
+    $attachment->set_content_type(scalar $cgi->param('contenttypeentry'));
+    $attachment->set_is_obsolete(scalar $cgi->param('isobsolete'));
+    $attachment->set_is_private(scalar $cgi->param('isprivate'));
+    $attachment->set_filename(scalar $cgi->param('filename'));
+
+    # Now make sure the attachment has not been edited since we loaded the page.
+    my $delta_ts          = $cgi->param('delta_ts');
+    my $modification_time = $attachment->modification_time;
+
+    if ($delta_ts && $delta_ts ne $modification_time) {
+      datetime_from($delta_ts)
+        or ThrowCodeError('invalid_timestamp', {timestamp => $delta_ts});
+      ($vars->{'operations'})
+        = Bugzilla::Bug::GetBugActivity($bug->id, $attachment->id, $delta_ts);
+
+      # If the modification date changed but there is no entry in
+      # the activity table, this means someone commented only.
+      # In this case, there is no reason to midair.
+      if (scalar(@{$vars->{'operations'}})) {
+        $cgi->param('delta_ts', $modification_time);
+
+        # The token contains the old modification_time. We need a new one.
+        $cgi->param('token', issue_hash_token([$attachment->id, $modification_time]));
+
+        $vars->{'attachment'} = $attachment;
+
+        print $cgi->header();
 
-    # We couldn't do this check earlier as we first had to validate attachment ID
-    # and display the mid-air collision page if modification_time changed.
-    my $token = $cgi->param('token');
-    check_hash_token($token, [$attachment->id, $attachment->modification_time]);
-
-    # If the user submitted a comment while editing the attachment,
-    # add the comment to the bug. Do this after having validated isprivate!
-    my $comment = $cgi->param('comment');
-    if (defined $comment && trim($comment) ne '') {
-        $bug->add_comment($comment, { isprivate => $attachment->isprivate,
-                                      type => CMT_ATTACHMENT_UPDATED,
-                                      extra_data => $attachment->id });
+        # Warn the user about the mid-air collision and ask them what to do.
+        $template->process("attachment/midair.html.tmpl", $vars)
+          || ThrowTemplateError($template->error());
+        exit;
+      }
     }
+  }
 
-    my ($flags, $new_flags) =
-      Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
+  # We couldn't do this check earlier as we first had to validate attachment ID
+  # and display the mid-air collision page if modification_time changed.
+  my $token = $cgi->param('token');
+  check_hash_token($token, [$attachment->id, $attachment->modification_time]);
+
+  # If the user submitted a comment while editing the attachment,
+  # add the comment to the bug. Do this after having validated isprivate!
+  my $comment = $cgi->param('comment');
+  if (defined $comment && trim($comment) ne '') {
+    $bug->add_comment(
+      $comment,
+      {
+        isprivate  => $attachment->isprivate,
+        type       => CMT_ATTACHMENT_UPDATED,
+        extra_data => $attachment->id
+      }
+    );
+  }
 
-    if ($can_edit) {
-        $attachment->set_flags($flags, $new_flags);
+  my ($flags, $new_flags)
+    = Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars);
+
+  if ($can_edit) {
+    $attachment->set_flags($flags, $new_flags);
+  }
+
+  # Requestees can set flags targetted to them, even if they cannot
+  # edit the attachment. Flag setters can edit their own flags too.
+  elsif (scalar @$flags) {
+    my @flag_ids  = map { $_->{id} } @$flags;
+    my $flag_objs = Bugzilla::Flag->new_from_list(\@flag_ids);
+    my %flag_list = map { $_->id => $_ } @$flag_objs;
+
+    my @editable_flags;
+    foreach my $flag (@$flags) {
+      my $flag_obj = $flag_list{$flag->{id}};
+      if ($flag_obj->setter_id == $user->id
+        || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
+      {
+        push(@editable_flags, $flag);
+      }
     }
-    # Requestees can set flags targetted to them, even if they cannot
-    # edit the attachment. Flag setters can edit their own flags too.
-    elsif (scalar @$flags) {
-        my @flag_ids = map { $_->{id} } @$flags;
-        my $flag_objs = Bugzilla::Flag->new_from_list(\@flag_ids);
-        my %flag_list = map { $_->id => $_ } @$flag_objs;
-
-        my @editable_flags;
-        foreach my $flag (@$flags) {
-            my $flag_obj = $flag_list{$flag->{id}};
-            if ($flag_obj->setter_id == $user->id
-                || ($flag_obj->requestee_id && $flag_obj->requestee_id == $user->id))
-            {
-                push(@editable_flags, $flag);
-            }
-        }
 
-        if (scalar @editable_flags) {
-            $attachment->set_flags(\@editable_flags, []);
-            # Flag changes must be committed.
-            $can_edit = 1;
-        }
+    if (scalar @editable_flags) {
+      $attachment->set_flags(\@editable_flags, []);
+
+      # Flag changes must be committed.
+      $can_edit = 1;
     }
+  }
 
-    # Figure out when the changes were made.
-    my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
+  # Figure out when the changes were made.
+  my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
 
-    # Commit the comment, if any.
-    # This has to happen before updating the attachment, to ensure new comments
-    # are available to $attachment->update.
-    $bug->update($timestamp);
+  # Commit the comment, if any.
+  # This has to happen before updating the attachment, to ensure new comments
+  # are available to $attachment->update.
+  $bug->update($timestamp);
 
-    if ($can_edit) {
-        my $changes = $attachment->update($timestamp);
-        # If there are changes, we updated delta_ts in the DB. We have to
-        # reflect this change in the bug object.
-        $bug->{delta_ts} = $timestamp if scalar(keys %$changes);
-    }
+  if ($can_edit) {
+    my $changes = $attachment->update($timestamp);
 
-    # Commit the transaction now that we are finished updating the database.
-    $dbh->bz_commit_transaction();
+    # If there are changes, we updated delta_ts in the DB. We have to
+    # reflect this change in the bug object.
+    $bug->{delta_ts} = $timestamp if scalar(keys %$changes);
+  }
 
-    # Define the variables and functions that will be passed to the UI template.
-    $vars->{'attachment'} = $attachment;
-    $vars->{'bugs'} = [$bug];
-    $vars->{'header_done'} = 1;
-    $vars->{'sent_bugmail'} =
-        Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
+  # Commit the transaction now that we are finished updating the database.
+  $dbh->bz_commit_transaction();
 
-    # BMO: add show_bug_format hook for experimental UI work
-    my $show_bug_format = {};
-    Bugzilla::Hook::process('show_bug_format', $show_bug_format);
+  # Define the variables and functions that will be passed to the UI template.
+  $vars->{'attachment'}  = $attachment;
+  $vars->{'bugs'}        = [$bug];
+  $vars->{'header_done'} = 1;
+  $vars->{'sent_bugmail'}
+    = Bugzilla::BugMail::Send($bug->id, {'changer' => $user});
 
-    if ($show_bug_format->{format} eq 'modal') {
-        $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id));
-    }
+  # BMO: add show_bug_format hook for experimental UI work
+  my $show_bug_format = {};
+  Bugzilla::Hook::process('show_bug_format', $show_bug_format);
 
-    print $cgi->header();
+  if ($show_bug_format->{format} eq 'modal') {
+    $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id));
+  }
 
-    # Generate and return the UI (HTML page) from the appropriate template.
-    $template->process("attachment/updated.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+
+  # Generate and return the UI (HTML page) from the appropriate template.
+  $template->process("attachment/updated.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 # Only administrators can delete attachments.
 sub delete_attachment {
-    my $user = Bugzilla->login(LOGIN_REQUIRED);
-    my $dbh = Bugzilla->dbh;
-
-    $user->in_group('admin')
-      || ThrowUserError('auth_failure', {group  => 'admin',
-                                         action => 'delete',
-                                         object => 'attachment'});
-
-    Bugzilla->params->{'allow_attachment_deletion'}
-      || ThrowUserError('attachment_deletion_disabled');
-
-    # Make sure the administrator is allowed to edit this attachment.
-    my $attachment = validateID();
-    Bugzilla::Attachment->_check_bug($attachment->bug);
-
-    $attachment->datasize || ThrowUserError('attachment_removed');
-
-    # We don't want to let a malicious URL accidentally delete an attachment.
-    my $token = trim($cgi->param('token'));
-    if ($token) {
-        my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
-        unless ($creator_id
-                  && ($creator_id == $user->id)
-                  && ($event eq 'delete_attachment' . $attachment->id))
-        {
-            # The token is invalid.
-            ThrowUserError('token_does_not_exist');
-        }
+  my $user = Bugzilla->login(LOGIN_REQUIRED);
+  my $dbh  = Bugzilla->dbh;
 
-        my $bug = new Bugzilla::Bug($attachment->bug_id);
+  $user->in_group('admin')
+    || ThrowUserError('auth_failure',
+    {group => 'admin', action => 'delete', object => 'attachment'});
 
-        # The token is valid. Delete the content of the attachment.
-        my $msg;
-        $vars->{'attachment'} = $attachment;
-        $vars->{'reason'} = clean_text($cgi->param('reason') || '');
+  Bugzilla->params->{'allow_attachment_deletion'}
+    || ThrowUserError('attachment_deletion_disabled');
 
-        $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
-          || ThrowTemplateError($template->error());
+  # Make sure the administrator is allowed to edit this attachment.
+  my $attachment = validateID();
+  Bugzilla::Attachment->_check_bug($attachment->bug);
+
+  $attachment->datasize || ThrowUserError('attachment_removed');
 
-        # Paste the reason provided by the admin into a comment.
-        $bug->add_comment($msg);
+  # We don't want to let a malicious URL accidentally delete an attachment.
+  my $token = trim($cgi->param('token'));
+  if ($token) {
+    my ($creator_id, $date, $event) = Bugzilla::Token::GetTokenData($token);
+    unless ($creator_id
+      && ($creator_id == $user->id)
+      && ($event eq 'delete_attachment' . $attachment->id))
+    {
+      # The token is invalid.
+      ThrowUserError('token_does_not_exist');
+    }
 
-        # Remove attachment.
-        $attachment->remove_from_db();
+    my $bug = new Bugzilla::Bug($attachment->bug_id);
 
-        # Now delete the token.
-        delete_token($token);
+    # The token is valid. Delete the content of the attachment.
+    my $msg;
+    $vars->{'attachment'} = $attachment;
+    $vars->{'reason'} = clean_text($cgi->param('reason') || '');
 
-        # Insert the comment.
-        $bug->update();
+    $template->process("attachment/delete_reason.txt.tmpl", $vars, \$msg)
+      || ThrowTemplateError($template->error());
 
-        # Required to display the bug the deleted attachment belongs to.
-        $vars->{'bugs'} = [$bug];
-        $vars->{'header_done'} = 1;
+    # Paste the reason provided by the admin into a comment.
+    $bug->add_comment($msg);
 
-        $vars->{'sent_bugmail'} =
-            Bugzilla::BugMail::Send($bug->id, { 'changer' => $user });
+    # Remove attachment.
+    $attachment->remove_from_db();
 
-        # BMO: add show_bug_format hook for experimental UI work
-        my $show_bug_format = {};
-        Bugzilla::Hook::process('show_bug_format', $show_bug_format);
+    # Now delete the token.
+    delete_token($token);
 
-        if ($show_bug_format->{format} eq 'modal') {
-            $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id));
-        }
+    # Insert the comment.
+    $bug->update();
 
-        print $cgi->header();
-        $template->process("attachment/updated.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-    }
-    else {
-        # Create a token.
-        $token = issue_session_token('delete_attachment' . $attachment->id);
+    # Required to display the bug the deleted attachment belongs to.
+    $vars->{'bugs'}        = [$bug];
+    $vars->{'header_done'} = 1;
 
-        $vars->{'a'} = $attachment;
-        $vars->{'token'} = $token;
+    $vars->{'sent_bugmail'}
+      = Bugzilla::BugMail::Send($bug->id, {'changer' => $user});
 
-        print $cgi->header();
-        $template->process("attachment/confirm-delete.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
+    # BMO: add show_bug_format hook for experimental UI work
+    my $show_bug_format = {};
+    Bugzilla::Hook::process('show_bug_format', $show_bug_format);
+
+    if ($show_bug_format->{format} eq 'modal') {
+      $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id));
     }
+
+    print $cgi->header();
+    $template->process("attachment/updated.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+  }
+  else {
+    # Create a token.
+    $token = issue_session_token('delete_attachment' . $attachment->id);
+
+    $vars->{'a'}     = $attachment;
+    $vars->{'token'} = $token;
+
+    print $cgi->header();
+    $template->process("attachment/confirm-delete.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+  }
 }
diff --git a/auth.cgi b/auth.cgi
index adf5d3475..dbb4629d9 100755
--- a/auth.cgi
+++ b/auth.cgi
@@ -28,13 +28,16 @@ use JSON qw(decode_json encode_json);
 
 Bugzilla->login(LOGIN_REQUIRED);
 
-ThrowUserError('auth_delegation_disabled') unless Bugzilla->params->{auth_delegation};
+ThrowUserError('auth_delegation_disabled')
+  unless Bugzilla->params->{auth_delegation};
 
-my $cgi         = Bugzilla->cgi;
-my $template    = Bugzilla->template;
-my $user        = Bugzilla->user;
-my $callback    = $cgi->param('callback') or ThrowUserError("auth_delegation_missing_callback");
-my $description = $cgi->param('description') or ThrowUserError("auth_delegation_missing_description");
+my $cgi      = Bugzilla->cgi;
+my $template = Bugzilla->template;
+my $user     = Bugzilla->user;
+my $callback = $cgi->param('callback')
+  or ThrowUserError("auth_delegation_missing_callback");
+my $description = $cgi->param('description')
+  or ThrowUserError("auth_delegation_missing_description");
 
 trick_taint($callback);
 trick_taint($description);
@@ -42,100 +45,105 @@ trick_taint($description);
 ThrowUserError("auth_delegation_invalid_description")
   unless $description =~ /^[\w\s]{3,255}$/;
 
-my $callback_uri  = URI->new($callback);
+my $callback_uri = URI->new($callback);
 
-my $legal_protocol
-    = $ENV{BUGZILLA_UNSAFE_AUTH_DELEGATION}
-    ? qr/^https?$/i # http or https
-    : qr/^https$/i; # https only
+my $legal_protocol = $ENV{BUGZILLA_UNSAFE_AUTH_DELEGATION}
+  ? qr/^https?$/i    # http or https
+  : qr/^https$/i;    # https only
 
-ThrowUserError('auth_delegation_illegal_protocol', { protocol => scalar $callback_uri->scheme })
-    unless $callback_uri->scheme =~ $legal_protocol;
+ThrowUserError(
+  'auth_delegation_illegal_protocol',
+  {protocol => scalar $callback_uri->scheme}
+) unless $callback_uri->scheme =~ $legal_protocol;
 my $callback_base = $callback_uri->clone;
 $callback_base->query(undef);
 
-my $app_id = sha256_hex($callback_base, $description);
+my $app_id            = sha256_hex($callback_base, $description);
 my $skip_confirmation = 0;
-my %args = ( skip_confirmation => \$skip_confirmation,
-             callback          => $callback_uri,
-             description       => $description,
-             app_id            => $app_id,
-             callback_base     => $callback_base );
+my %args              = (
+  skip_confirmation => \$skip_confirmation,
+  callback          => $callback_uri,
+  description       => $description,
+  app_id            => $app_id,
+  callback_base     => $callback_base
+);
 
 Bugzilla::Hook::process('auth_delegation_confirm', \%args);
 
 my $confirmed = lc($cgi->request_method) eq 'post' && $cgi->param('confirm');
 
 if ($confirmed || $skip_confirmation) {
-    my $token = $cgi->param('token');
-    unless ($skip_confirmation) {
-        ThrowUserError("auth_delegation_missing_token") unless $token;
-        trick_taint($token);
-
-        unless (check_auth_delegation_token($token, $callback)) {
-            ThrowUserError('auth_delegation_invalid_token',
-                           { token => $token, callback => $callback });
-        }
+  my $token = $cgi->param('token');
+  unless ($skip_confirmation) {
+    ThrowUserError("auth_delegation_missing_token") unless $token;
+    trick_taint($token);
+
+    unless (check_auth_delegation_token($token, $callback)) {
+      ThrowUserError('auth_delegation_invalid_token',
+        {token => $token, callback => $callback});
     }
-    my $keys = Bugzilla::User::APIKey->match({
-        user_id => $user->id,
-        app_id  => $app_id,
-        revoked => 0,
+  }
+  my $keys
+    = Bugzilla::User::APIKey->match({
+    user_id => $user->id, app_id => $app_id, revoked => 0,
     });
 
-    my $api_key;
-    if (@$keys) {
-        $api_key = $keys->[0];
-    }
-    else {
-        $api_key = Bugzilla::User::APIKey->create({
-            user_id     => $user->id,
-            description => $description,
-            app_id      => $app_id,
-        });
-        my $template = Bugzilla->template_inner($user->setting('lang'));
-        my $vars = { user => $user, new_key => $api_key };
-        my $message;
-        $template->process('email/new-api-key.txt.tmpl', $vars, \$message)
-          or ThrowTemplateError($template->error());
-
-        MessageToMTA($message);
-    }
+  my $api_key;
+  if (@$keys) {
+    $api_key = $keys->[0];
+  }
+  else {
+    $api_key
+      = Bugzilla::User::APIKey->create({
+      user_id => $user->id, description => $description, app_id => $app_id,
+      });
+    my $template = Bugzilla->template_inner($user->setting('lang'));
+    my $vars = {user => $user, new_key => $api_key};
+    my $message;
+    $template->process('email/new-api-key.txt.tmpl', $vars, \$message)
+      or ThrowTemplateError($template->error());
 
-    my $ua = LWP::UserAgent->new();
-    $ua->timeout(2);
-    $ua->protocols_allowed(['http', 'https']);
-    # If the URL of the proxy is given, use it, else get this information
-    # from the environment variable.
-    if (my $proxy_url = Bugzilla->params->{'proxy_url'}) {
-        $ua->proxy(['http', 'https'], $proxy_url);
-    }
-    else {
-        $ua->env_proxy;
-    }
-    my $content = encode_json({ client_api_key => $api_key->api_key,
-                                client_api_login => $user->login });
-    my $resp = $ua->post($callback_uri,
-                         'Content-Type' => 'application/json',
-                         Content => $content);
-    if ($resp->code == 200) {
-        $callback_uri->query_param(client_api_login => $user->login);
-        eval {
-            my $data = decode_json($resp->content);
-            $callback_uri->query_param(callback_result => $data->{result});
-        };
-        ThrowUserError('auth_delegation_json_error', { json_text => $resp->content }) if $@;
-
-        print $cgi->redirect($callback_uri);
-    }
-    else {
-        ThrowUserError('auth_delegation_post_error', { code => $resp->code });
-    }
+    MessageToMTA($message);
+  }
+
+  my $ua = LWP::UserAgent->new();
+  $ua->timeout(2);
+  $ua->protocols_allowed(['http', 'https']);
+
+  # If the URL of the proxy is given, use it, else get this information
+  # from the environment variable.
+  if (my $proxy_url = Bugzilla->params->{'proxy_url'}) {
+    $ua->proxy(['http', 'https'], $proxy_url);
+  }
+  else {
+    $ua->env_proxy;
+  }
+  my $content = encode_json(
+    {client_api_key => $api_key->api_key, client_api_login => $user->login});
+  my $resp = $ua->post(
+    $callback_uri,
+    'Content-Type' => 'application/json',
+    Content        => $content
+  );
+  if ($resp->code == 200) {
+    $callback_uri->query_param(client_api_login => $user->login);
+    eval {
+      my $data = decode_json($resp->content);
+      $callback_uri->query_param(callback_result => $data->{result});
+    };
+    ThrowUserError('auth_delegation_json_error', {json_text => $resp->content})
+      if $@;
+
+    print $cgi->redirect($callback_uri);
+  }
+  else {
+    ThrowUserError('auth_delegation_post_error', {code => $resp->code});
+  }
 }
 else {
-    $args{token} = issue_auth_delegation_token($callback);
+  $args{token} = issue_auth_delegation_token($callback);
 
-    print $cgi->header();
-    $template->process("account/auth/delegation.html.tmpl", \%args)
-      or ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("account/auth/delegation.html.tmpl", \%args)
+    or ThrowTemplateError($template->error());
 }
diff --git a/buglist.cgi b/buglist.cgi
index 8de38599f..3954c4f25 100755
--- a/buglist.cgi
+++ b/buglist.cgi
@@ -30,10 +30,10 @@ use Bugzilla::Token;
 
 use Date::Parse;
 
-my $cgi = Bugzilla->cgi;
-my $dbh = Bugzilla->dbh;
+my $cgi      = Bugzilla->cgi;
+my $dbh      = Bugzilla->dbh;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # We have to check the login here to get the correct footer if an error is
 # thrown and to prevent a logged out user to use QuickSearch if 'requirelogin'
@@ -46,26 +46,28 @@ DEBUG("After the redirect.");
 
 my $buffer = $cgi->query_string();
 if (length($buffer) == 0) {
-    ThrowUserError("buglist_parameters_required");
+  ThrowUserError("buglist_parameters_required");
 }
 
 
 # Determine whether this is a quicksearch query.
 my $searchstring = $cgi->param('quicksearch');
 if (defined($searchstring)) {
-    $buffer = quicksearch($searchstring);
-    # Quicksearch may do a redirect, in which case it does not return.
-    # If it does return, it has modified $cgi->params so we can use them here
-    # as if this had been a normal query from the beginning.
+  $buffer = quicksearch($searchstring);
+
+  # Quicksearch may do a redirect, in which case it does not return.
+  # If it does return, it has modified $cgi->params so we can use them here
+  # as if this had been a normal query from the beginning.
 }
 
 # If configured to not allow empty words, reject empty searches from the
 # Find a Specific Bug search form, including words being a single or
 # several consecutive whitespaces only.
-if (!Bugzilla->params->{'search_allow_no_criteria'}
-    && defined($cgi->param('content')) && $cgi->param('content') =~ /^\s*$/)
+if ( !Bugzilla->params->{'search_allow_no_criteria'}
+  && defined($cgi->param('content'))
+  && $cgi->param('content') =~ /^\s*$/)
 {
-    ThrowUserError("buglist_parameters_required");
+  ThrowUserError("buglist_parameters_required");
 }
 
 ################################################################################
@@ -77,19 +79,21 @@ my $dotweak = $cgi->param('tweak') ? 1 : 0;
 
 # Log the user in
 if ($dotweak) {
-    Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->login(LOGIN_REQUIRED);
 }
 
 # Hack to support legacy applications that think the RDF ctype is at format=rdf.
-if (defined $cgi->param('format') && $cgi->param('format') eq "rdf"
-    && !defined $cgi->param('ctype')) {
-    $cgi->param('ctype', "rdf");
-    $cgi->delete('format');
+if ( defined $cgi->param('format')
+  && $cgi->param('format') eq "rdf"
+  && !defined $cgi->param('ctype'))
+{
+  $cgi->param('ctype', "rdf");
+  $cgi->delete('format');
 }
 
 # Treat requests for ctype=rss as requests for ctype=atom
 if (defined $cgi->param('ctype') && $cgi->param('ctype') eq "rss") {
-    $cgi->param('ctype', "atom");
+  $cgi->param('ctype', "atom");
 }
 
 # An agent is a program that automatically downloads and extracts data
@@ -102,8 +106,11 @@ my $agent = ($cgi->http('X-Moz') && $cgi->http('X-Moz') =~ /\bmicrosummary\b/);
 # Determine the format in which the user would like to receive the output.
 # Uses the default format if the user did not specify an output format;
 # otherwise validates the user's choice against the list of available formats.
-my $format = $template->get_format("list/list", scalar $cgi->param('format'),
-                                   scalar $cgi->param('ctype'));
+my $format = $template->get_format(
+  "list/list",
+  scalar $cgi->param('format'),
+  scalar $cgi->param('ctype')
+);
 
 my $order = $cgi->param('order') || "";
 
@@ -113,25 +120,26 @@ my $params;
 # If the user is retrieving the last bug list they looked at, hack the buffer
 # storing the query string so that it looks like a query retrieving those bugs.
 if (my $last_list = $cgi->param('regetlastlist')) {
-    my $bug_ids;
-
-    # Logged-out users use the old cookie method for storing the last search.
-    if (!$user->id or $last_list eq 'cookie') {
-        $bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie");
-        $bug_ids =~ s/[:-]/,/g;
-        $order ||= "reuse last sort";
-    }
-    # But logged in users store the last X searches in the DB so they can
-    # have multiple bug lists available.
-    else {
-        my $last_search = Bugzilla::Search::Recent->check(
-            { id => $last_list });
-        $bug_ids = join(',', @{ $last_search->bug_list });
-        $order ||= $last_search->list_order;
-    }
-    # set up the params for this new query
-    $params = new Bugzilla::CGI({ bug_id => $bug_ids, order => $order });
-    $params->param('list_id', $last_list);
+  my $bug_ids;
+
+  # Logged-out users use the old cookie method for storing the last search.
+  if (!$user->id or $last_list eq 'cookie') {
+    $bug_ids = $cgi->cookie('BUGLIST') or ThrowUserError("missing_cookie");
+    $bug_ids =~ s/[:-]/,/g;
+    $order ||= "reuse last sort";
+  }
+
+  # But logged in users store the last X searches in the DB so they can
+  # have multiple bug lists available.
+  else {
+    my $last_search = Bugzilla::Search::Recent->check({id => $last_list});
+    $bug_ids = join(',', @{$last_search->bug_list});
+    $order ||= $last_search->list_order;
+  }
+
+  # set up the params for this new query
+  $params = new Bugzilla::CGI({bug_id => $bug_ids, order => $order});
+  $params->param('list_id', $last_list);
 }
 
 # Figure out whether or not the user is doing a fulltext search.  If not,
@@ -141,10 +149,10 @@ my $fulltext = 0;
 if ($cgi->param('content')) { $fulltext = 1 }
 my @charts = map(/^field(\d-\d-\d)$/ ? $1 : (), $cgi->param());
 foreach my $chart (@charts) {
-    if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) {
-        $fulltext = 1;
-        last;
-    }
+  if ($cgi->param("field$chart") eq 'content' && $cgi->param("value$chart")) {
+    $fulltext = 1;
+    last;
+  }
 }
 
 ################################################################################
@@ -152,34 +160,35 @@ foreach my $chart (@charts) {
 ################################################################################
 
 sub DiffDate {
-    my ($datestr) = @_;
-    my $date = str2time($datestr);
-    my $age = time() - $date;
-
-    if( $age < 18*60*60 ) {
-        $date = format_time($datestr, '%H:%M:%S');
-    } elsif( $age < 6*24*60*60 ) {
-        $date = format_time($datestr, '%a %H:%M');
-    } else {
-        $date = format_time($datestr, '%Y-%m-%d');
-    }
-    return $date;
+  my ($datestr) = @_;
+  my $date      = str2time($datestr);
+  my $age       = time() - $date;
+
+  if ($age < 18 * 60 * 60) {
+    $date = format_time($datestr, '%H:%M:%S');
+  }
+  elsif ($age < 6 * 24 * 60 * 60) {
+    $date = format_time($datestr, '%a %H:%M');
+  }
+  else {
+    $date = format_time($datestr, '%Y-%m-%d');
+  }
+  return $date;
 }
 
 sub LookupNamedQuery {
-    my ($name, $sharer_id) = @_;
+  my ($name, $sharer_id) = @_;
 
-    Bugzilla->login(LOGIN_REQUIRED);
+  Bugzilla->login(LOGIN_REQUIRED);
 
-    my $query = Bugzilla::Search::Saved->check(
-        { user => $sharer_id, name => $name, _error => 'missing_query' });
+  my $query = Bugzilla::Search::Saved->check(
+    {user => $sharer_id, name => $name, _error => 'missing_query'});
 
-    $query->url
-       || ThrowUserError("buglist_parameters_required");
+  $query->url || ThrowUserError("buglist_parameters_required");
 
-    # Detaint $sharer_id.
-    $sharer_id = $query->user->id if $sharer_id;
-    return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url;
+  # Detaint $sharer_id.
+  $sharer_id = $query->user->id if $sharer_id;
+  return wantarray ? ($query->url, $query->id, $sharer_id) : $query->url;
 }
 
 # Inserts a Named Query (a "Saved Search") into the database, or
@@ -202,78 +211,80 @@ sub LookupNamedQuery {
 # Returns: A boolean true value if the query existed in the database
 # before, and we updated it. A boolean false value otherwise.
 sub InsertNamedQuery {
-    my ($query_name, $query, $link_in_footer) = @_;
-    my $dbh = Bugzilla->dbh;
-
-    $query_name = trim($query_name);
-    my ($query_obj) = grep {lc($_->name) eq lc($query_name)} @{Bugzilla->user->queries};
-
-    if ($query_obj) {
-        $query_obj->set_name($query_name);
-        $query_obj->set_url($query);
-        $query_obj->update();
-    } else {
-        Bugzilla::Search::Saved->create({
-            name           => $query_name,
-            query          => $query,
-            link_in_footer => $link_in_footer
-        });
-    }
-
-    return $query_obj ? 1 : 0;
+  my ($query_name, $query, $link_in_footer) = @_;
+  my $dbh = Bugzilla->dbh;
+
+  $query_name = trim($query_name);
+  my ($query_obj)
+    = grep { lc($_->name) eq lc($query_name) } @{Bugzilla->user->queries};
+
+  if ($query_obj) {
+    $query_obj->set_name($query_name);
+    $query_obj->set_url($query);
+    $query_obj->update();
+  }
+  else {
+    Bugzilla::Search::Saved->create({
+      name => $query_name, query => $query, link_in_footer => $link_in_footer
+    });
+  }
+
+  return $query_obj ? 1 : 0;
 }
 
 sub LookupSeries {
-    my ($series_id) = @_;
-    detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
-
-    my $dbh = Bugzilla->dbh;
-    my $result = $dbh->selectrow_array("SELECT query FROM series " .
-                                       "WHERE series_id = ?"
-                                       , undef, ($series_id));
-    $result
-           || ThrowCodeError("invalid_series_id", {'series_id' => $series_id});
-    return $result;
+  my ($series_id) = @_;
+  detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
+
+  my $dbh = Bugzilla->dbh;
+  my $result
+    = $dbh->selectrow_array("SELECT query FROM series " . "WHERE series_id = ?",
+    undef, ($series_id));
+  $result || ThrowCodeError("invalid_series_id", {'series_id' => $series_id});
+  return $result;
 }
 
 sub GetQuip {
-    my $dbh = Bugzilla->dbh;
-    # COUNT is quick because it is cached for MySQL. We may want to revisit
-    # this when we support other databases.
-    my $count = $dbh->selectrow_array("SELECT COUNT(quip)"
-                                    . " FROM quips WHERE approved = 1");
-    my $random = int(rand($count));
-    my $quip =
-        $dbh->selectrow_array("SELECT quip FROM quips WHERE approved = 1 " .
-                              $dbh->sql_limit(1, $random));
-    return $quip;
+  my $dbh = Bugzilla->dbh;
+
+  # COUNT is quick because it is cached for MySQL. We may want to revisit
+  # this when we support other databases.
+  my $count = $dbh->selectrow_array(
+    "SELECT COUNT(quip)" . " FROM quips WHERE approved = 1");
+  my $random = int(rand($count));
+  my $quip   = $dbh->selectrow_array(
+    "SELECT quip FROM quips WHERE approved = 1 " . $dbh->sql_limit(1, $random));
+  return $quip;
 }
 
 # Return groups available for at least one product of the buglist.
 sub GetGroups {
-    my $product_names = shift;
-    my $user = Bugzilla->user;
-    my %legal_groups;
+  my $product_names = shift;
+  my $user          = Bugzilla->user;
+  my %legal_groups;
 
-    foreach my $product_name (@$product_names) {
-        my $product = new Bugzilla::Product({name => $product_name});
+  foreach my $product_name (@$product_names) {
+    my $product = new Bugzilla::Product({name => $product_name});
 
-        foreach my $gid (keys %{$product->group_controls}) {
-            # The user can only edit groups he belongs to.
-            next unless $user->in_group_id($gid);
+    foreach my $gid (keys %{$product->group_controls}) {
 
-            # The user has no control on groups marked as NA or MANDATORY.
-            my $group = $product->group_controls->{$gid};
-            next if ($group->{membercontrol} == CONTROLMAPMANDATORY
-                     || $group->{membercontrol} == CONTROLMAPNA);
+      # The user can only edit groups he belongs to.
+      next unless $user->in_group_id($gid);
 
-            # It's fine to include inactive groups. Those will be marked
-            # as "remove only" when editing several bugs at once.
-            $legal_groups{$gid} ||= $group->{group};
-        }
+      # The user has no control on groups marked as NA or MANDATORY.
+      my $group = $product->group_controls->{$gid};
+      next
+        if ($group->{membercontrol} == CONTROLMAPMANDATORY
+        || $group->{membercontrol} == CONTROLMAPNA);
+
+      # It's fine to include inactive groups. Those will be marked
+      # as "remove only" when editing several bugs at once.
+      $legal_groups{$gid} ||= $group->{group};
     }
-    # Return a list of group objects.
-    return [values %legal_groups];
+  }
+
+  # Return a list of group objects.
+  return [values %legal_groups];
 }
 
 ################################################################################
@@ -287,8 +298,8 @@ my $sharer_id;
 # Backwards-compatibility - the old interface had cmdtype="runnamed" to run
 # a named command, and we can't break this because it's in bookmarks.
 if ($cmdtype eq "runnamed") {
-    $cmdtype = "dorem";
-    $remaction = "run";
+  $cmdtype   = "dorem";
+  $remaction = "run";
 }
 
 # Now we're going to be running, so ensure that the params object is set up,
@@ -304,47 +315,50 @@ $params ||= new Bugzilla::CGI($cgi);
 # at the end, because the fact that there is a remembered query gets
 # forgotten in the process of retrieving it.
 my $disp_prefix = "bugs";
-if (($cmdtype eq "dorem" && $remaction =~ /^run/) || ($format->{extension} ne 'html' && defined $cgi->param('namedcmd'))) {
-    $disp_prefix = $cgi->param('namedcmd');
+if ( ($cmdtype eq "dorem" && $remaction =~ /^run/)
+  || ($format->{extension} ne 'html' && defined $cgi->param('namedcmd')))
+{
+  $disp_prefix = $cgi->param('namedcmd');
 }
 
 # Take appropriate action based on user's request.
 if ($cmdtype eq "dorem") {
-    if ($remaction eq "run") {
-        my $query_id;
-        ($buffer, $query_id, $sharer_id) =
-          LookupNamedQuery(scalar $cgi->param("namedcmd"),
-                           scalar $cgi->param('sharer_id'));
-        # If this is the user's own query, remember information about it
-        # so that it can be modified easily.
-        $vars->{'searchname'} = $cgi->param('namedcmd');
-        if (!$cgi->param('sharer_id') ||
-            $cgi->param('sharer_id') == $user->id) {
-            $vars->{'searchtype'} = "saved";
-            $vars->{'search_id'} = $query_id;
-        }
-        $params = new Bugzilla::CGI($buffer);
-        $order = $params->param('order') || $order;
-
-    }
-    elsif ($remaction eq "runseries") {
-        $buffer = LookupSeries(scalar $cgi->param("series_id"));
-        $vars->{'searchname'} = $cgi->param('namedcmd');
-        $vars->{'searchtype'} = "series";
-        $params = new Bugzilla::CGI($buffer);
-        $order = $params->param('order') || $order;
+  if ($remaction eq "run") {
+    my $query_id;
+    ($buffer, $query_id, $sharer_id)
+      = LookupNamedQuery(scalar $cgi->param("namedcmd"),
+      scalar $cgi->param('sharer_id'));
+
+    # If this is the user's own query, remember information about it
+    # so that it can be modified easily.
+    $vars->{'searchname'} = $cgi->param('namedcmd');
+    if (!$cgi->param('sharer_id') || $cgi->param('sharer_id') == $user->id) {
+      $vars->{'searchtype'} = "saved";
+      $vars->{'search_id'}  = $query_id;
     }
-    elsif ($remaction eq "forget") {
-        $user = Bugzilla->login(LOGIN_REQUIRED);
-        # Copy the name into a variable, so that we can trick_taint it for
-        # the DB. We know it's safe, because we're using placeholders in
-        # the SQL, and the SQL is only a DELETE.
-        my $qname = $cgi->param('namedcmd');
-        trick_taint($qname);
-
-        # Do not forget the saved search if it is being used in a whine
-        my $whines_in_use =
-            $dbh->selectcol_arrayref('SELECT DISTINCT whine_events.subject
+    $params = new Bugzilla::CGI($buffer);
+    $order = $params->param('order') || $order;
+
+  }
+  elsif ($remaction eq "runseries") {
+    $buffer               = LookupSeries(scalar $cgi->param("series_id"));
+    $vars->{'searchname'} = $cgi->param('namedcmd');
+    $vars->{'searchtype'} = "series";
+    $params               = new Bugzilla::CGI($buffer);
+    $order                = $params->param('order') || $order;
+  }
+  elsif ($remaction eq "forget") {
+    $user = Bugzilla->login(LOGIN_REQUIRED);
+
+    # Copy the name into a variable, so that we can trick_taint it for
+    # the DB. We know it's safe, because we're using placeholders in
+    # the SQL, and the SQL is only a DELETE.
+    my $qname = $cgi->param('namedcmd');
+    trick_taint($qname);
+
+    # Do not forget the saved search if it is being used in a whine
+    my $whines_in_use = $dbh->selectcol_arrayref(
+      'SELECT DISTINCT whine_events.subject
                                                  FROM whine_events
                                            INNER JOIN whine_queries
                                                    ON whine_queries.eventid
@@ -353,127 +367,138 @@ if ($cmdtype eq "dorem") {
                                                       = ?
                                                   AND whine_queries.query_name
                                                       = ?
-                                      ', undef, $user->id, $qname);
-        if (scalar(@$whines_in_use)) {
-            ThrowUserError('saved_search_used_by_whines',
-                           { subjects    => join(',', @$whines_in_use),
-                             search_name => $qname                      }
-            );
-        }
-
-        # If we are here, then we can safely remove the saved search
-        my $query_id;
-        ($buffer, $query_id) = LookupNamedQuery(scalar $cgi->param("namedcmd"),
-                                                $user->id);
-        if ($query_id) {
-            # Make sure the user really wants to delete his saved search.
-            my $token = $cgi->param('token');
-            check_hash_token($token, [$query_id, $qname]);
-
-            $dbh->do('DELETE FROM namedqueries
-                            WHERE id = ?',
-                     undef, $query_id);
-            $dbh->do('DELETE FROM namedqueries_link_in_footer
-                            WHERE namedquery_id = ?',
-                     undef, $query_id);
-            $dbh->do('DELETE FROM namedquery_group_map
-                            WHERE namedquery_id = ?',
-                     undef, $query_id);
-            Bugzilla->memcached->clear({ table => 'namedqueries', id => $query_id });
-        }
+                                      ', undef, $user->id, $qname
+    );
+    if (scalar(@$whines_in_use)) {
+      ThrowUserError('saved_search_used_by_whines',
+        {subjects => join(',', @$whines_in_use), search_name => $qname});
+    }
 
-        # Now reset the cached queries
-        $user->flush_queries_cache();
-
-        print $cgi->header();
-        # Generate and return the UI (HTML page) from the appropriate template.
-        $vars->{'message'} = "buglist_query_gone";
-        $vars->{'namedcmd'} = $qname;
-        $vars->{'url'} = "buglist.cgi?newquery=" . url_quote($buffer)
-                         . "&cmdtype=doit&remtype=asnamed&newqueryname=" . url_quote($qname)
-                         . "&token=" . url_quote(issue_hash_token(['savedsearch']));
-        $template->process("global/message.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        exit;
+    # If we are here, then we can safely remove the saved search
+    my $query_id;
+    ($buffer, $query_id)
+      = LookupNamedQuery(scalar $cgi->param("namedcmd"), $user->id);
+    if ($query_id) {
+
+      # Make sure the user really wants to delete his saved search.
+      my $token = $cgi->param('token');
+      check_hash_token($token, [$query_id, $qname]);
+
+      $dbh->do(
+        'DELETE FROM namedqueries
+                            WHERE id = ?', undef, $query_id
+      );
+      $dbh->do(
+        'DELETE FROM namedqueries_link_in_footer
+                            WHERE namedquery_id = ?', undef, $query_id
+      );
+      $dbh->do(
+        'DELETE FROM namedquery_group_map
+                            WHERE namedquery_id = ?', undef, $query_id
+      );
+      Bugzilla->memcached->clear({table => 'namedqueries', id => $query_id});
     }
+
+    # Now reset the cached queries
+    $user->flush_queries_cache();
+
+    print $cgi->header();
+
+    # Generate and return the UI (HTML page) from the appropriate template.
+    $vars->{'message'}  = "buglist_query_gone";
+    $vars->{'namedcmd'} = $qname;
+    $vars->{'url'}
+      = "buglist.cgi?newquery="
+      . url_quote($buffer)
+      . "&cmdtype=doit&remtype=asnamed&newqueryname="
+      . url_quote($qname)
+      . "&token="
+      . url_quote(issue_hash_token(['savedsearch']));
+    $template->process("global/message.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
 }
 elsif (($cmdtype eq "doit") && defined $cgi->param('remtype')) {
-    if ($cgi->param('remtype') eq "asdefault") {
-        $user = Bugzilla->login(LOGIN_REQUIRED);
-        my $token = $cgi->param('token');
-        check_hash_token($token, ['searchknob']);
-        $buffer = $params->canonicalise_query('cmdtype', 'remtype',
-                                              'query_based_on', 'token');
-        InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer);
-        $vars->{'message'} = "buglist_new_default_query";
-    }
-    elsif ($cgi->param('remtype') eq "asnamed") {
-        $user = Bugzilla->login(LOGIN_REQUIRED);
-        my $query_name = $cgi->param('newqueryname');
-        my $new_query = $cgi->param('newquery');
-        my $token = $cgi->param('token');
-        check_hash_token($token, ['savedsearch']);
-        # If list_of_bugs is true, we are adding/removing tags to/from
-        # individual bugs.
-        if ($cgi->param('list_of_bugs')) {
-            # We add/remove tags based on the action choosen.
-            my $action = trim($cgi->param('action') || '');
-            $action =~ /^(add|remove)$/
-              || ThrowUserError('unknown_action', {action => $action});
-
-            my $method = "${action}_tag";
-
-            # If no new tag name has been given, use the selected one.
-            $query_name ||= $cgi->param('oldqueryname')
-              or ThrowUserError('no_tag_to_edit', {action => $action});
-
-            my @buglist;
-            if ($cgi->param('bug_ids')) {
-                # Validate all bug IDs before editing tags in any of them.
-                foreach my $bug_id (split(/[\s,]+/, $cgi->param('bug_ids'))) {
-                    next unless $bug_id;
-                    push(@buglist, Bugzilla::Bug->check($bug_id));
-                }
-
-                foreach my $bug (@buglist) {
-                    $bug->$method($query_name);
-                }
-            }
-
-            $vars->{'message'} = 'tag_updated';
-            $vars->{'action'} = $action;
-            $vars->{'tag'} = $query_name;
-            $vars->{'buglist'} = [map { $_->id } @buglist];
+  if ($cgi->param('remtype') eq "asdefault") {
+    $user = Bugzilla->login(LOGIN_REQUIRED);
+    my $token = $cgi->param('token');
+    check_hash_token($token, ['searchknob']);
+    $buffer = $params->canonicalise_query('cmdtype', 'remtype', 'query_based_on',
+      'token');
+    InsertNamedQuery(DEFAULT_QUERY_NAME, $buffer);
+    $vars->{'message'} = "buglist_new_default_query";
+  }
+  elsif ($cgi->param('remtype') eq "asnamed") {
+    $user = Bugzilla->login(LOGIN_REQUIRED);
+    my $query_name = $cgi->param('newqueryname');
+    my $new_query  = $cgi->param('newquery');
+    my $token      = $cgi->param('token');
+    check_hash_token($token, ['savedsearch']);
+
+    # If list_of_bugs is true, we are adding/removing tags to/from
+    # individual bugs.
+    if ($cgi->param('list_of_bugs')) {
+
+      # We add/remove tags based on the action choosen.
+      my $action = trim($cgi->param('action') || '');
+      $action =~ /^(add|remove)$/
+        || ThrowUserError('unknown_action', {action => $action});
+
+      my $method = "${action}_tag";
+
+      # If no new tag name has been given, use the selected one.
+      $query_name ||= $cgi->param('oldqueryname')
+        or ThrowUserError('no_tag_to_edit', {action => $action});
+
+      my @buglist;
+      if ($cgi->param('bug_ids')) {
+
+        # Validate all bug IDs before editing tags in any of them.
+        foreach my $bug_id (split(/[\s,]+/, $cgi->param('bug_ids'))) {
+          next unless $bug_id;
+          push(@buglist, Bugzilla::Bug->check($bug_id));
         }
-        else {
-            my $existed_before = InsertNamedQuery($query_name, $new_query, 1);
-            if ($existed_before) {
-                $vars->{'message'} = "buglist_updated_named_query";
-            }
-            else {
-                $vars->{'message'} = "buglist_new_named_query";
-            }
-
-            # Make sure to invalidate any cached query data, so that the footer is
-            # correctly displayed
-            $user->flush_queries_cache();
-
-            $vars->{'queryname'} = $query_name;
+
+        foreach my $bug (@buglist) {
+          $bug->$method($query_name);
         }
+      }
 
-        print $cgi->header();
-        $template->process("global/message.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        exit;
+      $vars->{'message'} = 'tag_updated';
+      $vars->{'action'}  = $action;
+      $vars->{'tag'}     = $query_name;
+      $vars->{'buglist'} = [map { $_->id } @buglist];
+    }
+    else {
+      my $existed_before = InsertNamedQuery($query_name, $new_query, 1);
+      if ($existed_before) {
+        $vars->{'message'} = "buglist_updated_named_query";
+      }
+      else {
+        $vars->{'message'} = "buglist_new_named_query";
+      }
+
+      # Make sure to invalidate any cached query data, so that the footer is
+      # correctly displayed
+      $user->flush_queries_cache();
+
+      $vars->{'queryname'} = $query_name;
     }
+
+    print $cgi->header();
+    $template->process("global/message.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
 }
 
 # backward compatibility hack: if the saved query doesn't say which
 # form was used to create it, assume it was on the advanced query
 # form - see bug 252295
 if (!$params->param('query_format')) {
-    $params->param('query_format', 'advanced');
-    $buffer = $params->query_string;
+  $params->param('query_format', 'advanced');
+  $buffer = $params->query_string;
 }
 
 ################################################################################
@@ -490,32 +515,34 @@ my $columns = Bugzilla::Search::COLUMNS;
 # columnlist CGI parameter, the user's preferences, or the default.
 my @displaycolumns = ();
 if (defined $params->param('columnlist')) {
-    if ($params->param('columnlist') eq "all") {
-        # If the value of the CGI parameter is "all", display all columns,
-        # but remove the redundant "short_desc" column.
-        @displaycolumns = grep($_ ne 'short_desc', keys(%$columns));
-    }
-    else {
-        @displaycolumns = split(/[ ,]+/, $params->param('columnlist'));
-    }
+  if ($params->param('columnlist') eq "all") {
+
+    # If the value of the CGI parameter is "all", display all columns,
+    # but remove the redundant "short_desc" column.
+    @displaycolumns = grep($_ ne 'short_desc', keys(%$columns));
+  }
+  else {
+    @displaycolumns = split(/[ ,]+/, $params->param('columnlist'));
+  }
 }
 elsif (defined $cgi->cookie('COLUMNLIST')) {
-    # 2002-10-31 Rename column names (see bug 176461)
-    my $columnlist = $cgi->cookie('COLUMNLIST');
-    $columnlist =~ s/\bowner\b/assigned_to/;
-    $columnlist =~ s/\bowner_realname\b/assigned_to_realname/;
-    $columnlist =~ s/\bplatform\b/rep_platform/;
-    $columnlist =~ s/\bseverity\b/bug_severity/;
-    $columnlist =~ s/\bstatus\b/bug_status/;
-    $columnlist =~ s/\bsummaryfull\b/short_desc/;
-    $columnlist =~ s/\bsummary\b/short_short_desc/;
-
-    # Use the columns listed in the user's preferences.
-    @displaycolumns = split(/ /, $columnlist);
+
+  # 2002-10-31 Rename column names (see bug 176461)
+  my $columnlist = $cgi->cookie('COLUMNLIST');
+  $columnlist =~ s/\bowner\b/assigned_to/;
+  $columnlist =~ s/\bowner_realname\b/assigned_to_realname/;
+  $columnlist =~ s/\bplatform\b/rep_platform/;
+  $columnlist =~ s/\bseverity\b/bug_severity/;
+  $columnlist =~ s/\bstatus\b/bug_status/;
+  $columnlist =~ s/\bsummaryfull\b/short_desc/;
+  $columnlist =~ s/\bsummary\b/short_short_desc/;
+
+  # Use the columns listed in the user's preferences.
+  @displaycolumns = split(/ /, $columnlist);
 }
 else {
-    # Use the default list of columns.
-    @displaycolumns = DEFAULT_COLUMN_LIST;
+  # Use the default list of columns.
+  @displaycolumns = DEFAULT_COLUMN_LIST;
 }
 
 # Weed out columns that don't actually exist to prevent the user
@@ -530,16 +557,16 @@ else {
 # Remove the timetracking columns if they are not a part of the group
 # (happens if a user had access to time tracking and it was revoked/disabled)
 if (!Bugzilla->user->is_timetracker) {
-   @displaycolumns = grep($_ ne 'estimated_time', @displaycolumns);
-   @displaycolumns = grep($_ ne 'remaining_time', @displaycolumns);
-   @displaycolumns = grep($_ ne 'actual_time', @displaycolumns);
-   @displaycolumns = grep($_ ne 'percentage_complete', @displaycolumns);
-   @displaycolumns = grep($_ ne 'deadline', @displaycolumns);
+  @displaycolumns = grep($_ ne 'estimated_time',      @displaycolumns);
+  @displaycolumns = grep($_ ne 'remaining_time',      @displaycolumns);
+  @displaycolumns = grep($_ ne 'actual_time',         @displaycolumns);
+  @displaycolumns = grep($_ ne 'percentage_complete', @displaycolumns);
+  @displaycolumns = grep($_ ne 'deadline',            @displaycolumns);
 }
 
 # Remove the relevance column if the user is not doing a fulltext search.
 if (grep('relevance', @displaycolumns) && !$fulltext) {
-    @displaycolumns = grep($_ ne 'relevance', @displaycolumns);
+  @displaycolumns = grep($_ ne 'relevance', @displaycolumns);
 }
 
 
@@ -552,13 +579,14 @@ if (grep('relevance', @displaycolumns) && !$fulltext) {
 # The bug ID is always selected because bug IDs are always displayed.
 # Severity, priority, resolution and status are required for buglist
 # CSS classes.
-my @selectcolumns = ("bug_id", "bug_severity", "priority", "bug_status",
-                     "resolution", "product");
+my @selectcolumns
+  = ("bug_id", "bug_severity", "priority", "bug_status", "resolution",
+  "product");
 
 # remaining and actual_time are required for percentage_complete calculation:
 if (grep { $_ eq "percentage_complete" } @displaycolumns) {
-    push (@selectcolumns, "remaining_time");
-    push (@selectcolumns, "actual_time");
+  push(@selectcolumns, "remaining_time");
+  push(@selectcolumns, "actual_time");
 }
 
 # Make sure that the login_name version of a field is always also
@@ -566,51 +594,44 @@ if (grep { $_ eq "percentage_complete" } @displaycolumns) {
 # display the login name when the realname is empty.
 my @realname_fields = grep(/_realname$/, @displaycolumns);
 foreach my $item (@realname_fields) {
-    my $login_field = $item;
-    $login_field =~ s/_realname$//;
-    if (!grep($_ eq $login_field, @selectcolumns)) {
-        push(@selectcolumns, $login_field);
-    }
+  my $login_field = $item;
+  $login_field =~ s/_realname$//;
+  if (!grep($_ eq $login_field, @selectcolumns)) {
+    push(@selectcolumns, $login_field);
+  }
 }
 
 # Display columns are selected because otherwise we could not display them.
 foreach my $col (@displaycolumns) {
-    push (@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns);
+  push(@selectcolumns, $col) if !grep($_ eq $col, @selectcolumns);
 }
 
 # If the user is editing multiple bugs, we also make sure to select the
 # status, because the values of that field determines what options the user
 # has for modifying the bugs.
 if ($dotweak) {
-    push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns);
+  push(@selectcolumns, "bug_status") if !grep($_ eq 'bug_status', @selectcolumns);
 }
 
 if ($format->{'extension'} eq 'ics') {
-    push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns);
+  push(@selectcolumns, "opendate") if !grep($_ eq 'opendate', @selectcolumns);
 }
 
 if ($format->{'extension'} eq 'atom') {
-    # This is the list of fields that are needed by the Atom filter.
-    my @required_atom_columns = (
-      'short_desc',
-      'opendate',
-      'changeddate',
-      'reporter',
-      'reporter_realname',
-      'priority',
-      'bug_severity',
-      'assigned_to',
-      'assigned_to_realname',
-      'bug_status',
-      'product',
-      'component',
-      'resolution'
-    );
-    push(@required_atom_columns, 'target_milestone') if Bugzilla->params->{'usetargetmilestone'};
 
-    foreach my $required (@required_atom_columns) {
-        push(@selectcolumns, $required) if !grep($_ eq $required,@selectcolumns);
-    }
+  # This is the list of fields that are needed by the Atom filter.
+  my @required_atom_columns = (
+    'short_desc',           'opendate',   'changeddate',  'reporter',
+    'reporter_realname',    'priority',   'bug_severity', 'assigned_to',
+    'assigned_to_realname', 'bug_status', 'product',      'component',
+    'resolution'
+  );
+  push(@required_atom_columns, 'target_milestone')
+    if Bugzilla->params->{'usetargetmilestone'};
+
+  foreach my $required (@required_atom_columns) {
+    push(@selectcolumns, $required) if !grep($_ eq $required, @selectcolumns);
+  }
 }
 
 ################################################################################
@@ -622,54 +643,58 @@ if ($format->{'extension'} eq 'atom') {
 # First check if we'll want to reuse the last sorting order; that happens if
 # the order is not defined or its value is "reuse last sort"
 if (!$order || $order =~ /^reuse/i) {
-    if ($cgi->cookie('LASTORDER')) {
-        $order = $cgi->cookie('LASTORDER');
-
-        # Cookies from early versions of Specific Search included this text,
-        # which is now invalid.
-        $order =~ s/ LIMIT 200//;
-    }
-    else {
-        $order = '';  # Remove possible "reuse" identifier as unnecessary
-    }
+  if ($cgi->cookie('LASTORDER')) {
+    $order = $cgi->cookie('LASTORDER');
+
+    # Cookies from early versions of Specific Search included this text,
+    # which is now invalid.
+    $order =~ s/ LIMIT 200//;
+  }
+  else {
+    $order = '';    # Remove possible "reuse" identifier as unnecessary
+  }
 }
 
 my @order_columns;
 if ($order) {
-    # Convert the value of the "order" form field into a list of columns
-    # by which to sort the results.
-    my %order_types = (
-        "Bug Number"   => [ "bug_id" ],
-        "Importance"   => [ "priority", "bug_severity" ],
-        "Assignee"     => [ "assigned_to", "bug_status", "priority", "bug_id" ],
-        "Last Changed" => [ "changeddate", "bug_status", "priority",
-                            "assigned_to", "bug_id" ],
-    );
-    if ($order_types{$order}) {
-        @order_columns = @{ $order_types{$order} };
-    }
-    else {
-        @order_columns = split(/\s*,\s*/, $order);
-    }
+
+  # Convert the value of the "order" form field into a list of columns
+  # by which to sort the results.
+  my %order_types = (
+    "Bug Number" => ["bug_id"],
+    "Importance" => ["priority", "bug_severity"],
+    "Assignee"   => ["assigned_to", "bug_status", "priority", "bug_id"],
+    "Last Changed" =>
+      ["changeddate", "bug_status", "priority", "assigned_to", "bug_id"],
+  );
+  if ($order_types{$order}) {
+    @order_columns = @{$order_types{$order}};
+  }
+  else {
+    @order_columns = split(/\s*,\s*/, $order);
+  }
 }
 
 if (!scalar @order_columns) {
-    # DEFAULT
-    @order_columns = ("bug_status", "priority", "assigned_to", "bug_id");
+
+  # DEFAULT
+  @order_columns = ("bug_status", "priority", "assigned_to", "bug_id");
 }
 
 # In the HTML interface, by default, we limit the returned results,
 # which speeds up quite a few searches where people are really only looking
 # for the top results.
 if ($format->{'extension'} eq 'html' && !defined $params->param('limit')) {
-    $params->param('limit', Bugzilla->params->{'default_search_limit'});
-    $vars->{'default_limited'} = 1;
+  $params->param('limit', Bugzilla->params->{'default_search_limit'});
+  $vars->{'default_limited'} = 1;
 }
 
-my $fallback_search = Bugzilla::Search->new(fields => [@selectcolumns],
-                                            params => scalar $params->Vars,
-                                            order  => [@order_columns],
-                                            sharer => $sharer_id);
+my $fallback_search = Bugzilla::Search->new(
+  fields => [@selectcolumns],
+  params => scalar $params->Vars,
+  order  => [@order_columns],
+  sharer => $sharer_id
+);
 
 # Not-logged-in users get elasticsearch if possible
 my $elastic_default = !$user->id || $user->setting('use_elasticsearch') eq 'on';
@@ -677,37 +702,38 @@ my $elastic_default = !$user->id || $user->setting('use_elasticsearch') eq 'on';
 my $search;
 my $elastic = $cgi->param('elastic') // $elastic_default;
 if (defined $cgi->param('elastic')) {
-    $vars->{was_elastic} = $elastic;
+  $vars->{was_elastic} = $elastic;
 }
 
 # If turned off in the admin section, it is always off.
 $elastic = 0 unless Bugzilla->params->{elasticsearch};
 
 if ($elastic) {
-    local $SIG{__DIE__} = undef;
-    local $SIG{__WARN__} = undef;
-    my $ok = eval {
-        my @args = ( params => scalar $params->Vars );
-        if ($searchstring) {
-            @args = (quicksearch => $searchstring);
-        }
-        if (defined $params->param('limit')) {
-            push @args, limit => scalar $params->param('limit');
-        }
-        $search = Bugzilla::Elastic::Search->new(
-            fields => [@selectcolumns],
-            order  => [@order_columns],
-            @args,
-        );
-        $search->es_query;
-        1;
-    };
-    if (!$ok) {
-        warn "fallback from elasticsearch: $@\n";
-        $search = $fallback_search;
+  local $SIG{__DIE__}  = undef;
+  local $SIG{__WARN__} = undef;
+  my $ok = eval {
+    my @args = (params => scalar $params->Vars);
+    if ($searchstring) {
+      @args = (quicksearch => $searchstring);
+    }
+    if (defined $params->param('limit')) {
+      push @args, limit => scalar $params->param('limit');
     }
-} else {
+    $search = Bugzilla::Elastic::Search->new(
+      fields => [@selectcolumns],
+      order  => [@order_columns],
+      @args,
+    );
+    $search->es_query;
+    1;
+  };
+  if (!$ok) {
+    warn "fallback from elasticsearch: $@\n";
     $search = $fallback_search;
+  }
+}
+else {
+  $search = $fallback_search;
 }
 
 $order = join(',', $search->order);
@@ -736,53 +762,54 @@ $::SIG{PIPE} = 'DEFAULT';
 # Execute the query.
 my ($data, $extra_data);
 do {
-    local $SIG{__DIE__} = undef;
-    local $SIG{__WARN__} = undef;
-    ($data, $extra_data) = eval { $search->data };
+  local $SIG{__DIE__}  = undef;
+  local $SIG{__WARN__} = undef;
+  ($data, $extra_data) = eval { $search->data };
 };
 
 if ($elastic && not defined $data) {
-    warn "fallback from elasticsearch: $@\n";
-    $search = $fallback_search;
-    ($data, $extra_data) = $search->data;
-    $elastic = 0;
+  warn "fallback from elasticsearch: $@\n";
+  $search = $fallback_search;
+  ($data, $extra_data) = $search->data;
+  $elastic = 0;
 }
 
 $fulltext = 1 if $elastic;
 
 $vars->{'search_description'} = $search->search_description;
-if ($cgi->param('debug')
-    && Bugzilla->params->{debug_group}
-    && $user->in_group(Bugzilla->params->{debug_group})
-) {
-    $vars->{'debug'} = 1;
-    if ($search->isa('Bugzilla::Elastic::Search')) {
-        $vars->{query_time} = $search->query_time;
-    }
-    else {
-        $vars->{'queries'} = $extra_data;
-        my $query_time = 0;
-        $query_time += $_->{'time'} foreach @$extra_data;
-        $vars->{'query_time'} = $query_time;
-        # Explains are limited to admins because you could use them to figure
-        # out how many hidden bugs are in a particular product (by doing
-        # searches and looking at the number of rows the explain says it's
-        # examining).
-        if ($user->in_group('admin')) {
-            foreach my $query (@$extra_data) {
-                $query->{explain} = $dbh->bz_explain($query->{sql});
-            }
-        }
+if ( $cgi->param('debug')
+  && Bugzilla->params->{debug_group}
+  && $user->in_group(Bugzilla->params->{debug_group}))
+{
+  $vars->{'debug'} = 1;
+  if ($search->isa('Bugzilla::Elastic::Search')) {
+    $vars->{query_time} = $search->query_time;
+  }
+  else {
+    $vars->{'queries'} = $extra_data;
+    my $query_time = 0;
+    $query_time += $_->{'time'} foreach @$extra_data;
+    $vars->{'query_time'} = $query_time;
+
+    # Explains are limited to admins because you could use them to figure
+    # out how many hidden bugs are in a particular product (by doing
+    # searches and looking at the number of rows the explain says it's
+    # examining).
+    if ($user->in_group('admin')) {
+      foreach my $query (@$extra_data) {
+        $query->{explain} = $dbh->bz_explain($query->{sql});
+      }
     }
+  }
 }
 
 if (scalar @{$search->invalid_order_columns}) {
-    $vars->{'message'} = 'invalid_column_name';
-    $vars->{'invalid_fragments'} = $search->invalid_order_columns;
+  $vars->{'message'}           = 'invalid_column_name';
+  $vars->{'invalid_fragments'} = $search->invalid_order_columns;
 }
 
-if ($fulltext and grep { /^relevance/ } $search->order) {
-    $vars->{'message'} = 'buglist_sorted_by_relevance'
+if ($fulltext and grep {/^relevance/} $search->order) {
+  $vars->{'message'} = 'buglist_sorted_by_relevance';
 }
 
 ################################################################################
@@ -794,68 +821,69 @@ if ($fulltext and grep { /^relevance/ } $search->order) {
 
 # If we're doing time tracking, then keep totals for all bugs.
 my $percentage_complete = grep($_ eq 'percentage_complete', @displaycolumns);
-my $estimated_time      = grep($_ eq 'estimated_time', @displaycolumns);
-my $remaining_time      = grep($_ eq 'remaining_time', @displaycolumns)
-                            || $percentage_complete;
-my $actual_time         = grep($_ eq 'actual_time', @displaycolumns)
-                            || $percentage_complete;
-
-my $time_info = { 'estimated_time' => 0,
-                  'remaining_time' => 0,
-                  'actual_time' => 0,
-                  'percentage_complete' => 0,
-                  'time_present' => ($estimated_time || $remaining_time ||
-                                     $actual_time || $percentage_complete),
-                };
-
-my $bugowners = {};
+my $estimated_time      = grep($_ eq 'estimated_time',      @displaycolumns);
+my $remaining_time
+  = grep($_ eq 'remaining_time', @displaycolumns) || $percentage_complete;
+my $actual_time
+  = grep($_ eq 'actual_time', @displaycolumns) || $percentage_complete;
+
+my $time_info = {
+  'estimated_time'      => 0,
+  'remaining_time'      => 0,
+  'actual_time'         => 0,
+  'percentage_complete' => 0,
+  'time_present' =>
+    ($estimated_time || $remaining_time || $actual_time || $percentage_complete),
+};
+
+my $bugowners   = {};
 my $bugproducts = {};
 my $bugstatuses = {};
 my @bugidlist;
 
-my @bugs; # the list of records
+my @bugs;    # the list of records
 
 foreach my $row (@$data) {
-    my $bug = {}; # a record
-
-    # Slurp the row of data into the record.
-    # The second from last column in the record is the number of groups
-    # to which the bug is restricted.
-    foreach my $column (@selectcolumns) {
-        $bug->{$column} = shift @$row;
-    }
-
-    # Process certain values further (i.e. date format conversion).
-    if ($bug->{'changeddate'}) {
-        $bug->{'changeddate'} =~
-            s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
-
-        $bug->{'changedtime'} = $bug->{'changeddate'}; # for iCalendar and Atom
-        $bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
-    }
-
-    if ($bug->{'opendate'}) {
-        $bug->{'opentime'} = $bug->{'opendate'}; # for iCalendar
-        $bug->{'opendate'} = DiffDate($bug->{'opendate'});
-    }
-
-    # Record the assignee, product, and status in the big hashes of those things.
-    $bugowners->{$bug->{'assigned_to'}} = 1 if $bug->{'assigned_to'};
-    $bugproducts->{$bug->{'product'}} = 1 if $bug->{'product'};
-    $bugstatuses->{$bug->{'bug_status'}} = 1 if $bug->{'bug_status'};
-
-    $bug->{'secure_mode'} = undef;
-
-    # Add the record to the list.
-    push(@bugs, $bug);
-
-    # Add id to list for checking for bug privacy later
-    push(@bugidlist, $bug->{'bug_id'});
-
-    # Compute time tracking info.
-    $time_info->{'estimated_time'} += $bug->{'estimated_time'} if ($estimated_time);
-    $time_info->{'remaining_time'} += $bug->{'remaining_time'} if ($remaining_time);
-    $time_info->{'actual_time'}    += $bug->{'actual_time'}    if ($actual_time);
+  my $bug = {};    # a record
+
+  # Slurp the row of data into the record.
+  # The second from last column in the record is the number of groups
+  # to which the bug is restricted.
+  foreach my $column (@selectcolumns) {
+    $bug->{$column} = shift @$row;
+  }
+
+  # Process certain values further (i.e. date format conversion).
+  if ($bug->{'changeddate'}) {
+    $bug->{'changeddate'}
+      =~ s/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/$1-$2-$3 $4:$5:$6/;
+
+    $bug->{'changedtime'} = $bug->{'changeddate'};          # for iCalendar and Atom
+    $bug->{'changeddate'} = DiffDate($bug->{'changeddate'});
+  }
+
+  if ($bug->{'opendate'}) {
+    $bug->{'opentime'} = $bug->{'opendate'};                # for iCalendar
+    $bug->{'opendate'} = DiffDate($bug->{'opendate'});
+  }
+
+  # Record the assignee, product, and status in the big hashes of those things.
+  $bugowners->{$bug->{'assigned_to'}}  = 1 if $bug->{'assigned_to'};
+  $bugproducts->{$bug->{'product'}}    = 1 if $bug->{'product'};
+  $bugstatuses->{$bug->{'bug_status'}} = 1 if $bug->{'bug_status'};
+
+  $bug->{'secure_mode'} = undef;
+
+  # Add the record to the list.
+  push(@bugs, $bug);
+
+  # Add id to list for checking for bug privacy later
+  push(@bugidlist, $bug->{'bug_id'});
+
+  # Compute time tracking info.
+  $time_info->{'estimated_time'} += $bug->{'estimated_time'} if ($estimated_time);
+  $time_info->{'remaining_time'} += $bug->{'remaining_time'} if ($remaining_time);
+  $time_info->{'actual_time'}    += $bug->{'actual_time'}    if ($actual_time);
 }
 
 # Check for bug privacy and set $bug->{'secure_mode'} to 'implied' or 'manual'
@@ -863,38 +891,40 @@ foreach my $row (@$data) {
 # or because of human choice
 my %min_membercontrol;
 if (@bugidlist) {
-    my $sth = $dbh->prepare(
-        "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) " .
-          "FROM bugs " .
-    "INNER JOIN bug_group_map " .
-            "ON bugs.bug_id = bug_group_map.bug_id " .
-     "LEFT JOIN group_control_map " .
-            "ON group_control_map.product_id = bugs.product_id " .
-           "AND group_control_map.group_id = bug_group_map.group_id " .
-         "WHERE " . $dbh->sql_in('bugs.bug_id', \@bugidlist) .
-            $dbh->sql_group_by('bugs.bug_id'));
-    $sth->execute();
-    while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) {
-        $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA;
+  my $sth
+    = $dbh->prepare(
+        "SELECT DISTINCT bugs.bug_id, MIN(group_control_map.membercontrol) "
+      . "FROM bugs "
+      . "INNER JOIN bug_group_map "
+      . "ON bugs.bug_id = bug_group_map.bug_id "
+      . "LEFT JOIN group_control_map "
+      . "ON group_control_map.product_id = bugs.product_id "
+      . "AND group_control_map.group_id = bug_group_map.group_id "
+      . "WHERE "
+      . $dbh->sql_in('bugs.bug_id', \@bugidlist)
+      . $dbh->sql_group_by('bugs.bug_id'));
+  $sth->execute();
+  while (my ($bug_id, $min_membercontrol) = $sth->fetchrow_array()) {
+    $min_membercontrol{$bug_id} = $min_membercontrol || CONTROLMAPNA;
+  }
+  foreach my $bug (@bugs) {
+    next unless defined($min_membercontrol{$bug->{'bug_id'}});
+    if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) {
+      $bug->{'secure_mode'} = 'implied';
     }
-    foreach my $bug (@bugs) {
-        next unless defined($min_membercontrol{$bug->{'bug_id'}});
-        if ($min_membercontrol{$bug->{'bug_id'}} == CONTROLMAPMANDATORY) {
-            $bug->{'secure_mode'} = 'implied';
-        }
-        else {
-            $bug->{'secure_mode'} = 'manual';
-        }
+    else {
+      $bug->{'secure_mode'} = 'manual';
     }
+  }
 }
 
 # Compute percentage complete without rounding.
-my $sum = $time_info->{'actual_time'}+$time_info->{'remaining_time'};
+my $sum = $time_info->{'actual_time'} + $time_info->{'remaining_time'};
 if ($sum > 0) {
-    $time_info->{'percentage_complete'} = 100*$time_info->{'actual_time'}/$sum;
+  $time_info->{'percentage_complete'} = 100 * $time_info->{'actual_time'} / $sum;
 }
-else { # remaining_time <= 0
-    $time_info->{'percentage_complete'} = 0
+else {    # remaining_time <= 0
+  $time_info->{'percentage_complete'} = 0;
 }
 
 ################################################################################
@@ -904,54 +934,54 @@ else { # remaining_time <= 0
 # Define the variables and functions that will be passed to the UI template.
 
 if ($vars->{elastic} = $search->isa('Bugzilla::Elastic::Search')) {
-    $vars->{elastic_query_time} = $search->query_time;
+  $vars->{elastic_query_time} = $search->query_time;
 }
 else {
-    my $query_time = 0;
-    $query_time += $_->{'time'} foreach @$extra_data;
-    $vars->{'query_time'} = $query_time;
+  my $query_time = 0;
+  $query_time += $_->{'time'} foreach @$extra_data;
+  $vars->{'query_time'} = $query_time;
 }
 
-$vars->{'bugs'} = \@bugs;
-$vars->{'buglist'} = \@bugidlist;
+$vars->{'bugs'}           = \@bugs;
+$vars->{'buglist'}        = \@bugidlist;
 $vars->{'buglist_joined'} = join(',', @bugidlist);
-$vars->{'columns'} = $columns;
+$vars->{'columns'}        = $columns;
 $vars->{'displaycolumns'} = \@displaycolumns;
 
 $vars->{'openstates'} = [BUG_STATE_OPEN];
-$vars->{'closedstates'} = [map {$_->name} closed_bug_statuses()];
+$vars->{'closedstates'} = [map { $_->name } closed_bug_statuses()];
 
 # The iCal file needs priorities ordered from 1 to 9 (highest to lowest)
 # If there are more than 9 values, just make all the lower ones 9
 if ($format->{'extension'} eq 'ics') {
-    my $n = 1;
-    $vars->{'ics_priorities'} = {};
-    my $priorities = get_legal_field_values('priority');
-    foreach my $p (@$priorities) {
-        $vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++;
-    }
+  my $n = 1;
+  $vars->{'ics_priorities'} = {};
+  my $priorities = get_legal_field_values('priority');
+  foreach my $p (@$priorities) {
+    $vars->{'ics_priorities'}->{$p} = ($n > 9) ? 9 : $n++;
+  }
 }
 
-$vars->{'order'} = $order;
+$vars->{'order'}       = $order;
 $vars->{'caneditbugs'} = 1;
-$vars->{'time_info'} = $time_info;
+$vars->{'time_info'}   = $time_info;
 
 if (!$user->in_group('editbugs')) {
-    foreach my $product (keys %$bugproducts) {
-        my $prod = new Bugzilla::Product({name => $product});
-        if (!$user->in_group('editbugs', $prod->id)) {
-            $vars->{'caneditbugs'} = 0;
-            last;
-        }
+  foreach my $product (keys %$bugproducts) {
+    my $prod = new Bugzilla::Product({name => $product});
+    if (!$user->in_group('editbugs', $prod->id)) {
+      $vars->{'caneditbugs'} = 0;
+      last;
     }
+  }
 }
 
 my @bugowners = keys %$bugowners;
 if (scalar(@bugowners) > 1 && $user->in_group('editbugs')) {
-    my $suffix = Bugzilla->params->{'emailsuffix'};
-    map(s/$/$suffix/, @bugowners) if $suffix;
-    my $bugowners = join(",", @bugowners);
-    $vars->{'bugowners'} = $bugowners;
+  my $suffix = Bugzilla->params->{'emailsuffix'};
+  map(s/$/$suffix/, @bugowners) if $suffix;
+  my $bugowners = join(",", @bugowners);
+  $vars->{'bugowners'} = $bugowners;
 }
 
 # Whether or not to split the column titles across two rows to make
@@ -959,7 +989,7 @@ if (scalar(@bugowners) > 1 && $user->in_group('editbugs')) {
 $vars->{'splitheader'} = $cgi->cookie('SPLITHEADER') ? 1 : 0;
 
 if ($user->settings->{'display_quips'}->{'value'} eq 'on') {
-    $vars->{'quip'} = GetQuip();
+  $vars->{'quip'} = GetQuip();
 }
 
 $vars->{'currenttime'} = localtime(time());
@@ -969,52 +999,53 @@ $vars->{'currenttime'} = localtime(time());
 my @products = keys %$bugproducts;
 my $one_product;
 if (scalar(@products) == 1) {
-    $one_product = new Bugzilla::Product({ name => $products[0] });
+  $one_product = new Bugzilla::Product({name => $products[0]});
 }
+
 # This is used in the "Zarroo Boogs" case.
 elsif (my @product_input = $cgi->param('product')) {
-    if (scalar(@product_input) == 1 and $product_input[0] ne '') {
-        $one_product = new Bugzilla::Product({ name => $product_input[0] });
-    }
+  if (scalar(@product_input) == 1 and $product_input[0] ne '') {
+    $one_product = new Bugzilla::Product({name => $product_input[0]});
+  }
 }
+
 # We only want the template to use it if the user can actually
 # enter bugs against it.
 if ($one_product && $user->can_enter_product($one_product)) {
-    $vars->{'one_product'} = $one_product;
+  $vars->{'one_product'} = $one_product;
 }
 
 # The following variables are used when the user is making changes to multiple bugs.
 if ($dotweak && scalar @bugs) {
-    if (!$vars->{'caneditbugs'}) {
-        ThrowUserError('auth_failure', {group  => 'editbugs',
-                                        action => 'modify',
-                                        object => 'multiple_bugs'});
-    }
-    $vars->{'dotweak'} = 1;
-
-    # issue_session_token needs to write to the master DB.
-    Bugzilla->switch_to_main_db();
-    $vars->{'token'} = issue_session_token('buglist_mass_change');
-    Bugzilla->switch_to_shadow_db();
-
-    $vars->{'products'} = $user->get_enterable_products;
-    $vars->{'platforms'} = get_legal_field_values('rep_platform');
-    $vars->{'op_sys'} = get_legal_field_values('op_sys');
-    $vars->{'priorities'} = get_legal_field_values('priority');
-    $vars->{'severities'} = get_legal_field_values('bug_severity');
-    $vars->{'resolutions'} = get_legal_field_values('resolution');
-
-    # Convert bug statuses to their ID.
-    my @bug_statuses = map {$dbh->quote($_)} keys %$bugstatuses;
-    my $bug_status_ids =
-      $dbh->selectcol_arrayref('SELECT id FROM bug_status
-                               WHERE ' . $dbh->sql_in('value', \@bug_statuses));
-
-    # This query collects new statuses which are common to all current bug statuses.
-    # It also accepts transitions where the bug status doesn't change.
-    $bug_status_ids =
-      $dbh->selectcol_arrayref(
-            'SELECT DISTINCT sw1.new_status
+  if (!$vars->{'caneditbugs'}) {
+    ThrowUserError('auth_failure',
+      {group => 'editbugs', action => 'modify', object => 'multiple_bugs'});
+  }
+  $vars->{'dotweak'} = 1;
+
+  # issue_session_token needs to write to the master DB.
+  Bugzilla->switch_to_main_db();
+  $vars->{'token'} = issue_session_token('buglist_mass_change');
+  Bugzilla->switch_to_shadow_db();
+
+  $vars->{'products'}    = $user->get_enterable_products;
+  $vars->{'platforms'}   = get_legal_field_values('rep_platform');
+  $vars->{'op_sys'}      = get_legal_field_values('op_sys');
+  $vars->{'priorities'}  = get_legal_field_values('priority');
+  $vars->{'severities'}  = get_legal_field_values('bug_severity');
+  $vars->{'resolutions'} = get_legal_field_values('resolution');
+
+  # Convert bug statuses to their ID.
+  my @bug_statuses = map { $dbh->quote($_) } keys %$bugstatuses;
+  my $bug_status_ids = $dbh->selectcol_arrayref(
+    'SELECT id FROM bug_status
+                               WHERE ' . $dbh->sql_in('value', \@bug_statuses)
+  );
+
+  # This query collects new statuses which are common to all current bug statuses.
+  # It also accepts transitions where the bug status doesn't change.
+  $bug_status_ids = $dbh->selectcol_arrayref(
+    'SELECT DISTINCT sw1.new_status
                FROM status_workflow sw1
          INNER JOIN bug_status
                  ON bug_status.id = sw1.new_status
@@ -1023,30 +1054,32 @@ if ($dotweak && scalar @bugs) {
                    (SELECT * FROM status_workflow sw2
                      WHERE sw2.old_status != sw1.new_status
                            AND '
-                         . $dbh->sql_in('sw2.old_status', $bug_status_ids)
-                         . ' AND NOT EXISTS
+      . $dbh->sql_in('sw2.old_status', $bug_status_ids) . ' AND NOT EXISTS
                            (SELECT * FROM status_workflow sw3
                              WHERE sw3.new_status = sw1.new_status
-                                   AND sw3.old_status = sw2.old_status))');
-
-    $vars->{'current_bug_statuses'} = [keys %$bugstatuses];
-    $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids);
-
-    # The groups the user belongs to and which are editable for the given buglist.
-    $vars->{'groups'} = GetGroups(\@products);
-
-    # If all bugs being changed are in the same product, the user can change
-    # their version and component, so generate a list of products, a list of
-    # versions for the product (if there is only one product on the list of
-    # products), and a list of components for the product.
-    if ($one_product) {
-        $vars->{'versions'} = [map($_->name, grep($_->is_active, @{ $one_product->versions }))];
-        $vars->{'components'} = [map($_->name, grep($_->is_active, @{ $one_product->components }))];
-        if (Bugzilla->params->{'usetargetmilestone'}) {
-            $vars->{'targetmilestones'} = [map($_->name, grep($_->is_active,
-                                               @{ $one_product->milestones }))];
-        }
+                                   AND sw3.old_status = sw2.old_status))'
+  );
+
+  $vars->{'current_bug_statuses'} = [keys %$bugstatuses];
+  $vars->{'new_bug_statuses'} = Bugzilla::Status->new_from_list($bug_status_ids);
+
+  # The groups the user belongs to and which are editable for the given buglist.
+  $vars->{'groups'} = GetGroups(\@products);
+
+  # If all bugs being changed are in the same product, the user can change
+  # their version and component, so generate a list of products, a list of
+  # versions for the product (if there is only one product on the list of
+  # products), and a list of components for the product.
+  if ($one_product) {
+    $vars->{'versions'}
+      = [map($_->name, grep($_->is_active, @{$one_product->versions}))];
+    $vars->{'components'}
+      = [map($_->name, grep($_->is_active, @{$one_product->components}))];
+    if (Bugzilla->params->{'usetargetmilestone'}) {
+      $vars->{'targetmilestones'}
+        = [map($_->name, grep($_->is_active, @{$one_product->milestones}))];
     }
+  }
 }
 
 # If we're editing a stored query, use the existing query name as default for
@@ -1071,32 +1104,33 @@ my $contenttype;
 my $disposition = "inline";
 
 if ($format->{'extension'} eq "html" && !$agent) {
-    my $list_id = $cgi->param('list_id') || $cgi->param('regetlastlist');
-    my $search = $user->save_last_search(
-        { bugs => \@bugidlist, order => $order, vars => $vars, list_id => $list_id });
-    $cgi->param('list_id', $search->id) if $search;
-    $contenttype = "text/html";
+  my $list_id = $cgi->param('list_id') || $cgi->param('regetlastlist');
+  my $search = $user->save_last_search(
+    {bugs => \@bugidlist, order => $order, vars => $vars, list_id => $list_id});
+  $cgi->param('list_id', $search->id) if $search;
+  $contenttype = "text/html";
 }
 else {
-    $contenttype = $format->{'ctype'};
+  $contenttype = $format->{'ctype'};
 }
 
 # Set 'urlquerypart' once the buglist ID is known.
-$vars->{'urlquerypart'} = $params->canonicalise_query('order', 'cmdtype',
-                                                      'query_based_on',
-                                                      'token');
+$vars->{'urlquerypart'}
+  = $params->canonicalise_query('order', 'cmdtype', 'query_based_on', 'token');
 
 if ($format->{'extension'} eq "csv") {
-    # We set CSV files to be downloaded, as they are designed for importing
-    # into other programs.
-    $disposition = "attachment";
 
-    # If the user clicked the CSV link in the search results,
-    # They should get the Field Description, not the column name in the db
-    $vars->{'human'} = $cgi->param('human');
+  # We set CSV files to be downloaded, as they are designed for importing
+  # into other programs.
+  $disposition = "attachment";
+
+  # If the user clicked the CSV link in the search results,
+  # They should get the Field Description, not the column name in the db
+  $vars->{'human'} = $cgi->param('human');
 }
 
-$cgi->close_standby_message($contenttype, $disposition, $disp_prefix, $format->{'extension'});
+$cgi->close_standby_message($contenttype, $disposition, $disp_prefix,
+  $format->{'extension'});
 
 ################################################################################
 # Content Generation
diff --git a/bugzilla.pl b/bugzilla.pl
index efcea293b..0f385d42f 100755
--- a/bugzilla.pl
+++ b/bugzilla.pl
@@ -8,9 +8,9 @@ use File::Spec::Functions qw(catdir);
 use Cwd qw(realpath);
 
 BEGIN {
-    require lib;
-    my $dir = realpath( dirname(__FILE__) );
-    lib->import( $dir, catdir( $dir, 'lib' ), catdir( $dir, qw(local lib perl5) ) );
+  require lib;
+  my $dir = realpath(dirname(__FILE__));
+  lib->import($dir, catdir($dir, 'lib'), catdir($dir, qw(local lib perl5)));
 }
 use Mojolicious::Commands;
 
diff --git a/chart.cgi b/chart.cgi
index 36357cb3c..9ea747fce 100755
--- a/chart.cgi
+++ b/chart.cgi
@@ -47,27 +47,28 @@ use Bugzilla::Token;
 # when preparing Bugzilla for mod_perl, this script used these
 # variables in so many subroutines that it was easier to just
 # make them globals.
-local our $cgi = Bugzilla->cgi;
+local our $cgi      = Bugzilla->cgi;
 local our $template = Bugzilla->template;
-local our $vars = {};
+local our $vars     = {};
 my $dbh = Bugzilla->dbh;
 $cgi->content_security_policy(report_only => 0);
 
 my $user = Bugzilla->login(LOGIN_REQUIRED);
 
 if (!Bugzilla->feature('new_charts')) {
-    ThrowCodeError('feature_disabled', { feature => 'new_charts' });
+  ThrowCodeError('feature_disabled', {feature => 'new_charts'});
 }
 
 # Go back to query.cgi if we are adding a boolean chart parameter.
 if (grep(/^cmd-/, $cgi->param())) {
-    my $params = $cgi->canonicalise_query("format", "ctype", "action");
-    print $cgi->redirect("query.cgi?format=" . $cgi->param('query_format') .
-                                               ($params ? "&$params" : ""));
-    exit;
+  my $params = $cgi->canonicalise_query("format", "ctype", "action");
+  print $cgi->redirect("query.cgi?format="
+      . $cgi->param('query_format')
+      . ($params ? "&$params" : ""));
+  exit;
 }
 
-my $action = $cgi->param('action');
+my $action    = $cgi->param('action');
 my $series_id = $cgi->param('series_id');
 $vars->{'doc_section'} = 'reporting.html#charts';
 
@@ -77,284 +78,299 @@ $vars->{'doc_section'} = 'reporting.html#charts';
 # series_id they apply to (e.g. subscribe, unsubscribe).
 my @actions = grep(/^action-/, $cgi->param());
 if ($actions[0] && $actions[0] =~ /^action-([^\d]+)(\d*)$/) {
-    $action = $1;
-    $series_id = $2 if $2;
+  $action = $1;
+  $series_id = $2 if $2;
 }
 
 $action ||= "assemble";
 
 # Go to buglist.cgi if we are doing a search.
 if ($action eq "search") {
-    my $params = $cgi->canonicalise_query("format", "ctype", "action");
-    print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : ""));
-    exit;
+  my $params = $cgi->canonicalise_query("format", "ctype", "action");
+  print $cgi->redirect("buglist.cgi" . ($params ? "?$params" : ""));
+  exit;
 }
 
-$user->in_group(Bugzilla->params->{"chartgroup"})
-  || ThrowUserError("auth_failure", {group  => Bugzilla->params->{"chartgroup"},
-                                     action => "use",
-                                     object => "charts"});
+$user->in_group(Bugzilla->params->{"chartgroup"}) || ThrowUserError(
+  "auth_failure",
+  {
+    group  => Bugzilla->params->{"chartgroup"},
+    action => "use",
+    object => "charts"
+  }
+);
 
 # Only admins may create public queries
 $user->in_group('admin') || $cgi->delete('public');
 
 # All these actions relate to chart construction.
 if ($action =~ /^(assemble|add|remove|sum|subscribe|unsubscribe)$/) {
-    # These two need to be done before the creation of the Chart object, so
-    # that the changes they make will be reflected in it.
-    if ($action =~ /^subscribe|unsubscribe$/) {
-        detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
-        my $series = new Bugzilla::Series($series_id);
-        $series->$action($user->id);
-    }
-
-    my $chart = new Bugzilla::Chart($cgi);
-
-    if ($action =~ /^remove|sum$/) {
-        $chart->$action(getSelectedLines());
-    }
-    elsif ($action eq "add") {
-        my @series_ids = getAndValidateSeriesIDs();
-        $chart->add(@series_ids);
-    }
 
-    view($chart);
+  # These two need to be done before the creation of the Chart object, so
+  # that the changes they make will be reflected in it.
+  if ($action =~ /^subscribe|unsubscribe$/) {
+    detaint_natural($series_id) || ThrowCodeError("invalid_series_id");
+    my $series = new Bugzilla::Series($series_id);
+    $series->$action($user->id);
+  }
+
+  my $chart = new Bugzilla::Chart($cgi);
+
+  if ($action =~ /^remove|sum$/) {
+    $chart->$action(getSelectedLines());
+  }
+  elsif ($action eq "add") {
+    my @series_ids = getAndValidateSeriesIDs();
+    $chart->add(@series_ids);
+  }
+
+  view($chart);
 }
 elsif ($action eq "plot") {
-    plot();
+  plot();
 }
 elsif ($action eq "wrap") {
-    # For CSV "wrap", we go straight to "plot".
-    if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
-        plot();
-    }
-    else {
-        wrap();
-    }
+
+  # For CSV "wrap", we go straight to "plot".
+  if ($cgi->param('ctype') && $cgi->param('ctype') eq "csv") {
+    plot();
+  }
+  else {
+    wrap();
+  }
 }
 elsif ($action eq "create") {
-    assertCanCreate($cgi);
-    my $token = $cgi->param('token');
-    check_hash_token($token, ['create-series']);
+  assertCanCreate($cgi);
+  my $token = $cgi->param('token');
+  check_hash_token($token, ['create-series']);
 
-    my $series = new Bugzilla::Series($cgi);
+  my $series = new Bugzilla::Series($cgi);
 
-    ThrowUserError("series_already_exists", {'series' => $series})
-      if $series->existsInDatabase;
+  ThrowUserError("series_already_exists", {'series' => $series})
+    if $series->existsInDatabase;
 
-    $series->writeToDatabase();
-    $vars->{'message'} = "series_created";
-    $vars->{'series'} = $series;
+  $series->writeToDatabase();
+  $vars->{'message'} = "series_created";
+  $vars->{'series'}  = $series;
 
-    my $chart = new Bugzilla::Chart($cgi);
-    view($chart);
+  my $chart = new Bugzilla::Chart($cgi);
+  view($chart);
 }
 elsif ($action eq "edit") {
-    my $series = assertCanEdit($series_id);
-    edit($series);
+  my $series = assertCanEdit($series_id);
+  edit($series);
 }
 elsif ($action eq "alter") {
-    my $series = assertCanEdit($series_id);
-    my $token = $cgi->param('token');
-    check_hash_token($token, [$series->id, $series->name]);
-    # XXX - This should be replaced by $series->set_foo() methods.
-    $series = new Bugzilla::Series($cgi);
-
-    # We need to check if there is _another_ series in the database with
-    # our (potentially new) name. So we call existsInDatabase() to see if
-    # the return value is us or some other series we need to avoid stomping
-    # on.
-    my $id_of_series_in_db = $series->existsInDatabase();
-    if (defined($id_of_series_in_db) &&
-        $id_of_series_in_db != $series->{'series_id'})
-    {
-        ThrowUserError("series_already_exists", {'series' => $series});
-    }
-
-    $series->writeToDatabase();
-    $vars->{'changes_saved'} = 1;
-
-    edit($series);
+  my $series = assertCanEdit($series_id);
+  my $token  = $cgi->param('token');
+  check_hash_token($token, [$series->id, $series->name]);
+
+  # XXX - This should be replaced by $series->set_foo() methods.
+  $series = new Bugzilla::Series($cgi);
+
+  # We need to check if there is _another_ series in the database with
+  # our (potentially new) name. So we call existsInDatabase() to see if
+  # the return value is us or some other series we need to avoid stomping
+  # on.
+  my $id_of_series_in_db = $series->existsInDatabase();
+  if (defined($id_of_series_in_db)
+    && $id_of_series_in_db != $series->{'series_id'})
+  {
+    ThrowUserError("series_already_exists", {'series' => $series});
+  }
+
+  $series->writeToDatabase();
+  $vars->{'changes_saved'} = 1;
+
+  edit($series);
 }
 elsif ($action eq "confirm-delete") {
-    $vars->{'series'} = assertCanEdit($series_id);
+  $vars->{'series'} = assertCanEdit($series_id);
 
-    print $cgi->header();
-    $template->process("reports/delete-series.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("reports/delete-series.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 elsif ($action eq "delete") {
-    my $series = assertCanEdit($series_id);
-    my $token = $cgi->param('token');
-    check_hash_token($token, [$series->id, $series->name]);
-
-    $dbh->bz_start_transaction();
-
-    $series->remove_from_db();
-    # Remove (sub)categories which no longer have any series.
-    foreach my $cat (qw(category subcategory)) {
-        my $is_used = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?",
-                                             undef, $series->{"${cat}_id"});
-        if (!$is_used) {
-            $dbh->do('DELETE FROM series_categories WHERE id = ?',
-                      undef, $series->{"${cat}_id"});
-        }
+  my $series = assertCanEdit($series_id);
+  my $token  = $cgi->param('token');
+  check_hash_token($token, [$series->id, $series->name]);
+
+  $dbh->bz_start_transaction();
+
+  $series->remove_from_db();
+
+  # Remove (sub)categories which no longer have any series.
+  foreach my $cat (qw(category subcategory)) {
+    my $is_used
+      = $dbh->selectrow_array("SELECT COUNT(*) FROM series WHERE $cat = ?",
+      undef, $series->{"${cat}_id"});
+    if (!$is_used) {
+      $dbh->do('DELETE FROM series_categories WHERE id = ?',
+        undef, $series->{"${cat}_id"});
     }
-    $dbh->bz_commit_transaction();
+  }
+  $dbh->bz_commit_transaction();
 
-    $vars->{'message'} = "series_deleted";
-    $vars->{'series'} = $series;
-    view();
+  $vars->{'message'} = "series_deleted";
+  $vars->{'series'}  = $series;
+  view();
 }
 elsif ($action eq "convert_search") {
-    my $saved_search = $cgi->param('series_from_search') || '';
-    my ($query) = grep { $_->name eq $saved_search } @{ $user->queries };
-    my $url = '';
-    if ($query) {
-        my $params = new Bugzilla::CGI($query->edit_link);
-        # These two parameters conflict with the one below.
-        $url = $params->canonicalise_query('format', 'query_format');
-        $url = '&' . html_quote($url);
-    }
-    print $cgi->redirect(-location => Bugzilla->localconfig->{urlbase} . "query.cgi?format=create-series$url");
+  my $saved_search = $cgi->param('series_from_search') || '';
+  my ($query) = grep { $_->name eq $saved_search } @{$user->queries};
+  my $url = '';
+  if ($query) {
+    my $params = new Bugzilla::CGI($query->edit_link);
+
+    # These two parameters conflict with the one below.
+    $url = $params->canonicalise_query('format', 'query_format');
+    $url = '&' . html_quote($url);
+  }
+  print $cgi->redirect(-location => Bugzilla->localconfig->{urlbase}
+      . "query.cgi?format=create-series$url");
 }
 else {
-    ThrowUserError('unknown_action', {action => $action});
+  ThrowUserError('unknown_action', {action => $action});
 }
 
 exit;
 
 # Find any selected series and return either the first or all of them.
 sub getAndValidateSeriesIDs {
-    my @series_ids = grep(/^\d+$/, $cgi->param("name"));
+  my @series_ids = grep(/^\d+$/, $cgi->param("name"));
 
-    return wantarray ? @series_ids : $series_ids[0];
+  return wantarray ? @series_ids : $series_ids[0];
 }
 
 # Return a list of IDs of all the lines selected in the UI.
 sub getSelectedLines {
-    my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();
+  my @ids = map { /^select(\d+)$/ ? $1 : () } $cgi->param();
 
-    return @ids;
+  return @ids;
 }
 
 # Check if the user is the owner of series_id or is an admin.
 sub assertCanEdit {
-    my $series_id = shift;
-    my $user = Bugzilla->user;
+  my $series_id = shift;
+  my $user      = Bugzilla->user;
 
-    my $series = new Bugzilla::Series($series_id)
-      || ThrowCodeError('invalid_series_id');
+  my $series
+    = new Bugzilla::Series($series_id) || ThrowCodeError('invalid_series_id');
 
-    if (!$user->in_group('admin') && $series->{creator_id} != $user->id) {
-        ThrowUserError('illegal_series_edit');
-    }
+  if (!$user->in_group('admin') && $series->{creator_id} != $user->id) {
+    ThrowUserError('illegal_series_edit');
+  }
 
-    return $series;
+  return $series;
 }
 
 # Check if the user is permitted to create this series with these parameters.
 sub assertCanCreate {
-    my ($cgi) = shift;
-    my $user = Bugzilla->user;
+  my ($cgi) = shift;
+  my $user = Bugzilla->user;
 
-    $user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
+  $user->in_group("editbugs") || ThrowUserError("illegal_series_creation");
 
-    # Check permission for frequency
-    my $min_freq = 7;
-    # Upstreaming: denied, as this min_freq feature is going away.
-    if ($cgi->param('frequency') < $min_freq && !$user->in_group("bz_canusewhines")) {
-        ThrowUserError("illegal_frequency", { 'minimum' => $min_freq });
-    }
+  # Check permission for frequency
+  my $min_freq = 7;
+
+  # Upstreaming: denied, as this min_freq feature is going away.
+  if ($cgi->param('frequency') < $min_freq && !$user->in_group("bz_canusewhines"))
+  {
+    ThrowUserError("illegal_frequency", {'minimum' => $min_freq});
+  }
 }
 
 sub validateWidthAndHeight {
-    $vars->{'width'} = $cgi->param('width');
-    $vars->{'height'} = $cgi->param('height');
-
-    if (defined($vars->{'width'})) {
-       (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
-         || ThrowCodeError("invalid_dimensions");
-    }
-
-    if (defined($vars->{'height'})) {
-       (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
-         || ThrowCodeError("invalid_dimensions");
-    }
-
-    # The equivalent of 2000 square seems like a very reasonable maximum size.
-    # This is merely meant to prevent accidental or deliberate DOS, and should
-    # have no effect in practice.
-    if ($vars->{'width'} && $vars->{'height'}) {
-       (($vars->{'width'} * $vars->{'height'}) <= 4000000)
-         || ThrowUserError("chart_too_large");
-    }
+  $vars->{'width'}  = $cgi->param('width');
+  $vars->{'height'} = $cgi->param('height');
+
+  if (defined($vars->{'width'})) {
+    (detaint_natural($vars->{'width'}) && $vars->{'width'} > 0)
+      || ThrowCodeError("invalid_dimensions");
+  }
+
+  if (defined($vars->{'height'})) {
+    (detaint_natural($vars->{'height'}) && $vars->{'height'} > 0)
+      || ThrowCodeError("invalid_dimensions");
+  }
+
+  # The equivalent of 2000 square seems like a very reasonable maximum size.
+  # This is merely meant to prevent accidental or deliberate DOS, and should
+  # have no effect in practice.
+  if ($vars->{'width'} && $vars->{'height'}) {
+    (($vars->{'width'} * $vars->{'height'}) <= 4000000)
+      || ThrowUserError("chart_too_large");
+  }
 }
 
 sub edit {
-    my $series = shift;
+  my $series = shift;
 
-    $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
-    $vars->{'default'} = $series;
+  $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+  $vars->{'default'}  = $series;
 
-    print $cgi->header();
-    $template->process("reports/edit-series.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("reports/edit-series.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub plot {
-    validateWidthAndHeight();
-    $vars->{'chart'} = new Bugzilla::Chart($cgi);
+  validateWidthAndHeight();
+  $vars->{'chart'} = new Bugzilla::Chart($cgi);
 
-    my $format = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
+  my $format
+    = $template->get_format("reports/chart", "", scalar($cgi->param('ctype')));
 
-    # Debugging PNGs is a pain; we need to be able to see the error messages
-    if ($cgi->param('debug')) {
-        print $cgi->header();
-        $vars->{'chart'}->dump();
-    }
+  # Debugging PNGs is a pain; we need to be able to see the error messages
+  if ($cgi->param('debug')) {
+    print $cgi->header();
+    $vars->{'chart'}->dump();
+  }
 
-    print $cgi->header($format->{'ctype'});
-    disable_utf8() if ($format->{'ctype'} =~ /^image\//);
+  print $cgi->header($format->{'ctype'});
+  disable_utf8() if ($format->{'ctype'} =~ /^image\//);
 
-    $template->process($format->{'template'}, $vars)
-      || ThrowTemplateError($template->error());
+  $template->process($format->{'template'}, $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub wrap {
-    validateWidthAndHeight();
+  validateWidthAndHeight();
 
-    # We create a Chart object so we can validate the parameters
-    my $chart = new Bugzilla::Chart($cgi);
+  # We create a Chart object so we can validate the parameters
+  my $chart = new Bugzilla::Chart($cgi);
 
-    $vars->{'time'} = localtime(time());
+  $vars->{'time'} = localtime(time());
 
-    $vars->{'imagebase'} = $cgi->canonicalise_query(
-                "action", "action-wrap", "ctype", "format", "width", "height");
+  $vars->{'imagebase'}
+    = $cgi->canonicalise_query("action", "action-wrap", "ctype", "format",
+    "width", "height");
 
-    print $cgi->header();
-    $template->process("reports/chart.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  print $cgi->header();
+  $template->process("reports/chart.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
 
 sub view {
-    my $chart = shift;
+  my $chart = shift;
 
-    # Set defaults
-    foreach my $field ('category', 'subcategory', 'name', 'ctype') {
-        $vars->{'default'}{$field} = $cgi->param($field) || 0;
-    }
+  # Set defaults
+  foreach my $field ('category', 'subcategory', 'name', 'ctype') {
+    $vars->{'default'}{$field} = $cgi->param($field) || 0;
+  }
 
-    # Pass the state object to the display UI.
-    $vars->{'chart'} = $chart;
-    $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
+  # Pass the state object to the display UI.
+  $vars->{'chart'}    = $chart;
+  $vars->{'category'} = Bugzilla::Chart::getVisibleSeries();
 
-    print $cgi->header();
+  print $cgi->header();
 
-    # If we have having problems with bad data, we can set debug=1 to dump
-    # the data structure.
-    $chart->dump() if $cgi->param('debug');
+  # If we have having problems with bad data, we can set debug=1 to dump
+  # the data structure.
+  $chart->dump() if $cgi->param('debug');
 
-    $template->process("reports/create-chart.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
+  $template->process("reports/create-chart.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
 }
diff --git a/checksetup.pl b/checksetup.pl
index 7c9826ee3..6ede39efb 100755
--- a/checksetup.pl
+++ b/checksetup.pl
@@ -18,15 +18,21 @@ use warnings;
 
 use File::Basename;
 use File::Spec;
+
 BEGIN {
-    require lib;
-    my $dir = File::Spec->rel2abs(dirname(__FILE__));
-    lib->import($dir, File::Spec->catdir($dir, "lib"), File::Spec->catdir($dir, qw(local lib perl5)));
-    chdir($dir);
+  require lib;
+  my $dir = File::Spec->rel2abs(dirname(__FILE__));
+  lib->import(
+    $dir,
+    File::Spec->catdir($dir, "lib"),
+    File::Spec->catdir($dir, qw(local lib perl5))
+  );
+  chdir($dir);
 }
 
 use Getopt::Long qw(:config bundling);
 use Pod::Usage;
+
 # Bug 1270550 - Tie::Hash::NamedCapture must be loaded before Safe.
 use Tie::Hash::NamedCapture;
 use Safe;
@@ -35,7 +41,7 @@ use English qw(-no_match_vars $EUID $EGID);
 use Bugzilla::Constants;
 use Bugzilla::Install::Requirements;
 use Bugzilla::Install::Util qw(install_string get_version_and_os
-                               init_console success);
+  init_console success);
 
 ######################################################################
 # Live Code
@@ -49,12 +55,14 @@ Bugzilla::Install::Util::no_checksetup_from_cgi() if $ENV{'SERVER_SOFTWARE'};
 init_console();
 
 my %switch;
-GetOptions(\%switch, 'help|h|?',
-                     'no-templates|t', 'verbose|v|no-silent',
-                     'cpanm:s', 'check-modules',
-                     'make-admin=s', 'reset-password=s', 'version|V',
-                     'default-localconfig',
-                     'no-database', 'no-permissions|p');
+GetOptions(
+  \%switch,         'help|h|?',
+  'no-templates|t', 'verbose|v|no-silent',
+  'cpanm:s',        'check-modules',
+  'make-admin=s',   'reset-password=s',
+  'version|V',      'default-localconfig',
+  'no-database',    'no-permissions|p'
+);
 
 # Print the help message if that switch was selected.
 pod2usage({-verbose => 1, -exitval => 1}) if $switch{'help'};
@@ -67,44 +75,48 @@ print(install_string('header', get_version_and_os()) . "\n") unless $silent;
 exit 0 if $switch{'version'};
 
 if (defined $switch{cpanm}) {
-    my $default = 'all notest -oracle -mysql -pg -mod_perl -old_charts -new_charts -graphical_reports -detect_charset';
-    my @features = split(/\s+/, $switch{cpanm} || $default);
-    my @cpanm_args = ('-l', 'local', '--installdeps');
-    while (my $feature = shift @features) {
-        if ($feature eq 'all') {
-            push @cpanm_args, '--with-all-features';
-        }
-        elsif ($feature eq 'default') {
-            unshift @features, split(/\s+/, $default);
-        }
-        elsif ($feature eq 'notest' || $feature eq 'skip-satisfied' || $feature eq 'quiet') {
-            push @cpanm_args, "--$feature";
-        }
-        elsif ($feature =~ /^-(.+)$/) {
-            push @cpanm_args, "--without-feature=$1";
-        }
-        else {
-            push @cpanm_args, "--with-feature=$feature";
-        }
+  my $default
+    = 'all notest -oracle -mysql -pg -mod_perl -old_charts -new_charts -graphical_reports -detect_charset';
+  my @features = split(/\s+/, $switch{cpanm} || $default);
+  my @cpanm_args = ('-l', 'local', '--installdeps');
+  while (my $feature = shift @features) {
+    if ($feature eq 'all') {
+      push @cpanm_args, '--with-all-features';
+    }
+    elsif ($feature eq 'default') {
+      unshift @features, split(/\s+/, $default);
+    }
+    elsif ($feature eq 'notest'
+      || $feature eq 'skip-satisfied'
+      || $feature eq 'quiet')
+    {
+      push @cpanm_args, "--$feature";
     }
-    print "cpanm @cpanm_args \".\"\n" if !$silent;
-    my $rv = system('cpanm', @cpanm_args, '.');
-    exit 1 if $rv != 0;
+    elsif ($feature =~ /^-(.+)$/) {
+      push @cpanm_args, "--without-feature=$1";
+    }
+    else {
+      push @cpanm_args, "--with-feature=$feature";
+    }
+  }
+  print "cpanm @cpanm_args \".\"\n" if !$silent;
+  my $rv = system('cpanm', @cpanm_args, '.');
+  exit 1 if $rv != 0;
 }
 
 $ENV{PERL_MM_USE_DEFAULT} = 1;
 $ENV{BZ_SILENT_MAKEFILE}  = 1;
 system($^X, "Makefile.PL");
 
-if (! -f "MYMETA.json") {
-    die "Makefile.PL failed to generate a MYMETA.json file.",
-        "Try upgrading ExtUtils::MakeMaker";
+if (!-f "MYMETA.json") {
+  die "Makefile.PL failed to generate a MYMETA.json file.",
+    "Try upgrading ExtUtils::MakeMaker";
 }
 require Bugzilla::CPAN;
 
 my $meta = Bugzilla::CPAN->cpan_meta;
 if (keys %{$meta->{optional_features}} < 1) {
-    die "Your version of ExtUtils::MakeMaker is too old or broken\n";
+  die "Your version of ExtUtils::MakeMaker is too old or broken\n";
 }
 my $requirements = check_cpan_requirements($meta, [@INC], !$silent);
 
@@ -136,7 +148,7 @@ import Bugzilla::Install::Localconfig qw(update_localconfig);
 
 require Bugzilla::Install::Filesystem;
 import Bugzilla::Install::Filesystem qw(update_filesystem
-                                        fix_all_file_permissions);
+  fix_all_file_permissions);
 require Bugzilla::Install::DB;
 require Bugzilla::DB;
 require Bugzilla::Template;
@@ -151,27 +163,29 @@ Bugzilla->installation_answers($answers_file);
 ###########################################################################
 
 unless ($ENV{LOCALCONFIG_ENV}) {
-    print "Reading " .  bz_locations()->{'localconfig'} . "...\n" unless $silent;
-    update_localconfig({ output => !$silent, use_defaults => $switch{'default-localconfig'} });
+  print "Reading " . bz_locations()->{'localconfig'} . "...\n" unless $silent;
+  update_localconfig(
+    {output => !$silent, use_defaults => $switch{'default-localconfig'}});
 }
 my $lc_hash = Bugzilla->localconfig;
 
-if ( $EUID == 0 && $lc_hash->{webservergroup} && !ON_WINDOWS ) {
-    # So checksetup was run as root, and we have a webserver group set.
-    # Let's assume the user wants us to make files that are writable
-    # by the webserver group.
+if ($EUID == 0 && $lc_hash->{webservergroup} && !ON_WINDOWS) {
+
+  # So checksetup was run as root, and we have a webserver group set.
+  # Let's assume the user wants us to make files that are writable
+  # by the webserver group.
 
-    $EGID = getgrnam $lc_hash->{webservergroup}; ## no critic (Variables::RequireLocalizedPunctuationVars)
-    umask 002
-        or die "failed to set umask 002: $!";
+  $EGID = getgrnam $lc_hash->{webservergroup}; ## no critic (Variables::RequireLocalizedPunctuationVars)
+  umask 002 or die "failed to set umask 002: $!";
 }
 
 unless ($switch{'no-database'}) {
-    die "urlbase is not set\n" unless $lc_hash->{urlbase};
-    die "urlbase must end with slash\n" unless $lc_hash->{urlbase} =~ m{/$}ms;
-    if ($lc_hash->{attachment_base}) {
-        die "attachment_base must end with slash\n" unless $lc_hash->{attachment_base} =~ m{/$}ms;
-    }
+  die "urlbase is not set\n" unless $lc_hash->{urlbase};
+  die "urlbase must end with slash\n" unless $lc_hash->{urlbase} =~ m{/$}ms;
+  if ($lc_hash->{attachment_base}) {
+    die "attachment_base must end with slash\n"
+      unless $lc_hash->{attachment_base} =~ m{/$}ms;
+  }
 }
 
 ###########################################################################
@@ -183,24 +197,27 @@ unless ($switch{'no-database'}) {
 # because some data required to populate data/params.json is stored in the DB.
 
 unless ($switch{'no-database'}) {
-    Bugzilla::DB::bz_check_requirements(!$silent);
-    Bugzilla::DB::bz_create_database() if $lc_hash->{'db_check'};
-
-    # now get a handle to the database:
-    my $dbh = Bugzilla->dbh;
-    # Clear all keys from Memcached to ensure we see the correct schema.
-    Bugzilla->memcached->clear_all();
-    # Create the tables, and do any database-specific schema changes.
-    $dbh->bz_setup_database();
-    # Populate the tables that hold the values for the  fields.
+  $dbh->bz_populate_enum_tables();
 }
 
 ###########################################################################
 # Check --DATA-- directory
 ###########################################################################
 
-update_filesystem({ index_html => $lc_hash->{'index_html'} });
+update_filesystem({index_html => $lc_hash->{'index_html'}});
 
 # Remove parameters from the params file that no longer exist in Bugzilla,
 # and set the defaults for new ones
@@ -211,7 +228,7 @@ my %old_params = $switch{'no-database'} ? () : update_params();
 ###########################################################################
 
 Bugzilla::Template::precompile_templates(!$silent)
-    unless $switch{'no-templates'};
+  unless $switch{'no-templates'};
 
 ###########################################################################
 # Set proper rights (--CHMOD--)
@@ -240,66 +257,67 @@ check_font_file(!$silent) if $lc_hash->{'font_file'};
 ###########################################################################
 
 unless ($switch{'no-database'}) {
-    # Using Bugzilla::Field's create() or update() depends on the
-    # fielddefs table having a modern definition. So, we have to make
-    # these particular schema changes before we make any other schema changes.
-    Bugzilla::Install::DB::update_fielddefs_definition();
 
-    Bugzilla::Field::populate_field_definitions();
+  # Using Bugzilla::Field's create() or update() depends on the
+  # fielddefs table having a modern definition. So, we have to make
+  # these particular schema changes before we make any other schema changes.
+  Bugzilla::Install::DB::update_fielddefs_definition();
 
-    ###########################################################################
-    # Update the tables to the current definition --TABLE--
-    ###########################################################################
+  Bugzilla::Field::populate_field_definitions();
 
-    Bugzilla::Install::DB::update_table_definitions(\%old_params);
-    Bugzilla::Install::init_workflow();
+  ###########################################################################
+  # Update the tables to the current definition --TABLE--
+  ###########################################################################
 
-    ###########################################################################
-    # Bugzilla uses --GROUPS-- to assign various rights to its users.
-    ###########################################################################
+  Bugzilla::Install::DB::update_table_definitions(\%old_params);
+  Bugzilla::Install::init_workflow();
 
-    Bugzilla::Install::update_system_groups();
+  ###########################################################################
+  # Bugzilla uses --GROUPS-- to assign various rights to its users.
+  ###########################################################################
 
-    # "Log In" as the fake superuser who can do everything.
-    Bugzilla->set_user(Bugzilla::User->super_user);
+  Bugzilla::Install::update_system_groups();
 
-    ###########################################################################
-    # Create --SETTINGS-- users can adjust
-    ###########################################################################
+  # "Log In" as the fake superuser who can do everything.
+  Bugzilla->set_user(Bugzilla::User->super_user);
 
-    Bugzilla::Install::update_settings();
+  ###########################################################################
+  # Create --SETTINGS-- users can adjust
+  ###########################################################################
 
-    ###########################################################################
-    # Create Administrator  --ADMIN--
-    ###########################################################################
+  Bugzilla::Install::update_settings();
 
-    Bugzilla::Install::make_admin($switch{'make-admin'}) if $switch{'make-admin'};
-    Bugzilla::Install::create_admin();
+  ###########################################################################
+  # Create Administrator  --ADMIN--
+  ###########################################################################
 
-    Bugzilla::Install::reset_password($switch{'reset-password'})
-        if $switch{'reset-password'};
+  Bugzilla::Install::make_admin($switch{'make-admin'}) if $switch{'make-admin'};
+  Bugzilla::Install::create_admin();
 
-    ###########################################################################
-    # Create default Product
-    ###########################################################################
+  Bugzilla::Install::reset_password($switch{'reset-password'})
+    if $switch{'reset-password'};
 
-    Bugzilla::Install::create_default_product();
+  ###########################################################################
+  # Create default Product
+  ###########################################################################
 
-    Bugzilla::Hook::process('install_before_final_checks', { silent => $silent });
+  Bugzilla::Install::create_default_product();
 
-    ###########################################################################
-    # Final checks
-    ###########################################################################
+  Bugzilla::Hook::process('install_before_final_checks', {silent => $silent});
 
-    # Clear all keys from Memcached
-    Bugzilla->memcached->clear_all();
+  ###########################################################################
+  # Final checks
+  ###########################################################################
 
-    # Reset the mod_perl pre-load list
-    unlink(Bugzilla::Constants::bz_locations()->{datadir} . '/mod_perl_preload');
+  # Clear all keys from Memcached
+  Bugzilla->memcached->clear_all();
 
-    if (!$silent) {
-        success(get_text('install_success'));
-    }
+  # Reset the mod_perl pre-load list
+  unlink(Bugzilla::Constants::bz_locations()->{datadir} . '/mod_perl_preload');
+
+  if (!$silent) {
+    success(get_text('install_success'));
+  }
 }
 
 __END__
diff --git a/clean-bug-user-last-visit.pl b/clean-bug-user-last-visit.pl
index 810fe598c..e9b3badda 100755
--- a/clean-bug-user-last-visit.pl
+++ b/clean-bug-user-last-visit.pl
@@ -31,8 +31,6 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
 
 my $dbh = Bugzilla->dbh;
 my $sql = 'DELETE FROM bug_user_last_visit WHERE last_visit_ts < '
-  . $dbh->sql_date_math('NOW()',
-                        '-',
-                        Bugzilla->params->{last_visit_keep_days},
-                        'DAY');
+  . $dbh->sql_date_math('NOW()', '-', Bugzilla->params->{last_visit_keep_days},
+  'DAY');
 $dbh->do($sql);
diff --git a/colchange.cgi b/colchange.cgi
index bc18c3851..2e31207a5 100755
--- a/colchange.cgi
+++ b/colchange.cgi
@@ -26,132 +26,138 @@ use Storable qw(dclone);
 
 # Maps parameters that control columns to the names of columns.
 use constant COLUMN_PARAMS => {
-    'useclassification'   => ['classification'],
-    'usebugaliases'       => ['alias'],
-    'usetargetmilestone'  => ['target_milestone'],
-    'useqacontact'        => ['qa_contact', 'qa_contact_realname'],
-    'usestatuswhiteboard' => ['status_whiteboard'],
+  'useclassification'   => ['classification'],
+  'usebugaliases'       => ['alias'],
+  'usetargetmilestone'  => ['target_milestone'],
+  'useqacontact'        => ['qa_contact', 'qa_contact_realname'],
+  'usestatuswhiteboard' => ['status_whiteboard'],
 };
 
 # We only show these columns if an object of this type exists in the
 # database.
-use constant COLUMN_CLASSES => {
-    'Bugzilla::Flag'    => 'flagtypes.name',
-    'Bugzilla::Keyword' => 'keywords',
-};
+use constant COLUMN_CLASSES =>
+  {'Bugzilla::Flag' => 'flagtypes.name', 'Bugzilla::Keyword' => 'keywords',};
 
 Bugzilla->login();
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 my $columns = dclone(Bugzilla::Search::COLUMNS);
 
 # You can't manually select "relevance" as a column you want to see.
 delete $columns->{'relevance'};
 
-foreach my $param (keys %{ COLUMN_PARAMS() }) {
-    next if Bugzilla->params->{$param};
-    foreach my $column (@{ COLUMN_PARAMS->{$param} }) {
-        delete $columns->{$column};
-    }
+foreach my $param (keys %{COLUMN_PARAMS()}) {
+  next if Bugzilla->params->{$param};
+  foreach my $column (@{COLUMN_PARAMS->{$param}}) {
+    delete $columns->{$column};
+  }
 }
 
-foreach my $class (keys %{ COLUMN_CLASSES() }) {
-    require_module($class);
-    my $column = COLUMN_CLASSES->{$class};
-    delete $columns->{$column} if !$class->any_exist;
+foreach my $class (keys %{COLUMN_CLASSES()}) {
+  require_module($class);
+  my $column = COLUMN_CLASSES->{$class};
+  delete $columns->{$column} if !$class->any_exist;
 }
 
 if (!Bugzilla->user->is_timetracker) {
-    foreach my $column (TIMETRACKING_FIELDS) {
-        delete $columns->{$column};
-    }
+  foreach my $column (TIMETRACKING_FIELDS) {
+    delete $columns->{$column};
+  }
 }
 
 $vars->{'columns'} = $columns;
 
 my @collist;
 if (defined $cgi->param('rememberedquery')) {
-    my $search;
-    if (defined $cgi->param('saved_search')) {
-        $search = new Bugzilla::Search::Saved($cgi->param('saved_search'));
-    }
-
-    my $token = $cgi->param('token');
-    if ($search) {
-        check_hash_token($token, [$search->id, $search->name]);
-    }
-    else {
-        check_hash_token($token, ['default-list']);
-    }
-
-    my $splitheader = 0;
-    if (defined $cgi->param('resetit')) {
-        @collist = DEFAULT_COLUMN_LIST;
-    } else {
-        if (defined $cgi->param("selected_columns")) {
-            @collist = grep { exists $columns->{$_} }
-                            $cgi->param("selected_columns");
-        }
-        if (defined $cgi->param('splitheader')) {
-            $splitheader = $cgi->param('splitheader')? 1: 0;
-        }
-    }
-    my $list = join(" ", @collist);
-
-    if ($list) {
-        # Only set the cookie if this is not a saved search.
-        # Saved searches have their own column list
-        if (!$cgi->param('save_columns_for_search')) {
-            $cgi->send_cookie(-name => 'COLUMNLIST',
-                              -value => $list,
-                              -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
-        }
-    }
-    else {
-        $cgi->remove_cookie('COLUMNLIST');
+  my $search;
+  if (defined $cgi->param('saved_search')) {
+    $search = new Bugzilla::Search::Saved($cgi->param('saved_search'));
+  }
+
+  my $token = $cgi->param('token');
+  if ($search) {
+    check_hash_token($token, [$search->id, $search->name]);
+  }
+  else {
+    check_hash_token($token, ['default-list']);
+  }
+
+  my $splitheader = 0;
+  if (defined $cgi->param('resetit')) {
+    @collist = DEFAULT_COLUMN_LIST;
+  }
+  else {
+    if (defined $cgi->param("selected_columns")) {
+      @collist = grep { exists $columns->{$_} } $cgi->param("selected_columns");
     }
-    if ($splitheader) {
-        $cgi->send_cookie(-name => 'SPLITHEADER',
-                          -value => $splitheader,
-                          -expires => 'Fri, 01-Jan-2038 00:00:00 GMT');
+    if (defined $cgi->param('splitheader')) {
+      $splitheader = $cgi->param('splitheader') ? 1 : 0;
     }
-    else {
-        $cgi->remove_cookie('SPLITHEADER');
+  }
+  my $list = join(" ", @collist);
+
+  if ($list) {
+
+    # Only set the cookie if this is not a saved search.
+    # Saved searches have their own column list
+    if (!$cgi->param('save_columns_for_search')) {
+      $cgi->send_cookie(
+        -name    => 'COLUMNLIST',
+        -value   => $list,
+        -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+      );
     }
-
-    $vars->{'message'} = "change_columns";
-
-    if ($cgi->param('save_columns_for_search')
-        && defined $search && $search->user->id == Bugzilla->user->id)
-    {
-        my $params = new Bugzilla::CGI($search->url);
-        $params->param('columnlist', join(",", @collist));
-        $search->set_url($params->query_string());
-        $search->update();
-    }
-
-    my $params = new Bugzilla::CGI($cgi->param('rememberedquery'));
+  }
+  else {
+    $cgi->remove_cookie('COLUMNLIST');
+  }
+  if ($splitheader) {
+    $cgi->send_cookie(
+      -name    => 'SPLITHEADER',
+      -value   => $splitheader,
+      -expires => 'Fri, 01-Jan-2038 00:00:00 GMT'
+    );
+  }
+  else {
+    $cgi->remove_cookie('SPLITHEADER');
+  }
+
+  $vars->{'message'} = "change_columns";
+
+  if ( $cgi->param('save_columns_for_search')
+    && defined $search
+    && $search->user->id == Bugzilla->user->id)
+  {
+    my $params = new Bugzilla::CGI($search->url);
     $params->param('columnlist', join(",", @collist));
-    $vars->{'redirect_url'} = "buglist.cgi?".$params->query_string();
-
-    # If we're running on Microsoft IIS, $cgi->redirect discards
-    # the Set-Cookie lines. In mod_perl, $cgi->redirect with cookies
-    # causes the page to be rendered as text/plain.
-    # Workaround is to use the old-fashioned  redirection mechanism.
-    # See bug 214466 and bug 376044 for details.
-    print $cgi->redirect($vars->{'redirect_url'});
-    exit;
+    $search->set_url($params->query_string());
+    $search->update();
+  }
+
+  my $params = new Bugzilla::CGI($cgi->param('rememberedquery'));
+  $params->param('columnlist', join(",", @collist));
+  $vars->{'redirect_url'} = "buglist.cgi?" . $params->query_string();
+
+  # If we're running on Microsoft IIS, $cgi->redirect discards
+  # the Set-Cookie lines. In mod_perl, $cgi->redirect with cookies
+  # causes the page to be rendered as text/plain.
+  # Workaround is to use the old-fashioned  redirection mechanism.
+  # See bug 214466 and bug 376044 for details.
+  print $cgi->redirect($vars->{'redirect_url'});
+  exit;
 }
 
 if (defined $cgi->param('columnlist')) {
-    @collist = split(/[ ,]+/, $cgi->param('columnlist'));
-} elsif (defined $cgi->cookie('COLUMNLIST')) {
-    @collist = split(/ /, $cgi->cookie('COLUMNLIST'));
-} else {
-    @collist = DEFAULT_COLUMN_LIST;
+  @collist = split(/[ ,]+/, $cgi->param('columnlist'));
+}
+elsif (defined $cgi->cookie('COLUMNLIST')) {
+  @collist = split(/ /, $cgi->cookie('COLUMNLIST'));
+}
+else {
+  @collist = DEFAULT_COLUMN_LIST;
 }
 
 $vars->{'collist'} = \@collist;
@@ -161,12 +167,12 @@ $vars->{'buffer'} = $cgi->query_string();
 
 my $search;
 if (defined $cgi->param('query_based_on')) {
-    my $searches = Bugzilla->user->queries;
-    my ($search) = grep($_->name eq $cgi->param('query_based_on'), @$searches);
+  my $searches = Bugzilla->user->queries;
+  my ($search) = grep($_->name eq $cgi->param('query_based_on'), @$searches);
 
-    if ($search) {
-        $vars->{'saved_search'} = $search;
-    }
+  if ($search) {
+    $vars->{'saved_search'} = $search;
+  }
 }
 
 # Generate and return the UI (HTML page) from the appropriate template.
diff --git a/collectstats.pl b/collectstats.pl
index 81a59272d..485e497cd 100755
--- a/collectstats.pl
+++ b/collectstats.pl
@@ -37,16 +37,17 @@ pod2usage({-verbose => 1, -exitval => 1}) if $switch{'help'};
 # in the regenerate mode).
 $| = 1;
 
-my $datadir = bz_locations()->{'datadir'};
+my $datadir   = bz_locations()->{'datadir'};
 my $graphsdir = bz_locations()->{'graphsdir'};
 
 # Tidy up after graphing module
 my $cwd = Cwd::getcwd();
 if (chdir($graphsdir)) {
-    unlink <./*.gif>;
-    unlink <./*.png>;
-    # chdir("..") doesn't work if graphs is a symlink, see bug 429378
-    chdir($cwd);
+  unlink <./*.gif>;
+  unlink <./*.png>;
+
+  # chdir("..") doesn't work if graphs is a symlink, see bug 429378
+  chdir($cwd);
 }
 
 my $dbh = Bugzilla->switch_to_shadow_db();
@@ -56,9 +57,9 @@ my $dbh = Bugzilla->switch_to_shadow_db();
 # may have existed in the past, or have been renamed. We want them all.
 my $fields = {};
 foreach my $field ('bug_status', 'resolution') {
-    my $values = get_legal_field_values($field);
-    my $old_values = $dbh->selectcol_arrayref(
-                             "SELECT bugs_activity.added
+  my $values     = get_legal_field_values($field);
+  my $old_values = $dbh->selectcol_arrayref(
+    "SELECT bugs_activity.added
                                 FROM bugs_activity
                           INNER JOIN fielddefs
                                   ON fielddefs.id = bugs_activity.fieldid
@@ -76,15 +77,16 @@ foreach my $field ('bug_status', 'resolution') {
                            LEFT JOIN $field
                                   ON $field.value = bugs_activity.removed
                                WHERE fielddefs.name = ?
-                                 AND $field.id IS NULL",
-                               undef, ($field, $field));
+                                 AND $field.id IS NULL", undef, ($field, $field)
+  );
 
-    push(@$values, @$old_values);
-    $fields->{$field} = $values;
+  push(@$values, @$old_values);
+  $fields->{$field} = $values;
 }
 
-my @statuses = @{$fields->{'bug_status'}};
+my @statuses    = @{$fields->{'bug_status'}};
 my @resolutions = @{$fields->{'resolution'}};
+
 # Exclude "" from the resolution list.
 @resolutions = grep {$_} @resolutions;
 
@@ -93,30 +95,34 @@ my @resolutions = @{$fields->{'resolution'}};
 # at once and stuff it into some data structures.
 my (%bug_status, %bug_resolution, %removed);
 if ($switch{'regenerate'}) {
-    %bug_resolution = @{ $dbh->selectcol_arrayref(
-        'SELECT bug_id, resolution FROM bugs', {Columns=>[1,2]}) };
-    %bug_status = @{ $dbh->selectcol_arrayref(
-        'SELECT bug_id, bug_status FROM bugs', {Columns=>[1,2]}) };
-
-    my $removed_sth = $dbh->prepare(
+  %bug_resolution = @{
+    $dbh->selectcol_arrayref('SELECT bug_id, resolution FROM bugs',
+      {Columns => [1, 2]})
+  };
+  %bug_status = @{
+    $dbh->selectcol_arrayref('SELECT bug_id, bug_status FROM bugs',
+      {Columns => [1, 2]})
+  };
+
+  my $removed_sth = $dbh->prepare(
         q{SELECT bugs_activity.bug_id, bugs_activity.removed,}
-        . $dbh->sql_to_days('bugs_activity.bug_when')
-       . q{ FROM bugs_activity
+      . $dbh->sql_to_days('bugs_activity.bug_when')
+      . q{ FROM bugs_activity
            WHERE bugs_activity.fieldid = ?
-        ORDER BY bugs_activity.bug_when});
-
-    %removed = (bug_status => {}, resolution => {});
-    foreach my $field (qw(bug_status resolution)) {
-        my $field_id = Bugzilla::Field->check($field)->id;
-        my $rows = $dbh->selectall_arrayref($removed_sth, undef, $field_id);
-        my $hash = $removed{$field};
-        foreach my $row (@$rows) {
-            my ($bug_id, $removed, $when) = @$row;
-            $hash->{$bug_id} ||= [];
-            push(@{ $hash->{$bug_id} }, { when    => int($when),
-                                          removed => $removed });
-        }
+        ORDER BY bugs_activity.bug_when}
+  );
+
+  %removed = (bug_status => {}, resolution => {});
+  foreach my $field (qw(bug_status resolution)) {
+    my $field_id = Bugzilla::Field->check($field)->id;
+    my $rows     = $dbh->selectall_arrayref($removed_sth, undef, $field_id);
+    my $hash     = $removed{$field};
+    foreach my $row (@$rows) {
+      my ($bug_id, $removed, $when) = @$row;
+      $hash->{$bug_id} ||= [];
+      push(@{$hash->{$bug_id}}, {when => int($when), removed => $removed});
     }
+  }
 }
 
 my $tstart = time;
@@ -126,93 +132,96 @@ unshift(@myproducts, "-All-");
 
 my $dir = "$datadir/mining";
 if (!-d $dir) {
-    mkdir $dir or die "mkdir $dir failed: $!";
-    fix_dir_permissions($dir);
+  mkdir $dir or die "mkdir $dir failed: $!";
+  fix_dir_permissions($dir);
 }
 
 foreach (@myproducts) {
-    if ($switch{'regenerate'}) {
-        regenerate_stats($dir, $_, \%bug_resolution, \%bug_status, \%removed);
-    } else {
-        &collect_stats($dir, $_);
-    }
+  if ($switch{'regenerate'}) {
+    regenerate_stats($dir, $_, \%bug_resolution, \%bug_status, \%removed);
+  }
+  else {
+    &collect_stats($dir, $_);
+  }
 }
+
 # Fix permissions for all files in mining/.
 fix_dir_permissions($dir);
 
 my $tend = time;
+
 # Uncomment the following line for performance testing.
 #print "Total time taken " . delta_time($tstart, $tend) . "\n";
 
 CollectSeriesData();
 
 sub collect_stats {
-    my $dir = shift;
-    my $product = shift;
-    my $when = localtime (time);
-    my $dbh = Bugzilla->dbh;
-    my $product_id;
-
-    if (ref $product) {
-        $product_id = $product->id;
-        $product = $product->name;
-    }
-
-    # NB: Need to mangle the product for the filename, but use the real
-    # product name in the query
-    my $file_product = $product;
-    $file_product =~ s/\//-/gs;
-    my $file = join '/', $dir, $file_product;
-    my $exists = -f $file;
-
-    # if the file exists, get the old status and resolution list for that product.
-    my @data;
-    @data = get_old_data($file) if $exists;
-
-    # If @data is not empty, then we have to recreate the data file.
-    if (scalar(@data)) {
-        open(DATA, '>', $file)
-          || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
-    }
-    else {
-        open(DATA, '>>', $file)
-          || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
-    }
-
-    if (Bugzilla->params->{'utf8'}) {
-        binmode DATA, ':utf8';
-    }
-
-    # Now collect current data.
-    my @row = (today());
-    my $status_sql = q{SELECT COUNT(*) FROM bugs WHERE bug_status = ?};
-    my $reso_sql   = q{SELECT COUNT(*) FROM bugs WHERE resolution = ?};
-
-    if ($product ne '-All-') {
-        $status_sql .= q{ AND product_id = ?};
-        $reso_sql   .= q{ AND product_id = ?};
-    }
-
-    my $sth_status = $dbh->prepare($status_sql);
-    my $sth_reso   = $dbh->prepare($reso_sql);
-
-    my @values ;
-    foreach my $status (@statuses) {
-        @values = ($status);
-        push (@values, $product_id) if ($product ne '-All-');
-        my $count = $dbh->selectrow_array($sth_status, undef, @values);
-        push(@row, $count);
-    }
-    foreach my $resolution (@resolutions) {
-        @values = ($resolution);
-        push (@values, $product_id) if ($product ne '-All-');
-        my $count = $dbh->selectrow_array($sth_reso, undef, @values);
-        push(@row, $count);
-    }
-
-    if (!$exists || scalar(@data)) {
-        my $fields = join('|', ('DATE', @statuses, @resolutions));
-        print DATA <dbh;
+  my $product_id;
+
+  if (ref $product) {
+    $product_id = $product->id;
+    $product    = $product->name;
+  }
+
+  # NB: Need to mangle the product for the filename, but use the real
+  # product name in the query
+  my $file_product = $product;
+  $file_product =~ s/\//-/gs;
+  my $file = join '/', $dir, $file_product;
+  my $exists = -f $file;
+
+  # if the file exists, get the old status and resolution list for that product.
+  my @data;
+  @data = get_old_data($file) if $exists;
+
+  # If @data is not empty, then we have to recreate the data file.
+  if (scalar(@data)) {
+    open(DATA, '>', $file)
+      || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
+  }
+  else {
+    open(DATA, '>>', $file)
+      || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
+  }
+
+  if (Bugzilla->params->{'utf8'}) {
+    binmode DATA, ':utf8';
+  }
+
+  # Now collect current data.
+  my @row        = (today());
+  my $status_sql = q{SELECT COUNT(*) FROM bugs WHERE bug_status = ?};
+  my $reso_sql   = q{SELECT COUNT(*) FROM bugs WHERE resolution = ?};
+
+  if ($product ne '-All-') {
+    $status_sql .= q{ AND product_id = ?};
+    $reso_sql   .= q{ AND product_id = ?};
+  }
+
+  my $sth_status = $dbh->prepare($status_sql);
+  my $sth_reso   = $dbh->prepare($reso_sql);
+
+  my @values;
+  foreach my $status (@statuses) {
+    @values = ($status);
+    push(@values, $product_id) if ($product ne '-All-');
+    my $count = $dbh->selectrow_array($sth_status, undef, @values);
+    push(@row, $count);
+  }
+  foreach my $resolution (@resolutions) {
+    @values = ($resolution);
+    push(@values, $product_id) if ($product ne '-All-');
+    my $count = $dbh->selectrow_array($sth_reso, undef, @values);
+    push(@row, $count);
+  }
+
+  if (!$exists || scalar(@data)) {
+    my $fields = join('|', ('DATE', @statuses, @resolutions));
+    print DATA <{$_} ? $data->{$_} : ''}
-                                 ('DATE', @statuses, @resolutions)) . "\n";
-    }
-    print DATA (join '|', @row) . "\n";
-    close DATA;
+  }
+
+  # Add existing data, if needed. Note that no count is not treated
+  # the same way as a count with 0 bug.
+  foreach my $data (@data) {
+    print DATA join('|',
+      map { defined $data->{$_} ? $data->{$_} : '' }
+        ('DATE', @statuses, @resolutions))
+      . "\n";
+  }
+  print DATA (join '|', @row) . "\n";
+  close DATA;
 }
 
 sub get_old_data {
-    my $file = shift;
-
-    open(DATA, '<', $file)
-      || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
-
-    if (Bugzilla->params->{'utf8'}) {
-        binmode DATA, ':utf8';
-    }
-
-    my @data;
-    my @columns;
-    my $recreate = 0;
-    while () {
-        chomp;
-        next unless $_;
-        if (/^# fields?:\s*(.+)\s*$/) {
-            @columns = split(/\|/, $1);
-            # Compare this list with @statuses and @resolutions.
-            # If they are identical, then we can safely append new data
-            # to the end of the file; else we have to recreate it.
-            $recreate = 1;
-            my @new_cols = ($columns[0], @statuses, @resolutions);
-            if (scalar(@columns) == scalar(@new_cols)) {
-                my $identical = 1;
-                for (0 .. $#columns) {
-                    $identical = 0 if ($columns[$_] ne $new_cols[$_]);
-                }
-                last if $identical;
-            }
-        }
-        next unless $recreate;
-        next if (/^#/); # Ignore comments.
-        # If we have to recreate the file, we have to load all existing
-        # data first.
-        my @line = split /\|/;
-        my %data;
-        foreach my $column (@columns) {
-            $data{$column} = shift @line;
+  my $file = shift;
+
+  open(DATA, '<', $file)
+    || ThrowCodeError('chart_file_open_fail', {'filename' => $file});
+
+  if (Bugzilla->params->{'utf8'}) {
+    binmode DATA, ':utf8';
+  }
+
+  my @data;
+  my @columns;
+  my $recreate = 0;
+  while () {
+    chomp;
+    next unless $_;
+    if (/^# fields?:\s*(.+)\s*$/) {
+      @columns = split(/\|/, $1);
+
+      # Compare this list with @statuses and @resolutions.
+      # If they are identical, then we can safely append new data
+      # to the end of the file; else we have to recreate it.
+      $recreate = 1;
+      my @new_cols = ($columns[0], @statuses, @resolutions);
+      if (scalar(@columns) == scalar(@new_cols)) {
+        my $identical = 1;
+        for (0 .. $#columns) {
+          $identical = 0 if ($columns[$_] ne $new_cols[$_]);
         }
-        push(@data, \%data);
+        last if $identical;
+      }
     }
-    close(DATA);
-    return @data;
+    next unless $recreate;
+    next if (/^#/);  # Ignore comments.
+                     # If we have to recreate the file, we have to load all existing
+                     # data first.
+    my @line = split /\|/;
+    my %data;
+    foreach my $column (@columns) {
+      $data{$column} = shift @line;
+    }
+    push(@data, \%data);
+  }
+  close(DATA);
+  return @data;
 }
 
 # This regenerates all statistics from the database.
 sub regenerate_stats {
-    my ($dir, $product, $bug_resolution, $bug_status, $removed) = @_;
-
-    my $dbh = Bugzilla->dbh;
-    my $when = localtime(time());
-    my $tstart = time();
-
-    # NB: Need to mangle the product for the filename, but use the real
-    # product name in the query
-    if (ref $product) {
-        $product = $product->name;
-    }
-    my $file_product = $product;
-    $file_product =~ s/\//-/gs;
-    my $file = join '/', $dir, $file_product;
-
-    my $and_product = "";
-    my $from_product = "";
-
-    my @values = ();
-    if ($product ne '-All-') {
-        $and_product = q{ AND products.name = ?};
-        $from_product = q{ INNER JOIN products
+  my ($dir, $product, $bug_resolution, $bug_status, $removed) = @_;
+
+  my $dbh    = Bugzilla->dbh;
+  my $when   = localtime(time());
+  my $tstart = time();
+
+  # NB: Need to mangle the product for the filename, but use the real
+  # product name in the query
+  if (ref $product) {
+    $product = $product->name;
+  }
+  my $file_product = $product;
+  $file_product =~ s/\//-/gs;
+  my $file = join '/', $dir, $file_product;
+
+  my $and_product  = "";
+  my $from_product = "";
+
+  my @values = ();
+  if ($product ne '-All-') {
+    $and_product  = q{ AND products.name = ?};
+    $from_product = q{ INNER JOIN products
                           ON bugs.product_id = products.id};
-        push (@values, $product);
-    }
-
-    # Determine the start date from the date the first bug in the
-    # database was created, and the end date from the current day.
-    # If there were no bugs in the search, return early.
-    my $query = q{SELECT } .
-                $dbh->sql_to_days('creation_ts') . q{ AS start_day, } .
-                $dbh->sql_to_days('current_date') . q{ AS end_day, } .
-                $dbh->sql_to_days("'1970-01-01'") .
-                 qq{ FROM bugs $from_product
-                   WHERE } . $dbh->sql_to_days('creation_ts') .
-                         qq{ IS NOT NULL $and_product
+    push(@values, $product);
+  }
+
+  # Determine the start date from the date the first bug in the
+  # database was created, and the end date from the current day.
+  # If there were no bugs in the search, return early.
+  my $query
+    = q{SELECT }
+    . $dbh->sql_to_days('creation_ts')
+    . q{ AS start_day, }
+    . $dbh->sql_to_days('current_date')
+    . q{ AS end_day, }
+    . $dbh->sql_to_days("'1970-01-01'")
+    . qq{ FROM bugs $from_product
+                   WHERE }
+    . $dbh->sql_to_days('creation_ts') . qq{ IS NOT NULL $and_product
                 ORDER BY start_day } . $dbh->sql_limit(1);
-    my ($start, $end, $base) = $dbh->selectrow_array($query, undef, @values);
+  my ($start, $end, $base) = $dbh->selectrow_array($query, undef, @values);
 
-    if (!defined $start) {
-        return;
-    }
+  if (!defined $start) {
+    return;
+  }
 
-    if (open DATA, ">", $file) {
-        my $fields = join('|', ('DATE', @statuses, @resolutions));
-        print DATA <", $file) {
+    my $fields = join('|', ('DATE', @statuses, @resolutions));
+    print DATA <sql_from_days($day - 1) .
-                         q{ AND bugs.creation_ts >= } .
-                         $dbh->sql_from_days($day - 2) .
-                        $and_product . q{ ORDER BY bug_id};
-
-            my $bug_ids = $dbh->selectcol_arrayref($query, undef, @values);
-            push(@bugs, @$bug_ids);
-
-            my %bugcount;
-            foreach (@statuses) { $bugcount{$_} = 0; }
-            foreach (@resolutions) { $bugcount{$_} = 0; }
-            # Get information on bug states and resolutions.
-            for my $bug (@bugs) {
-                my $status = _get_value(
-                    $removed->{'bug_status'}->{$bug},
-                    $bug_status,  $day, $bug);
-
-                if (defined $bugcount{$status}) {
-                    $bugcount{$status}++;
-                }
-
-                my $resolution = _get_value(
-                    $removed->{'resolution'}->{$bug},
-                    $bug_resolution, $day, $bug);
-
-                if (defined $bugcount{$resolution}) {
-                    $bugcount{$resolution}++;
-                }
-            }
-
-            # Generate a line of output containing the date and counts
-            # of bugs in each state.
-            my $date = sqlday($day, $base);
-            print DATA "$date";
-            foreach (@statuses) { print DATA "|$bugcount{$_}"; }
-            foreach (@resolutions) { print DATA "|$bugcount{$_}"; }
-            print DATA "\n";
+                         WHERE bugs.creation_ts < }
+        . $dbh->sql_from_days($day - 1)
+        . q{ AND bugs.creation_ts >= }
+        . $dbh->sql_from_days($day - 2)
+        . $and_product
+        . q{ ORDER BY bug_id};
+
+      my $bug_ids = $dbh->selectcol_arrayref($query, undef, @values);
+      push(@bugs, @$bug_ids);
+
+      my %bugcount;
+      foreach (@statuses)    { $bugcount{$_} = 0; }
+      foreach (@resolutions) { $bugcount{$_} = 0; }
+
+      # Get information on bug states and resolutions.
+      for my $bug (@bugs) {
+        my $status
+          = _get_value($removed->{'bug_status'}->{$bug}, $bug_status, $day, $bug);
+
+        if (defined $bugcount{$status}) {
+          $bugcount{$status}++;
         }
 
-        # Finish up output feedback for this product.
-        my $tend = time;
-        print "\rRegenerating $product \[100.0\%] - " .
-            delta_time($tstart, $tend) . "\n";
+        my $resolution
+          = _get_value($removed->{'resolution'}->{$bug}, $bug_resolution, $day, $bug);
 
-        close DATA;
+        if (defined $bugcount{$resolution}) {
+          $bugcount{$resolution}++;
+        }
+      }
+
+      # Generate a line of output containing the date and counts
+      # of bugs in each state.
+      my $date = sqlday($day, $base);
+      print DATA "$date";
+      foreach (@statuses)    { print DATA "|$bugcount{$_}"; }
+      foreach (@resolutions) { print DATA "|$bugcount{$_}"; }
+      print DATA "\n";
     }
+
+    # Finish up output feedback for this product.
+    my $tend = time;
+    print "\rRegenerating $product \[100.0\%] - "
+      . delta_time($tstart, $tend) . "\n";
+
+    close DATA;
+  }
 }
 
 # A helper for --regenerate.
@@ -404,102 +421,114 @@ FIN
 # first "previous value" entry in the bugs_activity table for that
 # bug made on or after that day.
 sub _get_value {
-    my ($removed, $current, $day, $bug) = @_;
+  my ($removed, $current, $day, $bug) = @_;
 
-    # Get the first change that's on or after this day.
-    my $item = first { $_->{when} >= $day } @{ $removed || [] };
+  # Get the first change that's on or after this day.
+  my $item = first { $_->{when} >= $day } @{$removed || []};
 
-    # If there's no change on or after this day, then we just return the
-    # current value.
-    return $item ? $item->{removed} : $current->{$bug};
+  # If there's no change on or after this day, then we just return the
+  # current value.
+  return $item ? $item->{removed} : $current->{$bug};
 }
 
 sub today {
-    my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
-    return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
+  my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
+  return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
 }
 
 sub today_dash {
-    my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
-    return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
+  my ($dom, $mon, $year) = (localtime(time))[3, 4, 5];
+  return sprintf "%04d-%02d-%02d", 1900 + $year, ++$mon, $dom;
 }
 
 sub sqlday {
-    my ($day, $base) = @_;
-    $day = ($day - $base) * 86400;
-    my ($dom, $mon, $year) = (gmtime($day))[3, 4, 5];
-    return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
+  my ($day, $base) = @_;
+  $day = ($day - $base) * 86400;
+  my ($dom, $mon, $year) = (gmtime($day))[3, 4, 5];
+  return sprintf "%04d%02d%02d", 1900 + $year, ++$mon, $dom;
 }
 
 sub delta_time {
-    my $tstart = shift;
-    my $tend = shift;
-    my $delta = $tend - $tstart;
-    my $hours = int($delta/3600);
-    my $minutes = int($delta/60) - ($hours * 60);
-    my $seconds = $delta - ($minutes * 60) - ($hours * 3600);
-    return sprintf("%02d:%02d:%02d" , $hours, $minutes, $seconds);
+  my $tstart  = shift;
+  my $tend    = shift;
+  my $delta   = $tend - $tstart;
+  my $hours   = int($delta / 3600);
+  my $minutes = int($delta / 60) - ($hours * 60);
+  my $seconds = $delta - ($minutes * 60) - ($hours * 3600);
+  return sprintf("%02d:%02d:%02d", $hours, $minutes, $seconds);
 }
 
 sub CollectSeriesData {
-    # We need some way of randomising the distribution of series, such that
-    # all of the series which are to be run every 7 days don't run on the same
-    # day. This is because this might put the server under severe load if a
-    # particular frequency, such as once a week, is very common. We achieve
-    # this by only running queries when:
-    # (days_since_epoch + series_id) % frequency = 0. So they'll run every
-    #  days, but the start date depends on the series_id.
-    my $days_since_epoch = int(time() / (60 * 60 * 24));
-    my $today = today_dash();
-
-    # We save a copy of the main $dbh and then switch to the shadow and get
-    # that one too. Remember, these may be the same.
-    my $dbh = Bugzilla->switch_to_main_db();
-    my $shadow_dbh = Bugzilla->switch_to_shadow_db();
-
-    my $serieses = $dbh->selectall_hashref("SELECT series_id, query, creator " .
-                      "FROM series " .
-                      "WHERE frequency != 0 AND " .
-                      "MOD(($days_since_epoch + series_id), frequency) = 0",
-                      "series_id");
-
-    # We prepare the insertion into the data table, for efficiency.
-    my $sth = $dbh->prepare("INSERT INTO series_data " .
-                            "(series_id, series_date, series_value) " .
-                            "VALUES (?, " . $dbh->quote($today) . ", ?)");
-
-    # We delete from the table beforehand, to avoid SQL errors if people run
-    # collectstats.pl twice on the same day.
-    my $deletesth = $dbh->prepare("DELETE FROM series_data
-                                   WHERE series_id = ? AND series_date = " .
-                                   $dbh->quote($today));
-
-    foreach my $series_id (keys %$serieses) {
-        # We set up the user for Search.pm's permission checking - each series
-        # runs with the permissions of its creator.
-        my $user = new Bugzilla::User($serieses->{$series_id}->{'creator'});
-        my $cgi = new Bugzilla::CGI($serieses->{$series_id}->{'query'});
-        my $data;
-
-        # Do not die if Search->new() detects invalid data, such as an obsolete
-        # login name or a renamed product or component, etc.
-        eval {
-            my $search = new Bugzilla::Search('params' => scalar $cgi->Vars,
-                                              'fields' => ["bug_id"],
-                                              'allow_unlimited' => 1,
-                                              'user'   => $user);
-            $data = $search->data;
-        };
-
-        if (!$@) {
-            # We need to count the returned rows. Without subselects, we can't
-            # do this directly in the SQL for all queries. So we do it by hand.
-            my $count = scalar(@$data) || 0;
-
-            $deletesth->execute($series_id);
-            $sth->execute($series_id, $count);
-        }
+
+  # We need some way of randomising the distribution of series, such that
+  # all of the series which are to be run every 7 days don't run on the same
+  # day. This is because this might put the server under severe load if a
+  # particular frequency, such as once a week, is very common. We achieve
+  # this by only running queries when:
+  # (days_since_epoch + series_id) % frequency = 0. So they'll run every
+  #  days, but the start date depends on the series_id.
+  my $days_since_epoch = int(time() / (60 * 60 * 24));
+  my $today = today_dash();
+
+  # We save a copy of the main $dbh and then switch to the shadow and get
+  # that one too. Remember, these may be the same.
+  my $dbh        = Bugzilla->switch_to_main_db();
+  my $shadow_dbh = Bugzilla->switch_to_shadow_db();
+
+  my $serieses = $dbh->selectall_hashref(
+    "SELECT series_id, query, creator "
+      . "FROM series "
+      . "WHERE frequency != 0 AND "
+      . "MOD(($days_since_epoch + series_id), frequency) = 0",
+    "series_id"
+  );
+
+  # We prepare the insertion into the data table, for efficiency.
+  my $sth
+    = $dbh->prepare("INSERT INTO series_data "
+      . "(series_id, series_date, series_value) "
+      . "VALUES (?, "
+      . $dbh->quote($today)
+      . ", ?)");
+
+  # We delete from the table beforehand, to avoid SQL errors if people run
+  # collectstats.pl twice on the same day.
+  my $deletesth = $dbh->prepare(
+    "DELETE FROM series_data
+                                   WHERE series_id = ? AND series_date = "
+      . $dbh->quote($today)
+  );
+
+  foreach my $series_id (keys %$serieses) {
+
+    # We set up the user for Search.pm's permission checking - each series
+    # runs with the permissions of its creator.
+    my $user = new Bugzilla::User($serieses->{$series_id}->{'creator'});
+    my $cgi  = new Bugzilla::CGI($serieses->{$series_id}->{'query'});
+    my $data;
+
+    # Do not die if Search->new() detects invalid data, such as an obsolete
+    # login name or a renamed product or component, etc.
+    eval {
+      my $search = new Bugzilla::Search(
+        'params'          => scalar $cgi->Vars,
+        'fields'          => ["bug_id"],
+        'allow_unlimited' => 1,
+        'user'            => $user
+      );
+      $data = $search->data;
+    };
+
+    if (!$@) {
+
+      # We need to count the returned rows. Without subselects, we can't
+      # do this directly in the SQL for all queries. So we do it by hand.
+      my $count = scalar(@$data) || 0;
+
+      $deletesth->execute($series_id);
+      $sth->execute($series_id, $count);
     }
+  }
 }
 
 __END__
diff --git a/config.cgi b/config.cgi
index 95e9a842d..03d794f6e 100755
--- a/config.cgi
+++ b/config.cgi
@@ -31,48 +31,50 @@ Bugzilla->switch_to_shadow_db;
 # If the 'requirelogin' parameter is on and the user is not
 # authenticated, return empty fields.
 if (Bugzilla->params->{'requirelogin'} && !$user->id) {
-    display_data();
-    exit;
+  display_data();
+  exit;
 }
 
 # Pass a bunch of Bugzilla configuration to the templates.
 my $vars = {};
-$vars->{'priority'}  = get_legal_field_values('priority');
-$vars->{'severity'}  = get_legal_field_values('bug_severity');
-$vars->{'platform'}  = get_legal_field_values('rep_platform');
-$vars->{'op_sys'}    = get_legal_field_values('op_sys');
+$vars->{'priority'}   = get_legal_field_values('priority');
+$vars->{'severity'}   = get_legal_field_values('bug_severity');
+$vars->{'platform'}   = get_legal_field_values('rep_platform');
+$vars->{'op_sys'}     = get_legal_field_values('op_sys');
 $vars->{'keyword'}    = [map($_->name, Bugzilla::Keyword->get_all)];
 $vars->{'resolution'} = get_legal_field_values('resolution');
-$vars->{'status'}    = get_legal_field_values('bug_status');
-$vars->{'custom_fields'} =
-    [ grep {$_->is_select} Bugzilla->active_custom_fields ];
+$vars->{'status'}     = get_legal_field_values('bug_status');
+$vars->{'custom_fields'}
+  = [grep { $_->is_select } Bugzilla->active_custom_fields];
 
 # Include a list of product objects.
 if ($cgi->param('product')) {
-    my @products = $cgi->param('product');
-    foreach my $product_name (@products) {
-        # We don't use check() because config.cgi outputs mostly
-        # in XML and JS and we don't want to display an HTML error
-        # instead of that.
-        my $product = new Bugzilla::Product({ name => $product_name });
-        if ($product && $user->can_see_product($product->name)) {
-            push (@{$vars->{'products'}}, $product);
-        }
+  my @products = $cgi->param('product');
+  foreach my $product_name (@products) {
+
+    # We don't use check() because config.cgi outputs mostly
+    # in XML and JS and we don't want to display an HTML error
+    # instead of that.
+    my $product = new Bugzilla::Product({name => $product_name});
+    if ($product && $user->can_see_product($product->name)) {
+      push(@{$vars->{'products'}}, $product);
     }
-} else {
-    $vars->{'products'} = $user->get_selectable_products;
+  }
+}
+else {
+  $vars->{'products'} = $user->get_selectable_products;
 }
 
 # We set the 2nd argument to 1 to also preload flag types.
-Bugzilla::Product::preload($vars->{'products'}, 1, { is_active => 1 });
+Bugzilla::Product::preload($vars->{'products'}, 1, {is_active => 1});
 
 # Allow consumers to specify whether or not they want flag data.
 if (defined $cgi->param('flags')) {
-    $vars->{'show_flags'} = $cgi->param('flags');
+  $vars->{'show_flags'} = $cgi->param('flags');
 }
 else {
-    # We default to sending flag data.
-    $vars->{'show_flags'} = 1;
+  # We default to sending flag data.
+  $vars->{'show_flags'} = 1;
 }
 
 # Create separate lists of open versus resolved statuses.  This should really
@@ -80,17 +82,22 @@ else {
 my @open_status;
 my @closed_status;
 foreach my $status (@{$vars->{'status'}}) {
-    is_open_state($status) ? push(@open_status, $status)
-                           : push(@closed_status, $status);
+  is_open_state($status)
+    ? push(@open_status,   $status)
+    : push(@closed_status, $status);
 }
-$vars->{'open_status'} = \@open_status;
+$vars->{'open_status'}   = \@open_status;
 $vars->{'closed_status'} = \@closed_status;
 
 # Generate a list of fields that can be queried.
 my @fields = @{Bugzilla::Field->match({obsolete => 0})};
+
 # Exclude fields the user cannot query.
 if (!Bugzilla->user->is_timetracker) {
-    @fields = grep { $_->name !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ } @fields;
+  @fields = grep {
+    $_->name
+      !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/
+  } @fields;
 }
 $vars->{'field'} = \@fields;
 
@@ -98,33 +105,34 @@ display_data($vars);
 
 
 sub display_data {
-    my $vars = shift;
-
-    my $cgi      = Bugzilla->cgi;
-    my $template = Bugzilla->template;
-
-    # Determine how the user would like to receive the output;
-    # default is JavaScript.
-    my $format = $template->get_format("config", scalar($cgi->param('format')),
-                                       scalar($cgi->param('ctype')) || "js");
-
-    # Generate the configuration data.
-    my $output;
-    $template->process($format->{'template'}, $vars, \$output)
-      || ThrowTemplateError($template->error());
-
-    # Wide characters cause md5_base64() to die.
-    my $digest_data = $output;
-    utf8::encode($digest_data) if utf8::is_utf8($digest_data);
-    my $digest = md5_base64($digest_data);
-
-    if ($cgi->check_etag($digest)) {
-        print $cgi->header(-ETag   => $digest,
-                           -status => '304 Not Modified');
-        exit;
-    }
+  my $vars = shift;
+
+  my $cgi      = Bugzilla->cgi;
+  my $template = Bugzilla->template;
+
+  # Determine how the user would like to receive the output;
+  # default is JavaScript.
+  my $format = $template->get_format(
+    "config",
+    scalar($cgi->param('format')),
+    scalar($cgi->param('ctype')) || "js"
+  );
+
+  # Generate the configuration data.
+  my $output;
+  $template->process($format->{'template'}, $vars, \$output)
+    || ThrowTemplateError($template->error());
+
+  # Wide characters cause md5_base64() to die.
+  my $digest_data = $output;
+  utf8::encode($digest_data) if utf8::is_utf8($digest_data);
+  my $digest = md5_base64($digest_data);
+
+  if ($cgi->check_etag($digest)) {
+    print $cgi->header(-ETag => $digest, -status => '304 Not Modified');
+    exit;
+  }
 
-    print $cgi->header (-ETag => $digest,
-                        -type => $format->{'ctype'});
-    print $output;
+  print $cgi->header(-ETag => $digest, -type => $format->{'ctype'});
+  print $output;
 }
diff --git a/contrib/clear-memcached.pl b/contrib/clear-memcached.pl
index 58157770a..718fc0e98 100755
--- a/contrib/clear-memcached.pl
+++ b/contrib/clear-memcached.pl
@@ -17,8 +17,9 @@ use Bugzilla::Constants;
 Bugzilla->usage_mode(USAGE_MODE_CMDLINE);
 
 if (Bugzilla->memcached->{memcached}) {
-    Bugzilla->memcached->clear_all();
-    print "memcached cleared\n";
-} else {
-    print "memcached is not enabled\n";
+  Bugzilla->memcached->clear_all();
+  print "memcached cleared\n";
+}
+else {
+  print "memcached is not enabled\n";
 }
diff --git a/contrib/clear-templates.pl b/contrib/clear-templates.pl
index 5954e71ad..5569744f0 100755
--- a/contrib/clear-templates.pl
+++ b/contrib/clear-templates.pl
@@ -22,17 +22,16 @@ $| = 1;
 # rename the current directory and create a new empty one
 # the templates will lazy-compile on demand
 
-my $path = bz_locations()->{'template_cache'};
+my $path        = bz_locations()->{'template_cache'};
 my $delete_path = "$path.deleteme";
 
 print "clearing $path\n";
 
 rmtree("$delete_path") if -e "$delete_path";
 rename($path, $delete_path)
-    or die "renaming '$path' to '$delete_path' failed: $!\n";
+  or die "renaming '$path' to '$delete_path' failed: $!\n";
 
-mkpath($path)
-    or die "creating '$path' failed: $!\n";
+mkpath($path) or die "creating '$path' failed: $!\n";
 fix_dir_permissions($path);
 
 # delete the temp directory (it's ok if this fails)
diff --git a/createaccount.cgi b/createaccount.cgi
index c545d9ced..128ce06c8 100755
--- a/createaccount.cgi
+++ b/createaccount.cgi
@@ -20,10 +20,10 @@ use Bugzilla::Token;
 # Just in case someone already has an account, let them get the correct footer
 # on an error message. The user is logged out just after the account is
 # actually created.
-my $user = Bugzilla->login(LOGIN_OPTIONAL);
-my $cgi = Bugzilla->cgi;
+my $user     = Bugzilla->login(LOGIN_OPTIONAL);
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = { doc_section => 'myaccount.html' };
+my $vars     = {doc_section => 'myaccount.html'};
 
 print $cgi->header();
 
@@ -31,17 +31,18 @@ $user->check_account_creation_enabled;
 my $login = $cgi->param('login');
 
 if (defined($login)) {
-    # Check the hash token to make sure this user actually submitted
-    # the create account form.
-    my $token = $cgi->param('token');
-    check_hash_token($token, ['create_account']);
 
-    $user->check_and_send_account_creation_confirmation($login);
-    $vars->{'login'} = $login;
+  # Check the hash token to make sure this user actually submitted
+  # the create account form.
+  my $token = $cgi->param('token');
+  check_hash_token($token, ['create_account']);
 
-    $template->process("account/created.html.tmpl", $vars)
-      || ThrowTemplateError($template->error());
-    exit;
+  $user->check_and_send_account_creation_confirmation($login);
+  $vars->{'login'} = $login;
+
+  $template->process("account/created.html.tmpl", $vars)
+    || ThrowTemplateError($template->error());
+  exit;
 }
 
 # Show the standard "would you like to create an account?" form.
diff --git a/describecomponents.cgi b/describecomponents.cgi
index 86f652fcc..48b0c4bf6 100755
--- a/describecomponents.cgi
+++ b/describecomponents.cgi
@@ -33,53 +33,57 @@ use Bugzilla::Util;
 use Bugzilla::Error;
 use Bugzilla::Product;
 
-my $user = Bugzilla->login();
-my $cgi = Bugzilla->cgi;
+my $user     = Bugzilla->login();
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 print $cgi->header();
 
 # This script does nothing but displaying mostly static data.
 Bugzilla->switch_to_shadow_db;
 
-my $product_name   = trim($cgi->param('product') || '');
+my $product_name   = trim($cgi->param('product')   || '');
 my $component_mark = trim($cgi->param('component') || '');
 
 my $product = new Bugzilla::Product({'name' => $product_name});
 
 unless ($product && $user->can_access_product($product->name)) {
-    # Products which the user is allowed to see.
-    my @products = @{$user->get_accessible_products};
 
-    if (scalar(@products) == 0) {
-        ThrowUserError("no_products");
-    }
-    # If there is only one product available but the user entered
-    # another product name, we display a list with this single
-    # product only, to not confuse the user with components of a
-    # product he didn't request.
-    elsif (scalar(@products) > 1 || $product_name) {
-        $vars->{'classifications'} = [{object => undef, products => \@products}];
-        $vars->{'target'} = "describecomponents.cgi";
-        # If an invalid product name is given, or the user is not
-        # allowed to access that product, a message is displayed
-        # with a list of the products the user can choose from.
-        if ($product_name) {
-            $vars->{'message'} = "product_invalid";
-            # Do not use $product->name here, else you could use
-            # this way to determine whether the product exists or not.
-            $vars->{'product'} = $product_name;
-        }
-
-        $template->process("global/choose-product.html.tmpl", $vars)
-          || ThrowTemplateError($template->error());
-        exit;
+  # Products which the user is allowed to see.
+  my @products = @{$user->get_accessible_products};
+
+  if (scalar(@products) == 0) {
+    ThrowUserError("no_products");
+  }
+
+  # If there is only one product available but the user entered
+  # another product name, we display a list with this single
+  # product only, to not confuse the user with components of a
+  # product he didn't request.
+  elsif (scalar(@products) > 1 || $product_name) {
+    $vars->{'classifications'} = [{object => undef, products => \@products}];
+    $vars->{'target'} = "describecomponents.cgi";
+
+    # If an invalid product name is given, or the user is not
+    # allowed to access that product, a message is displayed
+    # with a list of the products the user can choose from.
+    if ($product_name) {
+      $vars->{'message'} = "product_invalid";
+
+      # Do not use $product->name here, else you could use
+      # this way to determine whether the product exists or not.
+      $vars->{'product'} = $product_name;
     }
 
-    # If there is only one product available and the user didn't specify
-    # any product name, we show this product.
-    $product = $products[0];
+    $template->process("global/choose-product.html.tmpl", $vars)
+      || ThrowTemplateError($template->error());
+    exit;
+  }
+
+  # If there is only one product available and the user didn't specify
+  # any product name, we show this product.
+  $product = $products[0];
 }
 
 ######################################################################
diff --git a/describekeywords.cgi b/describekeywords.cgi
index f48b88b7f..1a047931d 100755
--- a/describekeywords.cgi
+++ b/describekeywords.cgi
@@ -33,9 +33,9 @@ use Bugzilla::Keyword;
 
 Bugzilla->login();
 
-my $cgi = Bugzilla->cgi;
+my $cgi      = Bugzilla->cgi;
 my $template = Bugzilla->template;
-my $vars = {};
+my $vars     = {};
 
 # Run queries against the shadow DB.
 Bugzilla->switch_to_shadow_db;
@@ -43,14 +43,13 @@ Bugzilla->switch_to_shadow_db;
 # Hide bug counts for security keywords from users who aren't a member of the
 # security group
 my $can_see_security = Bugzilla->user->in_group('core-security-release');
-my $keywords = Bugzilla::Keyword->get_all_with_bug_count();
+my $keywords         = Bugzilla::Keyword->get_all_with_bug_count();
 foreach my $keyword (@$keywords) {
-    $keyword->{'bug_count'} = 0
-        if $keyword->name =~ /^(?:sec|csec|wsec|opsec)-/
-           && !$can_see_security;
+  $keyword->{'bug_count'} = 0
+    if $keyword->name =~ /^(?:sec|csec|wsec|opsec)-/ && !$can_see_security;
 }
 
-$vars->{'keywords'} = $keywords;
+$vars->{'keywords'}        = $keywords;
 $vars->{'caneditkeywords'} = Bugzilla->user->in_group("editkeywords");
 
 print Bugzilla->cgi->header();
diff --git a/docs/lib/Pod/Simple/HTML/Bugzilla.pm b/docs/lib/Pod/Simple/HTML/Bugzilla.pm
index ffbd0775c..5a2203473 100644
--- a/docs/lib/Pod/Simple/HTML/Bugzilla.pm
+++ b/docs/lib/Pod/Simple/HTML/Bugzilla.pm
@@ -16,53 +16,53 @@ use parent qw(Pod::Simple::HTML);
 # Without this constant, HTMLBatch will throw undef warnings.
 use constant VERSION    => $Pod::Simple::HTML::VERSION;
 use constant CODE_CLASS => ' class="code"';
-use constant META_CT  => '';
-use constant DOCTYPE  => '';
+use constant META_CT    => '';
+use constant DOCTYPE => '';
 
 sub new {
-    my $self    = shift->SUPER::new(@_);
+  my $self = shift->SUPER::new(@_);
 
-    my $doctype      = $self->DOCTYPE;
-    my $content_type = $self->META_CT;
+  my $doctype      = $self->DOCTYPE;
+  my $content_type = $self->META_CT;
 
-    my $html_pre_title = <
   
     
 END_HTML
 
-    my $html_post_title = <<END_HTML;
+  my $html_post_title = <<END_HTML;
 
     $content_type
   
   
 END_HTML
 
-    $self->html_header_before_title($html_pre_title);
-    $self->html_header_after_title($html_post_title);
+  $self->html_header_before_title($html_pre_title);
+  $self->html_header_after_title($html_post_title);
 
-    # Fix some tags to have classes so that we can adjust them.
-    my $code = CODE_CLASS;
-    $self->{'Tagmap'}->{'Verbatim'} = "\n
";
-    $self->{'Tagmap'}->{'VerbatimFormatted'} = "\n
";
-    $self->{'Tagmap'}->{'F'} = "";
-    $self->{'Tagmap'}->{'C'} = "";
+  # Fix some tags to have classes so that we can adjust them.
+  my $code = CODE_CLASS;
+  $self->{'Tagmap'}->{'Verbatim'}          = "\n
";
+  $self->{'Tagmap'}->{'VerbatimFormatted'} = "\n
";
+  $self->{'Tagmap'}->{'F'}                 = "";
+  $self->{'Tagmap'}->{'C'}                 = "";
 
-    # Don't put head4 tags into the Table of Contents. We have this
-    delete $Pod::Simple::HTML::ToIndex{'head4'};
+  # Don't put head4 tags into the Table of Contents. We have this
+  delete $Pod::Simple::HTML::ToIndex{'head4'};
 
-    return $self;
+  return $self;
 }
 
 # Override do_beginning to put the name of the module at the top
 sub do_beginning {
-    my $self = shift;
-    $self->SUPER::do_beginning(@_);
-    print {$self->{'output_fh'}} "

" . $self->get_short_title . "

"; - return 1; + my $self = shift; + $self->SUPER::do_beginning(@_); + print {$self->{'output_fh'}} "

" . $self->get_short_title . "

"; + return 1; } 1; diff --git a/docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm b/docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm index c9e28eab2..ae05ecf87 100644 --- a/docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm +++ b/docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm @@ -21,90 +21,92 @@ BEGIN { *esc = \&Pod::Simple::HTML::esc } # Note that if you leave out a category here, it will not be indexed # in the contents file, even though its HTML POD will still exist. use constant FILE_TRANSLATION => { - Files => ['importxml', 'contrib', 'checksetup', 'email_in', - 'install-module', 'sanitycheck', 'jobqueue', 'migrate', - 'collectstats'], - Modules => ['bugzilla'], - Extensions => ['extensions'], + Files => [ + 'importxml', 'contrib', 'checksetup', 'email_in', + 'install-module', 'sanitycheck', 'jobqueue', 'migrate', + 'collectstats' + ], + Modules => ['bugzilla'], + Extensions => ['extensions'], }; # This is basically copied from Pod::Simple::HTMLBatch, and overridden # so that we can format things more nicely. sub _write_contents_middle { - my ($self, $Contents, $outfile, $toplevel2submodules) = @_; - - my $file_trans = FILE_TRANSLATION; - - # For every top-level category... - foreach my $category (sort keys %$file_trans) { - # Get all of the HTMLBatch categories that should be in this - # category. - my @category_data; - foreach my $b_category (@{$file_trans->{$category}}) { - my $data = $toplevel2submodules->{$b_category}; - push(@category_data, @$data) if $data; - } - next unless @category_data; - - my @downlines = sort {$a->[-1] cmp $b->[-1]} @category_data; - - # And finally, actually print out the table for this category. - printf $Contents qq[
%s
\n
\n], - esc($category), esc($category); - print $Contents '' . "\n"; - - # For every POD... - my $row_count = 0; - foreach my $e (@downlines) { - $row_count++; - my $even_or_odd = $row_count % 2 ? 'even' : 'odd'; - my $name = esc($e->[0]); - my $path = join( "/", '.', esc(@{$e->[3]}) ) - . $Pod::Simple::HTML::HTML_EXTENSION; - my $description = $self->{bugzilla_desc}->{$name} || ''; - $description = esc($description); - my $html = <{$category}}) { + my $data = $toplevel2submodules->{$b_category}; + push(@category_data, @$data) if $data; + } + next unless @category_data; + + my @downlines = sort { $a->[-1] cmp $b->[-1] } @category_data; + + # And finally, actually print out the table for this category. + printf $Contents qq[
%s
\n
\n], esc($category), + esc($category); + print $Contents '
' . "\n"; + + # For every POD... + my $row_count = 0; + foreach my $e (@downlines) { + $row_count++; + my $even_or_odd = $row_count % 2 ? 'even' : 'odd'; + my $name = esc($e->[0]); + my $path = join("/", '.', esc(@{$e->[3]})) . $Pod::Simple::HTML::HTML_EXTENSION; + my $description = $self->{bugzilla_desc}->{$name} || ''; + $description = esc($description); + my $html = < END_HTML - print $Contents $html; - } - print $Contents "
$name $description
\n\n"; + print $Contents $html; } + print $Contents "\n\n"; + } - return 1; + return 1; } # This stores the name and description for each file, so that # we can get that information out later. sub note_for_contents_file { - my $self = shift; - my $retval = $self->SUPER::note_for_contents_file(@_); + my $self = shift; + my $retval = $self->SUPER::note_for_contents_file(@_); - my ($namelets, $infile) = @_; - my $parser = $self->html_render_class->new; - $parser->set_source($infile); - my $full_title = $parser->get_title; - $full_title =~ /^\S+\s+-+\s+(.+)/; - my $description = $1; + my ($namelets, $infile) = @_; + my $parser = $self->html_render_class->new; + $parser->set_source($infile); + my $full_title = $parser->get_title; + $full_title =~ /^\S+\s+-+\s+(.+)/; + my $description = $1; - $self->{bugzilla_desc} ||= {}; - $self->{bugzilla_desc}->{join('::', @$namelets)} = $description; + $self->{bugzilla_desc} ||= {}; + $self->{bugzilla_desc}->{join('::', @$namelets)} = $description; - return $retval; + return $retval; } # Exclude modules being in lib/. sub find_all_pods { - my($self, $dirs) = @_; - my $mod2path = $self->SUPER::find_all_pods($dirs); - foreach my $mod (keys %$mod2path) { - delete $mod2path->{$mod} if $mod =~ /^lib::/; - } - return $mod2path; + my ($self, $dirs) = @_; + my $mod2path = $self->SUPER::find_all_pods($dirs); + foreach my $mod (keys %$mod2path) { + delete $mod2path->{$mod} if $mod =~ /^lib::/; + } + return $mod2path; } 1; diff --git a/docs/makedocs.pl b/docs/makedocs.pl index 73bd6ce86..feec5f0f5 100755 --- a/docs/makedocs.pl +++ b/docs/makedocs.pl @@ -48,29 +48,30 @@ use Pod::Simple::HTML::Bugzilla; ############################################################################### my $error_found = 0; + sub MakeDocs { - my ($name, $cmdline) = @_; + my ($name, $cmdline) = @_; - say "Creating $name documentation ..." if defined $name; - say "make $cmdline\n"; - system('make', $cmdline) == 0 - or $error_found = 1; - print "\n"; + say "Creating $name documentation ..." if defined $name; + say "make $cmdline\n"; + system('make', $cmdline) == 0 or $error_found = 1; + print "\n"; } sub make_pod { - say "Creating API documentation..."; + say "Creating API documentation..."; + + my $converter = Pod::Simple::HTMLBatch::Bugzilla->new; - my $converter = Pod::Simple::HTMLBatch::Bugzilla->new; - # Don't output progress information. - $converter->verbose(0); - $converter->html_render_class('Pod::Simple::HTML::Bugzilla'); + # Don't output progress information. + $converter->verbose(0); + $converter->html_render_class('Pod::Simple::HTML::Bugzilla'); - my $doctype = Pod::Simple::HTML::Bugzilla->DOCTYPE; - my $content_type = Pod::Simple::HTML::Bugzilla->META_CT; - my $bz_version = BUGZILLA_VERSION; + my $doctype = Pod::Simple::HTML::Bugzilla->DOCTYPE; + my $content_type = Pod::Simple::HTML::Bugzilla->META_CT; + my $bz_version = BUGZILLA_VERSION; - my $contents_start = < @@ -81,16 +82,16 @@ $doctype

Bugzilla $bz_version API Documentation

END_HTML - $converter->contents_page_start($contents_start); - $converter->contents_page_end(""); - $converter->add_css('./../../../style.css'); - $converter->javascript_flurry(0); - $converter->css_flurry(0); - mkdir("html"); - mkdir("html/api"); - $converter->batch_convert(['../../'], 'html/api/'); + $converter->contents_page_start($contents_start); + $converter->contents_page_end(""); + $converter->add_css('./../../../style.css'); + $converter->javascript_flurry(0); + $converter->css_flurry(0); + mkdir("html"); + mkdir("html/api"); + $converter->batch_convert(['../../'], 'html/api/'); - print "\n"; + print "\n"; } ############################################################################### @@ -98,67 +99,68 @@ END_HTML ############################################################################### my @langs; + # search for sub directories which have a 'rst' sub-directory opendir(LANGS, './'); foreach my $dir (readdir(LANGS)) { - next if (($dir eq '.') || ($dir eq '..') || (! -d $dir)); - if (-d "$dir/rst") { - push(@langs, $dir); - } + next if (($dir eq '.') || ($dir eq '..') || (!-d $dir)); + if (-d "$dir/rst") { + push(@langs, $dir); + } } closedir(LANGS); my $docparent = getcwd(); foreach my $lang (@langs) { - chdir "$docparent/$lang"; + chdir "$docparent/$lang"; - make_pod(); + make_pod(); - next if grep { $_ eq '--pod-only' } @ARGV; + next if grep { $_ eq '--pod-only' } @ARGV; - chdir $docparent; + chdir $docparent; - # Generate extension documentation, both normal and API - my $ext_dir = bz_locations()->{'extensionsdir'}; - my @ext_paths = grep { $_ !~ /\/create\.pl$/ && ! -e "$_/disabled" } - glob("$ext_dir/*"); - my %extensions; - foreach my $item (@ext_paths) { - my $basename = basename($item); - if (-d "$item/docs/$lang/rst") { - $extensions{$basename} = "$item/docs/$lang/rst"; - } + # Generate extension documentation, both normal and API + my $ext_dir = bz_locations()->{'extensionsdir'}; + my @ext_paths + = grep { $_ !~ /\/create\.pl$/ && !-e "$_/disabled" } glob("$ext_dir/*"); + my %extensions; + foreach my $item (@ext_paths) { + my $basename = basename($item); + if (-d "$item/docs/$lang/rst") { + $extensions{$basename} = "$item/docs/$lang/rst"; } + } - # Collect up local extension documentation into the extensions/ dir. - rmtree("$lang/rst/extensions", 0, 1); + # Collect up local extension documentation into the extensions/ dir. + rmtree("$lang/rst/extensions", 0, 1); - foreach my $ext_name (keys %extensions) { - my $src = $extensions{$ext_name} . "/*"; - my $dst = "$docparent/$lang/rst/extensions/$ext_name"; - mkdir($dst) unless -d $dst; - rcopy($src, $dst); - } + foreach my $ext_name (keys %extensions) { + my $src = $extensions{$ext_name} . "/*"; + my $dst = "$docparent/$lang/rst/extensions/$ext_name"; + mkdir($dst) unless -d $dst; + rcopy($src, $dst); + } - chdir "$docparent/$lang"; - - MakeDocs('HTML', 'html'); - MakeDocs('TXT', 'text'); - - if (grep { $_ eq '--with-pdf' } @ARGV) { - if (which('pdflatex')) { - MakeDocs('PDF', 'latexpdf'); - } - elsif (which('rst2pdf')) { - rmtree('pdf', 0, 1); - MakeDocs('PDF', 'pdf'); - } - else { - say 'pdflatex or rst2pdf not found. Skipping PDF file creation'; - } + chdir "$docparent/$lang"; + + MakeDocs('HTML', 'html'); + MakeDocs('TXT', 'text'); + + if (grep { $_ eq '--with-pdf' } @ARGV) { + if (which('pdflatex')) { + MakeDocs('PDF', 'latexpdf'); + } + elsif (which('rst2pdf')) { + rmtree('pdf', 0, 1); + MakeDocs('PDF', 'pdf'); + } + else { + say 'pdflatex or rst2pdf not found. Skipping PDF file creation'; } + } - rmtree('doctrees', 0, 1); + rmtree('doctrees', 0, 1); } die "Error occurred building the documentation\n" if $error_found; diff --git a/duplicates.cgi b/duplicates.cgi index 118c1149a..7a778c749 100755 --- a/duplicates.cgi +++ b/duplicates.cgi @@ -21,21 +21,22 @@ use Bugzilla::Field; use Bugzilla::Product; use constant DEFAULTS => { - # We want to show bugs which: - # a) Aren't CLOSED; and - # b) i) Aren't VERIFIED; OR - # ii) Were resolved INVALID/WONTFIX - # - # The rationale behind this is that people will eventually stop - # reporting fixed bugs when they get newer versions of the software, - # but if the bug is determined to be erroneous, people will still - # keep reporting it, so we do need to show it here. - fully_exclude_status => ['CLOSED'], - partly_exclude_status => ['VERIFIED'], - except_resolution => ['INVALID', 'WONTFIX'], - changedsince => 7, - maxrows => 20, - sortby => 'count', + + # We want to show bugs which: + # a) Aren't CLOSED; and + # b) i) Aren't VERIFIED; OR + # ii) Were resolved INVALID/WONTFIX + # + # The rationale behind this is that people will eventually stop + # reporting fixed bugs when they get newer versions of the software, + # but if the bug is determined to be erroneous, people will still + # keep reporting it, so we do need to show it here. + fully_exclude_status => ['CLOSED'], + partly_exclude_status => ['VERIFIED'], + except_resolution => ['INVALID', 'WONTFIX'], + changedsince => 7, + maxrows => 20, + sortby => 'count', }; ############### @@ -48,55 +49,55 @@ use constant DEFAULTS => { # in $count is a duplicate of another bug in $count, we add their counts # together under the target bug. sub add_indirect_dups { - my ($counts, $dups) = @_; + my ($counts, $dups) = @_; - foreach my $add_from (keys %$dups) { - my $add_to = walk_dup_chain($dups, $add_from); - my $add_amount = delete $counts->{$add_from} || 0; - $counts->{$add_to} += $add_amount; - } + foreach my $add_from (keys %$dups) { + my $add_to = walk_dup_chain($dups, $add_from); + my $add_amount = delete $counts->{$add_from} || 0; + $counts->{$add_to} += $add_amount; + } } sub walk_dup_chain { - my ($dups, $from_id) = @_; - my $to_id = $dups->{$from_id}; - my %seen; - while (my $bug_id = $dups->{$to_id}) { - if ($seen{$bug_id}) { - warn "Duplicate loop: $to_id -> $bug_id\n"; - last; - } - $seen{$bug_id} = 1; - $to_id = $bug_id; + my ($dups, $from_id) = @_; + my $to_id = $dups->{$from_id}; + my %seen; + while (my $bug_id = $dups->{$to_id}) { + if ($seen{$bug_id}) { + warn "Duplicate loop: $to_id -> $bug_id\n"; + last; } - # Optimize for future calls to add_indirect_dups. - $dups->{$from_id} = $to_id; - return $to_id; + $seen{$bug_id} = 1; + $to_id = $bug_id; + } + + # Optimize for future calls to add_indirect_dups. + $dups->{$from_id} = $to_id; + return $to_id; } # Get params from URL sub formvalue { - my ($name) = (@_); - my $cgi = Bugzilla->cgi; - if (defined $cgi->param($name)) { - return $cgi->param($name); - } - elsif (exists DEFAULTS->{$name}) { - return ref DEFAULTS->{$name} ? @{ DEFAULTS->{$name} } - : DEFAULTS->{$name}; - } - return undef; + my ($name) = (@_); + my $cgi = Bugzilla->cgi; + if (defined $cgi->param($name)) { + return $cgi->param($name); + } + elsif (exists DEFAULTS->{$name}) { + return ref DEFAULTS->{$name} ? @{DEFAULTS->{$name}} : DEFAULTS->{$name}; + } + return undef; } sub sort_duplicates { - my ($a, $b, $sort_by) = @_; - if ($sort_by eq 'count' or $sort_by eq 'delta') { - return $a->{$sort_by} <=> $b->{$sort_by}; - } - if ($sort_by =~ /^(bug_)?id$/) { - return $a->{'bug'}->$sort_by <=> $b->{'bug'}->$sort_by; - } - return $a->{'bug'}->$sort_by cmp $b->{'bug'}->$sort_by; + my ($a, $b, $sort_by) = @_; + if ($sort_by eq 'count' or $sort_by eq 'delta') { + return $a->{$sort_by} <=> $b->{$sort_by}; + } + if ($sort_by =~ /^(bug_)?id$/) { + return $a->{'bug'}->$sort_by <=> $b->{'bug'}->$sort_by; + } + return $a->{'bug'}->$sort_by cmp $b->{'bug'}->$sort_by; } @@ -104,36 +105,37 @@ sub sort_duplicates { # Main Script # ############### -my $cgi = Bugzilla->cgi; +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; -my $user = Bugzilla->login(); +my $user = Bugzilla->login(); my $dbh = Bugzilla->switch_to_shadow_db(); my $changedsince = formvalue("changedsince"); -my $maxrows = formvalue("maxrows"); -my $openonly = formvalue("openonly"); -my $sortby = formvalue("sortby"); +my $maxrows = formvalue("maxrows"); +my $openonly = formvalue("openonly"); +my $sortby = formvalue("sortby"); if (!grep(lc($_) eq lc($sortby), qw(count delta id))) { - Bugzilla::Field->check($sortby); + Bugzilla::Field->check($sortby); } my $reverse = formvalue("reverse"); + # Reverse count and delta by default. if (!defined $reverse) { - if ($sortby eq 'count' or $sortby eq 'delta') { - $reverse = 1; - } - else { - $reverse = 0; - } + if ($sortby eq 'count' or $sortby eq 'delta') { + $reverse = 1; + } + else { + $reverse = 0; + } } my @query_products = $cgi->param('product'); -my $sortvisible = formvalue("sortvisible"); +my $sortvisible = formvalue("sortvisible"); my @bugs; if ($sortvisible) { - my @limit_to_ids = (split(/[:,]/, formvalue("bug_id") || '')); - @bugs = @{ Bugzilla::Bug->new_from_list(\@limit_to_ids) }; - @bugs = @{ $user->visible_bugs(\@bugs) }; + my @limit_to_ids = (split(/[:,]/, formvalue("bug_id") || '')); + @bugs = @{Bugzilla::Bug->new_from_list(\@limit_to_ids)}; + @bugs = @{$user->visible_bugs(\@bugs)}; } # Make sure all products are valid. @@ -144,102 +146,118 @@ $sortby = "count" if $sortby eq "dup_count"; my $origmaxrows = $maxrows; detaint_natural($maxrows) - || ThrowUserError("invalid_maxrows", { maxrows => $origmaxrows}); + || ThrowUserError("invalid_maxrows", {maxrows => $origmaxrows}); my $origchangedsince = $changedsince; detaint_natural($changedsince) || ThrowUserError("invalid_changedsince", - { changedsince => $origchangedsince }); + {changedsince => $origchangedsince}); -my %total_dups = @{$dbh->selectcol_arrayref( +my %total_dups = @{ + $dbh->selectcol_arrayref( "SELECT dupe_of, COUNT(dupe) FROM duplicates - GROUP BY dupe_of", {Columns => [1,2]})}; + GROUP BY dupe_of", {Columns => [1, 2]} + ) +}; -my %dupe_relation = @{$dbh->selectcol_arrayref( +my %dupe_relation = @{ + $dbh->selectcol_arrayref( "SELECT dupe, dupe_of FROM duplicates - WHERE dupe IN (SELECT dupe_of FROM duplicates)", - {Columns => [1,2]})}; + WHERE dupe IN (SELECT dupe_of FROM duplicates)", {Columns => [1, 2]} + ) +}; add_indirect_dups(\%total_dups, \%dupe_relation); my $reso_field_id = get_field_id('resolution'); -my %since_dups = @{$dbh->selectcol_arrayref( +my %since_dups = @{ + $dbh->selectcol_arrayref( "SELECT dupe_of, COUNT(dupe) FROM duplicates INNER JOIN bugs_activity ON bugs_activity.bug_id = duplicates.dupe WHERE added = 'DUPLICATE' AND fieldid = ? AND bug_when >= " - . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '?', 'DAY') . - " GROUP BY dupe_of", {Columns=>[1,2]}, - $reso_field_id, $changedsince)}; + . $dbh->sql_date_math('LOCALTIMESTAMP(0)', '-', '?', 'DAY') + . " GROUP BY dupe_of", {Columns => [1, 2]}, $reso_field_id, $changedsince + ) +}; add_indirect_dups(\%since_dups, \%dupe_relation); # Enforce the mostfreqthreshold parameter and the "bug_id" cgi param. my $mostfreq = Bugzilla->params->{'mostfreqthreshold'}; foreach my $id (keys %total_dups) { - if ($total_dups{$id} < $mostfreq) { - delete $total_dups{$id}; - next; - } - if ($sortvisible and !grep($_->id == $id, @bugs)) { - delete $total_dups{$id}; - } + if ($total_dups{$id} < $mostfreq) { + delete $total_dups{$id}; + next; + } + if ($sortvisible and !grep($_->id == $id, @bugs)) { + delete $total_dups{$id}; + } } if (!@bugs) { - @bugs = @{ Bugzilla::Bug->new_from_list([keys %total_dups]) }; - @bugs = @{ $user->visible_bugs(\@bugs) }; + @bugs = @{Bugzilla::Bug->new_from_list([keys %total_dups])}; + @bugs = @{$user->visible_bugs(\@bugs)}; } -my @fully_exclude_status = formvalue('fully_exclude_status'); +my @fully_exclude_status = formvalue('fully_exclude_status'); my @partly_exclude_status = formvalue('partly_exclude_status'); -my @except_resolution = formvalue('except_resolution'); +my @except_resolution = formvalue('except_resolution'); # Filter bugs by criteria my @result_bugs; foreach my $bug (@bugs) { - # It's possible, if somebody specified a bug ID that wasn't a dup - # in the "buglist" parameter and specified $sortvisible that there - # would be bugs in the list with 0 dups, so we want to avoid that. - next if !$total_dups{$bug->id}; - - next if ($openonly and !$bug->isopened); - # If the bug has a status in @fully_exclude_status, we skip it, - # no question. - next if grep($_ eq $bug->bug_status, @fully_exclude_status); - # If the bug has a status in @partly_exclude_status, we skip it... - if (grep($_ eq $bug->bug_status, @partly_exclude_status)) { - # ...unless it has a resolution in @except_resolution. - next if !grep($_ eq $bug->resolution, @except_resolution); - } - if (scalar @query_products) { - next if !grep($_->id == $bug->product_id, @query_products); - } + # It's possible, if somebody specified a bug ID that wasn't a dup + # in the "buglist" parameter and specified $sortvisible that there + # would be bugs in the list with 0 dups, so we want to avoid that. + next if !$total_dups{$bug->id}; + + next if ($openonly and !$bug->isopened); - # Note: maximum row count is dealt with later. - push (@result_bugs, { bug => $bug, - count => $total_dups{$bug->id}, - delta => $since_dups{$bug->id} || 0 }); + # If the bug has a status in @fully_exclude_status, we skip it, + # no question. + next if grep($_ eq $bug->bug_status, @fully_exclude_status); + + # If the bug has a status in @partly_exclude_status, we skip it... + if (grep($_ eq $bug->bug_status, @partly_exclude_status)) { + + # ...unless it has a resolution in @except_resolution. + next if !grep($_ eq $bug->resolution, @except_resolution); + } + + if (scalar @query_products) { + next if !grep($_->id == $bug->product_id, @query_products); + } + + # Note: maximum row count is dealt with later. + push( + @result_bugs, + { + bug => $bug, + count => $total_dups{$bug->id}, + delta => $since_dups{$bug->id} || 0 + } + ); } @bugs = @result_bugs; @bugs = sort { sort_duplicates($a, $b, $sortby) } @bugs; if ($reverse) { - @bugs = reverse @bugs; + @bugs = reverse @bugs; } -@bugs = @bugs[0..$maxrows-1] if scalar(@bugs) > $maxrows; +@bugs = @bugs[0 .. $maxrows - 1] if scalar(@bugs) > $maxrows; my %vars = ( - bugs => \@bugs, - bug_ids => [map { $_->{'bug'}->id } @bugs], - sortby => $sortby, - openonly => $openonly, - maxrows => $maxrows, - reverse => $reverse, - format => scalar $cgi->param('format'), - product => [map { $_->name } @query_products], - sortvisible => $sortvisible, - changedsince => $changedsince, + bugs => \@bugs, + bug_ids => [map { $_->{'bug'}->id } @bugs], + sortby => $sortby, + openonly => $openonly, + maxrows => $maxrows, + reverse => $reverse, + format => scalar $cgi->param('format'), + product => [map { $_->name } @query_products], + sortvisible => $sortvisible, + changedsince => $changedsince, ); my $format = $template->get_format("reports/duplicates", $vars{'format'}); diff --git a/editclassifications.cgi b/editclassifications.cgi index 285927a1e..1e7e3c27d 100755 --- a/editclassifications.cgi +++ b/editclassifications.cgi @@ -20,28 +20,29 @@ use Bugzilla::Error; use Bugzilla::Classification; use Bugzilla::Token; -my $dbh = Bugzilla->dbh; -my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; local our $vars = {}; sub LoadTemplate { - my $action = shift; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - - $vars->{'classifications'} = [Bugzilla::Classification->get_all] - if ($action eq 'select'); - # There is currently only one section about classifications, - # so all pages point to it. Let's define it here. - $vars->{'doc_section'} = 'classifications.html'; - - $action =~ /(\w+)/; - $action = $1; - print $cgi->header(); - $template->process("admin/classifications/$action.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $action = shift; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + + $vars->{'classifications'} = [Bugzilla::Classification->get_all] + if ($action eq 'select'); + + # There is currently only one section about classifications, + # so all pages point to it. Let's define it here. + $vars->{'doc_section'} = 'classifications.html'; + + $action =~ /(\w+)/; + $action = $1; + print $cgi->header(); + $template->process("admin/classifications/$action.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -52,13 +53,13 @@ Bugzilla->login(LOGIN_REQUIRED); print $cgi->header(); -Bugzilla->user->in_group('editclassifications') - || ThrowUserError("auth_failure", {group => "editclassifications", - action => "edit", - object => "classifications"}); +Bugzilla->user->in_group('editclassifications') || ThrowUserError( + "auth_failure", + {group => "editclassifications", action => "edit", object => "classifications"} +); ThrowUserError("auth_classification_not_enabled") - unless Bugzilla->params->{"useclassification"}; + unless Bugzilla->params->{"useclassification"}; # # often used variables @@ -79,8 +80,8 @@ LoadTemplate('select') unless $action; # if ($action eq 'add') { - $vars->{'token'} = issue_session_token('add_classification'); - LoadTemplate($action); + $vars->{'token'} = issue_session_token('add_classification'); + LoadTemplate($action); } # @@ -88,20 +89,21 @@ if ($action eq 'add') { # if ($action eq 'new') { - check_token_data($token, 'add_classification'); + check_token_data($token, 'add_classification'); - my $classification = - Bugzilla::Classification->create({name => $class_name, - description => scalar $cgi->param('description'), - sortkey => scalar $cgi->param('sortkey')}); + my $classification = Bugzilla::Classification->create({ + name => $class_name, + description => scalar $cgi->param('description'), + sortkey => scalar $cgi->param('sortkey') + }); - delete_token($token); + delete_token($token); - $vars->{'message'} = 'classification_created'; - $vars->{'classification'} = $classification; - $vars->{'classifications'} = [Bugzilla::Classification->get_all]; - $vars->{'token'} = issue_session_token('reclassify_classifications'); - LoadTemplate('reclassify'); + $vars->{'message'} = 'classification_created'; + $vars->{'classification'} = $classification; + $vars->{'classifications'} = [Bugzilla::Classification->get_all]; + $vars->{'token'} = issue_session_token('reclassify_classifications'); + LoadTemplate('reclassify'); } # @@ -112,20 +114,20 @@ if ($action eq 'new') { if ($action eq 'del') { - my $classification = Bugzilla::Classification->check($class_name); + my $classification = Bugzilla::Classification->check($class_name); - if ($classification->id == 1) { - ThrowUserError("classification_not_deletable"); - } + if ($classification->id == 1) { + ThrowUserError("classification_not_deletable"); + } - if ($classification->product_count()) { - ThrowUserError("classification_has_products"); - } + if ($classification->product_count()) { + ThrowUserError("classification_has_products"); + } - $vars->{'classification'} = $classification; - $vars->{'token'} = issue_session_token('delete_classification'); + $vars->{'classification'} = $classification; + $vars->{'token'} = issue_session_token('delete_classification'); - LoadTemplate($action); + LoadTemplate($action); } # @@ -133,15 +135,15 @@ if ($action eq 'del') { # if ($action eq 'delete') { - check_token_data($token, 'delete_classification'); + check_token_data($token, 'delete_classification'); - my $classification = Bugzilla::Classification->check($class_name); - $classification->remove_from_db; - delete_token($token); + my $classification = Bugzilla::Classification->check($class_name); + $classification->remove_from_db; + delete_token($token); - $vars->{'message'} = 'classification_deleted'; - $vars->{'classification'} = $classification; - LoadTemplate('select'); + $vars->{'message'} = 'classification_deleted'; + $vars->{'classification'} = $classification; + LoadTemplate('select'); } # @@ -151,12 +153,12 @@ if ($action eq 'delete') { # if ($action eq 'edit') { - my $classification = Bugzilla::Classification->check($class_name); + my $classification = Bugzilla::Classification->check($class_name); - $vars->{'classification'} = $classification; - $vars->{'token'} = issue_session_token('edit_classification'); + $vars->{'classification'} = $classification; + $vars->{'token'} = issue_session_token('edit_classification'); - LoadTemplate($action); + LoadTemplate($action); } # @@ -164,22 +166,22 @@ if ($action eq 'edit') { # if ($action eq 'update') { - check_token_data($token, 'edit_classification'); + check_token_data($token, 'edit_classification'); - my $class_old_name = trim($cgi->param('classificationold') || ''); - my $classification = Bugzilla::Classification->check($class_old_name); + my $class_old_name = trim($cgi->param('classificationold') || ''); + my $classification = Bugzilla::Classification->check($class_old_name); - $classification->set_name($class_name); - $classification->set_description(scalar $cgi->param('description')); - $classification->set_sortkey(scalar $cgi->param('sortkey')); + $classification->set_name($class_name); + $classification->set_description(scalar $cgi->param('description')); + $classification->set_sortkey(scalar $cgi->param('sortkey')); - my $changes = $classification->update; - delete_token($token); + my $changes = $classification->update; + delete_token($token); - $vars->{'message'} = 'classification_updated'; - $vars->{'classification'} = $classification; - $vars->{'changes'} = $changes; - LoadTemplate('select'); + $vars->{'message'} = 'classification_updated'; + $vars->{'classification'} = $classification; + $vars->{'changes'} = $changes; + LoadTemplate('select'); } # @@ -187,44 +189,47 @@ if ($action eq 'update') { # if ($action eq 'reclassify') { - my $classification = Bugzilla::Classification->check($class_name); - - my $sth = $dbh->prepare("UPDATE products SET classification_id = ? - WHERE name = ?"); - my @names; - - if (defined $cgi->param('add_products')) { - check_token_data($token, 'reclassify_classifications'); - if (defined $cgi->param('prodlist')) { - foreach my $prod ($cgi->param("prodlist")) { - trick_taint($prod); - $sth->execute($classification->id, $prod); - push @names, $prod; - } - } - delete_token($token); - } elsif (defined $cgi->param('remove_products')) { - check_token_data($token, 'reclassify_classifications'); - if (defined $cgi->param('myprodlist')) { - foreach my $prod ($cgi->param("myprodlist")) { - trick_taint($prod); - $sth->execute(1, $prod); - push @names, $prod; - } - } - delete_token($token); + my $classification = Bugzilla::Classification->check($class_name); + + my $sth = $dbh->prepare( + "UPDATE products SET classification_id = ? + WHERE name = ?" + ); + my @names; + + if (defined $cgi->param('add_products')) { + check_token_data($token, 'reclassify_classifications'); + if (defined $cgi->param('prodlist')) { + foreach my $prod ($cgi->param("prodlist")) { + trick_taint($prod); + $sth->execute($classification->id, $prod); + push @names, $prod; + } } + delete_token($token); + } + elsif (defined $cgi->param('remove_products')) { + check_token_data($token, 'reclassify_classifications'); + if (defined $cgi->param('myprodlist')) { + foreach my $prod ($cgi->param("myprodlist")) { + trick_taint($prod); + $sth->execute(1, $prod); + push @names, $prod; + } + } + delete_token($token); + } - $vars->{'classifications'} = [Bugzilla::Classification->get_all]; - $vars->{'classification'} = $classification; - $vars->{'token'} = issue_session_token('reclassify_classifications'); + $vars->{'classifications'} = [Bugzilla::Classification->get_all]; + $vars->{'classification'} = $classification; + $vars->{'token'} = issue_session_token('reclassify_classifications'); - foreach my $name (@names) { - Bugzilla->memcached->clear({ table => 'products', name => $name }); - } - Bugzilla->memcached->clear_config(); + foreach my $name (@names) { + Bugzilla->memcached->clear({table => 'products', name => $name}); + } + Bugzilla->memcached->clear_config(); - LoadTemplate($action); + LoadTemplate($action); } # diff --git a/editcomponents.cgi b/editcomponents.cgi index f8cef5971..5070b1d1f 100755 --- a/editcomponents.cgi +++ b/editcomponents.cgi @@ -20,9 +20,10 @@ use Bugzilla::User; use Bugzilla::Component; use Bugzilla::Token; -my $cgi = Bugzilla->cgi; +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; + # There is only one section about components in the documentation, # so all actions point to the same page. $vars->{'doc_section'} = 'components.html'; @@ -37,16 +38,15 @@ print $cgi->header(); $user->in_group('editcomponents') || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", {group => "editcomponents", - action => "edit", - object => "components"}); + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "components"}); # # often used variables # -my $product_name = trim($cgi->param('product') || ''); -my $comp_name = trim($cgi->param('component') || ''); -my $action = trim($cgi->param('action') || ''); +my $product_name = trim($cgi->param('product') || ''); +my $comp_name = trim($cgi->param('component') || ''); +my $action = trim($cgi->param('action') || ''); my $showbugcounts = (defined $cgi->param('showbugcounts')); my $token = $cgi->param('token'); @@ -55,18 +55,19 @@ my $token = $cgi->param('token'); # unless ($product_name) { - my $selectable_products = $user->get_selectable_products; - # If the user has editcomponents privs for some products only, - # we have to restrict the list of products to display. - unless ($user->in_group('editcomponents')) { - $selectable_products = $user->get_products_by_permission('editcomponents'); - } - $vars->{'products'} = $selectable_products; - $vars->{'showbugcounts'} = $showbugcounts; - - $template->process("admin/components/select-product.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $selectable_products = $user->get_selectable_products; + + # If the user has editcomponents privs for some products only, + # we have to restrict the list of products to display. + unless ($user->in_group('editcomponents')) { + $selectable_products = $user->get_products_by_permission('editcomponents'); + } + $vars->{'products'} = $selectable_products; + $vars->{'showbugcounts'} = $showbugcounts; + + $template->process("admin/components/select-product.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } my $product = $user->check_can_admin_product($product_name); @@ -76,11 +77,11 @@ my $product = $user->check_can_admin_product($product_name); # unless ($action) { - $vars->{'showbugcounts'} = $showbugcounts; - $vars->{'product'} = $product; - $template->process("admin/components/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'showbugcounts'} = $showbugcounts; + $vars->{'product'} = $product; + $template->process("admin/components/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -90,11 +91,11 @@ unless ($action) { # if ($action eq 'add') { - $vars->{'token'} = issue_session_token('add_component'); - $vars->{'product'} = $product; - $template->process("admin/components/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'token'} = issue_session_token('add_component'); + $vars->{'product'} = $product; + $template->process("admin/components/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -102,43 +103,45 @@ if ($action eq 'add') { # if ($action eq 'new') { - check_token_data($token, 'add_component'); - # Do the user matching - Bugzilla::User::match_field ({ - 'initialowner' => { 'type' => 'single' }, - 'initialqacontact' => { 'type' => 'single' }, - 'triage_owner' => { 'type' => 'single' }, - 'initialcc' => { 'type' => 'multi' }, - }); - - my $default_assignee = trim($cgi->param('initialowner') || ''); - my $default_qa_contact = trim($cgi->param('initialqacontact') || ''); - my $description = trim($cgi->param('description') || ''); - my $triage_owner = trim($cgi->param('triage_owner') || ''); - my @initial_cc = $cgi->param('initialcc'); - my $isactive = $cgi->param('isactive'); - - my $component = Bugzilla::Component->create({ - name => $comp_name, - product => $product, - description => $description, - initialowner => $default_assignee, - initialqacontact => $default_qa_contact, - initial_cc => \@initial_cc, - triage_owner_id => $triage_owner, - # XXX We should not be creating series for products that we - # didn't create series for. - create_series => 1, - }); - - $vars->{'message'} = 'component_created'; - $vars->{'comp'} = $component; - $vars->{'product'} = $product; - delete_token($token); - - $template->process("admin/components/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + check_token_data($token, 'add_component'); + + # Do the user matching + Bugzilla::User::match_field({ + 'initialowner' => {'type' => 'single'}, + 'initialqacontact' => {'type' => 'single'}, + 'triage_owner' => {'type' => 'single'}, + 'initialcc' => {'type' => 'multi'}, + }); + + my $default_assignee = trim($cgi->param('initialowner') || ''); + my $default_qa_contact = trim($cgi->param('initialqacontact') || ''); + my $description = trim($cgi->param('description') || ''); + my $triage_owner = trim($cgi->param('triage_owner') || ''); + my @initial_cc = $cgi->param('initialcc'); + my $isactive = $cgi->param('isactive'); + + my $component = Bugzilla::Component->create({ + name => $comp_name, + product => $product, + description => $description, + initialowner => $default_assignee, + initialqacontact => $default_qa_contact, + initial_cc => \@initial_cc, + triage_owner_id => $triage_owner, + + # XXX We should not be creating series for products that we + # didn't create series for. + create_series => 1, + }); + + $vars->{'message'} = 'component_created'; + $vars->{'comp'} = $component; + $vars->{'product'} = $product; + delete_token($token); + + $template->process("admin/components/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -148,14 +151,14 @@ if ($action eq 'new') { # if ($action eq 'del') { - $vars->{'token'} = issue_session_token('delete_component'); - $vars->{'comp'} = - Bugzilla::Component->check({ product => $product, name => $comp_name }); - $vars->{'product'} = $product; - - $template->process("admin/components/confirm-delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'token'} = issue_session_token('delete_component'); + $vars->{'comp'} + = Bugzilla::Component->check({product => $product, name => $comp_name}); + $vars->{'product'} = $product; + + $template->process("admin/components/confirm-delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -163,21 +166,21 @@ if ($action eq 'del') { # if ($action eq 'delete') { - check_token_data($token, 'delete_component'); - my $component = - Bugzilla::Component->check({ product => $product, name => $comp_name }); + check_token_data($token, 'delete_component'); + my $component + = Bugzilla::Component->check({product => $product, name => $comp_name}); - $component->remove_from_db; + $component->remove_from_db; - $vars->{'message'} = 'component_deleted'; - $vars->{'comp'} = $component; - $vars->{'product'} = $product; - $vars->{'no_edit_component_link'} = 1; - delete_token($token); + $vars->{'message'} = 'component_deleted'; + $vars->{'comp'} = $component; + $vars->{'product'} = $product; + $vars->{'no_edit_component_link'} = 1; + delete_token($token); - $template->process("admin/components/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/components/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -187,19 +190,19 @@ if ($action eq 'delete') { # if ($action eq 'edit') { - $vars->{'token'} = issue_session_token('edit_component'); - my $component = - Bugzilla::Component->check({ product => $product, name => $comp_name }); - $vars->{'comp'} = $component; + $vars->{'token'} = issue_session_token('edit_component'); + my $component + = Bugzilla::Component->check({product => $product, name => $comp_name}); + $vars->{'comp'} = $component; - $vars->{'initial_cc_names'} = - join(', ', map($_->login, @{$component->initial_cc})); + $vars->{'initial_cc_names'} + = join(', ', map($_->login, @{$component->initial_cc})); - $vars->{'product'} = $product; + $vars->{'product'} = $product; - $template->process("admin/components/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/components/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -207,44 +210,45 @@ if ($action eq 'edit') { # if ($action eq 'update') { - check_token_data($token, 'edit_component'); - # Do the user matching - Bugzilla::User::match_field ({ - 'initialowner' => { 'type' => 'single' }, - 'initialqacontact' => { 'type' => 'single' }, - 'triage_owner' => { 'type' => 'single' }, - 'initialcc' => { 'type' => 'multi' }, - }); - - my $comp_old_name = trim($cgi->param('componentold') || ''); - my $default_assignee = trim($cgi->param('initialowner') || ''); - my $default_qa_contact = trim($cgi->param('initialqacontact') || ''); - my $description = trim($cgi->param('description') || ''); - my $triage_owner = trim($cgi->param('triage_owner') || ''); - my @initial_cc = $cgi->param('initialcc'); - my $isactive = $cgi->param('isactive'); - - my $component = - Bugzilla::Component->check({ product => $product, name => $comp_old_name }); - - $component->set_name($comp_name); - $component->set_description($description); - $component->set_default_assignee($default_assignee); - $component->set_default_qa_contact($default_qa_contact); - $component->set_triage_owner($triage_owner); - $component->set_cc_list(\@initial_cc); - $component->set_is_active($isactive); - my $changes = $component->update(); - - $vars->{'message'} = 'component_updated'; - $vars->{'comp'} = $component; - $vars->{'product'} = $product; - $vars->{'changes'} = $changes; - delete_token($token); - - $template->process("admin/components/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + check_token_data($token, 'edit_component'); + + # Do the user matching + Bugzilla::User::match_field({ + 'initialowner' => {'type' => 'single'}, + 'initialqacontact' => {'type' => 'single'}, + 'triage_owner' => {'type' => 'single'}, + 'initialcc' => {'type' => 'multi'}, + }); + + my $comp_old_name = trim($cgi->param('componentold') || ''); + my $default_assignee = trim($cgi->param('initialowner') || ''); + my $default_qa_contact = trim($cgi->param('initialqacontact') || ''); + my $description = trim($cgi->param('description') || ''); + my $triage_owner = trim($cgi->param('triage_owner') || ''); + my @initial_cc = $cgi->param('initialcc'); + my $isactive = $cgi->param('isactive'); + + my $component + = Bugzilla::Component->check({product => $product, name => $comp_old_name}); + + $component->set_name($comp_name); + $component->set_description($description); + $component->set_default_assignee($default_assignee); + $component->set_default_qa_contact($default_qa_contact); + $component->set_triage_owner($triage_owner); + $component->set_cc_list(\@initial_cc); + $component->set_is_active($isactive); + my $changes = $component->update(); + + $vars->{'message'} = 'component_updated'; + $vars->{'comp'} = $component; + $vars->{'product'} = $product; + $vars->{'changes'} = $changes; + delete_token($token); + + $template->process("admin/components/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # No valid action found diff --git a/editfields.cgi b/editfields.cgi index 747fa1e89..b62c632e8 100755 --- a/editfields.cgi +++ b/editfields.cgi @@ -19,151 +19,155 @@ use Bugzilla::Util; use Bugzilla::Field; use Bugzilla::Token; -my $cgi = Bugzilla->cgi; +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; # Make sure the user is logged in and is an administrator. my $user = Bugzilla->login(LOGIN_REQUIRED); $user->in_group('admin') - || ThrowUserError('auth_failure', {group => 'admin', - action => 'edit', - object => 'custom_fields'}); + || ThrowUserError('auth_failure', + {group => 'admin', action => 'edit', object => 'custom_fields'}); my $action = trim($cgi->param('action') || ''); -my $token = $cgi->param('token'); +my $token = $cgi->param('token'); print $cgi->header(); # List all existing custom fields if no action is given. if (!$action) { - $template->process('admin/custom_fields/list.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + $template->process('admin/custom_fields/list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } + # Interface to add a new custom field. elsif ($action eq 'add') { - $vars->{'token'} = issue_session_token('add_field'); + $vars->{'token'} = issue_session_token('add_field'); - $template->process('admin/custom_fields/create.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + $template->process('admin/custom_fields/create.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } elsif ($action eq 'new') { - check_token_data($token, 'add_field'); - $vars->{'field'} = Bugzilla::Field->create({ - name => scalar $cgi->param('name'), - description => scalar $cgi->param('desc'), - type => scalar $cgi->param('type'), - sortkey => scalar $cgi->param('sortkey'), - mailhead => scalar $cgi->param('new_bugmail'), - enter_bug => scalar $cgi->param('enter_bug'), - obsolete => scalar $cgi->param('obsolete'), - custom => 1, - buglist => 1, - visibility_field_id => scalar $cgi->param('visibility_field_id'), - visibility_values => [ $cgi->param('visibility_values') ], - value_field_id => scalar $cgi->param('value_field_id'), - reverse_desc => scalar $cgi->param('reverse_desc'), - is_mandatory => scalar $cgi->param('is_mandatory'), - }); - - delete_token($token); - - $vars->{'message'} = 'custom_field_created'; - - $template->process('admin/custom_fields/list.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + check_token_data($token, 'add_field'); + $vars->{'field'} = Bugzilla::Field->create({ + name => scalar $cgi->param('name'), + description => scalar $cgi->param('desc'), + type => scalar $cgi->param('type'), + sortkey => scalar $cgi->param('sortkey'), + mailhead => scalar $cgi->param('new_bugmail'), + enter_bug => scalar $cgi->param('enter_bug'), + obsolete => scalar $cgi->param('obsolete'), + custom => 1, + buglist => 1, + visibility_field_id => scalar $cgi->param('visibility_field_id'), + visibility_values => [$cgi->param('visibility_values')], + value_field_id => scalar $cgi->param('value_field_id'), + reverse_desc => scalar $cgi->param('reverse_desc'), + is_mandatory => scalar $cgi->param('is_mandatory'), + }); + + delete_token($token); + + $vars->{'message'} = 'custom_field_created'; + + $template->process('admin/custom_fields/list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } elsif ($action eq 'edit') { - my $name = $cgi->param('name') || ThrowUserError('field_missing_name'); - # Custom field names must start with "cf_". - if ($name !~ /^cf_/) { - $name = 'cf_' . $name; - } - my $field = new Bugzilla::Field({'name' => $name}); - $field || ThrowUserError('customfield_nonexistent', {'name' => $name}); - - $vars->{'field'} = $field; - $vars->{'token'} = issue_session_token('edit_field'); - - $template->process('admin/custom_fields/edit.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + my $name = $cgi->param('name') || ThrowUserError('field_missing_name'); + + # Custom field names must start with "cf_". + if ($name !~ /^cf_/) { + $name = 'cf_' . $name; + } + my $field = new Bugzilla::Field({'name' => $name}); + $field || ThrowUserError('customfield_nonexistent', {'name' => $name}); + + $vars->{'field'} = $field; + $vars->{'token'} = issue_session_token('edit_field'); + + $template->process('admin/custom_fields/edit.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } elsif ($action eq 'update') { - check_token_data($token, 'edit_field'); - my $name = $cgi->param('name'); - - # Validate fields. - $name || ThrowUserError('field_missing_name'); - # Custom field names must start with "cf_". - if ($name !~ /^cf_/) { - $name = 'cf_' . $name; - } - my $field = new Bugzilla::Field({'name' => $name}); - $field || ThrowUserError('customfield_nonexistent', {'name' => $name}); - - $field->set_description($cgi->param('desc')); - $field->set_sortkey($cgi->param('sortkey')); - $field->set_in_new_bugmail($cgi->param('new_bugmail')); - $field->set_enter_bug($cgi->param('enter_bug')); - $field->set_obsolete($cgi->param('obsolete')); - $field->set_is_mandatory($cgi->param('is_mandatory')); - $field->set_visibility_field($cgi->param('visibility_field_id')); - $field->set_visibility_values([ $cgi->param('visibility_values') ]); - $field->set_value_field($cgi->param('value_field_id')); - $field->set_reverse_desc($cgi->param('reverse_desc')); - $field->update(); - - delete_token($token); - - $vars->{'field'} = $field; - $vars->{'message'} = 'custom_field_updated'; - - $template->process('admin/custom_fields/list.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + check_token_data($token, 'edit_field'); + my $name = $cgi->param('name'); + + # Validate fields. + $name || ThrowUserError('field_missing_name'); + + # Custom field names must start with "cf_". + if ($name !~ /^cf_/) { + $name = 'cf_' . $name; + } + my $field = new Bugzilla::Field({'name' => $name}); + $field || ThrowUserError('customfield_nonexistent', {'name' => $name}); + + $field->set_description($cgi->param('desc')); + $field->set_sortkey($cgi->param('sortkey')); + $field->set_in_new_bugmail($cgi->param('new_bugmail')); + $field->set_enter_bug($cgi->param('enter_bug')); + $field->set_obsolete($cgi->param('obsolete')); + $field->set_is_mandatory($cgi->param('is_mandatory')); + $field->set_visibility_field($cgi->param('visibility_field_id')); + $field->set_visibility_values([$cgi->param('visibility_values')]); + $field->set_value_field($cgi->param('value_field_id')); + $field->set_reverse_desc($cgi->param('reverse_desc')); + $field->update(); + + delete_token($token); + + $vars->{'field'} = $field; + $vars->{'message'} = 'custom_field_updated'; + + $template->process('admin/custom_fields/list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } elsif ($action eq 'del') { - my $name = $cgi->param('name'); - - # Validate field. - $name || ThrowUserError('field_missing_name'); - # Custom field names must start with "cf_". - if ($name !~ /^cf_/) { - $name = 'cf_' . $name; - } - my $field = new Bugzilla::Field({'name' => $name}); - $field || ThrowUserError('customfield_nonexistent', {'name' => $name}); - - $vars->{'field'} = $field; - $vars->{'token'} = issue_session_token('delete_field'); - - $template->process('admin/custom_fields/confirm-delete.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + my $name = $cgi->param('name'); + + # Validate field. + $name || ThrowUserError('field_missing_name'); + + # Custom field names must start with "cf_". + if ($name !~ /^cf_/) { + $name = 'cf_' . $name; + } + my $field = new Bugzilla::Field({'name' => $name}); + $field || ThrowUserError('customfield_nonexistent', {'name' => $name}); + + $vars->{'field'} = $field; + $vars->{'token'} = issue_session_token('delete_field'); + + $template->process('admin/custom_fields/confirm-delete.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } elsif ($action eq 'delete') { - check_token_data($token, 'delete_field'); - my $name = $cgi->param('name'); + check_token_data($token, 'delete_field'); + my $name = $cgi->param('name'); + + # Validate fields. + $name || ThrowUserError('field_missing_name'); - # Validate fields. - $name || ThrowUserError('field_missing_name'); - # Custom field names must start with "cf_". - if ($name !~ /^cf_/) { - $name = 'cf_' . $name; - } - my $field = new Bugzilla::Field({'name' => $name}); - $field || ThrowUserError('customfield_nonexistent', {'name' => $name}); + # Custom field names must start with "cf_". + if ($name !~ /^cf_/) { + $name = 'cf_' . $name; + } + my $field = new Bugzilla::Field({'name' => $name}); + $field || ThrowUserError('customfield_nonexistent', {'name' => $name}); - # Calling remove_from_db will check if field can be deleted. - # If the field cannot be deleted, it will throw an error. - $field->remove_from_db(); + # Calling remove_from_db will check if field can be deleted. + # If the field cannot be deleted, it will throw an error. + $field->remove_from_db(); - $vars->{'field'} = $field; - $vars->{'message'} = 'custom_field_deleted'; + $vars->{'field'} = $field; + $vars->{'message'} = 'custom_field_deleted'; - delete_token($token); + delete_token($token); - $template->process('admin/custom_fields/list.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + $template->process('admin/custom_fields/list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } else { - ThrowUserError('unknown_action', {action => $action}); + ThrowUserError('unknown_action', {action => $action}); } diff --git a/editflagtypes.cgi b/editflagtypes.cgi index ff21d6aaf..af2686938 100755 --- a/editflagtypes.cgi +++ b/editflagtypes.cgi @@ -23,405 +23,430 @@ use Bugzilla::Product; use Bugzilla::Token; # Make sure the user is logged in and has the right privileges. -my $user = Bugzilla->login(LOGIN_REQUIRED); -my $cgi = Bugzilla->cgi; +my $user = Bugzilla->login(LOGIN_REQUIRED); +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; print $cgi->header(); $user->in_group('editcomponents') || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", {group => "editcomponents", - action => "edit", - object => "flagtypes"}); + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "flagtypes"}); # We need this everywhere. -my $vars = get_products_and_components(); +my $vars = get_products_and_components(); my @products = @{$vars->{products}}; -my $action = $cgi->param('action') || 'list'; -my $token = $cgi->param('token'); +my $action = $cgi->param('action') || 'list'; +my $token = $cgi->param('token'); my $prod_name = $cgi->param('product'); my $comp_name = $cgi->param('component'); -my $flag_id = $cgi->param('id'); +my $flag_id = $cgi->param('id'); my ($product, $component); if ($prod_name) { - # Make sure the user is allowed to view this product name. - # Users with global editcomponents privs can see all product names. - ($product) = grep { lc($_->name) eq lc($prod_name) } @products; - $product || ThrowUserError('product_access_denied', { name => $prod_name }); + + # Make sure the user is allowed to view this product name. + # Users with global editcomponents privs can see all product names. + ($product) = grep { lc($_->name) eq lc($prod_name) } @products; + $product || ThrowUserError('product_access_denied', {name => $prod_name}); } if ($comp_name) { - $product || ThrowUserError('flag_type_component_without_product'); - ($component) = grep { lc($_->name) eq lc($comp_name) } @{$product->components}; - $component || ThrowUserError('product_unknown_component', { product => $product->name, - comp => $comp_name }); + $product || ThrowUserError('flag_type_component_without_product'); + ($component) = grep { lc($_->name) eq lc($comp_name) } @{$product->components}; + $component + || ThrowUserError('product_unknown_component', + {product => $product->name, comp => $comp_name}); } # If 'categoryAction' is set, it has priority over 'action'. -if (my ($category_action) = grep { $_ =~ /^categoryAction-(?:\w+)$/ } $cgi->param()) { - $category_action =~ s/^categoryAction-//; - - my @inclusions = $cgi->param('inclusions'); - my @exclusions = $cgi->param('exclusions'); - my @categories; - if ($category_action =~ /^(in|ex)clude$/) { - if (!$user->in_group('editcomponents') && !$product) { - # The user can only add the flag type to products he can administrate. - foreach my $prod (@products) { - push(@categories, $prod->id . ':0') - } - } - else { - my $category = ($product ? $product->id : 0) . ':' . - ($component ? $component->id : 0); - push(@categories, $category); - } +if (my ($category_action) + = grep { $_ =~ /^categoryAction-(?:\w+)$/ } $cgi->param()) +{ + $category_action =~ s/^categoryAction-//; + + my @inclusions = $cgi->param('inclusions'); + my @exclusions = $cgi->param('exclusions'); + my @categories; + if ($category_action =~ /^(in|ex)clude$/) { + if (!$user->in_group('editcomponents') && !$product) { + + # The user can only add the flag type to products he can administrate. + foreach my $prod (@products) { + push(@categories, $prod->id . ':0'); + } + } + else { + my $category + = ($product ? $product->id : 0) . ':' . ($component ? $component->id : 0); + push(@categories, $category); } + } - if ($category_action eq 'include') { - foreach my $category (@categories) { - push(@inclusions, $category) unless grep($_ eq $category, @inclusions); - } + if ($category_action eq 'include') { + foreach my $category (@categories) { + push(@inclusions, $category) unless grep($_ eq $category, @inclusions); } - elsif ($category_action eq 'exclude') { - foreach my $category (@categories) { - push(@exclusions, $category) unless grep($_ eq $category, @exclusions); - } + } + elsif ($category_action eq 'exclude') { + foreach my $category (@categories) { + push(@exclusions, $category) unless grep($_ eq $category, @exclusions); } - elsif ($category_action eq 'removeInclusion') { - my @inclusion_to_remove = $cgi->param('inclusion_to_remove'); - foreach my $remove (@inclusion_to_remove) { - @inclusions = grep { $_ ne $remove } @inclusions; - } + } + elsif ($category_action eq 'removeInclusion') { + my @inclusion_to_remove = $cgi->param('inclusion_to_remove'); + foreach my $remove (@inclusion_to_remove) { + @inclusions = grep { $_ ne $remove } @inclusions; } - elsif ($category_action eq 'removeExclusion') { - my @exclusion_to_remove = $cgi->param('exclusion_to_remove'); - foreach my $remove (@exclusion_to_remove) { - @exclusions = grep { $_ ne $remove } @exclusions; - } + } + elsif ($category_action eq 'removeExclusion') { + my @exclusion_to_remove = $cgi->param('exclusion_to_remove'); + foreach my $remove (@exclusion_to_remove) { + @exclusions = grep { $_ ne $remove } @exclusions; } - - $vars->{'groups'} = get_settable_groups(); - $vars->{'action'} = $action; - - my $type = {}; - $type->{$_} = $cgi->param($_) foreach $cgi->param(); - # Make sure boolean fields are defined, else they fall back to 1. - foreach my $boolean (qw(is_active is_requestable is_requesteeble is_multiplicable)) { - $type->{$boolean} ||= 0; - } - - # That's what I call a big hack. The template expects to see a group object. - $type->{'grant_group'} = {}; - $type->{'grant_group'}->{'name'} = $cgi->param('grant_group'); - $type->{'request_group'} = {}; - $type->{'request_group'}->{'name'} = $cgi->param('request_group'); - - $vars->{'inclusions'} = clusion_array_to_hash(\@inclusions, \@products); - $vars->{'exclusions'} = clusion_array_to_hash(\@exclusions, \@products); - - $vars->{'type'} = $type; - $vars->{'token'} = $token; - $vars->{'check_clusions'} = 1; - $vars->{'can_fully_edit'} = $cgi->param('can_fully_edit'); - - $template->process("admin/flag-type/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + } + + $vars->{'groups'} = get_settable_groups(); + $vars->{'action'} = $action; + + my $type = {}; + $type->{$_} = $cgi->param($_) foreach $cgi->param(); + + # Make sure boolean fields are defined, else they fall back to 1. + foreach + my $boolean (qw(is_active is_requestable is_requesteeble is_multiplicable)) + { + $type->{$boolean} ||= 0; + } + + # That's what I call a big hack. The template expects to see a group object. + $type->{'grant_group'} = {}; + $type->{'grant_group'}->{'name'} = $cgi->param('grant_group'); + $type->{'request_group'} = {}; + $type->{'request_group'}->{'name'} = $cgi->param('request_group'); + + $vars->{'inclusions'} = clusion_array_to_hash(\@inclusions, \@products); + $vars->{'exclusions'} = clusion_array_to_hash(\@exclusions, \@products); + + $vars->{'type'} = $type; + $vars->{'token'} = $token; + $vars->{'check_clusions'} = 1; + $vars->{'can_fully_edit'} = $cgi->param('can_fully_edit'); + + $template->process("admin/flag-type/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'list') { - my $product_id = $product ? $product->id : 0; - my $component_id = $component ? $component->id : 0; - my $show_flag_counts = $cgi->param('show_flag_counts') ? 1 : 0; - my $group_id = $cgi->param('group'); - if ($group_id) { - detaint_natural($group_id) || ThrowUserError('invalid_group_ID'); - } + my $product_id = $product ? $product->id : 0; + my $component_id = $component ? $component->id : 0; + my $show_flag_counts = $cgi->param('show_flag_counts') ? 1 : 0; + my $group_id = $cgi->param('group'); + if ($group_id) { + detaint_natural($group_id) || ThrowUserError('invalid_group_ID'); + } + + my $bug_flagtypes; + my $attach_flagtypes; + + # If a component is given, restrict the list to flag types available + # for this component. + if ($component) { + $bug_flagtypes = $component->flag_types->{'bug'}; + $attach_flagtypes = $component->flag_types->{'attachment'}; - my $bug_flagtypes; - my $attach_flagtypes; + # Filter flag types if a group ID is given. + $bug_flagtypes = filter_group($bug_flagtypes, $group_id); + $attach_flagtypes = filter_group($attach_flagtypes, $group_id); - # If a component is given, restrict the list to flag types available - # for this component. - if ($component) { - $bug_flagtypes = $component->flag_types->{'bug'}; - $attach_flagtypes = $component->flag_types->{'attachment'}; + } - # Filter flag types if a group ID is given. - $bug_flagtypes = filter_group($bug_flagtypes, $group_id); - $attach_flagtypes = filter_group($attach_flagtypes, $group_id); + # If only a product is specified but no component, then restrict the list + # to flag types available in at least one component of that product. + elsif ($product) { + $bug_flagtypes = $product->flag_types->{'bug'}; + $attach_flagtypes = $product->flag_types->{'attachment'}; + # Filter flag types if a group ID is given. + $bug_flagtypes = filter_group($bug_flagtypes, $group_id); + $attach_flagtypes = filter_group($attach_flagtypes, $group_id); + } + + # If no product is given, then show all flag types available. + else { + my $flagtypes = get_editable_flagtypes(\@products, $group_id); + $bug_flagtypes = [grep { $_->target_type eq 'bug' } @$flagtypes]; + $attach_flagtypes = [grep { $_->target_type eq 'attachment' } @$flagtypes]; + } + + if ($show_flag_counts) { + my %bug_lists; + my %map = ('+' => 'granted', '-' => 'denied', '?' => 'pending'); + + foreach my $flagtype (@$bug_flagtypes, @$attach_flagtypes) { + $bug_lists{$flagtype->id} = {}; + my $flags = Bugzilla::Flag->match({type_id => $flagtype->id}); + + # Build lists of bugs, triaged by flag status. + push(@{$bug_lists{$flagtype->id}->{$map{$_->status}}}, $_->bug_id) + foreach @$flags; } - # If only a product is specified but no component, then restrict the list - # to flag types available in at least one component of that product. - elsif ($product) { - $bug_flagtypes = $product->flag_types->{'bug'}; - $attach_flagtypes = $product->flag_types->{'attachment'}; - - # Filter flag types if a group ID is given. - $bug_flagtypes = filter_group($bug_flagtypes, $group_id); - $attach_flagtypes = filter_group($attach_flagtypes, $group_id); - } - # If no product is given, then show all flag types available. - else { - my $flagtypes = get_editable_flagtypes(\@products, $group_id); - $bug_flagtypes = [grep { $_->target_type eq 'bug' } @$flagtypes]; - $attach_flagtypes = [grep { $_->target_type eq 'attachment' } @$flagtypes]; - } - - if ($show_flag_counts) { - my %bug_lists; - my %map = ('+' => 'granted', '-' => 'denied', '?' => 'pending'); - - foreach my $flagtype (@$bug_flagtypes, @$attach_flagtypes) { - $bug_lists{$flagtype->id} = {}; - my $flags = Bugzilla::Flag->match({type_id => $flagtype->id}); - # Build lists of bugs, triaged by flag status. - push(@{$bug_lists{$flagtype->id}->{$map{$_->status}}}, $_->bug_id) foreach @$flags; - } - $vars->{'bug_lists'} = \%bug_lists; - $vars->{'show_flag_counts'} = 1; - } - - $vars->{'selected_product'} = $product ? $product->name : ''; - $vars->{'selected_component'} = $component ? $component->name : ''; - $vars->{'bug_types'} = $bug_flagtypes; - $vars->{'attachment_types'} = $attach_flagtypes; - - $template->process("admin/flag-type/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'bug_lists'} = \%bug_lists; + $vars->{'show_flag_counts'} = 1; + } + + $vars->{'selected_product'} = $product ? $product->name : ''; + $vars->{'selected_component'} = $component ? $component->name : ''; + $vars->{'bug_types'} = $bug_flagtypes; + $vars->{'attachment_types'} = $attach_flagtypes; + + $template->process("admin/flag-type/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'enter') { - my $type = $cgi->param('target_type'); - ($type eq 'bug' || $type eq 'attachment') - || ThrowCodeError('flag_type_target_type_invalid', { target_type => $type }); - - $vars->{'action'} = 'insert'; - $vars->{'token'} = issue_session_token('add_flagtype'); - $vars->{'type'} = { 'target_type' => $type }; - # Only users with global editcomponents privs can add a flagtype - # to all products. - $vars->{'inclusions'} = { '__Any__:__Any__' => '0:0' } - if $user->in_group('editcomponents'); - $vars->{'can_fully_edit'} = 1; - # Get a list of groups available to restrict this flag type against. - $vars->{'groups'} = get_settable_groups(); - - $template->process("admin/flag-type/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $type = $cgi->param('target_type'); + ($type eq 'bug' || $type eq 'attachment') + || ThrowCodeError('flag_type_target_type_invalid', {target_type => $type}); + + $vars->{'action'} = 'insert'; + $vars->{'token'} = issue_session_token('add_flagtype'); + $vars->{'type'} = {'target_type' => $type}; + + # Only users with global editcomponents privs can add a flagtype + # to all products. + $vars->{'inclusions'} = {'__Any__:__Any__' => '0:0'} + if $user->in_group('editcomponents'); + $vars->{'can_fully_edit'} = 1; + + # Get a list of groups available to restrict this flag type against. + $vars->{'groups'} = get_settable_groups(); + + $template->process("admin/flag-type/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'edit' || $action eq 'copy') { - my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); - $vars->{'type'} = $flagtype; - $vars->{'can_fully_edit'} = $can_fully_edit; - - if ($user->in_group('editcomponents')) { - $vars->{'inclusions'} = $flagtype->inclusions; - $vars->{'exclusions'} = $flagtype->exclusions; - } - else { - # Filter products the user shouldn't know about. - $vars->{'inclusions'} = clusion_array_to_hash([values %{$flagtype->inclusions}], \@products); - $vars->{'exclusions'} = clusion_array_to_hash([values %{$flagtype->exclusions}], \@products); - } - - if ($action eq 'copy') { - $vars->{'action'} = "insert"; - $vars->{'token'} = issue_session_token('add_flagtype'); - } - else { - $vars->{'action'} = "update"; - $vars->{'token'} = issue_session_token('edit_flagtype'); - } - - # Get a list of groups available to restrict this flag type against. - $vars->{'groups'} = get_settable_groups(); - - $template->process("admin/flag-type/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); + $vars->{'type'} = $flagtype; + $vars->{'can_fully_edit'} = $can_fully_edit; + + if ($user->in_group('editcomponents')) { + $vars->{'inclusions'} = $flagtype->inclusions; + $vars->{'exclusions'} = $flagtype->exclusions; + } + else { + # Filter products the user shouldn't know about. + $vars->{'inclusions'} + = clusion_array_to_hash([values %{$flagtype->inclusions}], \@products); + $vars->{'exclusions'} + = clusion_array_to_hash([values %{$flagtype->exclusions}], \@products); + } + + if ($action eq 'copy') { + $vars->{'action'} = "insert"; + $vars->{'token'} = issue_session_token('add_flagtype'); + } + else { + $vars->{'action'} = "update"; + $vars->{'token'} = issue_session_token('edit_flagtype'); + } + + # Get a list of groups available to restrict this flag type against. + $vars->{'groups'} = get_settable_groups(); + + $template->process("admin/flag-type/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'insert') { - check_token_data($token, 'add_flagtype'); - - my $name = $cgi->param('name'); - my $description = $cgi->param('description'); - my $target_type = $cgi->param('target_type'); - my $cc_list = $cgi->param('cc_list'); - my $sortkey = $cgi->param('sortkey'); - my $is_active = $cgi->param('is_active'); - my $is_requestable = $cgi->param('is_requestable'); - my $is_specifically = $cgi->param('is_requesteeble'); - my $is_multiplicable = $cgi->param('is_multiplicable'); - my $grant_group = $cgi->param('grant_group'); - my $request_group = $cgi->param('request_group'); - my @inclusions = $cgi->param('inclusions'); - my @exclusions = $cgi->param('exclusions'); - - # Filter inclusion and exclusion lists to products the user can see. - unless ($user->in_group('editcomponents')) { - @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)}; - @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)}; - } - - my $flagtype = Bugzilla::FlagType->create({ - name => $name, - description => $description, - target_type => $target_type, - cc_list => $cc_list, - sortkey => $sortkey, - is_active => $is_active, - is_requestable => $is_requestable, - is_requesteeble => $is_specifically, - is_multiplicable => $is_multiplicable, - grant_group => $grant_group, - request_group => $request_group, - inclusions => \@inclusions, - exclusions => \@exclusions - }); - - delete_token($token); - - $vars->{'name'} = $flagtype->name; - $vars->{'message'} = "flag_type_created"; - - my $flagtypes = get_editable_flagtypes(\@products); - $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes]; - $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @$flagtypes]; - - $template->process("admin/flag-type/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + check_token_data($token, 'add_flagtype'); + + my $name = $cgi->param('name'); + my $description = $cgi->param('description'); + my $target_type = $cgi->param('target_type'); + my $cc_list = $cgi->param('cc_list'); + my $sortkey = $cgi->param('sortkey'); + my $is_active = $cgi->param('is_active'); + my $is_requestable = $cgi->param('is_requestable'); + my $is_specifically = $cgi->param('is_requesteeble'); + my $is_multiplicable = $cgi->param('is_multiplicable'); + my $grant_group = $cgi->param('grant_group'); + my $request_group = $cgi->param('request_group'); + my @inclusions = $cgi->param('inclusions'); + my @exclusions = $cgi->param('exclusions'); + + # Filter inclusion and exclusion lists to products the user can see. + unless ($user->in_group('editcomponents')) { + @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)}; + @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)}; + } + + my $flagtype = Bugzilla::FlagType->create({ + name => $name, + description => $description, + target_type => $target_type, + cc_list => $cc_list, + sortkey => $sortkey, + is_active => $is_active, + is_requestable => $is_requestable, + is_requesteeble => $is_specifically, + is_multiplicable => $is_multiplicable, + grant_group => $grant_group, + request_group => $request_group, + inclusions => \@inclusions, + exclusions => \@exclusions + }); + + delete_token($token); + + $vars->{'name'} = $flagtype->name; + $vars->{'message'} = "flag_type_created"; + + my $flagtypes = get_editable_flagtypes(\@products); + $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes]; + $vars->{'attachment_types'} + = [grep { $_->target_type eq 'attachment' } @$flagtypes]; + + $template->process("admin/flag-type/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'update') { - check_token_data($token, 'edit_flagtype'); - - my $name = $cgi->param('name'); - my $description = $cgi->param('description'); - my $cc_list = $cgi->param('cc_list'); - my $sortkey = $cgi->param('sortkey'); - my $is_active = $cgi->param('is_active'); - my $is_requestable = $cgi->param('is_requestable'); - my $is_specifically = $cgi->param('is_requesteeble'); - my $is_multiplicable = $cgi->param('is_multiplicable'); - my $grant_group = $cgi->param('grant_group'); - my $request_group = $cgi->param('request_group'); - my @inclusions = $cgi->param('inclusions'); - my @exclusions = $cgi->param('exclusions'); - - my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); - if ($cgi->param('check_clusions') && !$user->in_group('editcomponents')) { - # Filter inclusion and exclusion lists to products the user can edit. - @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)}; - @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)}; - # Bring back the products the user cannot edit. - foreach my $item (values %{$flagtype->inclusions}) { - my ($prod_id, $comp_id) = split(':', $item); - push(@inclusions, $item) unless grep { $_->id == $prod_id } @products; - } - foreach my $item (values %{$flagtype->exclusions}) { - my ($prod_id, $comp_id) = split(':', $item); - push(@exclusions, $item) unless grep { $_->id == $prod_id } @products; - } + check_token_data($token, 'edit_flagtype'); + + my $name = $cgi->param('name'); + my $description = $cgi->param('description'); + my $cc_list = $cgi->param('cc_list'); + my $sortkey = $cgi->param('sortkey'); + my $is_active = $cgi->param('is_active'); + my $is_requestable = $cgi->param('is_requestable'); + my $is_specifically = $cgi->param('is_requesteeble'); + my $is_multiplicable = $cgi->param('is_multiplicable'); + my $grant_group = $cgi->param('grant_group'); + my $request_group = $cgi->param('request_group'); + my @inclusions = $cgi->param('inclusions'); + my @exclusions = $cgi->param('exclusions'); + + my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); + if ($cgi->param('check_clusions') && !$user->in_group('editcomponents')) { + + # Filter inclusion and exclusion lists to products the user can edit. + @inclusions = values %{clusion_array_to_hash(\@inclusions, \@products)}; + @exclusions = values %{clusion_array_to_hash(\@exclusions, \@products)}; + + # Bring back the products the user cannot edit. + foreach my $item (values %{$flagtype->inclusions}) { + my ($prod_id, $comp_id) = split(':', $item); + push(@inclusions, $item) unless grep { $_->id == $prod_id } @products; } - - if ($can_fully_edit) { - $flagtype->set_name($name); - $flagtype->set_description($description); - $flagtype->set_cc_list($cc_list); - $flagtype->set_sortkey($sortkey); - $flagtype->set_is_active($is_active); - $flagtype->set_is_requestable($is_requestable); - $flagtype->set_is_specifically_requestable($is_specifically); - $flagtype->set_is_multiplicable($is_multiplicable); - $flagtype->set_grant_group($grant_group); - $flagtype->set_request_group($request_group); + foreach my $item (values %{$flagtype->exclusions}) { + my ($prod_id, $comp_id) = split(':', $item); + push(@exclusions, $item) unless grep { $_->id == $prod_id } @products; } - $flagtype->set_clusions({ inclusions => \@inclusions, exclusions => \@exclusions}) - if $cgi->param('check_clusions'); - my $changes = $flagtype->update(); - - delete_token($token); - - $vars->{'flagtype'} = $flagtype; - $vars->{'changes'} = $changes; - $vars->{'message'} = 'flag_type_updated'; - - my $flagtypes = get_editable_flagtypes(\@products); - $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes]; - $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @$flagtypes]; - - $template->process("admin/flag-type/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + } + + if ($can_fully_edit) { + $flagtype->set_name($name); + $flagtype->set_description($description); + $flagtype->set_cc_list($cc_list); + $flagtype->set_sortkey($sortkey); + $flagtype->set_is_active($is_active); + $flagtype->set_is_requestable($is_requestable); + $flagtype->set_is_specifically_requestable($is_specifically); + $flagtype->set_is_multiplicable($is_multiplicable); + $flagtype->set_grant_group($grant_group); + $flagtype->set_request_group($request_group); + } + $flagtype->set_clusions( + {inclusions => \@inclusions, exclusions => \@exclusions}) + if $cgi->param('check_clusions'); + my $changes = $flagtype->update(); + + delete_token($token); + + $vars->{'flagtype'} = $flagtype; + $vars->{'changes'} = $changes; + $vars->{'message'} = 'flag_type_updated'; + + my $flagtypes = get_editable_flagtypes(\@products); + $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @$flagtypes]; + $vars->{'attachment_types'} + = [grep { $_->target_type eq 'attachment' } @$flagtypes]; + + $template->process("admin/flag-type/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'confirmdelete') { - my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); - ThrowUserError('flag_type_cannot_delete', { flagtype => $flagtype }) unless $can_fully_edit; + my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); + ThrowUserError('flag_type_cannot_delete', {flagtype => $flagtype}) + unless $can_fully_edit; - $vars->{'flag_type'} = $flagtype; - $vars->{'token'} = issue_session_token('delete_flagtype'); + $vars->{'flag_type'} = $flagtype; + $vars->{'token'} = issue_session_token('delete_flagtype'); - $template->process("admin/flag-type/confirm-delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/flag-type/confirm-delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'delete') { - check_token_data($token, 'delete_flagtype'); + check_token_data($token, 'delete_flagtype'); - my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); - ThrowUserError('flag_type_cannot_delete', { flagtype => $flagtype }) unless $can_fully_edit; + my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); + ThrowUserError('flag_type_cannot_delete', {flagtype => $flagtype}) + unless $can_fully_edit; - $flagtype->remove_from_db(); + $flagtype->remove_from_db(); - delete_token($token); + delete_token($token); - $vars->{'name'} = $flagtype->name; - $vars->{'message'} = "flag_type_deleted"; + $vars->{'name'} = $flagtype->name; + $vars->{'message'} = "flag_type_deleted"; - my @flagtypes = Bugzilla::FlagType->get_all; - $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes]; - $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @flagtypes]; + my @flagtypes = Bugzilla::FlagType->get_all; + $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes]; + $vars->{'attachment_types'} + = [grep { $_->target_type eq 'attachment' } @flagtypes]; - $template->process("admin/flag-type/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/flag-type/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'deactivate') { - check_token_data($token, 'delete_flagtype'); + check_token_data($token, 'delete_flagtype'); - my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); - ThrowUserError('flag_type_cannot_deactivate', { flagtype => $flagtype }) unless $can_fully_edit; + my ($flagtype, $can_fully_edit) = $user->check_can_admin_flagtype($flag_id); + ThrowUserError('flag_type_cannot_deactivate', {flagtype => $flagtype}) + unless $can_fully_edit; - $flagtype->set_is_active(0); - $flagtype->update(); + $flagtype->set_is_active(0); + $flagtype->update(); - delete_token($token); + delete_token($token); - $vars->{'message'} = "flag_type_deactivated"; - $vars->{'flag_type'} = $flagtype; + $vars->{'message'} = "flag_type_deactivated"; + $vars->{'flag_type'} = $flagtype; - my @flagtypes = Bugzilla::FlagType->get_all; - $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes]; - $vars->{'attachment_types'} = [grep { $_->target_type eq 'attachment' } @flagtypes]; + my @flagtypes = Bugzilla::FlagType->get_all; + $vars->{'bug_types'} = [grep { $_->target_type eq 'bug' } @flagtypes]; + $vars->{'attachment_types'} + = [grep { $_->target_type eq 'attachment' } @flagtypes]; - $template->process("admin/flag-type/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/flag-type/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } ThrowUserError('unknown_action', {action => $action}); @@ -431,102 +456,110 @@ ThrowUserError('unknown_action', {action => $action}); ##################### sub get_products_and_components { - my $vars = {}; - my $user = Bugzilla->user; - - my @products; - if ($user->in_group('editcomponents')) { - @products = Bugzilla::Product->get_all; - } - else { - @products = @{$user->get_products_by_permission('editcomponents')}; + my $vars = {}; + my $user = Bugzilla->user; + + my @products; + if ($user->in_group('editcomponents')) { + @products = Bugzilla::Product->get_all; + } + else { + @products = @{$user->get_products_by_permission('editcomponents')}; + } + + # We require all unique component names. + my %components; + foreach my $product (@products) { + foreach my $component (@{$product->components}) { + $components{$component->name} = 1; } - # We require all unique component names. - my %components; - foreach my $product (@products) { - foreach my $component (@{$product->components}) { - $components{$component->name} = 1; - } - } - $vars->{'products'} = \@products; - $vars->{'components'} = [sort(keys %components)]; - return $vars; + } + $vars->{'products'} = \@products; + $vars->{'components'} = [sort(keys %components)]; + return $vars; } sub get_editable_flagtypes { - my ($products, $group_id) = @_; - my $flagtypes; + my ($products, $group_id) = @_; + my $flagtypes; - if (Bugzilla->user->in_group('editcomponents')) { - $flagtypes = Bugzilla::FlagType::match({ group => $group_id }); - return $flagtypes; - } + if (Bugzilla->user->in_group('editcomponents')) { + $flagtypes = Bugzilla::FlagType::match({group => $group_id}); + return $flagtypes; + } - my %visible_flagtypes; - foreach my $product (@$products) { - foreach my $target ('bug', 'attachment') { - my $prod_flagtypes = $product->flag_types->{$target}; - $visible_flagtypes{$_->id} ||= $_ foreach @$prod_flagtypes; - } + my %visible_flagtypes; + foreach my $product (@$products) { + foreach my $target ('bug', 'attachment') { + my $prod_flagtypes = $product->flag_types->{$target}; + $visible_flagtypes{$_->id} ||= $_ foreach @$prod_flagtypes; } - @$flagtypes = sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } - values %visible_flagtypes; - # Filter flag types if a group ID is given. - $flagtypes = filter_group($flagtypes, $group_id); - return $flagtypes; + } + @$flagtypes = sort { $a->sortkey <=> $b->sortkey || $a->name cmp $b->name } + values %visible_flagtypes; + + # Filter flag types if a group ID is given. + $flagtypes = filter_group($flagtypes, $group_id); + return $flagtypes; } sub get_settable_groups { - my $user = Bugzilla->user; - my $groups = $user->in_group('editcomponents') ? [Bugzilla::Group->get_all] : $user->groups; - return $groups; + my $user = Bugzilla->user; + my $groups + = $user->in_group('editcomponents') + ? [Bugzilla::Group->get_all] + : $user->groups; + return $groups; } sub filter_group { - my ($flag_types, $gid) = @_; - return $flag_types unless $gid; + my ($flag_types, $gid) = @_; + return $flag_types unless $gid; - my @flag_types = grep {($_->grant_group && $_->grant_group->id == $gid) - || ($_->request_group && $_->request_group->id == $gid)} @$flag_types; + my @flag_types = grep { + ($_->grant_group && $_->grant_group->id == $gid) + || ($_->request_group && $_->request_group->id == $gid) + } @$flag_types; - return \@flag_types; + return \@flag_types; } # Convert the array @clusions('prod_ID:comp_ID') back to a hash of # the form %clusions{'prod_name:comp_name'} = 'prod_ID:comp_ID' sub clusion_array_to_hash { - my ($array, $visible_products) = @_; - my $user = Bugzilla->user; - my $has_privs = $user->in_group('editcomponents'); - - my %hash; - my %products; - my %components; - - foreach my $ids (@$array) { - my ($product_id, $component_id) = split(":", $ids); - my $product_name = "__Any__"; - my $component_name = "__Any__"; - - if ($product_id) { - ($products{$product_id}) = grep { $_->id == $product_id } @$visible_products; - next unless $products{$product_id}; - $product_name = $products{$product_id}->name; - - if ($component_id) { - ($components{$component_id}) = - grep { $_->id == $component_id } @{$products{$product_id}->components}; - next unless $components{$component_id}; - $component_name = $components{$component_id}->name; - } - } - else { - # Users with local editcomponents privs cannot use __Any__:__Any__. - next unless $has_privs; - # It's illegal to select a component without a product. - next if $component_id; - } - $hash{"$product_name:$component_name"} = $ids; + my ($array, $visible_products) = @_; + my $user = Bugzilla->user; + my $has_privs = $user->in_group('editcomponents'); + + my %hash; + my %products; + my %components; + + foreach my $ids (@$array) { + my ($product_id, $component_id) = split(":", $ids); + my $product_name = "__Any__"; + my $component_name = "__Any__"; + + if ($product_id) { + ($products{$product_id}) = grep { $_->id == $product_id } @$visible_products; + next unless $products{$product_id}; + $product_name = $products{$product_id}->name; + + if ($component_id) { + ($components{$component_id}) + = grep { $_->id == $component_id } @{$products{$product_id}->components}; + next unless $components{$component_id}; + $component_name = $components{$component_id}->name; + } + } + else { + # Users with local editcomponents privs cannot use __Any__:__Any__. + next unless $has_privs; + + # It's illegal to select a component without a product. + next if $component_id; } - return \%hash; + $hash{"$product_name:$component_name"} = $ids; + } + return \%hash; } diff --git a/editgroups.cgi b/editgroups.cgi index b67b76755..9e3ffc6bf 100755 --- a/editgroups.cgi +++ b/editgroups.cgi @@ -22,39 +22,40 @@ use Bugzilla::Product; use Bugzilla::User; use Bugzilla::Token; -use constant SPECIAL_GROUPS => ('chartgroup', 'insidergroup', - 'timetrackinggroup', 'querysharegroup'); +use constant SPECIAL_GROUPS => + ('chartgroup', 'insidergroup', 'timetrackinggroup', 'querysharegroup'); -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; my $user = Bugzilla->login(LOGIN_REQUIRED); print $cgi->header(); $user->in_group('creategroups') - || ThrowUserError("auth_failure", {group => "creategroups", - action => "edit", - object => "groups"}); + || ThrowUserError("auth_failure", + {group => "creategroups", action => "edit", object => "groups"}); my $action = trim($cgi->param('action') || ''); -my $token = $cgi->param('token'); +my $token = $cgi->param('token'); # CheckGroupID checks that a positive integer is given and is # actually a valid group ID. If all tests are successful, the # trimmed group ID is returned. sub CheckGroupID { - my ($group_id) = @_; - $group_id = trim($group_id || 0); - ThrowUserError("group_not_specified") unless $group_id; - (detaint_natural($group_id) - && Bugzilla->dbh->selectrow_array("SELECT id FROM groups WHERE id = ?", - undef, $group_id)) - || ThrowUserError("invalid_group_ID"); - return $group_id; + my ($group_id) = @_; + $group_id = trim($group_id || 0); + ThrowUserError("group_not_specified") unless $group_id; + ( + detaint_natural($group_id) && Bugzilla->dbh->selectrow_array( + "SELECT id FROM groups WHERE id = ?", + undef, $group_id + ) + ) || ThrowUserError("invalid_group_ID"); + return $group_id; } # CheckGroupRegexp checks that the regular expression is valid @@ -63,86 +64,87 @@ sub CheckGroupID { # is returned. sub CheckGroupRegexp { - my ($regexp) = @_; - $regexp = trim($regexp || ''); - trick_taint($regexp); - ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/}); - return $regexp; + my ($regexp) = @_; + $regexp = trim($regexp || ''); + trick_taint($regexp); + ThrowUserError("invalid_regexp") unless (eval {qr/$regexp/}); + return $regexp; } # A helper for displaying the edit.html.tmpl template. sub get_current_and_available { - my ($group, $vars) = @_; - - my @all_groups = Bugzilla::Group->get_all; - my @members_current = @{$group->grant_direct(GROUP_MEMBERSHIP)}; - my @member_of_current = @{$group->granted_by_direct(GROUP_MEMBERSHIP)}; - my @bless_from_current = @{$group->grant_direct(GROUP_BLESS)}; - my @bless_to_current = @{$group->granted_by_direct(GROUP_BLESS)}; - my (@visible_from_current, @visible_to_me_current); + my ($group, $vars) = @_; + + my @all_groups = Bugzilla::Group->get_all; + my @members_current = @{$group->grant_direct(GROUP_MEMBERSHIP)}; + my @member_of_current = @{$group->granted_by_direct(GROUP_MEMBERSHIP)}; + my @bless_from_current = @{$group->grant_direct(GROUP_BLESS)}; + my @bless_to_current = @{$group->granted_by_direct(GROUP_BLESS)}; + my (@visible_from_current, @visible_to_me_current); + if (Bugzilla->params->{'usevisibilitygroups'}) { + @visible_from_current = @{$group->grant_direct(GROUP_VISIBLE)}; + @visible_to_me_current = @{$group->granted_by_direct(GROUP_VISIBLE)}; + } + + # Figure out what groups are not currently a member of this group, + # and what groups this group is not currently a member of. + my ( + @members_available, @member_of_available, @bless_from_available, + @bless_to_available, @visible_from_available, @visible_to_me_available + ); + foreach my $group_option (@all_groups) { if (Bugzilla->params->{'usevisibilitygroups'}) { - @visible_from_current = @{$group->grant_direct(GROUP_VISIBLE)}; - @visible_to_me_current = @{$group->granted_by_direct(GROUP_VISIBLE)}; + push(@visible_from_available, $group_option) + if !grep($_->id == $group_option->id, @visible_from_current); + push(@visible_to_me_available, $group_option) + if !grep($_->id == $group_option->id, @visible_to_me_current); } - # Figure out what groups are not currently a member of this group, - # and what groups this group is not currently a member of. - my (@members_available, @member_of_available, - @bless_from_available, @bless_to_available, - @visible_from_available, @visible_to_me_available); - foreach my $group_option (@all_groups) { - if (Bugzilla->params->{'usevisibilitygroups'}) { - push(@visible_from_available, $group_option) - if !grep($_->id == $group_option->id, @visible_from_current); - push(@visible_to_me_available, $group_option) - if !grep($_->id == $group_option->id, @visible_to_me_current); - } - - push(@bless_from_available, $group_option) - if !grep($_->id == $group_option->id, @bless_from_current); - - # The group itself should never show up in the membership lists, - # and should show up in only one of the bless lists (otherwise - # you can try to allow it to bless itself twice, leading to a - # database unique constraint error). - next if $group_option->id == $group->id; - - push(@members_available, $group_option) - if !grep($_->id == $group_option->id, @members_current); - push(@member_of_available, $group_option) - if !grep($_->id == $group_option->id, @member_of_current); - push(@bless_to_available, $group_option) - if !grep($_->id == $group_option->id, @bless_to_current); - } - - $vars->{'members_current'} = \@members_current; - $vars->{'members_available'} = \@members_available; - $vars->{'member_of_current'} = \@member_of_current; - $vars->{'member_of_available'} = \@member_of_available; - - $vars->{'bless_from_current'} = \@bless_from_current; - $vars->{'bless_from_available'} = \@bless_from_available; - $vars->{'bless_to_current'} = \@bless_to_current; - $vars->{'bless_to_available'} = \@bless_to_available; - - if (Bugzilla->params->{'usevisibilitygroups'}) { - $vars->{'visible_from_current'} = \@visible_from_current; - $vars->{'visible_from_available'} = \@visible_from_available; - $vars->{'visible_to_me_current'} = \@visible_to_me_current; - $vars->{'visible_to_me_available'} = \@visible_to_me_available; - } + push(@bless_from_available, $group_option) + if !grep($_->id == $group_option->id, @bless_from_current); + + # The group itself should never show up in the membership lists, + # and should show up in only one of the bless lists (otherwise + # you can try to allow it to bless itself twice, leading to a + # database unique constraint error). + next if $group_option->id == $group->id; + + push(@members_available, $group_option) + if !grep($_->id == $group_option->id, @members_current); + push(@member_of_available, $group_option) + if !grep($_->id == $group_option->id, @member_of_current); + push(@bless_to_available, $group_option) + if !grep($_->id == $group_option->id, @bless_to_current); + } + + $vars->{'members_current'} = \@members_current; + $vars->{'members_available'} = \@members_available; + $vars->{'member_of_current'} = \@member_of_current; + $vars->{'member_of_available'} = \@member_of_available; + + $vars->{'bless_from_current'} = \@bless_from_current; + $vars->{'bless_from_available'} = \@bless_from_available; + $vars->{'bless_to_current'} = \@bless_to_current; + $vars->{'bless_to_available'} = \@bless_to_available; + + if (Bugzilla->params->{'usevisibilitygroups'}) { + $vars->{'visible_from_current'} = \@visible_from_current; + $vars->{'visible_from_available'} = \@visible_from_available; + $vars->{'visible_to_me_current'} = \@visible_to_me_current; + $vars->{'visible_to_me_available'} = \@visible_to_me_available; + } } # If no action is specified, get a list of all groups available. unless ($action) { - my @groups = Bugzilla::Group->get_all; - $vars->{'groups'} = \@groups; + my @groups = Bugzilla::Group->get_all; + $vars->{'groups'} = \@groups; - print $cgi->header(); - $template->process("admin/groups/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + print $cgi->header(); + $template->process("admin/groups/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -152,20 +154,21 @@ unless ($action) { # if ($action eq 'changeform') { - # Check that an existing group ID is given - my $group_id = CheckGroupID($cgi->param('group')); - my $group = new Bugzilla::Group($group_id); - check_for_restricted_groups([ $group ]); - get_current_and_available($group, $vars); - $vars->{'group'} = $group; - $vars->{'token'} = issue_session_token('edit_group'); + # Check that an existing group ID is given + my $group_id = CheckGroupID($cgi->param('group')); + my $group = new Bugzilla::Group($group_id); + check_for_restricted_groups([$group]); + + get_current_and_available($group, $vars); + $vars->{'group'} = $group; + $vars->{'token'} = issue_session_token('edit_group'); - print $cgi->header(); - $template->process("admin/groups/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("admin/groups/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # @@ -175,51 +178,52 @@ if ($action eq 'changeform') { # if ($action eq 'add') { - $vars->{'token'} = issue_session_token('add_group'); - print $cgi->header(); - $template->process("admin/groups/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $vars->{'token'} = issue_session_token('add_group'); + print $cgi->header(); + $template->process("admin/groups/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } - # # action='new' -> add group entered in the 'action=add' screen # if ($action eq 'new') { - check_token_data($token, 'add_group'); - my $group = Bugzilla::Group->create({ - name => scalar $cgi->param('name'), - description => scalar $cgi->param('desc'), - userregexp => scalar $cgi->param('regexp'), - isactive => scalar $cgi->param('isactive'), - icon_url => scalar $cgi->param('icon_url'), - idle_member_removal => scalar $cgi->param('idle_member_removal'), - isbuggroup => 1, - owner_user_id => scalar $cgi->param('owner'), - }); - - # Permit all existing products to use the new group if makeproductgroups. - if ($cgi->param('insertnew')) { - $dbh->do('INSERT INTO group_control_map + check_token_data($token, 'add_group'); + my $group = Bugzilla::Group->create({ + name => scalar $cgi->param('name'), + description => scalar $cgi->param('desc'), + userregexp => scalar $cgi->param('regexp'), + isactive => scalar $cgi->param('isactive'), + icon_url => scalar $cgi->param('icon_url'), + idle_member_removal => scalar $cgi->param('idle_member_removal'), + isbuggroup => 1, + owner_user_id => scalar $cgi->param('owner'), + }); + + # Permit all existing products to use the new group if makeproductgroups. + if ($cgi->param('insertnew')) { + $dbh->do( + 'INSERT INTO group_control_map (group_id, product_id, membercontrol, othercontrol) - SELECT ?, products.id, ?, ? FROM products', - undef, ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA)); - } - delete_token($token); - - $vars->{'message'} = 'group_created'; - $vars->{'group'} = $group; - get_current_and_available($group, $vars); - $vars->{'token'} = issue_session_token('edit_group'); - - print $cgi->header(); - $template->process("admin/groups/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + SELECT ?, products.id, ?, ? FROM products', undef, + ($group->id, CONTROLMAPSHOWN, CONTROLMAPNA) + ); + } + delete_token($token); + + $vars->{'message'} = 'group_created'; + $vars->{'group'} = $group; + get_current_and_available($group, $vars); + $vars->{'token'} = issue_session_token('edit_group'); + + print $cgi->header(); + $template->process("admin/groups/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -229,23 +233,25 @@ if ($action eq 'new') { # if ($action eq 'del') { - # Check that an existing group ID is given - my $group = Bugzilla::Group->check({ id => scalar $cgi->param('group') }); - check_for_restricted_groups([ $group ]); - $group->check_remove({ test_only => 1 }); - $vars->{'shared_queries'} = - $dbh->selectrow_array('SELECT COUNT(*) + + # Check that an existing group ID is given + my $group = Bugzilla::Group->check({id => scalar $cgi->param('group')}); + check_for_restricted_groups([$group]); + $group->check_remove({test_only => 1}); + $vars->{'shared_queries'} = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM namedquery_group_map - WHERE group_id = ?', undef, $group->id); + WHERE group_id = ?', undef, $group->id + ); - $vars->{'group'} = $group; - $vars->{'token'} = issue_session_token('delete_group'); + $vars->{'group'} = $group; + $vars->{'token'} = issue_session_token('delete_group'); - print $cgi->header(); - $template->process("admin/groups/delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("admin/groups/delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # @@ -253,26 +259,27 @@ if ($action eq 'del') { # if ($action eq 'delete') { - check_token_data($token, 'delete_group'); - # Check that an existing group ID is given - my $group = Bugzilla::Group->check({ id => scalar $cgi->param('group') }); - check_for_restricted_groups([ $group ]); - $vars->{'name'} = $group->name; - $group->remove_from_db({ - remove_from_users => scalar $cgi->param('removeusers'), - remove_from_bugs => scalar $cgi->param('removebugs'), - remove_from_flags => scalar $cgi->param('removeflags'), - remove_from_products => scalar $cgi->param('unbind'), - }); - delete_token($token); - - $vars->{'message'} = 'group_deleted'; - $vars->{'groups'} = [Bugzilla::Group->get_all]; - - print $cgi->header(); - $template->process("admin/groups/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + check_token_data($token, 'delete_group'); + + # Check that an existing group ID is given + my $group = Bugzilla::Group->check({id => scalar $cgi->param('group')}); + check_for_restricted_groups([$group]); + $vars->{'name'} = $group->name; + $group->remove_from_db({ + remove_from_users => scalar $cgi->param('removeusers'), + remove_from_bugs => scalar $cgi->param('removebugs'), + remove_from_flags => scalar $cgi->param('removeflags'), + remove_from_products => scalar $cgi->param('unbind'), + }); + delete_token($token); + + $vars->{'message'} = 'group_deleted'; + $vars->{'groups'} = [Bugzilla::Group->get_all]; + + print $cgi->header(); + $template->process("admin/groups/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -280,73 +287,75 @@ if ($action eq 'delete') { # if ($action eq 'postchanges') { - check_token_data($token, 'edit_group'); - my $changes = doGroupChanges(); - delete_token($token); - - my $group = new Bugzilla::Group($cgi->param('group_id')); - get_current_and_available($group, $vars); - $vars->{'message'} = 'group_updated'; - $vars->{'group'} = $group; - $vars->{'changes'} = $changes; - $vars->{'token'} = issue_session_token('edit_group'); - - print $cgi->header(); - $template->process("admin/groups/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + check_token_data($token, 'edit_group'); + my $changes = doGroupChanges(); + delete_token($token); + + my $group = new Bugzilla::Group($cgi->param('group_id')); + get_current_and_available($group, $vars); + $vars->{'message'} = 'group_updated'; + $vars->{'group'} = $group; + $vars->{'changes'} = $changes; + $vars->{'token'} = issue_session_token('edit_group'); + + print $cgi->header(); + $template->process("admin/groups/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'confirm_remove') { - my $group = new Bugzilla::Group(CheckGroupID($cgi->param('group_id'))); - check_for_restricted_groups([ $group ]); - $vars->{'group'} = $group; - $vars->{'regexp'} = CheckGroupRegexp($cgi->param('regexp')); - $vars->{'token'} = issue_session_token('remove_group_members'); - $template->process('admin/groups/confirm-remove.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; + my $group = new Bugzilla::Group(CheckGroupID($cgi->param('group_id'))); + check_for_restricted_groups([$group]); + $vars->{'group'} = $group; + $vars->{'regexp'} = CheckGroupRegexp($cgi->param('regexp')); + $vars->{'token'} = issue_session_token('remove_group_members'); + $template->process('admin/groups/confirm-remove.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'remove_regexp') { - check_token_data($token, 'remove_group_members'); - # remove all explicit users from the group with - # gid = $cgi->param('group') that match the regular expression - # stored in the DB for that group or all of them period - - my $group = new Bugzilla::Group(CheckGroupID($cgi->param('group_id'))); - check_for_restricted_groups([ $group ]); - my $regexp = CheckGroupRegexp($cgi->param('regexp')); - - $dbh->bz_start_transaction(); - - my $users = $group->members_direct(); - my $sth_delete = $dbh->prepare( - "DELETE FROM user_group_map - WHERE user_id = ? AND isbless = 0 AND group_id = ?"); - - my @deleted; - foreach my $member (@$users) { - if ($regexp eq '' || $member->login =~ m/$regexp/i) { - $sth_delete->execute($member->id, $group->id); - push(@deleted, $member); - } + check_token_data($token, 'remove_group_members'); + + # remove all explicit users from the group with + # gid = $cgi->param('group') that match the regular expression + # stored in the DB for that group or all of them period + + my $group = new Bugzilla::Group(CheckGroupID($cgi->param('group_id'))); + check_for_restricted_groups([$group]); + my $regexp = CheckGroupRegexp($cgi->param('regexp')); + + $dbh->bz_start_transaction(); + + my $users = $group->members_direct(); + my $sth_delete = $dbh->prepare( + "DELETE FROM user_group_map + WHERE user_id = ? AND isbless = 0 AND group_id = ?" + ); + + my @deleted; + foreach my $member (@$users) { + if ($regexp eq '' || $member->login =~ m/$regexp/i) { + $sth_delete->execute($member->id, $group->id); + push(@deleted, $member); } - $dbh->bz_commit_transaction(); + } + $dbh->bz_commit_transaction(); - $vars->{'users'} = \@deleted; - $vars->{'regexp'} = $regexp; - delete_token($token); + $vars->{'users'} = \@deleted; + $vars->{'regexp'} = $regexp; + delete_token($token); - $vars->{'message'} = 'group_membership_removed'; - $vars->{'group'} = $group->name; - $vars->{'groups'} = [Bugzilla::Group->get_all]; + $vars->{'message'} = 'group_membership_removed'; + $vars->{'group'} = $group->name; + $vars->{'groups'} = [Bugzilla::Group->get_all]; - print $cgi->header(); - $template->process("admin/groups/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("admin/groups/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # No valid action found @@ -354,153 +363,156 @@ ThrowUserError('unknown_action', {action => $action}); # Helper sub to handle the making of changes to a group sub doGroupChanges { - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - # Check that the given group ID is valid and make a Group. - my $group = new Bugzilla::Group(CheckGroupID($cgi->param('group_id'))); - check_for_restricted_groups([ $group ]); + # Check that the given group ID is valid and make a Group. + my $group = new Bugzilla::Group(CheckGroupID($cgi->param('group_id'))); + check_for_restricted_groups([$group]); - if (defined $cgi->param('regexp')) { - $group->set_user_regexp($cgi->param('regexp')); - } + if (defined $cgi->param('regexp')) { + $group->set_user_regexp($cgi->param('regexp')); + } - if ($group->is_bug_group) { - if (defined $cgi->param('name')) { - $group->set_name($cgi->param('name')); - } - if (defined $cgi->param('desc')) { - $group->set_description($cgi->param('desc')); - } - # Only set isactive if we came from the right form. - if (defined $cgi->param('regexp')) { - $group->set_is_active($cgi->param('isactive')); - } + if ($group->is_bug_group) { + if (defined $cgi->param('name')) { + $group->set_name($cgi->param('name')); } - - if (defined $cgi->param('icon_url')) { - $group->set_icon_url($cgi->param('icon_url')); + if (defined $cgi->param('desc')) { + $group->set_description($cgi->param('desc')); } - if (defined $cgi->param('owner')) { - $group->set_owner($cgi->param('owner')); + # Only set isactive if we came from the right form. + if (defined $cgi->param('regexp')) { + $group->set_is_active($cgi->param('isactive')); } + } - if (defined $cgi->param('idle_member_removal')) { - $group->set_idle_member_removal($cgi->param('idle_member_removal')); - } + if (defined $cgi->param('icon_url')) { + $group->set_icon_url($cgi->param('icon_url')); + } - my $changes = $group->update(); + if (defined $cgi->param('owner')) { + $group->set_owner($cgi->param('owner')); + } - my $sth_insert = $dbh->prepare('INSERT INTO group_group_map + if (defined $cgi->param('idle_member_removal')) { + $group->set_idle_member_removal($cgi->param('idle_member_removal')); + } + + my $changes = $group->update(); + + my $sth_insert = $dbh->prepare( + 'INSERT INTO group_group_map (member_id, grantor_id, grant_type) - VALUES (?, ?, ?)'); + VALUES (?, ?, ?)' + ); - my $sth_delete = $dbh->prepare('DELETE FROM group_group_map + my $sth_delete = $dbh->prepare( + 'DELETE FROM group_group_map WHERE member_id = ? AND grantor_id = ? - AND grant_type = ?'); - - # First item is the type, second is whether or not it's "reverse" - # (granted_by) (see _do_add for more explanation). - my %fields = ( - members => [GROUP_MEMBERSHIP, 0], - bless_from => [GROUP_BLESS, 0], - visible_from => [GROUP_VISIBLE, 0], - member_of => [GROUP_MEMBERSHIP, 1], - bless_to => [GROUP_BLESS, 1], - visible_to_me => [GROUP_VISIBLE, 1] - ); - while (my ($field, $data) = each %fields) { - _do_add($group, $changes, $sth_insert, "${field}_add", - $data->[0], $data->[1]); - _do_remove($group, $changes, $sth_delete, "${field}_remove", - $data->[0], $data->[1]); - } - - $dbh->bz_commit_transaction(); - return $changes; + AND grant_type = ?' + ); + + # First item is the type, second is whether or not it's "reverse" + # (granted_by) (see _do_add for more explanation). + my %fields = ( + members => [GROUP_MEMBERSHIP, 0], + bless_from => [GROUP_BLESS, 0], + visible_from => [GROUP_VISIBLE, 0], + member_of => [GROUP_MEMBERSHIP, 1], + bless_to => [GROUP_BLESS, 1], + visible_to_me => [GROUP_VISIBLE, 1] + ); + while (my ($field, $data) = each %fields) { + _do_add($group, $changes, $sth_insert, "${field}_add", $data->[0], $data->[1]); + _do_remove($group, $changes, $sth_delete, "${field}_remove", $data->[0], + $data->[1]); + } + + $dbh->bz_commit_transaction(); + return $changes; } sub _do_add { - my ($group, $changes, $sth_insert, $field, $type, $reverse) = @_; - my $cgi = Bugzilla->cgi; - - my $current; - # $reverse means we're doing a granted_by--that is, somebody else - # is granting us something. - if ($reverse) { - $current = $group->granted_by_direct($type); - } - else { - $current = $group->grant_direct($type); - } - - my $add_items = Bugzilla::Group->new_from_list([$cgi->param($field)]); - check_for_restricted_groups($add_items); - - foreach my $add (@$add_items) { - next if grep($_->id == $add->id, @$current); - - $changes->{$field} ||= []; - push(@{$changes->{$field}}, $add->name); - # They go this direction for a normal "This group is granting - # $add something." - my @ids = ($add->id, $group->id); - # But they get reversed for "This group is being granted something - # by $add." - @ids = reverse @ids if $reverse; - $sth_insert->execute(@ids, $type); - } + my ($group, $changes, $sth_insert, $field, $type, $reverse) = @_; + my $cgi = Bugzilla->cgi; + + my $current; + + # $reverse means we're doing a granted_by--that is, somebody else + # is granting us something. + if ($reverse) { + $current = $group->granted_by_direct($type); + } + else { + $current = $group->grant_direct($type); + } + + my $add_items = Bugzilla::Group->new_from_list([$cgi->param($field)]); + check_for_restricted_groups($add_items); + + foreach my $add (@$add_items) { + next if grep($_->id == $add->id, @$current); + + $changes->{$field} ||= []; + push(@{$changes->{$field}}, $add->name); + + # They go this direction for a normal "This group is granting + # $add something." + my @ids = ($add->id, $group->id); + + # But they get reversed for "This group is being granted something + # by $add." + @ids = reverse @ids if $reverse; + $sth_insert->execute(@ids, $type); + } } sub _do_remove { - my ($group, $changes, $sth_delete, $field, $type, $reverse) = @_; - my $cgi = Bugzilla->cgi; - my $remove_items = Bugzilla::Group->new_from_list([$cgi->param($field)]); - check_for_restricted_groups($remove_items); - - foreach my $remove (@$remove_items) { - my @ids = ($remove->id, $group->id); - # See _do_add for an explanation of $reverse - @ids = reverse @ids if $reverse; - # Deletions always succeed and are harmless if they fail, so we - # don't need to do any checks. - $sth_delete->execute(@ids, $type); - $changes->{$field} ||= []; - push(@{$changes->{$field}}, $remove->name); - } + my ($group, $changes, $sth_delete, $field, $type, $reverse) = @_; + my $cgi = Bugzilla->cgi; + my $remove_items = Bugzilla::Group->new_from_list([$cgi->param($field)]); + check_for_restricted_groups($remove_items); + + foreach my $remove (@$remove_items) { + my @ids = ($remove->id, $group->id); + + # See _do_add for an explanation of $reverse + @ids = reverse @ids if $reverse; + + # Deletions always succeed and are harmless if they fail, so we + # don't need to do any checks. + $sth_delete->execute(@ids, $type); + $changes->{$field} ||= []; + push(@{$changes->{$field}}, $remove->name); + } } # ensure non-admins cannot edit the admin group # likewise you must be a member of the insider group in order to update it sub check_for_restricted_groups { - my ($groups) = @_; - - my $user = Bugzilla->user; - return if $user->in_group('admin'); - - # check for admin changes - foreach my $group (@$groups) { - if ($group->name eq 'admin') { - ThrowUserError('auth_failure', { - action => 'edit', - object => 'admin_group', - }); - } - } + my ($groups) = @_; - # check for insider group changes - my $insider_group = Bugzilla->params->{insidergroup}; - return if $user->in_group($insider_group); - foreach my $group (@$groups) { - if ($group->name eq $insider_group) { - ThrowUserError('auth_failure', { - action => 'edit', - object => 'insider_group', - }); - } + my $user = Bugzilla->user; + return if $user->in_group('admin'); + + # check for admin changes + foreach my $group (@$groups) { + if ($group->name eq 'admin') { + ThrowUserError('auth_failure', {action => 'edit', object => 'admin_group',}); + } + } + + # check for insider group changes + my $insider_group = Bugzilla->params->{insidergroup}; + return if $user->in_group($insider_group); + foreach my $group (@$groups) { + if ($group->name eq $insider_group) { + ThrowUserError('auth_failure', {action => 'edit', object => 'insider_group',}); } + } } diff --git a/editkeywords.cgi b/editkeywords.cgi index 6c5a68e68..571e7412d 100755 --- a/editkeywords.cgi +++ b/editkeywords.cgi @@ -19,10 +19,10 @@ use Bugzilla::Error; use Bugzilla::Keyword; use Bugzilla::Token; -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; # # Preliminary checks: @@ -33,11 +33,10 @@ my $user = Bugzilla->login(LOGIN_REQUIRED); print $cgi->header(); $user->in_group('editkeywords') - || ThrowUserError("auth_failure", {group => "editkeywords", - action => "edit", - object => "keywords"}); + || ThrowUserError("auth_failure", + {group => "editkeywords", action => "edit", object => "keywords"}); -my $action = trim($cgi->param('action') || ''); +my $action = trim($cgi->param('action') || ''); my $key_id = $cgi->param('id'); my $token = $cgi->param('token'); @@ -45,50 +44,49 @@ $vars->{'action'} = $action; if ($action eq "") { - $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); + $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); - print $cgi->header(); - $template->process("admin/keywords/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("admin/keywords/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } if ($action eq 'add') { - $vars->{'token'} = issue_session_token('add_keyword'); + $vars->{'token'} = issue_session_token('add_keyword'); - print $cgi->header(); + print $cgi->header(); - $template->process("admin/keywords/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("admin/keywords/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # # action='new' -> add keyword entered in the 'action=add' screen # if ($action eq 'new') { - check_token_data($token, 'add_keyword'); - my $name = $cgi->param('name') || ''; - my $is_active = $cgi->param('is_active'); - my $desc = $cgi->param('description') || ''; + check_token_data($token, 'add_keyword'); + my $name = $cgi->param('name') || ''; + my $is_active = $cgi->param('is_active'); + my $desc = $cgi->param('description') || ''; - my $keyword = Bugzilla::Keyword->create( - { name => $name, description => $desc }); + my $keyword = Bugzilla::Keyword->create({name => $name, description => $desc}); - delete_token($token); + delete_token($token); - print $cgi->header(); + print $cgi->header(); - $vars->{'message'} = 'keyword_created'; - $vars->{'name'} = $keyword->name; - $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); + $vars->{'message'} = 'keyword_created'; + $vars->{'name'} = $keyword->name; + $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); - $template->process("admin/keywords/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/keywords/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } @@ -99,16 +97,16 @@ if ($action eq 'new') { # if ($action eq 'edit') { - my $keyword = new Bugzilla::Keyword($key_id) - || ThrowCodeError('invalid_keyword_id', { id => $key_id }); + my $keyword = new Bugzilla::Keyword($key_id) + || ThrowCodeError('invalid_keyword_id', {id => $key_id}); - $vars->{'keyword'} = $keyword; - $vars->{'token'} = issue_session_token('edit_keyword'); + $vars->{'keyword'} = $keyword; + $vars->{'token'} = issue_session_token('edit_keyword'); - print $cgi->header(); - $template->process("admin/keywords/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + print $cgi->header(); + $template->process("admin/keywords/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } @@ -117,62 +115,62 @@ if ($action eq 'edit') { # if ($action eq 'update') { - check_token_data($token, 'edit_keyword'); - my $keyword = new Bugzilla::Keyword($key_id) - || ThrowCodeError('invalid_keyword_id', { id => $key_id }); + check_token_data($token, 'edit_keyword'); + my $keyword = new Bugzilla::Keyword($key_id) + || ThrowCodeError('invalid_keyword_id', {id => $key_id}); - $keyword->set_all({ - name => scalar $cgi->param('name'), - description => scalar $cgi->param('description'), - is_active => scalar $cgi->param('is_active'), - }); - my $changes = $keyword->update(); + $keyword->set_all({ + name => scalar $cgi->param('name'), + description => scalar $cgi->param('description'), + is_active => scalar $cgi->param('is_active'), + }); + my $changes = $keyword->update(); - delete_token($token); + delete_token($token); - print $cgi->header(); + print $cgi->header(); - $vars->{'message'} = 'keyword_updated'; - $vars->{'keyword'} = $keyword; - $vars->{'changes'} = $changes; - $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); + $vars->{'message'} = 'keyword_updated'; + $vars->{'keyword'} = $keyword; + $vars->{'changes'} = $changes; + $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); - $template->process("admin/keywords/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/keywords/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'del') { - my $keyword = new Bugzilla::Keyword($key_id) - || ThrowCodeError('invalid_keyword_id', { id => $key_id }); + my $keyword = new Bugzilla::Keyword($key_id) + || ThrowCodeError('invalid_keyword_id', {id => $key_id}); - $vars->{'keyword'} = $keyword; - $vars->{'token'} = issue_session_token('delete_keyword'); + $vars->{'keyword'} = $keyword; + $vars->{'token'} = issue_session_token('delete_keyword'); - print $cgi->header(); - $template->process("admin/keywords/confirm-delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + print $cgi->header(); + $template->process("admin/keywords/confirm-delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'delete') { - check_token_data($token, 'delete_keyword'); - my $keyword = new Bugzilla::Keyword($key_id) - || ThrowCodeError('invalid_keyword_id', { id => $key_id }); + check_token_data($token, 'delete_keyword'); + my $keyword = new Bugzilla::Keyword($key_id) + || ThrowCodeError('invalid_keyword_id', {id => $key_id}); - $keyword->remove_from_db(); + $keyword->remove_from_db(); - delete_token($token); + delete_token($token); - print $cgi->header(); + print $cgi->header(); - $vars->{'message'} = 'keyword_deleted'; - $vars->{'keyword'} = $keyword; - $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); + $vars->{'message'} = 'keyword_deleted'; + $vars->{'keyword'} = $keyword; + $vars->{'keywords'} = Bugzilla::Keyword->get_all_with_bug_count(); - $template->process("admin/keywords/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/keywords/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } ThrowUserError('unknown_action', {action => $action}); diff --git a/editmilestones.cgi b/editmilestones.cgi index 0803ca9cf..e8db2586d 100755 --- a/editmilestones.cgi +++ b/editmilestones.cgi @@ -19,10 +19,11 @@ use Bugzilla::Error; use Bugzilla::Milestone; use Bugzilla::Token; -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; + # There is only one section about milestones in the documentation, # so all actions point to the same page. $vars->{'doc_section'} = 'milestones.html'; @@ -37,18 +38,17 @@ print $cgi->header(); $user->in_group('editcomponents') || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", {group => "editcomponents", - action => "edit", - object => "milestones"}); + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "milestones"}); # # often used variables # -my $product_name = trim($cgi->param('product') || ''); -my $milestone_name = trim($cgi->param('milestone') || ''); -my $sortkey = trim($cgi->param('sortkey') || 0); -my $action = trim($cgi->param('action') || ''); -my $showbugcounts = (defined $cgi->param('showbugcounts')); +my $product_name = trim($cgi->param('product') || ''); +my $milestone_name = trim($cgi->param('milestone') || ''); +my $sortkey = trim($cgi->param('sortkey') || 0); +my $action = trim($cgi->param('action') || ''); +my $showbugcounts = (defined $cgi->param('showbugcounts')); my $token = $cgi->param('token'); my $isactive = $cgi->param('isactive'); @@ -57,18 +57,19 @@ my $isactive = $cgi->param('isactive'); # unless ($product_name) { - my $selectable_products = $user->get_selectable_products; - # If the user has editcomponents privs for some products only, - # we have to restrict the list of products to display. - unless ($user->in_group('editcomponents')) { - $selectable_products = $user->get_products_by_permission('editcomponents'); - } - $vars->{'products'} = $selectable_products; - $vars->{'showbugcounts'} = $showbugcounts; - - $template->process("admin/milestones/select-product.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $selectable_products = $user->get_selectable_products; + + # If the user has editcomponents privs for some products only, + # we have to restrict the list of products to display. + unless ($user->in_group('editcomponents')) { + $selectable_products = $user->get_products_by_permission('editcomponents'); + } + $vars->{'products'} = $selectable_products; + $vars->{'showbugcounts'} = $showbugcounts; + + $template->process("admin/milestones/select-product.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } my $product = $user->check_can_admin_product($product_name); @@ -79,11 +80,11 @@ my $product = $user->check_can_admin_product($product_name); unless ($action) { - $vars->{'showbugcounts'} = $showbugcounts; - $vars->{'product'} = $product; - $template->process("admin/milestones/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'showbugcounts'} = $showbugcounts; + $vars->{'product'} = $product; + $template->process("admin/milestones/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -93,11 +94,11 @@ unless ($action) { # if ($action eq 'add') { - $vars->{'token'} = issue_session_token('add_milestone'); - $vars->{'product'} = $product; - $template->process("admin/milestones/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'token'} = issue_session_token('add_milestone'); + $vars->{'product'} = $product; + $template->process("admin/milestones/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -105,20 +106,21 @@ if ($action eq 'add') { # if ($action eq 'new') { - check_token_data($token, 'add_milestone'); + check_token_data($token, 'add_milestone'); - my $milestone = Bugzilla::Milestone->create({ value => $milestone_name, - product => $product, - sortkey => $sortkey }); + my $milestone + = Bugzilla::Milestone->create({ + value => $milestone_name, product => $product, sortkey => $sortkey + }); - delete_token($token); + delete_token($token); - $vars->{'message'} = 'milestone_created'; - $vars->{'milestone'} = $milestone; - $vars->{'product'} = $product; - $template->process("admin/milestones/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'message'} = 'milestone_created'; + $vars->{'milestone'} = $milestone; + $vars->{'product'} = $product; + $template->process("admin/milestones/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -128,21 +130,21 @@ if ($action eq 'new') { # if ($action eq 'del') { - my $milestone = Bugzilla::Milestone->check({ product => $product, - name => $milestone_name }); + my $milestone + = Bugzilla::Milestone->check({product => $product, name => $milestone_name}); - $vars->{'milestone'} = $milestone; - $vars->{'product'} = $product; + $vars->{'milestone'} = $milestone; + $vars->{'product'} = $product; - # The default milestone cannot be deleted. - if ($product->default_milestone eq $milestone->name) { - ThrowUserError("milestone_is_default", { milestone => $milestone }); - } - $vars->{'token'} = issue_session_token('delete_milestone'); + # The default milestone cannot be deleted. + if ($product->default_milestone eq $milestone->name) { + ThrowUserError("milestone_is_default", {milestone => $milestone}); + } + $vars->{'token'} = issue_session_token('delete_milestone'); - $template->process("admin/milestones/confirm-delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/milestones/confirm-delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -150,20 +152,20 @@ if ($action eq 'del') { # if ($action eq 'delete') { - check_token_data($token, 'delete_milestone'); - my $milestone = Bugzilla::Milestone->check({ product => $product, - name => $milestone_name }); - $milestone->remove_from_db; - delete_token($token); - - $vars->{'message'} = 'milestone_deleted'; - $vars->{'milestone'} = $milestone; - $vars->{'product'} = $product; - $vars->{'no_edit_milestone_link'} = 1; - - $template->process("admin/milestones/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + check_token_data($token, 'delete_milestone'); + my $milestone + = Bugzilla::Milestone->check({product => $product, name => $milestone_name}); + $milestone->remove_from_db; + delete_token($token); + + $vars->{'message'} = 'milestone_deleted'; + $vars->{'milestone'} = $milestone; + $vars->{'product'} = $product; + $vars->{'no_edit_milestone_link'} = 1; + + $template->process("admin/milestones/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -174,16 +176,16 @@ if ($action eq 'delete') { if ($action eq 'edit') { - my $milestone = Bugzilla::Milestone->check({ product => $product, - name => $milestone_name }); + my $milestone + = Bugzilla::Milestone->check({product => $product, name => $milestone_name}); - $vars->{'milestone'} = $milestone; - $vars->{'product'} = $product; - $vars->{'token'} = issue_session_token('edit_milestone'); + $vars->{'milestone'} = $milestone; + $vars->{'product'} = $product; + $vars->{'token'} = issue_session_token('edit_milestone'); - $template->process("admin/milestones/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/milestones/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -191,28 +193,29 @@ if ($action eq 'edit') { # if ($action eq 'update') { - check_token_data($token, 'edit_milestone'); - my $milestone_old_name = trim($cgi->param('milestoneold') || ''); - my $milestone = Bugzilla::Milestone->check({ product => $product, - name => $milestone_old_name }); - - $milestone->set_name($milestone_name); - $milestone->set_sortkey($sortkey); - $milestone->set_is_active($isactive); - my $changes = $milestone->update(); - # Reloading the product since the default milestone name - # could have been changed. - $product = new Bugzilla::Product({ name => $product_name }); - - delete_token($token); - - $vars->{'message'} = 'milestone_updated'; - $vars->{'milestone'} = $milestone; - $vars->{'product'} = $product; - $vars->{'changes'} = $changes; - $template->process("admin/milestones/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + check_token_data($token, 'edit_milestone'); + my $milestone_old_name = trim($cgi->param('milestoneold') || ''); + my $milestone = Bugzilla::Milestone->check( + {product => $product, name => $milestone_old_name}); + + $milestone->set_name($milestone_name); + $milestone->set_sortkey($sortkey); + $milestone->set_is_active($isactive); + my $changes = $milestone->update(); + + # Reloading the product since the default milestone name + # could have been changed. + $product = new Bugzilla::Product({name => $product_name}); + + delete_token($token); + + $vars->{'message'} = 'milestone_updated'; + $vars->{'milestone'} = $milestone; + $vars->{'product'} = $product; + $vars->{'changes'} = $changes; + $template->process("admin/milestones/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # No valid action found diff --git a/editparams.cgi b/editparams.cgi index 2dd9ff08d..01e824d55 100755 --- a/editparams.cgi +++ b/editparams.cgi @@ -25,148 +25,157 @@ use Bugzilla::User::Setting; use Bugzilla::Status; use Module::Runtime qw(require_module); -my $user = Bugzilla->login(LOGIN_REQUIRED); -my $cgi = Bugzilla->cgi; +my $user = Bugzilla->login(LOGIN_REQUIRED); +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; print $cgi->header(); $user->in_group('tweakparams') - || ThrowUserError("auth_failure", {group => "tweakparams", - action => "access", - object => "parameters"}); + || ThrowUserError("auth_failure", + {group => "tweakparams", action => "access", object => "parameters"}); -my $action = trim($cgi->param('action') || ''); -my $token = $cgi->param('token'); +my $action = trim($cgi->param('action') || ''); +my $token = $cgi->param('token'); my $current_panel = $cgi->param('section') || 'general'; $current_panel =~ /^([A-Za-z0-9_-]+)$/; $current_panel = $1; my $current_module; -my @panels = (); +my @panels = (); my $param_panels = Bugzilla::Config::param_panels(); -my $override = Bugzilla->localconfig->{param_override}; +my $override = Bugzilla->localconfig->{param_override}; foreach my $panel (keys %$param_panels) { - my $module = $param_panels->{$panel}; - require_module($module); - my @module_param_list = $module->get_param_list(); - my $item = { - name => lc($panel), - current => ($current_panel eq lc($panel)) ? 1 : 0, - param_list => \@module_param_list, - param_override => { map { $_->{name} => $override->{$_->{name}} } @module_param_list }, - sortkey => eval "\$${module}::sortkey;", - module => $module, - }; - $item->{sortkey} //= 100000; - push(@panels, $item); - $current_module = $panel if ($current_panel eq lc($panel)); + my $module = $param_panels->{$panel}; + require_module($module); + my @module_param_list = $module->get_param_list(); + my $item = { + name => lc($panel), + current => ($current_panel eq lc($panel)) ? 1 : 0, + param_list => \@module_param_list, + param_override => + {map { $_->{name} => $override->{$_->{name}} } @module_param_list}, + sortkey => eval "\$${module}::sortkey;", + module => $module, + }; + $item->{sortkey} //= 100000; + push(@panels, $item); + $current_module = $panel if ($current_panel eq lc($panel)); } -my %hook_panels = map { $_->{name} => { params => $_->{param_list} } } - @panels; +my %hook_panels = map { $_->{name} => {params => $_->{param_list}} } @panels; + # Note that this hook is also called in Bugzilla::Config. -Bugzilla::Hook::process('config_modify_panels', { panels => \%hook_panels }); +Bugzilla::Hook::process('config_modify_panels', {panels => \%hook_panels}); $vars->{panels} = \@panels; if ($action eq 'save' && $current_module) { - check_token_data($token, 'edit_parameters'); - my @changes = (); - my @module_param_list = @{ $hook_panels{lc($current_module)}->{params} }; - - my $any_changed = 0; - foreach my $i (@module_param_list) { - my $name = $i->{'name'}; - my $value = $cgi->param($name); - - if (defined $cgi->param("reset-$name") && !$i->{'no_reset'}) { - $value = $i->{'default'}; - } else { - if ($i->{'type'} eq 'm') { - # This simplifies the code below - $value = [ $cgi->param($name) ]; - } else { - # Get rid of windows/mac-style line endings. - $value =~ s/\r\n?/\n/g; - # assume single linefeed is an empty string - $value =~ s/^\n$//; - } - # Stop complaining if the URL has no trailing slash. - # XXX - This hack can go away once bug 303662 is implemented. - if ($name =~ /(?{params}}; - my $changed; - if ($i->{'type'} eq 'm') { - my @old = sort @{Bugzilla->params->{$name}}; - my @new = sort @$value; - if (scalar(@old) != scalar(@new)) { - $changed = 1; - } else { - $changed = 0; # Assume not changed... - for (my $cnt = 0; $cnt < scalar(@old); ++$cnt) { - if ($old[$cnt] ne $new[$cnt]) { - # entry is different, therefore changed - $changed = 1; - last; - } - } - } - } else { - $changed = ($value eq Bugzilla->params->{$name})? 0 : 1; - } + my $any_changed = 0; + foreach my $i (@module_param_list) { + my $name = $i->{'name'}; + my $value = $cgi->param($name); - if ($changed) { - if (exists $i->{'checker'}) { - my $ok = $i->{'checker'}->($value, $i); - if ($ok ne "") { - ThrowUserError('invalid_parameter', { name => $name, err => $ok }); - } - } elsif ($name eq 'globalwatchers') { - # can't check this as others, as Bugzilla::Config::Common - # can not use Bugzilla::User - foreach my $watcher (split(/[,\s]+/, $value)) { - ThrowUserError( - 'invalid_parameter', - { name => $name, err => "no such user $watcher" } - ) unless login_to_id($watcher); - } - } - push(@changes, $name); - SetParam($name, $value); - if ($name eq 'duplicate_or_move_bug_status') { - Bugzilla::Status::add_missing_bug_status_transitions($value); - } - $any_changed = 1; - } + if (defined $cgi->param("reset-$name") && !$i->{'no_reset'}) { + $value = $i->{'default'}; + } + else { + if ($i->{'type'} eq 'm') { + + # This simplifies the code below + $value = [$cgi->param($name)]; + } + else { + # Get rid of windows/mac-style line endings. + $value =~ s/\r\n?/\n/g; + + # assume single linefeed is an empty string + $value =~ s/^\n$//; + } + + # Stop complaining if the URL has no trailing slash. + # XXX - This hack can go away once bug 303662 is implemented. + if ($name =~ /(?{name} eq lc($current_module); - my $module = $panel->{module}; - next unless $module->can('check_params'); - my $err = $module->check_params(Bugzilla->params); - if ($err ne '') { - ThrowUserError('invalid_parameters', { err => $err }); - } + my $changed; + if ($i->{'type'} eq 'm') { + my @old = sort @{Bugzilla->params->{$name}}; + my @new = sort @$value; + if (scalar(@old) != scalar(@new)) { + $changed = 1; + } + else { + $changed = 0; # Assume not changed... + for (my $cnt = 0; $cnt < scalar(@old); ++$cnt) { + if ($old[$cnt] ne $new[$cnt]) { + + # entry is different, therefore changed + $changed = 1; last; + } } + } + } + else { + $changed = ($value eq Bugzilla->params->{$name}) ? 0 : 1; + } + + if ($changed) { + if (exists $i->{'checker'}) { + my $ok = $i->{'checker'}->($value, $i); + if ($ok ne "") { + ThrowUserError('invalid_parameter', {name => $name, err => $ok}); + } + } + elsif ($name eq 'globalwatchers') { + + # can't check this as others, as Bugzilla::Config::Common + # can not use Bugzilla::User + foreach my $watcher (split(/[,\s]+/, $value)) { + ThrowUserError('invalid_parameter', + {name => $name, err => "no such user $watcher"}) + unless login_to_id($watcher); + } + } + push(@changes, $name); + SetParam($name, $value); + if ($name eq 'duplicate_or_move_bug_status') { + Bugzilla::Status::add_missing_bug_status_transitions($value); + } + $any_changed = 1; + } + } + + # allow panels to check inter-dependent params + if ($any_changed) { + foreach my $panel (@panels) { + next unless $panel->{name} eq lc($current_module); + my $module = $panel->{module}; + next unless $module->can('check_params'); + my $err = $module->check_params(Bugzilla->params); + if ($err ne '') { + ThrowUserError('invalid_parameters', {err => $err}); + } + last; } + } - $vars->{'message'} = 'parameters_updated'; - $vars->{'param_changed'} = \@changes; + $vars->{'message'} = 'parameters_updated'; + $vars->{'param_changed'} = \@changes; - write_params(); - delete_token($token); + write_params(); + delete_token($token); } $vars->{'token'} = issue_session_token('edit_parameters'); $template->process("admin/params/editparams.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + || ThrowTemplateError($template->error()); diff --git a/editproducts.cgi b/editproducts.cgi index a989e4bc1..7deab1d2d 100755 --- a/editproducts.cgi +++ b/editproducts.cgi @@ -25,13 +25,14 @@ use Bugzilla::Token; # Preliminary checks: # -my $user = Bugzilla->login(LOGIN_REQUIRED); +my $user = Bugzilla->login(LOGIN_REQUIRED); my $whoid = $user->id; -my $dbh = Bugzilla->dbh; -my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; + # Remove this as soon as the documentation about products has been # improved and each action has its own section. $vars->{'doc_section'} = 'products.html'; @@ -40,43 +41,42 @@ print $cgi->header(); $user->in_group('editcomponents') || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", {group => "editcomponents", - action => "edit", - object => "products"}); + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "products"}); # # often used variables # my $classification_name = trim($cgi->param('classification') || ''); -my $product_name = trim($cgi->param('product') || ''); -my $action = trim($cgi->param('action') || ''); -my $token = $cgi->param('token'); +my $product_name = trim($cgi->param('product') || ''); +my $action = trim($cgi->param('action') || ''); +my $token = $cgi->param('token'); # # product = '' -> Show nice list of classifications (if # classifications enabled) # -if (Bugzilla->params->{'useclassification'} - && !$classification_name - && !$product_name) +if ( Bugzilla->params->{'useclassification'} + && !$classification_name + && !$product_name) { - my $class; - if ($user->in_group('editcomponents')) { - $class = [Bugzilla::Classification->get_all]; - } - else { - # Only keep classifications containing at least one product - # which you can administer. - my $products = $user->get_products_by_permission('editcomponents'); - my %class_ids = map { $_->classification_id => 1 } @$products; - $class = Bugzilla::Classification->new_from_list([keys %class_ids]); - } - $vars->{'classifications'} = $class; - - $template->process("admin/products/list-classifications.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $class; + if ($user->in_group('editcomponents')) { + $class = [Bugzilla::Classification->get_all]; + } + else { + # Only keep classifications containing at least one product + # which you can administer. + my $products = $user->get_products_by_permission('editcomponents'); + my %class_ids = map { $_->classification_id => 1 } @$products; + $class = Bugzilla::Classification->new_from_list([keys %class_ids]); + } + $vars->{'classifications'} = $class; + + $template->process("admin/products/list-classifications.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } @@ -86,36 +86,35 @@ if (Bugzilla->params->{'useclassification'} # if (!$action && !$product_name) { - my $classification; - my $products; - + my $classification; + my $products; + + if (Bugzilla->params->{'useclassification'}) { + $classification = Bugzilla::Classification->check($classification_name); + $products = $user->get_selectable_products($classification->id); + $vars->{'classification'} = $classification; + } + else { + $products = $user->get_selectable_products; + } + + # If the user has editcomponents privs for some products only, + # we have to restrict the list of products to display. + unless ($user->in_group('editcomponents')) { + $products = $user->get_products_by_permission('editcomponents'); if (Bugzilla->params->{'useclassification'}) { - $classification = Bugzilla::Classification->check($classification_name); - $products = $user->get_selectable_products($classification->id); - $vars->{'classification'} = $classification; - } else { - $products = $user->get_selectable_products; + @$products = grep { $_->classification_id == $classification->id } @$products; } + } + $vars->{'products'} = $products; + $vars->{'showbugcounts'} = $cgi->param('showbugcounts') ? 1 : 0; - # If the user has editcomponents privs for some products only, - # we have to restrict the list of products to display. - unless ($user->in_group('editcomponents')) { - $products = $user->get_products_by_permission('editcomponents'); - if (Bugzilla->params->{'useclassification'}) { - @$products = grep {$_->classification_id == $classification->id} @$products; - } - } - $vars->{'products'} = $products; - $vars->{'showbugcounts'} = $cgi->param('showbugcounts') ? 1 : 0; - - $template->process("admin/products/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/products/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } - - # # action='add' -> present form for parameters for new product # @@ -123,23 +122,23 @@ if (!$action && !$product_name) { # if ($action eq 'add') { - # The user must have the global editcomponents privs to add - # new products. - $user->in_group('editcomponents') - || ThrowUserError("auth_failure", {group => "editcomponents", - action => "add", - object => "products"}); - if (Bugzilla->params->{'useclassification'}) { - my $classification = Bugzilla::Classification->check($classification_name); - $vars->{'classification'} = $classification; - } - $vars->{'token'} = issue_session_token('add_product'); + # The user must have the global editcomponents privs to add + # new products. + $user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "products"}); - $template->process("admin/products/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + if (Bugzilla->params->{'useclassification'}) { + my $classification = Bugzilla::Classification->check($classification_name); + $vars->{'classification'} = $classification; + } + $vars->{'token'} = issue_session_token('add_product'); + + $template->process("admin/products/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } @@ -148,39 +147,40 @@ if ($action eq 'add') { # if ($action eq 'new') { - # The user must have the global editcomponents privs to add - # new products. - $user->in_group('editcomponents') - || ThrowUserError("auth_failure", {group => "editcomponents", - action => "add", - object => "products"}); - - check_token_data($token, 'add_product'); - - my %create_params = ( - classification => $classification_name, - name => $product_name, - description => scalar $cgi->param('description'), - version => scalar $cgi->param('version'), - defaultmilestone => scalar $cgi->param('defaultmilestone'), - isactive => scalar $cgi->param('is_active'), - create_series => scalar $cgi->param('createseries'), - allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'), - ); - my $product = Bugzilla::Product->create(\%create_params); - - delete_token($token); - - $vars->{'message'} = 'product_created'; - $vars->{'product'} = $product; - if (Bugzilla->params->{'useclassification'}) { - $vars->{'classification'} = new Bugzilla::Classification($product->classification_id); - } - $vars->{'token'} = issue_session_token('edit_product'); - $template->process("admin/products/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + # The user must have the global editcomponents privs to add + # new products. + $user->in_group('editcomponents') + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "add", object => "products"}); + + check_token_data($token, 'add_product'); + + my %create_params = ( + classification => $classification_name, + name => $product_name, + description => scalar $cgi->param('description'), + version => scalar $cgi->param('version'), + defaultmilestone => scalar $cgi->param('defaultmilestone'), + isactive => scalar $cgi->param('is_active'), + create_series => scalar $cgi->param('createseries'), + allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'), + ); + my $product = Bugzilla::Product->create(\%create_params); + + delete_token($token); + + $vars->{'message'} = 'product_created'; + $vars->{'product'} = $product; + if (Bugzilla->params->{'useclassification'}) { + $vars->{'classification'} + = new Bugzilla::Classification($product->classification_id); + } + $vars->{'token'} = issue_session_token('edit_product'); + + $template->process("admin/products/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -190,19 +190,20 @@ if ($action eq 'new') { # if ($action eq 'del') { - my $product = $user->check_can_admin_product($product_name); + my $product = $user->check_can_admin_product($product_name); - if (Bugzilla->params->{'useclassification'}) { - $vars->{'classification'} = new Bugzilla::Classification($product->classification_id); - } - $vars->{'product'} = $product; - $vars->{'token'} = issue_session_token('delete_product'); + if (Bugzilla->params->{'useclassification'}) { + $vars->{'classification'} + = new Bugzilla::Classification($product->classification_id); + } + $vars->{'product'} = $product; + $vars->{'token'} = issue_session_token('delete_product'); - Bugzilla::Hook::process('product_confirm_delete', { vars => $vars }); + Bugzilla::Hook::process('product_confirm_delete', {vars => $vars}); - $template->process("admin/products/confirm-delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/products/confirm-delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -210,36 +211,40 @@ if ($action eq 'del') { # if ($action eq 'delete') { - my $product = $user->check_can_admin_product($product_name); - check_token_data($token, 'delete_product'); + my $product = $user->check_can_admin_product($product_name); + check_token_data($token, 'delete_product'); - $product->remove_from_db({ delete_series => scalar $cgi->param('delete_series')}); - delete_token($token); + $product->remove_from_db( + {delete_series => scalar $cgi->param('delete_series')}); + delete_token($token); - $vars->{'message'} = 'product_deleted'; - $vars->{'product'} = $product; - $vars->{'no_edit_product_link'} = 1; + $vars->{'message'} = 'product_deleted'; + $vars->{'product'} = $product; + $vars->{'no_edit_product_link'} = 1; - if (Bugzilla->params->{'useclassification'}) { - $vars->{'classifications'} = $user->in_group('editcomponents') ? - [Bugzilla::Classification->get_all] : $user->get_selectable_classifications; + if (Bugzilla->params->{'useclassification'}) { + $vars->{'classifications'} + = $user->in_group('editcomponents') + ? [Bugzilla::Classification->get_all] + : $user->get_selectable_classifications; - $template->process("admin/products/list-classifications.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - } - else { - my $products = $user->get_selectable_products; - # If the user has editcomponents privs for some products only, - # we have to restrict the list of products to display. - unless ($user->in_group('editcomponents')) { - $products = $user->get_products_by_permission('editcomponents'); - } - $vars->{'products'} = $products; - - $template->process("admin/products/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("admin/products/list-classifications.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + } + else { + my $products = $user->get_selectable_products; + + # If the user has editcomponents privs for some products only, + # we have to restrict the list of products to display. + unless ($user->in_group('editcomponents')) { + $products = $user->get_products_by_permission('editcomponents'); } - exit; + $vars->{'products'} = $products; + + $template->process("admin/products/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + } + exit; } # @@ -250,48 +255,50 @@ if ($action eq 'delete') { # if ($action eq 'edit' || (!$action && $product_name)) { - my $product = $user->check_can_admin_product($product_name); - - if (Bugzilla->params->{'useclassification'}) { - $vars->{'classification'} = new Bugzilla::Classification($product->classification_id); - } - $vars->{'product'} = $product; - $vars->{'token'} = issue_session_token('edit_product'); - - $template->process("admin/products/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $product = $user->check_can_admin_product($product_name); + + if (Bugzilla->params->{'useclassification'}) { + $vars->{'classification'} + = new Bugzilla::Classification($product->classification_id); + } + $vars->{'product'} = $product; + $vars->{'token'} = issue_session_token('edit_product'); + + $template->process("admin/products/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # # action='update' -> update the product # if ($action eq 'update') { - check_token_data($token, 'edit_product'); - my $product_old_name = trim($cgi->param('product_old_name') || ''); - my $product = $user->check_can_admin_product($product_old_name); - - $product->set_all({ - name => $product_name, - description => scalar $cgi->param('description'), - is_active => scalar $cgi->param('is_active'), - allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'), - default_milestone => scalar $cgi->param('defaultmilestone'), - }); - - my $changes = $product->update(); - - delete_token($token); - - if (Bugzilla->params->{'useclassification'}) { - $vars->{'classification'} = new Bugzilla::Classification($product->classification_id); - } - $vars->{'product'} = $product; - $vars->{'changes'} = $changes; - - $template->process("admin/products/updated.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + check_token_data($token, 'edit_product'); + my $product_old_name = trim($cgi->param('product_old_name') || ''); + my $product = $user->check_can_admin_product($product_old_name); + + $product->set_all({ + name => $product_name, + description => scalar $cgi->param('description'), + is_active => scalar $cgi->param('is_active'), + allows_unconfirmed => scalar $cgi->param('allows_unconfirmed'), + default_milestone => scalar $cgi->param('defaultmilestone'), + }); + + my $changes = $product->update(); + + delete_token($token); + + if (Bugzilla->params->{'useclassification'}) { + $vars->{'classification'} + = new Bugzilla::Classification($product->classification_id); + } + $vars->{'product'} = $product; + $vars->{'changes'} = $changes; + + $template->process("admin/products/updated.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -299,14 +306,14 @@ if ($action eq 'update') { # if ($action eq 'editgroupcontrols') { - my $product = $user->check_can_admin_product($product_name); + my $product = $user->check_can_admin_product($product_name); - $vars->{'product'} = $product; - $vars->{'token'} = issue_session_token('edit_group_controls'); + $vars->{'product'} = $product; + $vars->{'token'} = issue_session_token('edit_group_controls'); - $template->process("admin/products/groupcontrol/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/products/groupcontrol/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # @@ -314,44 +321,45 @@ if ($action eq 'editgroupcontrols') { # if ($action eq 'updategroupcontrols') { - my $product = $user->check_can_admin_product($product_name); - check_token_data($token, 'edit_group_controls'); - - my @now_na = (); - my @now_mandatory = (); - foreach my $f ($cgi->param()) { - if ($f =~ /^membercontrol_(\d+)$/) { - my $id = $1; - if ($cgi->param($f) == CONTROLMAPNA) { - push @now_na,$id; - } elsif ($cgi->param($f) == CONTROLMAPMANDATORY) { - push @now_mandatory,$id; - } - } + my $product = $user->check_can_admin_product($product_name); + check_token_data($token, 'edit_group_controls'); + + my @now_na = (); + my @now_mandatory = (); + foreach my $f ($cgi->param()) { + if ($f =~ /^membercontrol_(\d+)$/) { + my $id = $1; + if ($cgi->param($f) == CONTROLMAPNA) { + push @now_na, $id; + } + elsif ($cgi->param($f) == CONTROLMAPMANDATORY) { + push @now_mandatory, $id; + } } - if (!defined $cgi->param('confirmed')) { - my $na_groups; - if (@now_na) { - $na_groups = $dbh->selectall_arrayref( - 'SELECT groups.name, COUNT(bugs.bug_id) AS count + } + if (!defined $cgi->param('confirmed')) { + my $na_groups; + if (@now_na) { + $na_groups = $dbh->selectall_arrayref( + 'SELECT groups.name, COUNT(bugs.bug_id) AS count FROM bugs INNER JOIN bug_group_map ON bug_group_map.bug_id = bugs.bug_id INNER JOIN groups ON bug_group_map.group_id = groups.id WHERE groups.id IN (' . join(', ', @now_na) . ') - AND bugs.product_id = ? ' . - $dbh->sql_group_by('groups.name'), - {'Slice' => {}}, $product->id); - } - - # return the mandatory groups which need to have bug entries - # added to the bug_group_map and the corresponding bug count - - my $mandatory_groups; - if (@now_mandatory) { - $mandatory_groups = $dbh->selectall_arrayref( - 'SELECT groups.name, + AND bugs.product_id = ? ' . $dbh->sql_group_by('groups.name'), + {'Slice' => {}}, $product->id + ); + } + + # return the mandatory groups which need to have bug entries + # added to the bug_group_map and the corresponding bug count + + my $mandatory_groups; + if (@now_mandatory) { + $mandatory_groups = $dbh->selectall_arrayref( + 'SELECT groups.name, (SELECT COUNT(bugs.bug_id) FROM bugs WHERE bugs.product_id = ? @@ -361,46 +369,51 @@ if ($action eq 'updategroupcontrols') { AS count FROM groups WHERE groups.id IN (' . join(', ', @now_mandatory) . ') - ORDER BY groups.name', - {'Slice' => {}}, $product->id); - # remove zero counts - @$mandatory_groups = grep { $_->{count} } @$mandatory_groups; - - } - if (($na_groups && scalar(@$na_groups)) - || ($mandatory_groups && scalar(@$mandatory_groups))) - { - $vars->{'product'} = $product; - $vars->{'na_groups'} = $na_groups; - $vars->{'mandatory_groups'} = $mandatory_groups; - $template->process("admin/products/groupcontrol/confirm-edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } - } + ORDER BY groups.name', {'Slice' => {}}, $product->id + ); + + # remove zero counts + @$mandatory_groups = grep { $_->{count} } @$mandatory_groups; - my $groups = Bugzilla::Group->match({isactive => 1, isbuggroup => 1}); - foreach my $group (@$groups) { - my $group_id = $group->id; - $product->set_group_controls($group, - {entry => scalar $cgi->param("entry_$group_id") || 0, - membercontrol => scalar $cgi->param("membercontrol_$group_id") || CONTROLMAPNA, - othercontrol => scalar $cgi->param("othercontrol_$group_id") || CONTROLMAPNA, - canedit => scalar $cgi->param("canedit_$group_id") || 0, - editcomponents => scalar $cgi->param("editcomponents_$group_id") || 0, - editbugs => scalar $cgi->param("editbugs_$group_id") || 0, - canconfirm => scalar $cgi->param("canconfirm_$group_id") || 0}); } - my $changes = $product->update; + if ( ($na_groups && scalar(@$na_groups)) + || ($mandatory_groups && scalar(@$mandatory_groups))) + { + $vars->{'product'} = $product; + $vars->{'na_groups'} = $na_groups; + $vars->{'mandatory_groups'} = $mandatory_groups; + $template->process("admin/products/groupcontrol/confirm-edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + } + + my $groups = Bugzilla::Group->match({isactive => 1, isbuggroup => 1}); + foreach my $group (@$groups) { + my $group_id = $group->id; + $product->set_group_controls( + $group, + { + entry => scalar $cgi->param("entry_$group_id") || 0, + membercontrol => scalar $cgi->param("membercontrol_$group_id") || CONTROLMAPNA, + othercontrol => scalar $cgi->param("othercontrol_$group_id") || CONTROLMAPNA, + canedit => scalar $cgi->param("canedit_$group_id") || 0, + editcomponents => scalar $cgi->param("editcomponents_$group_id") || 0, + editbugs => scalar $cgi->param("editbugs_$group_id") || 0, + canconfirm => scalar $cgi->param("canconfirm_$group_id") || 0 + } + ); + } + my $changes = $product->update; - delete_token($token); + delete_token($token); - $vars->{'product'} = $product; - $vars->{'changes'} = $changes; + $vars->{'product'} = $product; + $vars->{'changes'} = $changes; - $template->process("admin/products/groupcontrol/updated.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $template->process("admin/products/groupcontrol/updated.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # No valid action found diff --git a/editsettings.cgi b/editsettings.cgi index ef419c7e3..9993ef882 100755 --- a/editsettings.cgi +++ b/editsettings.cgi @@ -20,48 +20,47 @@ use Bugzilla::User::Setting; use Bugzilla::Token; my $template = Bugzilla->template; -my $user = Bugzilla->login(LOGIN_REQUIRED); -my $cgi = Bugzilla->cgi; -my $vars = {}; +my $user = Bugzilla->login(LOGIN_REQUIRED); +my $cgi = Bugzilla->cgi; +my $vars = {}; print $cgi->header; $user->in_group('tweakparams') - || ThrowUserError("auth_failure", {group => "tweakparams", - action => "modify", - object => "settings"}); + || ThrowUserError("auth_failure", + {group => "tweakparams", action => "modify", object => "settings"}); my $action = trim($cgi->param('action') || ''); my $token = $cgi->param('token'); if ($action eq 'update') { - check_token_data($token, 'edit_settings'); - my $settings = Bugzilla::User::Setting::get_defaults(); - my $changed = 0; + check_token_data($token, 'edit_settings'); + my $settings = Bugzilla::User::Setting::get_defaults(); + my $changed = 0; - foreach my $name (keys %$settings) { - my $old_enabled = $settings->{$name}->{'is_enabled'}; - my $old_value = $settings->{$name}->{'default_value'}; - my $enabled = defined $cgi->param("${name}-enabled") || 0; - my $value = $cgi->param("${name}"); - my $setting = new Bugzilla::User::Setting($name); + foreach my $name (keys %$settings) { + my $old_enabled = $settings->{$name}->{'is_enabled'}; + my $old_value = $settings->{$name}->{'default_value'}; + my $enabled = defined $cgi->param("${name}-enabled") || 0; + my $value = $cgi->param("${name}"); + my $setting = new Bugzilla::User::Setting($name); - $setting->validate_value($value); + $setting->validate_value($value); - if ($old_enabled != $enabled || $old_value ne $value) { - Bugzilla::User::Setting::set_default($name, $value, $enabled); - $changed = 1; - } + if ($old_enabled != $enabled || $old_value ne $value) { + Bugzilla::User::Setting::set_default($name, $value, $enabled); + $changed = 1; } - $vars->{'message'} = 'default_settings_updated'; - $vars->{'changes_saved'} = $changed; - Bugzilla->memcached->clear_config(); - delete_token($token); + } + $vars->{'message'} = 'default_settings_updated'; + $vars->{'changes_saved'} = $changed; + Bugzilla->memcached->clear_config(); + delete_token($token); } # Don't use $settings as defaults may have changed. $vars->{'settings'} = Bugzilla::User::Setting::get_defaults(); -$vars->{'token'} = issue_session_token('edit_settings'); +$vars->{'token'} = issue_session_token('edit_settings'); $template->process("admin/settings/edit.html.tmpl", $vars) || ThrowTemplateError($template->error()); diff --git a/editusers.cgi b/editusers.cgi index beb9b3a4c..fce3748ec 100755 --- a/editusers.cgi +++ b/editusers.cgi @@ -32,16 +32,18 @@ my $dbh = Bugzilla->dbh; my $userid = $user->id; my $editusers = $user->in_group('editusers'); my $disableusers = $user->in_group('disableusers'); -local our $vars = {}; +local our $vars = {}; # Reject access if there is no sense in continuing. -$editusers - || $disableusers - || $user->can_bless() - || ThrowUserError("auth_failure", {group => "editusers", - reason => "cant_bless", - action => "edit", - object => "users"}); +$editusers || $disableusers || $user->can_bless() || ThrowUserError( + "auth_failure", + { + group => "editusers", + reason => "cant_bless", + action => "edit", + object => "users" + } +); print $cgi->header(); @@ -52,290 +54,309 @@ my $otherUserLogin = $cgi->param('user'); my $token = $cgi->param('token'); # Prefill template vars with data used in all or nearly all templates -$vars->{'editusers'} = $editusers; +$vars->{'editusers'} = $editusers; $vars->{'disableusers'} = $disableusers; mirrorListSelectionValues(); Bugzilla::Hook::process('admin_editusers_action', - { vars => $vars, user => $user, action => $action }); + {vars => $vars, user => $user, action => $action}); ########################################################################### if ($action eq 'search') { - # Allow to restrict the search to any group the user is allowed to bless. - $vars->{'restrictablegroups'} = $user->bless_groups(); - $template->process('admin/users/search.html.tmpl', $vars) - || ThrowTemplateError($template->error()); -########################################################################### -} elsif ($action eq 'list') { - my $matchvalue = $cgi->param('matchvalue') || ''; - my $matchstr = trim($cgi->param('matchstr')); - my $matchtype = $cgi->param('matchtype'); - my $grouprestrict = $cgi->param('grouprestrict') || '0'; - my $query = 'SELECT DISTINCT userid, login_name, realname, is_enabled, ' . - $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') . ' AS last_seen_date ' . - 'FROM profiles'; - my @bindValues; - my $nextCondition; - my $visibleGroups; - - # If a group ID is given, make sure it is a valid one. - my $group; - if ($grouprestrict) { - $group = new Bugzilla::Group(scalar $cgi->param('groupid')); - $group || ThrowUserError('invalid_group_ID'); - } + # Allow to restrict the search to any group the user is allowed to bless. + $vars->{'restrictablegroups'} = $user->bless_groups(); + $template->process('admin/users/search.html.tmpl', $vars) + || ThrowTemplateError($template->error()); - if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { - # Show only users in visible groups. - $visibleGroups = $user->visible_groups_as_string(); - - if ($visibleGroups) { - $query .= qq{, user_group_map AS ugm +########################################################################### +} +elsif ($action eq 'list') { + my $matchvalue = $cgi->param('matchvalue') || ''; + my $matchstr = trim($cgi->param('matchstr')); + my $matchtype = $cgi->param('matchtype'); + my $grouprestrict = $cgi->param('grouprestrict') || '0'; + my $query + = 'SELECT DISTINCT userid, login_name, realname, is_enabled, ' + . $dbh->sql_date_format('last_seen_date', '%Y-%m-%d') + . ' AS last_seen_date ' + . 'FROM profiles'; + my @bindValues; + my $nextCondition; + my $visibleGroups; + + # If a group ID is given, make sure it is a valid one. + my $group; + if ($grouprestrict) { + $group = new Bugzilla::Group(scalar $cgi->param('groupid')); + $group || ThrowUserError('invalid_group_ID'); + } + + if (!$editusers && Bugzilla->params->{'usevisibilitygroups'}) { + + # Show only users in visible groups. + $visibleGroups = $user->visible_groups_as_string(); + + if ($visibleGroups) { + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 AND ugm.group_id IN ($visibleGroups) }; - $nextCondition = 'AND'; - } - } else { - $visibleGroups = 1; - if ($grouprestrict eq '1') { - $query .= qq{, user_group_map AS ugm + $nextCondition = 'AND'; + } + } + else { + $visibleGroups = 1; + if ($grouprestrict eq '1') { + $query .= qq{, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 }; - $nextCondition = 'AND'; - } - else { - $nextCondition = 'WHERE'; - } - } - - if (!$visibleGroups) { - $vars->{'users'} = {}; + $nextCondition = 'AND'; } else { - # Handle selection by login name, real name, or userid. - if (defined($matchtype)) { - $query .= " $nextCondition "; - my $expr = ""; - if ($matchvalue eq 'userid') { - if ($matchstr) { - my $stored_matchstr = $matchstr; - detaint_natural($matchstr) - || ThrowUserError('illegal_user_id', {userid => $stored_matchstr}); - } - $expr = "profiles.userid"; - } elsif ($matchvalue eq 'realname') { - $expr = "profiles.realname"; - } else { - $expr = "profiles.login_name"; - } - - if ($matchtype =~ /^(regexp|notregexp|exact)$/) { - $matchstr ||= '.'; - } - else { - $matchstr = '' unless defined $matchstr; - } - # We can trick_taint because we use the value in a SELECT only, - # using a placeholder. - trick_taint($matchstr); - - if ($matchtype eq 'regexp') { - $query .= $dbh->sql_regexp($expr, '?', 0, $dbh->quote($matchstr)); - } elsif ($matchtype eq 'notregexp') { - $query .= $dbh->sql_not_regexp($expr, '?', 0, $dbh->quote($matchstr)); - } elsif ($matchtype eq 'exact') { - $query .= $expr . ' = ?'; - } else { # substr or unknown - $query .= $dbh->sql_istrcmp($expr, '?', 'LIKE'); - $matchstr = "%$matchstr%"; - } - $nextCondition = 'AND'; - push(@bindValues, $matchstr); + $nextCondition = 'WHERE'; + } + } + + if (!$visibleGroups) { + $vars->{'users'} = {}; + } + else { + # Handle selection by login name, real name, or userid. + if (defined($matchtype)) { + $query .= " $nextCondition "; + my $expr = ""; + if ($matchvalue eq 'userid') { + if ($matchstr) { + my $stored_matchstr = $matchstr; + detaint_natural($matchstr) + || ThrowUserError('illegal_user_id', {userid => $stored_matchstr}); } + $expr = "profiles.userid"; + } + elsif ($matchvalue eq 'realname') { + $expr = "profiles.realname"; + } + else { + $expr = "profiles.login_name"; + } + + if ($matchtype =~ /^(regexp|notregexp|exact)$/) { + $matchstr ||= '.'; + } + else { + $matchstr = '' unless defined $matchstr; + } + + # We can trick_taint because we use the value in a SELECT only, + # using a placeholder. + trick_taint($matchstr); + + if ($matchtype eq 'regexp') { + $query .= $dbh->sql_regexp($expr, '?', 0, $dbh->quote($matchstr)); + } + elsif ($matchtype eq 'notregexp') { + $query .= $dbh->sql_not_regexp($expr, '?', 0, $dbh->quote($matchstr)); + } + elsif ($matchtype eq 'exact') { + $query .= $expr . ' = ?'; + } + else { # substr or unknown + $query .= $dbh->sql_istrcmp($expr, '?', 'LIKE'); + $matchstr = "%$matchstr%"; + } + $nextCondition = 'AND'; + push(@bindValues, $matchstr); + } - # Handle selection by group. - if ($grouprestrict eq '1') { - my $grouplist = join(',', - @{Bugzilla::Group->flatten_group_membership($group->id)}); - $query .= " $nextCondition ugm.group_id IN($grouplist) "; - } - $query .= ' ORDER BY profiles.login_name'; + # Handle selection by group. + if ($grouprestrict eq '1') { + my $grouplist + = join(',', @{Bugzilla::Group->flatten_group_membership($group->id)}); + $query .= " $nextCondition ugm.group_id IN($grouplist) "; + } + $query .= ' ORDER BY profiles.login_name'; - $vars->{'users'} = $dbh->selectall_arrayref($query, - {'Slice' => {}}, - @bindValues); + $vars->{'users'} + = $dbh->selectall_arrayref($query, {'Slice' => {}}, @bindValues); - } + } - if ($matchtype && $matchtype eq 'exact' && scalar(@{$vars->{'users'}}) == 1) { - my $match_user_id = $vars->{'users'}[0]->{'userid'}; - my $match_user = check_user($match_user_id); - edit_processing($match_user); - } else { - $template->process('admin/users/list.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - } + if ($matchtype && $matchtype eq 'exact' && scalar(@{$vars->{'users'}}) == 1) { + my $match_user_id = $vars->{'users'}[0]->{'userid'}; + my $match_user = check_user($match_user_id); + edit_processing($match_user); + } + else { + $template->process('admin/users/list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + } ########################################################################### -} elsif ($action eq 'add') { - $editusers || ThrowUserError("auth_failure", {group => "editusers", - action => "add", - object => "users"}); +} +elsif ($action eq 'add') { + $editusers + || ThrowUserError("auth_failure", + {group => "editusers", action => "add", object => "users"}); - $vars->{'token'} = issue_session_token('add_user'); + $vars->{'token'} = issue_session_token('add_user'); - $template->process('admin/users/create.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + $template->process('admin/users/create.html.tmpl', $vars) + || ThrowTemplateError($template->error()); ########################################################################### -} elsif ($action eq 'new') { - $editusers || ThrowUserError("auth_failure", {group => "editusers", - action => "add", - object => "users"}); +} +elsif ($action eq 'new') { + $editusers + || ThrowUserError("auth_failure", + {group => "editusers", action => "add", object => "users"}); - check_token_data($token, 'add_user'); + check_token_data($token, 'add_user'); - # When e.g. the 'Env' auth method is used, the password field - # is not displayed. In that case, set the password to *. - my $password = $cgi->param('password'); - $password = '*' if !defined $password; + # When e.g. the 'Env' auth method is used, the password field + # is not displayed. In that case, set the password to *. + my $password = $cgi->param('password'); + $password = '*' if !defined $password; - my $new_user = Bugzilla::User->create({ - login_name => scalar $cgi->param('login'), - cryptpassword => $password, - realname => scalar $cgi->param('name'), - disabledtext => scalar $cgi->param('disabledtext'), - disable_mail => scalar $cgi->param('disable_mail'), - extern_id => scalar $cgi->param('extern_id'), - }); + my $new_user = Bugzilla::User->create({ + login_name => scalar $cgi->param('login'), + cryptpassword => $password, + realname => scalar $cgi->param('name'), + disabledtext => scalar $cgi->param('disabledtext'), + disable_mail => scalar $cgi->param('disable_mail'), + extern_id => scalar $cgi->param('extern_id'), + }); - userDataToVars($new_user->id); + userDataToVars($new_user->id); - delete_token($token); + delete_token($token); - # We already display the updated page. We have to recreate a token now. - $vars->{'token'} = issue_session_token('edit_user'); - $vars->{'message'} = 'account_created'; - $template->process('admin/users/edit.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + # We already display the updated page. We have to recreate a token now. + $vars->{'token'} = issue_session_token('edit_user'); + $vars->{'message'} = 'account_created'; + $template->process('admin/users/edit.html.tmpl', $vars) + || ThrowTemplateError($template->error()); ########################################################################### -} elsif ($action eq 'edit') { - my $otherUser = check_user($otherUserID, $otherUserLogin); - edit_processing($otherUser); +} +elsif ($action eq 'edit') { + my $otherUser = check_user($otherUserID, $otherUserLogin); + edit_processing($otherUser); ########################################################################### -} elsif ($action eq 'update') { - check_token_data($token, 'edit_user'); - my $otherUser = check_user($otherUserID, $otherUserLogin); - $otherUserID = $otherUser->id; - - # Lock tables during the check+update session. - $dbh->bz_start_transaction(); - - $editusers || $disableusers || $user->can_see_user($otherUser) - || ThrowUserError('auth_failure', {reason => "not_visible", - action => "modify", - object => "user"}); +} +elsif ($action eq 'update') { + check_token_data($token, 'edit_user'); + my $otherUser = check_user($otherUserID, $otherUserLogin); + $otherUserID = $otherUser->id; - $vars->{'loginold'} = $otherUser->login; + # Lock tables during the check+update session. + $dbh->bz_start_transaction(); - # Update profiles table entry; silently skip doing this if the user - # is not authorized. - my $changes = {}; - if ($editusers) { - $otherUser->set_login($cgi->param('login')); - $otherUser->set_password($cgi->param('password')) - if $cgi->param('password'); - $otherUser->set_extern_id($cgi->param('extern_id')) - if defined($cgi->param('extern_id')); - $otherUser->set_password_change_required($cgi->param('password_change_required')); - $otherUser->set_password_change_reason( - $otherUser->password_change_required - ? $cgi->param('password_change_reason') - : '' - ); - if ($user->in_group('bz_can_disable_mfa') && $otherUser->mfa && $cgi->param('mfa') eq '') { - $otherUser->set_mfa(''); - } + $editusers + || $disableusers + || $user->can_see_user($otherUser) + || ThrowUserError('auth_failure', + {reason => "not_visible", action => "modify", object => "user"}); + + $vars->{'loginold'} = $otherUser->login; + + # Update profiles table entry; silently skip doing this if the user + # is not authorized. + my $changes = {}; + if ($editusers) { + $otherUser->set_login($cgi->param('login')); + $otherUser->set_password($cgi->param('password')) if $cgi->param('password'); + $otherUser->set_extern_id($cgi->param('extern_id')) + if defined($cgi->param('extern_id')); + $otherUser->set_password_change_required( + $cgi->param('password_change_required')); + $otherUser->set_password_change_reason($otherUser->password_change_required + ? $cgi->param('password_change_reason') + : ''); + if ( $user->in_group('bz_can_disable_mfa') + && $otherUser->mfa + && $cgi->param('mfa') eq '') + { + $otherUser->set_mfa(''); } + } - if ($editusers || $disableusers) { - $otherUser->set_name($cgi->param('name')); - $otherUser->set_disabledtext($cgi->param('disabledtext')); - $otherUser->set_disable_mail($cgi->param('disable_mail')); - } + if ($editusers || $disableusers) { + $otherUser->set_name($cgi->param('name')); + $otherUser->set_disabledtext($cgi->param('disabledtext')); + $otherUser->set_disable_mail($cgi->param('disable_mail')); + } - $changes = $otherUser->update(); + $changes = $otherUser->update(); - # Update group settings. - my $sth_add_mapping = $dbh->prepare( - qq{INSERT INTO user_group_map ( + # Update group settings. + my $sth_add_mapping = $dbh->prepare( + qq{INSERT INTO user_group_map ( user_id, group_id, isbless, grant_type ) VALUES ( ?, ?, ?, ? ) - }); - my $sth_remove_mapping = $dbh->prepare( - qq{DELETE FROM user_group_map + } + ); + my $sth_remove_mapping = $dbh->prepare( + qq{DELETE FROM user_group_map WHERE user_id = ? AND group_id = ? AND isbless = ? AND grant_type = ? - }); - - my @groupsAddedTo; - my @groupsRemovedFrom; - my @groupsGrantedRightsToBless; - my @groupsDeniedRightsToBless; - - # Regard only groups the user is allowed to bless and skip all others - # silently. - # XXX: checking for existence of each user_group_map entry - # would allow to display a friendlier error message on page reloads. - userDataToVars($otherUserID); - my $permissions = $vars->{'permissions'}; - foreach my $blessable (@{$user->bless_groups()}) { - my $id = $blessable->id; - my $name = $blessable->name; - - # Change memberships. - my $groupid = $cgi->param("group_$id") || 0; - if ($groupid != $permissions->{$id}->{'directmember'}) { - if (!$groupid) { - $sth_remove_mapping->execute( - $otherUserID, $id, 0, GRANT_DIRECT); - push(@groupsRemovedFrom, $name); - } else { - $sth_add_mapping->execute( - $otherUserID, $id, 0, GRANT_DIRECT); - push(@groupsAddedTo, $name); - } - } + } + ); + + my @groupsAddedTo; + my @groupsRemovedFrom; + my @groupsGrantedRightsToBless; + my @groupsDeniedRightsToBless; + + # Regard only groups the user is allowed to bless and skip all others + # silently. + # XXX: checking for existence of each user_group_map entry + # would allow to display a friendlier error message on page reloads. + userDataToVars($otherUserID); + my $permissions = $vars->{'permissions'}; + foreach my $blessable (@{$user->bless_groups()}) { + my $id = $blessable->id; + my $name = $blessable->name; + + # Change memberships. + my $groupid = $cgi->param("group_$id") || 0; + if ($groupid != $permissions->{$id}->{'directmember'}) { + if (!$groupid) { + $sth_remove_mapping->execute($otherUserID, $id, 0, GRANT_DIRECT); + push(@groupsRemovedFrom, $name); + } + else { + $sth_add_mapping->execute($otherUserID, $id, 0, GRANT_DIRECT); + push(@groupsAddedTo, $name); + } + } - # Only members of the editusers group may change bless grants. - # Skip silently if this is not the case. - if ($editusers) { - my $groupid = $cgi->param("bless_$id") || 0; - if ($groupid != $permissions->{$id}->{'directbless'}) { - if (!$groupid) { - $sth_remove_mapping->execute( - $otherUserID, $id, 1, GRANT_DIRECT); - push(@groupsDeniedRightsToBless, $name); - } else { - $sth_add_mapping->execute( - $otherUserID, $id, 1, GRANT_DIRECT); - push(@groupsGrantedRightsToBless, $name); - } - } + # Only members of the editusers group may change bless grants. + # Skip silently if this is not the case. + if ($editusers) { + my $groupid = $cgi->param("bless_$id") || 0; + if ($groupid != $permissions->{$id}->{'directbless'}) { + if (!$groupid) { + $sth_remove_mapping->execute($otherUserID, $id, 1, GRANT_DIRECT); + push(@groupsDeniedRightsToBless, $name); } + else { + $sth_add_mapping->execute($otherUserID, $id, 1, GRANT_DIRECT); + push(@groupsGrantedRightsToBless, $name); + } + } } - if (@groupsAddedTo || @groupsRemovedFrom) { - $dbh->do(qq{INSERT INTO profiles_activity ( + } + if (@groupsAddedTo || @groupsRemovedFrom) { + $dbh->do( + qq{INSERT INTO profiles_activity ( userid, who, profiles_when, fieldid, oldvalue, newvalue @@ -343,344 +364,362 @@ if ($action eq 'search') { ?, ?, now(), ?, ?, ? ) }, - undef, - ($otherUserID, $userid, - get_field_id('bug_group'), - join(', ', @groupsRemovedFrom), join(', ', @groupsAddedTo))); - Bugzilla->memcached->clear_config({ key => "user_groups.$otherUserID" }) - } - # XXX: should create profiles_activity entries for blesser changes. + undef, + ( + $otherUserID, $userid, + get_field_id('bug_group'), join(', ', @groupsRemovedFrom), + join(', ', @groupsAddedTo) + ) + ); + Bugzilla->memcached->clear_config({key => "user_groups.$otherUserID"}); + } - $dbh->bz_commit_transaction(); + # XXX: should create profiles_activity entries for blesser changes. - # XXX: userDataToVars may be off when editing ourselves. - userDataToVars($otherUserID); - delete_token($token); + $dbh->bz_commit_transaction(); - $vars->{'message'} = 'account_updated'; - $vars->{'changed_fields'} = [keys %$changes]; - $vars->{'groups_added_to'} = \@groupsAddedTo; - $vars->{'groups_removed_from'} = \@groupsRemovedFrom; - $vars->{'groups_granted_rights_to_bless'} = \@groupsGrantedRightsToBless; - $vars->{'groups_denied_rights_to_bless'} = \@groupsDeniedRightsToBless; - # We already display the updated page. We have to recreate a token now. - $vars->{'token'} = issue_session_token('edit_user'); + # XXX: userDataToVars may be off when editing ourselves. + userDataToVars($otherUserID); + delete_token($token); - $template->process('admin/users/edit.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + $vars->{'message'} = 'account_updated'; + $vars->{'changed_fields'} = [keys %$changes]; + $vars->{'groups_added_to'} = \@groupsAddedTo; + $vars->{'groups_removed_from'} = \@groupsRemovedFrom; + $vars->{'groups_granted_rights_to_bless'} = \@groupsGrantedRightsToBless; + $vars->{'groups_denied_rights_to_bless'} = \@groupsDeniedRightsToBless; + + # We already display the updated page. We have to recreate a token now. + $vars->{'token'} = issue_session_token('edit_user'); + + $template->process('admin/users/edit.html.tmpl', $vars) + || ThrowTemplateError($template->error()); ########################################################################### -} elsif ($action eq 'del') { - my $otherUser = check_user($otherUserID, $otherUserLogin); - $otherUserID = $otherUser->id; - - Bugzilla->params->{'allowuserdeletion'} - || ThrowUserError('users_deletion_disabled'); - $editusers || ThrowUserError('auth_failure', {group => "editusers", - action => "delete", - object => "users"}); - $vars->{'otheruser'} = $otherUser; - - # Find other cross references. - $vars->{'attachments'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM attachments WHERE submitter_id = ?', - undef, $otherUserID); - $vars->{'assignee_or_qa'} = $dbh->selectrow_array( - qq{SELECT COUNT(*) +} +elsif ($action eq 'del') { + my $otherUser = check_user($otherUserID, $otherUserLogin); + $otherUserID = $otherUser->id; + + Bugzilla->params->{'allowuserdeletion'} + || ThrowUserError('users_deletion_disabled'); + $editusers + || ThrowUserError('auth_failure', + {group => "editusers", action => "delete", object => "users"}); + $vars->{'otheruser'} = $otherUser; + + # Find other cross references. + $vars->{'attachments'} + = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM attachments WHERE submitter_id = ?', + undef, $otherUserID); + $vars->{'assignee_or_qa'} = $dbh->selectrow_array( + qq{SELECT COUNT(*) FROM bugs - WHERE assigned_to = ? OR qa_contact = ?}, - undef, ($otherUserID, $otherUserID)); - $vars->{'reporter'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM bugs WHERE reporter = ?', - undef, $otherUserID); - $vars->{'cc'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM cc WHERE who = ?', - undef, $otherUserID); - $vars->{'bugs_activity'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM bugs_activity WHERE who = ?', - undef, $otherUserID); - $vars->{'component_cc'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM component_cc WHERE user_id = ?', - undef, $otherUserID); - $vars->{'email_setting'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM email_setting WHERE user_id = ?', - undef, $otherUserID); - $vars->{'flags'}{'requestee'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM flags WHERE requestee_id = ?', - undef, $otherUserID); - $vars->{'flags'}{'setter'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM flags WHERE setter_id = ?', - undef, $otherUserID); - $vars->{'longdescs'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM longdescs WHERE who = ?', - undef, $otherUserID); - my $namedquery_ids = $dbh->selectcol_arrayref( - 'SELECT id FROM namedqueries WHERE userid = ?', - undef, $otherUserID); - $vars->{'namedqueries'} = scalar(@$namedquery_ids); - if (scalar(@$namedquery_ids)) { - $vars->{'namedquery_group_map'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM namedquery_group_map WHERE namedquery_id IN' . - ' (' . join(', ', @$namedquery_ids) . ')'); - } - else { - $vars->{'namedquery_group_map'} = 0; - } - $vars->{'profile_setting'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM profile_setting WHERE user_id = ?', - undef, $otherUserID); - $vars->{'profiles_activity'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM profiles_activity WHERE who = ? AND userid != ?', - undef, ($otherUserID, $otherUserID)); - $vars->{'quips'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM quips WHERE userid = ?', - undef, $otherUserID); - $vars->{'series'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM series WHERE creator = ?', - undef, $otherUserID); - $vars->{'watch'}{'watched'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM watch WHERE watched = ?', - undef, $otherUserID); - $vars->{'watch'}{'watcher'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM watch WHERE watcher = ?', - undef, $otherUserID); - $vars->{'whine_events'} = $dbh->selectrow_array( - 'SELECT COUNT(*) FROM whine_events WHERE owner_userid = ?', - undef, $otherUserID); - $vars->{'whine_schedules'} = $dbh->selectrow_array( - qq{SELECT COUNT(distinct eventid) + WHERE assigned_to = ? OR qa_contact = ?}, undef, + ($otherUserID, $otherUserID) + ); + $vars->{'reporter'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs WHERE reporter = ?', + undef, $otherUserID); + $vars->{'cc'} = $dbh->selectrow_array('SELECT COUNT(*) FROM cc WHERE who = ?', + undef, $otherUserID); + $vars->{'bugs_activity'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM bugs_activity WHERE who = ?', + undef, $otherUserID); + $vars->{'component_cc'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM component_cc WHERE user_id = ?', + undef, $otherUserID); + $vars->{'email_setting'} + = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM email_setting WHERE user_id = ?', + undef, $otherUserID); + $vars->{'flags'}{'requestee'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM flags WHERE requestee_id = ?', + undef, $otherUserID); + $vars->{'flags'}{'setter'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM flags WHERE setter_id = ?', + undef, $otherUserID); + $vars->{'longdescs'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM longdescs WHERE who = ?', + undef, $otherUserID); + my $namedquery_ids + = $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?', + undef, $otherUserID); + $vars->{'namedqueries'} = scalar(@$namedquery_ids); + + if (scalar(@$namedquery_ids)) { + $vars->{'namedquery_group_map'} + = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM namedquery_group_map WHERE namedquery_id IN' . ' (' + . join(', ', @$namedquery_ids) + . ')'); + } + else { + $vars->{'namedquery_group_map'} = 0; + } + $vars->{'profile_setting'} + = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM profile_setting WHERE user_id = ?', + undef, $otherUserID); + $vars->{'profiles_activity'} + = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM profiles_activity WHERE who = ? AND userid != ?', + undef, ($otherUserID, $otherUserID)); + $vars->{'quips'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM quips WHERE userid = ?', + undef, $otherUserID); + $vars->{'series'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM series WHERE creator = ?', + undef, $otherUserID); + $vars->{'watch'}{'watched'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM watch WHERE watched = ?', + undef, $otherUserID); + $vars->{'watch'}{'watcher'} + = $dbh->selectrow_array('SELECT COUNT(*) FROM watch WHERE watcher = ?', + undef, $otherUserID); + $vars->{'whine_events'} + = $dbh->selectrow_array( + 'SELECT COUNT(*) FROM whine_events WHERE owner_userid = ?', + undef, $otherUserID); + $vars->{'whine_schedules'} = $dbh->selectrow_array( + qq{SELECT COUNT(distinct eventid) FROM whine_schedules WHERE mailto = ? AND mailto_type = ? - }, - undef, ($otherUserID, MAILTO_USER)); - $vars->{'token'} = issue_session_token('delete_user'); + }, undef, ($otherUserID, MAILTO_USER) + ); + $vars->{'token'} = issue_session_token('delete_user'); - $template->process('admin/users/confirm-delete.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + $template->process('admin/users/confirm-delete.html.tmpl', $vars) + || ThrowTemplateError($template->error()); ########################################################################### -} elsif ($action eq 'delete') { - check_token_data($token, 'delete_user'); - my $otherUser = check_user($otherUserID, $otherUserLogin); - $otherUserID = $otherUser->id; - - # Cache for user accounts. - my %usercache = (0 => new Bugzilla::User()); - my %updatedbugs; - - # Lock tables during the check+removal session. - # XXX: if there was some change on these tables after the deletion - # confirmation checks, we may do something here we haven't warned - # about. - $dbh->bz_start_transaction(); - - Bugzilla->params->{'allowuserdeletion'} - || ThrowUserError('users_deletion_disabled'); - $editusers || ThrowUserError('auth_failure', - {group => "editusers", - action => "delete", - object => "users"}); - @{$otherUser->product_responsibilities()} - && ThrowUserError('user_has_responsibility'); - - Bugzilla->logout_user($otherUser); - - # Get the named query list so we can delete namedquery_group_map entries. - my $namedqueries_as_string = join(', ', @{$dbh->selectcol_arrayref( - 'SELECT id FROM namedqueries WHERE userid = ?', undef, $otherUserID)}); - - # Get the timestamp for LogActivityEntry. - my $timestamp = $dbh->selectrow_array('SELECT NOW()'); - - # When we update a bug_activity entry, we update the bug timestamp, too. - my $sth_set_bug_timestamp = - $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); - - # Flags - my $flag_ids = - $dbh->selectcol_arrayref('SELECT id FROM flags WHERE requestee_id = ?', - undef, $otherUserID); - - my $flags = Bugzilla::Flag->new_from_list($flag_ids); - - $dbh->do('UPDATE flags SET requestee_id = NULL, modification_date = ? - WHERE requestee_id = ?', undef, ($timestamp, $otherUserID)); - - # We want to remove the requestee but leave the requester alone, - # so we have to log these changes manually. - my %bugs; - push(@{$bugs{$_->bug_id}->{$_->attach_id || 0}}, $_) foreach @$flags; - foreach my $bug_id (keys %bugs) { - foreach my $attach_id (keys %{$bugs{$bug_id}}) { - my @old_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id}); - $_->_set_requestee() foreach @{$bugs{$bug_id}->{$attach_id}}; - my @new_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id}); - my ($removed, $added) = - Bugzilla::Flag->update_activity(\@old_summaries, \@new_summaries); - LogActivityEntry($bug_id, 'flagtypes.name', $removed, $added, - $userid, $timestamp, undef, $attach_id); - } - $sth_set_bug_timestamp->execute($timestamp, $bug_id); - $updatedbugs{$bug_id} = 1; - } - - # Simple deletions in referred tables. - $dbh->do('DELETE FROM email_setting WHERE user_id = ?', undef, - $otherUserID); - $dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $otherUserID); - $dbh->do('DELETE FROM namedqueries WHERE userid = ?', undef, $otherUserID); - $dbh->do('DELETE FROM namedqueries_link_in_footer WHERE user_id = ?', undef, - $otherUserID); - if ($namedqueries_as_string) { - $dbh->do('DELETE FROM namedquery_group_map WHERE namedquery_id IN ' . - "($namedqueries_as_string)"); - } - $dbh->do('DELETE FROM profile_setting WHERE user_id = ?', undef, - $otherUserID); - $dbh->do('DELETE FROM profiles_activity WHERE userid = ? OR who = ?', undef, - ($otherUserID, $otherUserID)); - $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $otherUserID); - $dbh->do('DELETE FROM user_group_map WHERE user_id = ?', undef, - $otherUserID); - $dbh->do('DELETE FROM watch WHERE watcher = ? OR watched = ?', undef, - ($otherUserID, $otherUserID)); - - # Deletions in referred tables which need LogActivityEntry. - my $buglist = $dbh->selectcol_arrayref('SELECT bug_id FROM cc WHERE who = ?', - undef, $otherUserID); - $dbh->do('DELETE FROM cc WHERE who = ?', undef, $otherUserID); - foreach my $bug_id (@$buglist) { - LogActivityEntry($bug_id, 'cc', $otherUser->login, '', $userid, - $timestamp); - $sth_set_bug_timestamp->execute($timestamp, $bug_id); - $updatedbugs{$bug_id} = 1; +} +elsif ($action eq 'delete') { + check_token_data($token, 'delete_user'); + my $otherUser = check_user($otherUserID, $otherUserLogin); + $otherUserID = $otherUser->id; + + # Cache for user accounts. + my %usercache = (0 => new Bugzilla::User()); + my %updatedbugs; + + # Lock tables during the check+removal session. + # XXX: if there was some change on these tables after the deletion + # confirmation checks, we may do something here we haven't warned + # about. + $dbh->bz_start_transaction(); + + Bugzilla->params->{'allowuserdeletion'} + || ThrowUserError('users_deletion_disabled'); + $editusers + || ThrowUserError('auth_failure', + {group => "editusers", action => "delete", object => "users"}); + @{$otherUser->product_responsibilities()} + && ThrowUserError('user_has_responsibility'); + + Bugzilla->logout_user($otherUser); + + # Get the named query list so we can delete namedquery_group_map entries. + my $namedqueries_as_string = join( + ', ', + @{ + $dbh->selectcol_arrayref('SELECT id FROM namedqueries WHERE userid = ?', + undef, $otherUserID) } - - # Even more complex deletions in referred tables. - my $id; - - # 1) Series - my $sth_seriesid = $dbh->prepare( - 'SELECT series_id FROM series WHERE creator = ?'); - my $sth_deleteSeries = $dbh->prepare( - 'DELETE FROM series WHERE series_id = ?'); - my $sth_deleteSeriesData = $dbh->prepare( - 'DELETE FROM series_data WHERE series_id = ?'); - - $sth_seriesid->execute($otherUserID); - while ($id = $sth_seriesid->fetchrow_array()) { - $sth_deleteSeriesData->execute($id); - $sth_deleteSeries->execute($id); + ); + + # Get the timestamp for LogActivityEntry. + my $timestamp = $dbh->selectrow_array('SELECT NOW()'); + + # When we update a bug_activity entry, we update the bug timestamp, too. + my $sth_set_bug_timestamp + = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); + + # Flags + my $flag_ids + = $dbh->selectcol_arrayref('SELECT id FROM flags WHERE requestee_id = ?', + undef, $otherUserID); + + my $flags = Bugzilla::Flag->new_from_list($flag_ids); + + $dbh->do( + 'UPDATE flags SET requestee_id = NULL, modification_date = ? + WHERE requestee_id = ?', undef, ($timestamp, $otherUserID) + ); + + # We want to remove the requestee but leave the requester alone, + # so we have to log these changes manually. + my %bugs; + push(@{$bugs{$_->bug_id}->{$_->attach_id || 0}}, $_) foreach @$flags; + foreach my $bug_id (keys %bugs) { + foreach my $attach_id (keys %{$bugs{$bug_id}}) { + my @old_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id}); + $_->_set_requestee() foreach @{$bugs{$bug_id}->{$attach_id}}; + my @new_summaries = Bugzilla::Flag->snapshot($bugs{$bug_id}->{$attach_id}); + my ($removed, $added) + = Bugzilla::Flag->update_activity(\@old_summaries, \@new_summaries); + LogActivityEntry($bug_id, 'flagtypes.name', $removed, $added, $userid, + $timestamp, undef, $attach_id); } - - # 2) Whines - my $sth_whineidFromEvents = $dbh->prepare( - 'SELECT id FROM whine_events WHERE owner_userid = ?'); - my $sth_deleteWhineEvent = $dbh->prepare( - 'DELETE FROM whine_events WHERE id = ?'); - my $sth_deleteWhineQuery = $dbh->prepare( - 'DELETE FROM whine_queries WHERE eventid = ?'); - my $sth_deleteWhineSchedule = $dbh->prepare( - 'DELETE FROM whine_schedules WHERE eventid = ?'); - - $dbh->do('DELETE FROM whine_schedules WHERE mailto = ? AND mailto_type = ?', - undef, ($otherUserID, MAILTO_USER)); - - $sth_whineidFromEvents->execute($otherUserID); - while ($id = $sth_whineidFromEvents->fetchrow_array()) { - $sth_deleteWhineQuery->execute($id); - $sth_deleteWhineSchedule->execute($id); - $sth_deleteWhineEvent->execute($id); - } - - # 3) Bugs - # 3.1) fall back to the default assignee - $buglist = $dbh->selectall_arrayref( - 'SELECT bug_id, initialowner + $sth_set_bug_timestamp->execute($timestamp, $bug_id); + $updatedbugs{$bug_id} = 1; + } + + # Simple deletions in referred tables. + $dbh->do('DELETE FROM email_setting WHERE user_id = ?', undef, $otherUserID); + $dbh->do('DELETE FROM logincookies WHERE userid = ?', undef, $otherUserID); + $dbh->do('DELETE FROM namedqueries WHERE userid = ?', undef, $otherUserID); + $dbh->do('DELETE FROM namedqueries_link_in_footer WHERE user_id = ?', + undef, $otherUserID); + if ($namedqueries_as_string) { + $dbh->do('DELETE FROM namedquery_group_map WHERE namedquery_id IN ' + . "($namedqueries_as_string)"); + } + $dbh->do('DELETE FROM profile_setting WHERE user_id = ?', undef, $otherUserID); + $dbh->do('DELETE FROM profiles_activity WHERE userid = ? OR who = ?', + undef, ($otherUserID, $otherUserID)); + $dbh->do('DELETE FROM tokens WHERE userid = ?', undef, $otherUserID); + $dbh->do('DELETE FROM user_group_map WHERE user_id = ?', undef, $otherUserID); + $dbh->do('DELETE FROM watch WHERE watcher = ? OR watched = ?', + undef, ($otherUserID, $otherUserID)); + + # Deletions in referred tables which need LogActivityEntry. + my $buglist = $dbh->selectcol_arrayref('SELECT bug_id FROM cc WHERE who = ?', + undef, $otherUserID); + $dbh->do('DELETE FROM cc WHERE who = ?', undef, $otherUserID); + foreach my $bug_id (@$buglist) { + LogActivityEntry($bug_id, 'cc', $otherUser->login, '', $userid, $timestamp); + $sth_set_bug_timestamp->execute($timestamp, $bug_id); + $updatedbugs{$bug_id} = 1; + } + + # Even more complex deletions in referred tables. + my $id; + + # 1) Series + my $sth_seriesid + = $dbh->prepare('SELECT series_id FROM series WHERE creator = ?'); + my $sth_deleteSeries = $dbh->prepare('DELETE FROM series WHERE series_id = ?'); + my $sth_deleteSeriesData + = $dbh->prepare('DELETE FROM series_data WHERE series_id = ?'); + + $sth_seriesid->execute($otherUserID); + while ($id = $sth_seriesid->fetchrow_array()) { + $sth_deleteSeriesData->execute($id); + $sth_deleteSeries->execute($id); + } + + # 2) Whines + my $sth_whineidFromEvents + = $dbh->prepare('SELECT id FROM whine_events WHERE owner_userid = ?'); + my $sth_deleteWhineEvent + = $dbh->prepare('DELETE FROM whine_events WHERE id = ?'); + my $sth_deleteWhineQuery + = $dbh->prepare('DELETE FROM whine_queries WHERE eventid = ?'); + my $sth_deleteWhineSchedule + = $dbh->prepare('DELETE FROM whine_schedules WHERE eventid = ?'); + + $dbh->do('DELETE FROM whine_schedules WHERE mailto = ? AND mailto_type = ?', + undef, ($otherUserID, MAILTO_USER)); + + $sth_whineidFromEvents->execute($otherUserID); + while ($id = $sth_whineidFromEvents->fetchrow_array()) { + $sth_deleteWhineQuery->execute($id); + $sth_deleteWhineSchedule->execute($id); + $sth_deleteWhineEvent->execute($id); + } + + # 3) Bugs + # 3.1) fall back to the default assignee + $buglist = $dbh->selectall_arrayref( + 'SELECT bug_id, initialowner FROM bugs INNER JOIN components ON components.id = bugs.component_id - WHERE assigned_to = ?', undef, $otherUserID); - - my $sth_updateAssignee = $dbh->prepare( - 'UPDATE bugs SET assigned_to = ?, delta_ts = ? WHERE bug_id = ?'); - - foreach my $bug (@$buglist) { - my ($bug_id, $default_assignee_id) = @$bug; - $sth_updateAssignee->execute($default_assignee_id, - $timestamp, $bug_id); - $updatedbugs{$bug_id} = 1; - $default_assignee_id ||= 0; - $usercache{$default_assignee_id} ||= - new Bugzilla::User($default_assignee_id); - LogActivityEntry($bug_id, 'assigned_to', $otherUser->login, - $usercache{$default_assignee_id}->login, - $userid, $timestamp); - } - - # 3.2) fall back to the default QA contact - $buglist = $dbh->selectall_arrayref( - 'SELECT bug_id, initialqacontact + WHERE assigned_to = ?', undef, $otherUserID + ); + + my $sth_updateAssignee = $dbh->prepare( + 'UPDATE bugs SET assigned_to = ?, delta_ts = ? WHERE bug_id = ?'); + + foreach my $bug (@$buglist) { + my ($bug_id, $default_assignee_id) = @$bug; + $sth_updateAssignee->execute($default_assignee_id, $timestamp, $bug_id); + $updatedbugs{$bug_id} = 1; + $default_assignee_id ||= 0; + $usercache{$default_assignee_id} ||= new Bugzilla::User($default_assignee_id); + LogActivityEntry($bug_id, 'assigned_to', $otherUser->login, + $usercache{$default_assignee_id}->login, + $userid, $timestamp); + } + + # 3.2) fall back to the default QA contact + $buglist = $dbh->selectall_arrayref( + 'SELECT bug_id, initialqacontact FROM bugs INNER JOIN components ON components.id = bugs.component_id - WHERE qa_contact = ?', undef, $otherUserID); - - my $sth_updateQAcontact = $dbh->prepare( - 'UPDATE bugs SET qa_contact = ?, delta_ts = ? WHERE bug_id = ?'); - - foreach my $bug (@$buglist) { - my ($bug_id, $default_qa_contact_id) = @$bug; - $sth_updateQAcontact->execute($default_qa_contact_id, - $timestamp, $bug_id); - $updatedbugs{$bug_id} = 1; - $default_qa_contact_id ||= 0; - $usercache{$default_qa_contact_id} ||= - new Bugzilla::User($default_qa_contact_id); - LogActivityEntry($bug_id, 'qa_contact', $otherUser->login, - $usercache{$default_qa_contact_id}->login, - $userid, $timestamp); - } - - # Finally, remove the user account itself. - $dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $otherUserID); - - $dbh->bz_commit_transaction(); - delete_token($token); - - # It's complex to determine which items now need to be flushed from - # memcached. As user deletion is expected to be a rare event, we just - # flush the entire cache when a user is deleted. - Bugzilla->memcached->clear_all(); - - $vars->{'message'} = 'account_deleted'; - $vars->{'otheruser'}{'login'} = $otherUser->login; - $vars->{'restrictablegroups'} = $user->bless_groups(); - $template->process('admin/users/search.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - - # Send mail about what we've done to bugs. - # The deleted user is not notified of the changes. - foreach (keys(%updatedbugs)) { - Bugzilla::BugMail::Send($_, {'changer' => $user} ); - } + WHERE qa_contact = ?', undef, $otherUserID + ); + + my $sth_updateQAcontact = $dbh->prepare( + 'UPDATE bugs SET qa_contact = ?, delta_ts = ? WHERE bug_id = ?'); + + foreach my $bug (@$buglist) { + my ($bug_id, $default_qa_contact_id) = @$bug; + $sth_updateQAcontact->execute($default_qa_contact_id, $timestamp, $bug_id); + $updatedbugs{$bug_id} = 1; + $default_qa_contact_id ||= 0; + $usercache{$default_qa_contact_id} + ||= new Bugzilla::User($default_qa_contact_id); + LogActivityEntry($bug_id, 'qa_contact', $otherUser->login, + $usercache{$default_qa_contact_id}->login, + $userid, $timestamp); + } + + # Finally, remove the user account itself. + $dbh->do('DELETE FROM profiles WHERE userid = ?', undef, $otherUserID); + + $dbh->bz_commit_transaction(); + delete_token($token); + + # It's complex to determine which items now need to be flushed from + # memcached. As user deletion is expected to be a rare event, we just + # flush the entire cache when a user is deleted. + Bugzilla->memcached->clear_all(); + + $vars->{'message'} = 'account_deleted'; + $vars->{'otheruser'}{'login'} = $otherUser->login; + $vars->{'restrictablegroups'} = $user->bless_groups(); + $template->process('admin/users/search.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + + # Send mail about what we've done to bugs. + # The deleted user is not notified of the changes. + foreach (keys(%updatedbugs)) { + Bugzilla::BugMail::Send($_, {'changer' => $user}); + } ########################################################################### -} elsif ($action eq 'activity' || $action eq 'admin_activity') { - my $otherUser = check_user($otherUserID, $otherUserLogin); - my $activity_who = "profiles_activity.who"; - my $activity_userid = "profiles_activity.userid"; - - if ($action eq 'admin_activity') { - $editusers || ThrowUserError("auth_failure", { group => "editusers", - action => "admin_activity", - object => "users" }); - ($activity_userid, $activity_who) = ($activity_who, $activity_userid); - } - - my $sql = " +} +elsif ($action eq 'activity' || $action eq 'admin_activity') { + my $otherUser = check_user($otherUserID, $otherUserLogin); + my $activity_who = "profiles_activity.who"; + my $activity_userid = "profiles_activity.userid"; + + if ($action eq 'admin_activity') { + $editusers + || ThrowUserError("auth_failure", + {group => "editusers", action => "admin_activity", object => "users"}); + ($activity_userid, $activity_who) = ($activity_who, $activity_userid); + } + + my $sql = " SELECT profiles.login_name AS who, - " . $dbh->sql_date_format('profiles_activity.profiles_when') . " AS activity_when, + " + . $dbh->sql_date_format('profiles_activity.profiles_when') + . " AS activity_when, fielddefs.name AS what, profiles_activity.oldvalue AS removed, profiles_activity.newvalue AS added @@ -691,10 +730,10 @@ if ($action eq 'search') { WHERE $activity_userid = ? "; - my @values = ($otherUser->id); + my @values = ($otherUser->id); - if ($action ne 'admin_activity') { - $sql .= " + if ($action ne 'admin_activity') { + $sql .= " UNION ALL SELECT @@ -711,35 +750,36 @@ if ($action eq 'search') { AND audit_log.class = 'Bugzilla::User' AND audit_log.field != 'last_activity_ts' "; - push @values, $otherUser->id; + push @values, $otherUser->id; + } + + $sql .= " ORDER BY activity_when"; + + # massage some fields to improve readability + my $profile_changes = $dbh->selectall_arrayref($sql, {Slice => {}}, @values); + foreach my $change (@$profile_changes) { + if ($change->{what} eq 'cryptpassword') { + $change->{what} = 'password'; + $change->{removed} = ''; + $change->{added} = '(updated)'; } - - $sql .= " ORDER BY activity_when"; - - # massage some fields to improve readability - my $profile_changes = $dbh->selectall_arrayref($sql, { Slice => {} }, @values); - foreach my $change (@$profile_changes) { - if ($change->{what} eq 'cryptpassword') { - $change->{what} = 'password'; - $change->{removed} = ''; - $change->{added} = '(updated)'; - } - elsif ($change->{what} eq 'public_key') { - $change->{removed} = '(updated)' if $change->{removed} ne ''; - $change->{added} = '(updated)' if $change->{added} ne ''; - } + elsif ($change->{what} eq 'public_key') { + $change->{removed} = '(updated)' if $change->{removed} ne ''; + $change->{added} = '(updated)' if $change->{added} ne ''; } + } - $vars->{'profile_changes'} = $profile_changes; - $vars->{'otheruser'} = $otherUser; - $vars->{'action'} = $action; + $vars->{'profile_changes'} = $profile_changes; + $vars->{'otheruser'} = $otherUser; + $vars->{'action'} = $action; - $template->process("account/profile-activity.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("account/profile-activity.html.tmpl", $vars) + || ThrowTemplateError($template->error()); ########################################################################### -} else { - ThrowUserError('unknown_action', {action => $action}); +} +else { + ThrowUserError('unknown_action', {action => $action}); } exit; @@ -751,63 +791,61 @@ exit; # Try to build a user object using its ID, else its login name, and throw # an error if the user does not exist. sub check_user { - my ($otherUserID, $otherUserLogin) = @_; - - my $otherUser; - my $vars = {}; - - if ($otherUserID) { - $otherUser = Bugzilla::User->new($otherUserID); - $vars->{'user_id'} = $otherUserID; - } - elsif ($otherUserLogin) { - $otherUser = new Bugzilla::User({ name => $otherUserLogin }); - $vars->{'user_login'} = $otherUserLogin; - } - ($otherUser && $otherUser->id) || ThrowCodeError('invalid_user', $vars); - - if (!$user->in_group('admin')) { - my $insider_group = Bugzilla->params->{insidergroup}; - my $can_edit_insider = $user->in_group($insider_group) || $user->in_group('servicedesk'); - if ($otherUser->in_group('admin') - || ($otherUser->in_group($insider_group) && !$can_edit_insider) - ) { - ThrowUserError('auth_failure', { - action => 'modify', - object => 'user' - }); - } + my ($otherUserID, $otherUserLogin) = @_; + + my $otherUser; + my $vars = {}; + + if ($otherUserID) { + $otherUser = Bugzilla::User->new($otherUserID); + $vars->{'user_id'} = $otherUserID; + } + elsif ($otherUserLogin) { + $otherUser = new Bugzilla::User({name => $otherUserLogin}); + $vars->{'user_login'} = $otherUserLogin; + } + ($otherUser && $otherUser->id) || ThrowCodeError('invalid_user', $vars); + + if (!$user->in_group('admin')) { + my $insider_group = Bugzilla->params->{insidergroup}; + my $can_edit_insider + = $user->in_group($insider_group) || $user->in_group('servicedesk'); + if ($otherUser->in_group('admin') + || ($otherUser->in_group($insider_group) && !$can_edit_insider)) + { + ThrowUserError('auth_failure', {action => 'modify', object => 'user'}); } + } - return $otherUser; + return $otherUser; } # Copy incoming list selection values from CGI params to template variables. sub mirrorListSelectionValues { - my $cgi = Bugzilla->cgi; - if (defined($cgi->param('matchtype'))) { - foreach ('matchvalue', 'matchstr', 'matchtype', 'grouprestrict', 'groupid') { - $vars->{'listselectionvalues'}{$_} = $cgi->param($_); - } + my $cgi = Bugzilla->cgi; + if (defined($cgi->param('matchtype'))) { + foreach ('matchvalue', 'matchstr', 'matchtype', 'grouprestrict', 'groupid') { + $vars->{'listselectionvalues'}{$_} = $cgi->param($_); } + } } # Retrieve user data for the user editing form. User creation and user # editing code rely on this to call derive_groups(). sub userDataToVars { - my $otheruserid = shift; - my $otheruser = new Bugzilla::User($otheruserid); - my $query; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; + my $otheruserid = shift; + my $otheruser = new Bugzilla::User($otheruserid); + my $query; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; - my $grouplist = $otheruser->groups_as_string; + my $grouplist = $otheruser->groups_as_string; - $vars->{'otheruser'} = $otheruser; - $vars->{'groups'} = $user->bless_groups(); + $vars->{'otheruser'} = $otheruser; + $vars->{'groups'} = $user->bless_groups(); - $vars->{'permissions'} = $dbh->selectall_hashref( - qq{SELECT id, + $vars->{'permissions'} = $dbh->selectall_hashref( + qq{SELECT id, COUNT(directmember.group_id) AS directmember, COUNT(regexpmember.group_id) AS regexpmember, (CASE WHEN (groups.id IN ($grouplist) @@ -833,40 +871,41 @@ sub userDataToVars { AND directbless.isbless = 1 AND directbless.grant_type = ? } . $dbh->sql_group_by('id'), - 'id', undef, - ($otheruserid, GRANT_DIRECT, - $otheruserid, GRANT_REGEXP, - $otheruserid, GRANT_DIRECT)); - - # Find indirect bless permission. - $query = qq{SELECT groups.id + 'id', undef, + ( + $otheruserid, GRANT_DIRECT, $otheruserid, GRANT_REGEXP, + $otheruserid, GRANT_DIRECT + ) + ); + + # Find indirect bless permission. + $query = qq{SELECT groups.id FROM groups, group_group_map AS ggm WHERE groups.id = ggm.grantor_id AND ggm.member_id IN ($grouplist) AND ggm.grant_type = ? } . $dbh->sql_group_by('id'); - foreach (@{$dbh->selectall_arrayref($query, undef, - (GROUP_BLESS))}) { - # Merge indirect bless permissions into permission variable. - $vars->{'permissions'}{${$_}[0]}{'indirectbless'} = 1; - } + foreach (@{$dbh->selectall_arrayref($query, undef, (GROUP_BLESS))}) { + + # Merge indirect bless permissions into permission variable. + $vars->{'permissions'}{${$_}[0]}{'indirectbless'} = 1; + } } sub edit_processing { - my $otherUser = shift; - my $user = Bugzilla->user; - my $template = Bugzilla->template; - - $user->in_group('editusers') - || $user->in_group('disableusers') - || $user->can_see_user($otherUser) - || ThrowUserError('auth_failure', {reason => "not_visible", - action => "modify", - object => "user"}); - - userDataToVars($otherUser->id); - $vars->{'token'} = issue_session_token('edit_user'); - - $template->process('admin/users/edit.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + my $otherUser = shift; + my $user = Bugzilla->user; + my $template = Bugzilla->template; + + $user->in_group('editusers') + || $user->in_group('disableusers') + || $user->can_see_user($otherUser) + || ThrowUserError('auth_failure', + {reason => "not_visible", action => "modify", object => "user"}); + + userDataToVars($otherUser->id); + $vars->{'token'} = issue_session_token('edit_user'); + + $template->process('admin/users/edit.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } diff --git a/editvalues.cgi b/editvalues.cgi index 878e38d95..f5be52fc2 100755 --- a/editvalues.cgi +++ b/editvalues.cgi @@ -25,12 +25,12 @@ use Bugzilla::Field::Choice; ############### sub display_field_values { - my $vars = shift; - my $template = Bugzilla->template; - $vars->{'values'} = $vars->{'field'}->legal_values; - $template->process("admin/fieldvalues/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $vars = shift; + my $template = Bugzilla->template; + $vars->{'values'} = $vars->{'field'}->legal_values; + $template->process("admin/fieldvalues/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } ###################################################################### @@ -43,7 +43,7 @@ Bugzilla->login(LOGIN_REQUIRED); my $dbh = Bugzilla->dbh; my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; # Replace this entry by separate entries in templates when # the documentation about legal values becomes bigger. @@ -51,34 +51,32 @@ $vars->{'doc_section'} = 'edit-values.html'; print $cgi->header(); -Bugzilla->user->in_group('admin') || - ThrowUserError('auth_failure', {group => "admin", - action => "edit", - object => "field_values"}); +Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => "admin", action => "edit", object => "field_values"}); # # often-used variables # -my $action = trim($cgi->param('action') || ''); -my $token = $cgi->param('token'); +my $action = trim($cgi->param('action') || ''); +my $token = $cgi->param('token'); # # field = '' -> Show nice list of fields # if (!$cgi->param('field')) { - my @field_list = - @{ Bugzilla->fields({ is_select => 1, is_abnormal => 0 }) }; + my @field_list = @{Bugzilla->fields({is_select => 1, is_abnormal => 0})}; - $vars->{'fields'} = \@field_list; - $template->process("admin/fieldvalues/select-field.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'fields'} = \@field_list; + $template->process("admin/fieldvalues/select-field.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # At this point, the field must be defined. my $field = Bugzilla::Field->check($cgi->param('field')); if (!$field->is_select || $field->is_abnormal) { - ThrowUserError('fieldname_invalid', { field => $field }); + ThrowUserError('fieldname_invalid', {field => $field}); } $vars->{'field'} = $field; @@ -92,30 +90,30 @@ display_field_values($vars) unless $action; # (next action will be 'new') # if ($action eq 'add') { - $vars->{'token'} = issue_session_token('add_field_value'); - $template->process("admin/fieldvalues/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + $vars->{'token'} = issue_session_token('add_field_value'); + $template->process("admin/fieldvalues/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } # # action='new' -> add field value entered in the 'action=add' screen # if ($action eq 'new') { - check_token_data($token, 'add_field_value'); + check_token_data($token, 'add_field_value'); - my $created_value = Bugzilla::Field::Choice->type($field)->create({ - value => scalar $cgi->param('value'), - sortkey => scalar $cgi->param('sortkey'), - is_open => scalar $cgi->param('is_open'), - visibility_value_id => scalar $cgi->param('visibility_value_id'), - }); + my $created_value = Bugzilla::Field::Choice->type($field)->create({ + value => scalar $cgi->param('value'), + sortkey => scalar $cgi->param('sortkey'), + is_open => scalar $cgi->param('is_open'), + visibility_value_id => scalar $cgi->param('visibility_value_id'), + }); - delete_token($token); + delete_token($token); - $vars->{'message'} = 'field_value_created'; - $vars->{'value'} = $created_value; - display_field_values($vars); + $vars->{'message'} = 'field_value_created'; + $vars->{'value'} = $created_value; + display_field_values($vars); } # After this, we always have a value @@ -127,16 +125,17 @@ $vars->{'value'} = $value; # (next action would be 'delete') # if ($action eq 'del') { - # If the value cannot be deleted, throw an error. - if ($value->is_static) { - ThrowUserError('fieldvalue_not_deletable', $vars); - } - $vars->{'token'} = issue_session_token('delete_field_value'); - $template->process("admin/fieldvalues/confirm-delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + # If the value cannot be deleted, throw an error. + if ($value->is_static) { + ThrowUserError('fieldvalue_not_deletable', $vars); + } + $vars->{'token'} = issue_session_token('delete_field_value'); - exit; + $template->process("admin/fieldvalues/confirm-delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + + exit; } @@ -144,12 +143,12 @@ if ($action eq 'del') { # action='delete' -> really delete the field value # if ($action eq 'delete') { - check_token_data($token, 'delete_field_value'); - $value->remove_from_db(); - delete_token($token); - $vars->{'message'} = 'field_value_deleted'; - $vars->{'no_edit_link'} = 1; - display_field_values($vars); + check_token_data($token, 'delete_field_value'); + $value->remove_from_db(); + delete_token($token); + $vars->{'message'} = 'field_value_deleted'; + $vars->{'no_edit_link'} = 1; + display_field_values($vars); } @@ -158,11 +157,11 @@ if ($action eq 'delete') { # (next action would be 'update') # if ($action eq 'edit') { - $vars->{'token'} = issue_session_token('edit_field_value'); - $template->process("admin/fieldvalues/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $vars->{'token'} = issue_session_token('edit_field_value'); + $template->process("admin/fieldvalues/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } @@ -170,18 +169,18 @@ if ($action eq 'edit') { # action='update' -> update the field value # if ($action eq 'update') { - check_token_data($token, 'edit_field_value'); - $vars->{'value_old'} = $value->name; - if ($cgi->should_set('is_active')) { - $value->set_is_active($cgi->param('is_active')); - } - $value->set_name($cgi->param('value_new')); - $value->set_sortkey($cgi->param('sortkey')); - $value->set_visibility_value($cgi->param('visibility_value_id')); - $vars->{'changes'} = $value->update(); - delete_token($token); - $vars->{'message'} = 'field_value_updated'; - display_field_values($vars); + check_token_data($token, 'edit_field_value'); + $vars->{'value_old'} = $value->name; + if ($cgi->should_set('is_active')) { + $value->set_is_active($cgi->param('is_active')); + } + $value->set_name($cgi->param('value_new')); + $value->set_sortkey($cgi->param('sortkey')); + $value->set_visibility_value($cgi->param('visibility_value_id')); + $vars->{'changes'} = $value->update(); + delete_token($token); + $vars->{'message'} = 'field_value_updated'; + display_field_values($vars); } # No valid action found diff --git a/editversions.cgi b/editversions.cgi index 150151715..e7aa884f9 100755 --- a/editversions.cgi +++ b/editversions.cgi @@ -19,10 +19,11 @@ use Bugzilla::Error; use Bugzilla::Version; use Bugzilla::Token; -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; + # There is only one section about versions in the documentation, # so all actions point to the same page. $vars->{'doc_section'} = 'versions.html'; @@ -37,9 +38,8 @@ print $cgi->header(); $user->in_group('editcomponents') || scalar(@{$user->get_products_by_permission('editcomponents')}) - || ThrowUserError("auth_failure", {group => "editcomponents", - action => "edit", - object => "versions"}); + || ThrowUserError("auth_failure", + {group => "editcomponents", action => "edit", object => "versions"}); # # often used variables @@ -48,26 +48,27 @@ my $product_name = trim($cgi->param('product') || ''); my $version_name = trim($cgi->param('version') || ''); my $action = trim($cgi->param('action') || ''); my $showbugcounts = (defined $cgi->param('showbugcounts')); -my $token = $cgi->param('token'); -my $isactive = $cgi->param('isactive'); +my $token = $cgi->param('token'); +my $isactive = $cgi->param('isactive'); # # product = '' -> Show nice list of products # unless ($product_name) { - my $selectable_products = $user->get_selectable_products; - # If the user has editcomponents privs for some products only, - # we have to restrict the list of products to display. - unless ($user->in_group('editcomponents')) { - $selectable_products = $user->get_products_by_permission('editcomponents'); - } - $vars->{'products'} = $selectable_products; - $vars->{'showbugcounts'} = $showbugcounts; - - $template->process("admin/versions/select-product.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $selectable_products = $user->get_selectable_products; + + # If the user has editcomponents privs for some products only, + # we have to restrict the list of products to display. + unless ($user->in_group('editcomponents')) { + $selectable_products = $user->get_products_by_permission('editcomponents'); + } + $vars->{'products'} = $selectable_products; + $vars->{'showbugcounts'} = $showbugcounts; + + $template->process("admin/versions/select-product.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } my $product = $user->check_can_admin_product($product_name); @@ -77,12 +78,12 @@ my $product = $user->check_can_admin_product($product_name); # unless ($action) { - $vars->{'showbugcounts'} = $showbugcounts; - $vars->{'product'} = $product; - $template->process("admin/versions/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $vars->{'showbugcounts'} = $showbugcounts; + $vars->{'product'} = $product; + $template->process("admin/versions/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # @@ -92,12 +93,12 @@ unless ($action) { # if ($action eq 'add') { - $vars->{'token'} = issue_session_token('add_version'); - $vars->{'product'} = $product; - $template->process("admin/versions/create.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $vars->{'token'} = issue_session_token('add_version'); + $vars->{'product'} = $product; + $template->process("admin/versions/create.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # @@ -105,18 +106,18 @@ if ($action eq 'add') { # if ($action eq 'new') { - check_token_data($token, 'add_version'); - my $version = Bugzilla::Version->create( - { value => $version_name, product => $product }); - delete_token($token); - - $vars->{'message'} = 'version_created'; - $vars->{'version'} = $version; - $vars->{'product'} = $product; - $template->process("admin/versions/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - - exit; + check_token_data($token, 'add_version'); + my $version + = Bugzilla::Version->create({value => $version_name, product => $product}); + delete_token($token); + + $vars->{'message'} = 'version_created'; + $vars->{'version'} = $version; + $vars->{'product'} = $product; + $template->process("admin/versions/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + + exit; } # @@ -126,15 +127,15 @@ if ($action eq 'new') { # if ($action eq 'del') { - my $version = Bugzilla::Version->check({ product => $product, - name => $version_name }); - $vars->{'version'} = $version; - $vars->{'product'} = $product; - $vars->{'token'} = issue_session_token('delete_version'); - $template->process("admin/versions/confirm-delete.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - - exit; + my $version + = Bugzilla::Version->check({product => $product, name => $version_name}); + $vars->{'version'} = $version; + $vars->{'product'} = $product; + $vars->{'token'} = issue_session_token('delete_version'); + $template->process("admin/versions/confirm-delete.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + + exit; } # @@ -142,21 +143,21 @@ if ($action eq 'del') { # if ($action eq 'delete') { - check_token_data($token, 'delete_version'); - my $version = Bugzilla::Version->check({ product => $product, - name => $version_name }); - $version->remove_from_db; - delete_token($token); + check_token_data($token, 'delete_version'); + my $version + = Bugzilla::Version->check({product => $product, name => $version_name}); + $version->remove_from_db; + delete_token($token); - $vars->{'message'} = 'version_deleted'; - $vars->{'version'} = $version; - $vars->{'product'} = $product; - $vars->{'no_edit_version_link'} = 1; + $vars->{'message'} = 'version_deleted'; + $vars->{'version'} = $version; + $vars->{'product'} = $product; + $vars->{'no_edit_version_link'} = 1; - $template->process("admin/versions/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("admin/versions/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # @@ -166,16 +167,16 @@ if ($action eq 'delete') { # if ($action eq 'edit') { - my $version = Bugzilla::Version->check({ product => $product, - name => $version_name }); - $vars->{'version'} = $version; - $vars->{'product'} = $product; - $vars->{'token'} = issue_session_token('edit_version'); + my $version + = Bugzilla::Version->check({product => $product, name => $version_name}); + $vars->{'version'} = $version; + $vars->{'product'} = $product; + $vars->{'token'} = issue_session_token('edit_version'); - $template->process("admin/versions/edit.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("admin/versions/edit.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # @@ -183,28 +184,28 @@ if ($action eq 'edit') { # if ($action eq 'update') { - check_token_data($token, 'edit_version'); - my $version_old_name = trim($cgi->param('versionold') || ''); - my $version = Bugzilla::Version->check({ product => $product, - name => $version_old_name }); + check_token_data($token, 'edit_version'); + my $version_old_name = trim($cgi->param('versionold') || ''); + my $version + = Bugzilla::Version->check({product => $product, name => $version_old_name}); - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - $version->set_name($version_name); - $version->set_is_active($isactive); - my $changes = $version->update(); + $version->set_name($version_name); + $version->set_is_active($isactive); + my $changes = $version->update(); - $dbh->bz_commit_transaction(); - delete_token($token); + $dbh->bz_commit_transaction(); + delete_token($token); - $vars->{'message'} = 'version_updated'; - $vars->{'version'} = $version; - $vars->{'product'} = $product; - $vars->{'changes'} = $changes; - $template->process("admin/versions/list.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $vars->{'message'} = 'version_updated'; + $vars->{'version'} = $version; + $vars->{'product'} = $product; + $vars->{'changes'} = $changes; + $template->process("admin/versions/list.html.tmpl", $vars) + || ThrowTemplateError($template->error()); - exit; + exit; } # No valid action found diff --git a/editwhines.cgi b/editwhines.cgi index 31b3dcfaf..1ad1292f5 100755 --- a/editwhines.cgi +++ b/editwhines.cgi @@ -39,9 +39,9 @@ my $template = Bugzilla->template; my $vars = {}; my $dbh = Bugzilla->dbh; -my $userid = $user->id; -my $token = $cgi->param('token'); -my $sth; # database statement handle +my $userid = $user->id; +my $token = $cgi->param('token'); +my $sth; # database statement handle # $events is a hash ref of Bugzilla::Whine objects keyed by event id, # that stores the active user's events. @@ -64,9 +64,8 @@ my $events = get_events($userid); # First see if this user may use whines $user->in_group('bz_canusewhines') - || ThrowUserError("auth_failure", {group => "bz_canusewhines", - action => "schedule", - object => "reports"}); + || ThrowUserError("auth_failure", + {group => "bz_canusewhines", action => "schedule", object => "reports"}); # May this user send mail to other users? my $can_mail_others = Bugzilla->user->in_group('bz_canusewhineatothers'); @@ -75,240 +74,245 @@ my $can_mail_others = Bugzilla->user->in_group('bz_canusewhineatothers'); # removed, then what was altered. if ($cgi->param('update')) { - check_token_data($token, 'edit_whine'); - - if ($cgi->param("add_event")) { - # we create a new event - $sth = $dbh->prepare("INSERT INTO whine_events " . - "(owner_userid) " . - "VALUES (?)"); - $sth->execute($userid); - } - else { - for my $eventid (keys %{$events}) { - # delete an entire event - if ($cgi->param("remove_event_$eventid")) { - # We need to make sure these belong to the same user, - # otherwise we could simply delete whatever matched that ID. - # - # schedules - my $schedules = Bugzilla::Whine::Schedule->match({ eventid => $eventid }); - $sth = $dbh->prepare("DELETE FROM whine_schedules " - . "WHERE id=?"); - foreach my $schedule (@$schedules) { - $sth->execute($schedule->id); - } - - # queries - $sth = $dbh->prepare("SELECT whine_queries.id " . - "FROM whine_queries " . - "LEFT JOIN whine_events " . - "ON whine_events.id = " . - "whine_queries.eventid " . - "WHERE whine_events.id = ? " . - "AND whine_events.owner_userid = ?"); - $sth->execute($eventid, $userid); - my @ids = @{$sth->fetchall_arrayref}; - $sth = $dbh->prepare("DELETE FROM whine_queries " . - "WHERE id=?"); - for (@ids) { - my $delete_id = $_->[0]; - $sth->execute($delete_id); - } - - # events - $sth = $dbh->prepare("DELETE FROM whine_events " . - "WHERE id=? AND owner_userid=?"); - $sth->execute($eventid, $userid); - } - else { - # check the subject, body and mailifnobugs for changes - my $subject = ($cgi->param("event_${eventid}_subject") or ''); - my $body = ($cgi->param("event_${eventid}_body") or ''); - my $mailifnobugs = $cgi->param("event_${eventid}_mailifnobugs") ? 1 : 0; - - trick_taint($subject) if $subject; - trick_taint($body) if $body; - - if ( ($subject ne $events->{$eventid}->subject) - || ($mailifnobugs != $events->{$eventid}->mail_if_no_bugs) - || ($body ne $events->{$eventid}->body) ) { - - $sth = $dbh->prepare("UPDATE whine_events " . - "SET subject=?, body=?, mailifnobugs=? " . - "WHERE id=?"); - $sth->execute($subject, $body, $mailifnobugs, $eventid); - } - - # add a schedule - if ($cgi->param("add_schedule_$eventid")) { - # the schedule table must be locked before altering - $sth = $dbh->prepare("INSERT INTO whine_schedules " . - "(eventid, mailto_type, mailto, " . - "run_day, run_time) " . - "VALUES (?, ?, ?, 'Sun', 2)"); - $sth->execute($eventid, MAILTO_USER, $userid); - } - # add a query - elsif ($cgi->param("add_query_$eventid")) { - $sth = $dbh->prepare("INSERT INTO whine_queries " - . "(eventid) " - . "VALUES (?)"); - $sth->execute($eventid); - } - } + check_token_data($token, 'edit_whine'); + + if ($cgi->param("add_event")) { + + # we create a new event + $sth = $dbh->prepare( + "INSERT INTO whine_events " . "(owner_userid) " . "VALUES (?)"); + $sth->execute($userid); + } + else { + for my $eventid (keys %{$events}) { + + # delete an entire event + if ($cgi->param("remove_event_$eventid")) { + + # We need to make sure these belong to the same user, + # otherwise we could simply delete whatever matched that ID. + # + # schedules + my $schedules = Bugzilla::Whine::Schedule->match({eventid => $eventid}); + $sth = $dbh->prepare("DELETE FROM whine_schedules " . "WHERE id=?"); + foreach my $schedule (@$schedules) { + $sth->execute($schedule->id); + } + + # queries + $sth + = $dbh->prepare("SELECT whine_queries.id " + . "FROM whine_queries " + . "LEFT JOIN whine_events " + . "ON whine_events.id = " + . "whine_queries.eventid " + . "WHERE whine_events.id = ? " + . "AND whine_events.owner_userid = ?"); + $sth->execute($eventid, $userid); + my @ids = @{$sth->fetchall_arrayref}; + $sth = $dbh->prepare("DELETE FROM whine_queries " . "WHERE id=?"); + for (@ids) { + my $delete_id = $_->[0]; + $sth->execute($delete_id); + } - # now check all of the schedules and queries to see if they need - # to be altered or deleted + # events + $sth = $dbh->prepare( + "DELETE FROM whine_events " . "WHERE id=? AND owner_userid=?"); + $sth->execute($eventid, $userid); + } + else { + # check the subject, body and mailifnobugs for changes + my $subject = ($cgi->param("event_${eventid}_subject") or ''); + my $body = ($cgi->param("event_${eventid}_body") or ''); + my $mailifnobugs = $cgi->param("event_${eventid}_mailifnobugs") ? 1 : 0; + + trick_taint($subject) if $subject; + trick_taint($body) if $body; + + if ( ($subject ne $events->{$eventid}->subject) + || ($mailifnobugs != $events->{$eventid}->mail_if_no_bugs) + || ($body ne $events->{$eventid}->body)) + { + + $sth + = $dbh->prepare("UPDATE whine_events " + . "SET subject=?, body=?, mailifnobugs=? " + . "WHERE id=?"); + $sth->execute($subject, $body, $mailifnobugs, $eventid); + } - # Check schedules for changes - my $schedules = Bugzilla::Whine::Schedule->match({ eventid => $eventid }); - my @scheduleids = (); - foreach my $schedule (@$schedules) { - push @scheduleids, $schedule->id; - } + # add a schedule + if ($cgi->param("add_schedule_$eventid")) { - # we need to double-check all of the user IDs in mailto to make - # sure they exist - my $arglist = {}; # args for match_field - for my $sid (@scheduleids) { - if ($cgi->param("mailto_type_$sid") == MAILTO_USER) { - $arglist->{"mailto_$sid"} = { - 'type' => 'single', - }; - } - } - if (scalar %{$arglist}) { - Bugzilla::User::match_field($arglist); - } + # the schedule table must be locked before altering + $sth + = $dbh->prepare("INSERT INTO whine_schedules " + . "(eventid, mailto_type, mailto, " + . "run_day, run_time) " + . "VALUES (?, ?, ?, 'Sun', 2)"); + $sth->execute($eventid, MAILTO_USER, $userid); + } - for my $sid (@scheduleids) { - if ($cgi->param("remove_schedule_$sid")) { - # having the assignee id in here is a security failsafe - $sth = $dbh->prepare("SELECT whine_schedules.id " . - "FROM whine_schedules " . - "LEFT JOIN whine_events " . - "ON whine_events.id = " . - "whine_schedules.eventid " . - "WHERE whine_events.owner_userid=? " . - "AND whine_schedules.id =?"); - $sth->execute($userid, $sid); - - my @ids = @{$sth->fetchall_arrayref}; - for (@ids) { - $sth = $dbh->prepare("DELETE FROM whine_schedules " . - "WHERE id=?"); - $sth->execute($_->[0]); - } - } - else { - my $o_day = $cgi->param("orig_day_$sid") || ''; - my $day = $cgi->param("day_$sid") || ''; - my $o_time = $cgi->param("orig_time_$sid") || 0; - my $time = $cgi->param("time_$sid") || 0; - my $o_mailto = $cgi->param("orig_mailto_$sid") || ''; - my $mailto = $cgi->param("mailto_$sid") || ''; - my $o_mailto_type = $cgi->param("orig_mailto_type_$sid") || 0; - my $mailto_type = $cgi->param("mailto_type_$sid") || 0; - - my $mailto_id = $userid; - - # get an id for the mailto address - if ($can_mail_others && $mailto) { - if ($mailto_type == MAILTO_USER) { - $mailto_id = login_to_id($mailto); - } - elsif ($mailto_type == MAILTO_GROUP) { - # The group name is used in a placeholder. - trick_taint($mailto); - $mailto_id = Bugzilla::Group::ValidateGroupName($mailto, ($user)) - || ThrowUserError('invalid_group_name', { name => $mailto }); - } - else { - # bad value, so it will just mail to the whine - # owner. $mailto_id was already set above. - $mailto_type = MAILTO_USER; - } - } - - detaint_natural($mailto_type); - - if ( ($o_day ne $day) || - ($o_time ne $time) || - ($o_mailto ne $mailto) || - ($o_mailto_type != $mailto_type) ){ - - trick_taint($day); - trick_taint($time); - - # the schedule table must be locked - $sth = $dbh->prepare("UPDATE whine_schedules " . - "SET run_day=?, run_time=?, " . - "mailto_type=?, mailto=?, " . - "run_next=NULL " . - "WHERE id=?"); - $sth->execute($day, $time, $mailto_type, - $mailto_id, $sid); - } - } + # add a query + elsif ($cgi->param("add_query_$eventid")) { + $sth + = $dbh->prepare("INSERT INTO whine_queries " . "(eventid) " . "VALUES (?)"); + $sth->execute($eventid); + } + } + + # now check all of the schedules and queries to see if they need + # to be altered or deleted + + # Check schedules for changes + my $schedules = Bugzilla::Whine::Schedule->match({eventid => $eventid}); + my @scheduleids = (); + foreach my $schedule (@$schedules) { + push @scheduleids, $schedule->id; + } + + # we need to double-check all of the user IDs in mailto to make + # sure they exist + my $arglist = {}; # args for match_field + for my $sid (@scheduleids) { + if ($cgi->param("mailto_type_$sid") == MAILTO_USER) { + $arglist->{"mailto_$sid"} = {'type' => 'single',}; + } + } + if (scalar %{$arglist}) { + Bugzilla::User::match_field($arglist); + } + + for my $sid (@scheduleids) { + if ($cgi->param("remove_schedule_$sid")) { + + # having the assignee id in here is a security failsafe + $sth + = $dbh->prepare("SELECT whine_schedules.id " + . "FROM whine_schedules " + . "LEFT JOIN whine_events " + . "ON whine_events.id = " + . "whine_schedules.eventid " + . "WHERE whine_events.owner_userid=? " + . "AND whine_schedules.id =?"); + $sth->execute($userid, $sid); + + my @ids = @{$sth->fetchall_arrayref}; + for (@ids) { + $sth = $dbh->prepare("DELETE FROM whine_schedules " . "WHERE id=?"); + $sth->execute($_->[0]); + } + } + else { + my $o_day = $cgi->param("orig_day_$sid") || ''; + my $day = $cgi->param("day_$sid") || ''; + my $o_time = $cgi->param("orig_time_$sid") || 0; + my $time = $cgi->param("time_$sid") || 0; + my $o_mailto = $cgi->param("orig_mailto_$sid") || ''; + my $mailto = $cgi->param("mailto_$sid") || ''; + my $o_mailto_type = $cgi->param("orig_mailto_type_$sid") || 0; + my $mailto_type = $cgi->param("mailto_type_$sid") || 0; + + my $mailto_id = $userid; + + # get an id for the mailto address + if ($can_mail_others && $mailto) { + if ($mailto_type == MAILTO_USER) { + $mailto_id = login_to_id($mailto); } + elsif ($mailto_type == MAILTO_GROUP) { - # Check queries for changes - my $queries = Bugzilla::Whine::Query->match({ eventid => $eventid }); - for my $query (@$queries) { - my $qid = $query->id; - if ($cgi->param("remove_query_$qid")) { - - $sth = $dbh->prepare("SELECT whine_queries.id " . - "FROM whine_queries " . - "LEFT JOIN whine_events " . - "ON whine_events.id = " . - "whine_queries.eventid " . - "WHERE whine_events.owner_userid=? " . - "AND whine_queries.id =?"); - $sth->execute($userid, $qid); - - for (@{$sth->fetchall_arrayref}) { - $sth = $dbh->prepare("DELETE FROM whine_queries " . - "WHERE id=?"); - $sth->execute($_->[0]); - } - } - else { - my $o_sort = $cgi->param("orig_query_sort_$qid") || 0; - my $sort = $cgi->param("query_sort_$qid") || 0; - my $o_queryname = $cgi->param("orig_query_name_$qid") || ''; - my $queryname = $cgi->param("query_name_$qid") || ''; - my $o_title = $cgi->param("orig_query_title_$qid") || ''; - my $title = $cgi->param("query_title_$qid") || ''; - my $o_onemailperbug = - $cgi->param("orig_query_onemailperbug_$qid") || 0; - my $onemailperbug = - $cgi->param("query_onemailperbug_$qid") ? 1 : 0; - - if ( ($o_sort != $sort) || - ($o_queryname ne $queryname) || - ($o_onemailperbug != $onemailperbug) || - ($o_title ne $title) ){ - - detaint_natural($sort); - trick_taint($queryname); - trick_taint($title); - - $sth = $dbh->prepare("UPDATE whine_queries " . - "SET sortkey=?, " . - "query_name=?, " . - "title=?, " . - "onemailperbug=? " . - "WHERE id=?"); - $sth->execute($sort, $queryname, $title, - $onemailperbug, $qid); - } - } + # The group name is used in a placeholder. + trick_taint($mailto); + $mailto_id = Bugzilla::Group::ValidateGroupName($mailto, ($user)) + || ThrowUserError('invalid_group_name', {name => $mailto}); } + else { + # bad value, so it will just mail to the whine + # owner. $mailto_id was already set above. + $mailto_type = MAILTO_USER; + } + } + + detaint_natural($mailto_type); + + if ( ($o_day ne $day) + || ($o_time ne $time) + || ($o_mailto ne $mailto) + || ($o_mailto_type != $mailto_type)) + { + + trick_taint($day); + trick_taint($time); + + # the schedule table must be locked + $sth + = $dbh->prepare("UPDATE whine_schedules " + . "SET run_day=?, run_time=?, " + . "mailto_type=?, mailto=?, " + . "run_next=NULL " + . "WHERE id=?"); + $sth->execute($day, $time, $mailto_type, $mailto_id, $sid); + } + } + } + + # Check queries for changes + my $queries = Bugzilla::Whine::Query->match({eventid => $eventid}); + for my $query (@$queries) { + my $qid = $query->id; + if ($cgi->param("remove_query_$qid")) { + + $sth + = $dbh->prepare("SELECT whine_queries.id " + . "FROM whine_queries " + . "LEFT JOIN whine_events " + . "ON whine_events.id = " + . "whine_queries.eventid " + . "WHERE whine_events.owner_userid=? " + . "AND whine_queries.id =?"); + $sth->execute($userid, $qid); + + for (@{$sth->fetchall_arrayref}) { + $sth = $dbh->prepare("DELETE FROM whine_queries " . "WHERE id=?"); + $sth->execute($_->[0]); + } } + else { + my $o_sort = $cgi->param("orig_query_sort_$qid") || 0; + my $sort = $cgi->param("query_sort_$qid") || 0; + my $o_queryname = $cgi->param("orig_query_name_$qid") || ''; + my $queryname = $cgi->param("query_name_$qid") || ''; + my $o_title = $cgi->param("orig_query_title_$qid") || ''; + my $title = $cgi->param("query_title_$qid") || ''; + my $o_onemailperbug = $cgi->param("orig_query_onemailperbug_$qid") || 0; + my $onemailperbug = $cgi->param("query_onemailperbug_$qid") ? 1 : 0; + + if ( ($o_sort != $sort) + || ($o_queryname ne $queryname) + || ($o_onemailperbug != $onemailperbug) + || ($o_title ne $title)) + { + + detaint_natural($sort); + trick_taint($queryname); + trick_taint($title); + + $sth + = $dbh->prepare("UPDATE whine_queries " + . "SET sortkey=?, " + . "query_name=?, " + . "title=?, " + . "onemailperbug=? " + . "WHERE id=?"); + $sth->execute($sort, $queryname, $title, $onemailperbug, $qid); + } + } + } } - delete_token($token); + } + delete_token($token); } $vars->{'mail_others'} = $can_mail_others; @@ -334,44 +338,43 @@ $events = get_events($userid); # # build the whine list by event id for my $event_id (keys %{$events}) { - $events->{$event_id}->{'schedule'} = []; - $events->{$event_id}->{'queries'} = []; - - # schedules - my $schedules = Bugzilla::Whine::Schedule->match({ eventid => $event_id }); - foreach my $schedule (@$schedules) { - my $mailto_type = $schedule->mailto_is_group ? MAILTO_GROUP - : MAILTO_USER; - my $mailto = ''; - if ($mailto_type == MAILTO_USER) { - $mailto = $schedule->mailto->login; - } - elsif ($mailto_type == MAILTO_GROUP) { - $mailto = $schedule->mailto->name; - } - - push @{$events->{$event_id}->{'schedule'}}, - { - 'day' => $schedule->run_day, - 'time' => $schedule->run_time, - 'mailto_type' => $mailto_type, - 'mailto' => $mailto, - 'id' => $schedule->id, - }; + $events->{$event_id}->{'schedule'} = []; + $events->{$event_id}->{'queries'} = []; + + # schedules + my $schedules = Bugzilla::Whine::Schedule->match({eventid => $event_id}); + foreach my $schedule (@$schedules) { + my $mailto_type = $schedule->mailto_is_group ? MAILTO_GROUP : MAILTO_USER; + my $mailto = ''; + if ($mailto_type == MAILTO_USER) { + $mailto = $schedule->mailto->login; } - - # queries - my $queries = Bugzilla::Whine::Query->match({ eventid => $event_id }); - for my $query (@$queries) { - push @{$events->{$event_id}->{'queries'}}, - { - 'name' => $query->name, - 'title' => $query->title, - 'sort' => $query->sortkey, - 'id' => $query->id, - 'onemailperbug' => $query->one_email_per_bug, - }; + elsif ($mailto_type == MAILTO_GROUP) { + $mailto = $schedule->mailto->name; } + + push @{$events->{$event_id}->{'schedule'}}, + { + 'day' => $schedule->run_day, + 'time' => $schedule->run_time, + 'mailto_type' => $mailto_type, + 'mailto' => $mailto, + 'id' => $schedule->id, + }; + } + + # queries + my $queries = Bugzilla::Whine::Query->match({eventid => $event_id}); + for my $query (@$queries) { + push @{$events->{$event_id}->{'queries'}}, + { + 'name' => $query->name, + 'title' => $query->title, + 'sort' => $query->sortkey, + 'id' => $query->id, + 'onemailperbug' => $query->one_email_per_bug, + }; + } } $vars->{'events'} = $events; @@ -382,10 +385,11 @@ $sth->execute($userid); $vars->{'available_queries'} = []; while (my ($query) = $sth->fetchrow_array) { - push @{$vars->{'available_queries'}}, $query; + push @{$vars->{'available_queries'}}, $query; } $vars->{'token'} = issue_session_token('edit_whine'); -$vars->{'local_timezone'} = Bugzilla->local_timezone->short_name_for_datetime(DateTime->now()); +$vars->{'local_timezone'} + = Bugzilla->local_timezone->short_name_for_datetime(DateTime->now()); $template->process("whine/schedule.html.tmpl", $vars) || ThrowTemplateError($template->error()); @@ -393,10 +397,10 @@ $template->process("whine/schedule.html.tmpl", $vars) # get_events takes a userid and returns a hash of # Bugzilla::Whine objects keyed by event ID. sub get_events { - my $userid = shift; - my $event_rows = Bugzilla::Whine->match({ owner_userid => $userid }); - my %events = map { $_->{id} => $_ } @$event_rows; + my $userid = shift; + my $event_rows = Bugzilla::Whine->match({owner_userid => $userid}); + my %events = map { $_->{id} => $_ } @$event_rows; - return \%events; + return \%events; } diff --git a/editworkflow.cgi b/editworkflow.cgi index eef7a9f62..f97a7e388 100755 --- a/editworkflow.cgi +++ b/editworkflow.cgi @@ -18,123 +18,134 @@ use Bugzilla::Error; use Bugzilla::Token; use Bugzilla::Status; -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $user = Bugzilla->login(LOGIN_REQUIRED); print $cgi->header(); $user->in_group('admin') - || ThrowUserError('auth_failure', {group => 'admin', - action => 'modify', - object => 'workflow'}); + || ThrowUserError('auth_failure', + {group => 'admin', action => 'modify', object => 'workflow'}); my $action = $cgi->param('action') || 'edit'; my $token = $cgi->param('token'); sub get_workflow { - my $dbh = Bugzilla->dbh; - my $workflow = $dbh->selectall_arrayref('SELECT old_status, new_status, require_comment - FROM status_workflow'); - my %workflow; - foreach my $row (@$workflow) { - my ($old, $new, $type) = @$row; - $workflow{$old || 0}{$new} = $type; - } - return \%workflow; + my $dbh = Bugzilla->dbh; + my $workflow = $dbh->selectall_arrayref( + 'SELECT old_status, new_status, require_comment + FROM status_workflow' + ); + my %workflow; + foreach my $row (@$workflow) { + my ($old, $new, $type) = @$row; + $workflow{$old || 0}{$new} = $type; + } + return \%workflow; } sub load_template { - my ($filename, $message) = @_; - my $template = Bugzilla->template; - my $vars = {}; - - $vars->{'statuses'} = [Bugzilla::Status->get_all]; - $vars->{'workflow'} = get_workflow(); - $vars->{'token'} = issue_session_token("workflow_$filename"); - $vars->{'message'} = $message; - - $template->process("admin/workflow/$filename.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my ($filename, $message) = @_; + my $template = Bugzilla->template; + my $vars = {}; + + $vars->{'statuses'} = [Bugzilla::Status->get_all]; + $vars->{'workflow'} = get_workflow(); + $vars->{'token'} = issue_session_token("workflow_$filename"); + $vars->{'message'} = $message; + + $template->process("admin/workflow/$filename.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } if ($action eq 'edit') { - load_template('edit'); + load_template('edit'); } elsif ($action eq 'update') { - check_token_data($token, 'workflow_edit'); - my $statuses = [Bugzilla::Status->get_all]; - my $workflow = get_workflow(); - - my $sth_insert = $dbh->prepare('INSERT INTO status_workflow (old_status, new_status) - VALUES (?, ?)'); - my $sth_delete = $dbh->prepare('DELETE FROM status_workflow - WHERE old_status = ? AND new_status = ?'); - my $sth_delnul = $dbh->prepare('DELETE FROM status_workflow - WHERE old_status IS NULL AND new_status = ?'); - - # Part 1: Initial bug statuses. - foreach my $new (@$statuses) { - if ($new->is_open && $cgi->param('w_0_' . $new->id)) { - $sth_insert->execute(undef, $new->id) - unless defined $workflow->{0}->{$new->id}; - } - else { - $sth_delnul->execute($new->id); - } + check_token_data($token, 'workflow_edit'); + my $statuses = [Bugzilla::Status->get_all]; + my $workflow = get_workflow(); + + my $sth_insert = $dbh->prepare( + 'INSERT INTO status_workflow (old_status, new_status) + VALUES (?, ?)' + ); + my $sth_delete = $dbh->prepare( + 'DELETE FROM status_workflow + WHERE old_status = ? AND new_status = ?' + ); + my $sth_delnul = $dbh->prepare( + 'DELETE FROM status_workflow + WHERE old_status IS NULL AND new_status = ?' + ); + + # Part 1: Initial bug statuses. + foreach my $new (@$statuses) { + if ($new->is_open && $cgi->param('w_0_' . $new->id)) { + $sth_insert->execute(undef, $new->id) unless defined $workflow->{0}->{$new->id}; } + else { + $sth_delnul->execute($new->id); + } + } - # Part 2: Bug status changes. - foreach my $old (@$statuses) { - foreach my $new (@$statuses) { - next if $old->id == $new->id; - - # All transitions to 'duplicate_or_move_bug_status' must be valid. - if ($cgi->param('w_' . $old->id . '_' . $new->id) - || ($new->name eq Bugzilla->params->{'duplicate_or_move_bug_status'})) - { - $sth_insert->execute($old->id, $new->id) - unless defined $workflow->{$old->id}->{$new->id}; - } - else { - $sth_delete->execute($old->id, $new->id); - } - } + # Part 2: Bug status changes. + foreach my $old (@$statuses) { + foreach my $new (@$statuses) { + next if $old->id == $new->id; + + # All transitions to 'duplicate_or_move_bug_status' must be valid. + if ($cgi->param('w_' . $old->id . '_' . $new->id) + || ($new->name eq Bugzilla->params->{'duplicate_or_move_bug_status'})) + { + $sth_insert->execute($old->id, $new->id) + unless defined $workflow->{$old->id}->{$new->id}; + } + else { + $sth_delete->execute($old->id, $new->id); + } } - delete_token($token); - load_template('edit', 'workflow_updated'); + } + delete_token($token); + load_template('edit', 'workflow_updated'); } elsif ($action eq 'edit_comment') { - load_template('comment'); + load_template('comment'); } elsif ($action eq 'update_comment') { - check_token_data($token, 'workflow_comment'); - my $workflow = get_workflow(); - - my $sth_update = $dbh->prepare('UPDATE status_workflow SET require_comment = ? - WHERE old_status = ? AND new_status = ?'); - my $sth_updnul = $dbh->prepare('UPDATE status_workflow SET require_comment = ? - WHERE old_status IS NULL AND new_status = ?'); - - foreach my $old (keys %$workflow) { - # Hashes cannot have undef as a key, so we use 0. But the DB - # must store undef, for referential integrity. - my $old_id_for_db = $old || undef; - foreach my $new (keys %{$workflow->{$old}}) { - my $comment_required = $cgi->param("c_${old}_$new") ? 1 : 0; - next if ($workflow->{$old}->{$new} == $comment_required); - if ($old_id_for_db) { - $sth_update->execute($comment_required, $old_id_for_db, $new); - } - else { - $sth_updnul->execute($comment_required, $new); - } - } + check_token_data($token, 'workflow_comment'); + my $workflow = get_workflow(); + + my $sth_update = $dbh->prepare( + 'UPDATE status_workflow SET require_comment = ? + WHERE old_status = ? AND new_status = ?' + ); + my $sth_updnul = $dbh->prepare( + 'UPDATE status_workflow SET require_comment = ? + WHERE old_status IS NULL AND new_status = ?' + ); + + foreach my $old (keys %$workflow) { + + # Hashes cannot have undef as a key, so we use 0. But the DB + # must store undef, for referential integrity. + my $old_id_for_db = $old || undef; + foreach my $new (keys %{$workflow->{$old}}) { + my $comment_required = $cgi->param("c_${old}_$new") ? 1 : 0; + next if ($workflow->{$old}->{$new} == $comment_required); + if ($old_id_for_db) { + $sth_update->execute($comment_required, $old_id_for_db, $new); + } + else { + $sth_updnul->execute($comment_required, $new); + } } - delete_token($token); - load_template('comment', 'workflow_updated'); + } + delete_token($token); + load_template('comment', 'workflow_updated'); } else { - ThrowUserError('unknown_action', {action => $action}); + ThrowUserError('unknown_action', {action => $action}); } diff --git a/email_in.pl b/email_in.pl index d2d57ed59..382e60660 100755 --- a/email_in.pl +++ b/email_in.pl @@ -16,10 +16,11 @@ use lib qw(. lib local/lib/perl5); # run from this one so that it can find its modules. use Cwd qw(abs_path); use File::Basename qw(dirname); + BEGIN { - # Untaint the abs_path. - my ($a) = abs_path($0) =~ /^(.*)$/; - chdir dirname($a); + # Untaint the abs_path. + my ($a) = abs_path($0) =~ /^(.*)$/; + chdir dirname($a); } use Data::Dumper; @@ -60,213 +61,221 @@ our ($input_email, %switch); #################### sub parse_mail { - my ($mail_text) = @_; - debug_print('Parsing Email'); - $input_email = Email::MIME->new($mail_text); - - my %fields = %{ $switch{'default'} || {} }; - Bugzilla::Hook::process('email_in_before_parse', { mail => $input_email, - fields => \%fields }); - - my $summary = $input_email->header('Subject'); - if ($summary =~ /\[\S+ (\d+)\](.*)/i) { - $fields{'bug_id'} = $1; - $summary = trim($2); + my ($mail_text) = @_; + debug_print('Parsing Email'); + $input_email = Email::MIME->new($mail_text); + + my %fields = %{$switch{'default'} || {}}; + Bugzilla::Hook::process('email_in_before_parse', + {mail => $input_email, fields => \%fields}); + + my $summary = $input_email->header('Subject'); + if ($summary =~ /\[\S+ (\d+)\](.*)/i) { + $fields{'bug_id'} = $1; + $summary = trim($2); + } + + # Ignore automatic replies. + # XXX - Improve the way to detect such subjects in different languages. + my $auto_submitted = $input_email->header('Auto-Submitted') || ''; + if ($summary =~ /out of( the)? office/i || $auto_submitted eq 'auto-replied') { + debug_print("Automatic reply detected: $summary"); + exit; + } + + my ($body, $attachments) = get_body_and_attachments($input_email); + if (@$attachments) { + $fields{'attachments'} = $attachments; + } + + debug_print("Body:\n" . $body, 3); + + $body = remove_leading_blank_lines($body); + my @body_lines = split(/\r?\n/s, $body); + + # If there are fields specified. + if ($body =~ /^\s*@/s) { + my $current_field; + while (my $line = shift @body_lines) { + + # If the sig is starting, we want to keep this in the + # @body_lines so that we don't keep the sig as part of the + # comment down below. + if ($line eq SIGNATURE_DELIMITER) { + unshift(@body_lines, $line); + last; + } + + # Otherwise, we stop parsing fields on the first blank line. + $line = trim($line); + last if !$line; + if ($line =~ /^\@(\w+)\s*(?:=|\s|$)\s*(.*)\s*/) { + $current_field = lc($1); + $fields{$current_field} = $2; + } + else { + $fields{$current_field} .= " $line"; + } } + } - # Ignore automatic replies. - # XXX - Improve the way to detect such subjects in different languages. - my $auto_submitted = $input_email->header('Auto-Submitted') || ''; - if ($summary =~ /out of( the)? office/i || $auto_submitted eq 'auto-replied') { - debug_print("Automatic reply detected: $summary"); - exit; - } + %fields = %{Bugzilla::Bug::map_fields(\%fields)}; - my ($body, $attachments) = get_body_and_attachments($input_email); - if (@$attachments) { - $fields{'attachments'} = $attachments; - } + my ($reporter) = Email::Address->parse($input_email->header('From')); + $fields{'reporter'} = $reporter->address; - debug_print("Body:\n" . $body, 3); - - $body = remove_leading_blank_lines($body); - my @body_lines = split(/\r?\n/s, $body); - - # If there are fields specified. - if ($body =~ /^\s*@/s) { - my $current_field; - while (my $line = shift @body_lines) { - # If the sig is starting, we want to keep this in the - # @body_lines so that we don't keep the sig as part of the - # comment down below. - if ($line eq SIGNATURE_DELIMITER) { - unshift(@body_lines, $line); - last; - } - # Otherwise, we stop parsing fields on the first blank line. - $line = trim($line); - last if !$line; - if ($line =~ /^\@(\w+)\s*(?:=|\s|$)\s*(.*)\s*/) { - $current_field = lc($1); - $fields{$current_field} = $2; - } - else { - $fields{$current_field} .= " $line"; - } - } - } + # The summary line only affects us if we're doing a post_bug. + # We have to check it down here because there might have been + # a bug_id specified in the body of the email. + if (!$fields{'bug_id'} && !$fields{'short_desc'}) { + $fields{'short_desc'} = $summary; + } - %fields = %{ Bugzilla::Bug::map_fields(\%fields) }; + my $comment = ''; - my ($reporter) = Email::Address->parse($input_email->header('From')); - $fields{'reporter'} = $reporter->address; + # Get the description, except the signature. + foreach my $line (@body_lines) { + last if $line eq SIGNATURE_DELIMITER; + $comment .= "$line\n"; + } + $fields{'comment'} = $comment; - # The summary line only affects us if we're doing a post_bug. - # We have to check it down here because there might have been - # a bug_id specified in the body of the email. - if (!$fields{'bug_id'} && !$fields{'short_desc'}) { - $fields{'short_desc'} = $summary; - } + my %override = %{$switch{'override'} || {}}; + foreach my $key (keys %override) { + $fields{$key} = $override{$key}; + } - my $comment = ''; - # Get the description, except the signature. - foreach my $line (@body_lines) { - last if $line eq SIGNATURE_DELIMITER; - $comment .= "$line\n"; - } - $fields{'comment'} = $comment; - - my %override = %{ $switch{'override'} || {} }; - foreach my $key (keys %override) { - $fields{$key} = $override{$key}; - } + debug_print("Parsed Fields:\n" . Dumper(\%fields), 2); - debug_print("Parsed Fields:\n" . Dumper(\%fields), 2); - - return \%fields; + return \%fields; } sub check_email_fields { - my ($fields) = @_; - - my ($retval, $non_conclusive_fields) = - Bugzilla::User::match_field({ - 'assigned_to' => { 'type' => 'single' }, - 'qa_contact' => { 'type' => 'single' }, - 'cc' => { 'type' => 'multi' }, - 'newcc' => { 'type' => 'multi' } - }, $fields, MATCH_SKIP_CONFIRM); - - if ($retval != USER_MATCH_SUCCESS) { - ThrowUserError('user_match_too_many', {fields => $non_conclusive_fields}); - } + my ($fields) = @_; + + my ($retval, $non_conclusive_fields) = Bugzilla::User::match_field( + { + 'assigned_to' => {'type' => 'single'}, + 'qa_contact' => {'type' => 'single'}, + 'cc' => {'type' => 'multi'}, + 'newcc' => {'type' => 'multi'} + }, + $fields, + MATCH_SKIP_CONFIRM + ); + + if ($retval != USER_MATCH_SUCCESS) { + ThrowUserError('user_match_too_many', {fields => $non_conclusive_fields}); + } } sub post_bug { - my ($fields) = @_; - debug_print('Posting a new bug...'); + my ($fields) = @_; + debug_print('Posting a new bug...'); - my $user = Bugzilla->user; + my $user = Bugzilla->user; - check_email_fields($fields); + check_email_fields($fields); - my $bug = Bugzilla::Bug->create($fields); - debug_print("Created bug " . $bug->id); - return ($bug, $bug->comments->[0]); + my $bug = Bugzilla::Bug->create($fields); + debug_print("Created bug " . $bug->id); + return ($bug, $bug->comments->[0]); } sub process_bug { - my ($fields_in) = @_; - my %fields = %$fields_in; - - my $bug_id = $fields{'bug_id'}; - $fields{'id'} = $bug_id; - delete $fields{'bug_id'}; - - debug_print("Updating Bug $fields{id}..."); - - my $bug = Bugzilla::Bug->check($bug_id); - - if ($fields{'bug_status'}) { - $fields{'knob'} = $fields{'bug_status'}; - } - # If no status is given, then we only want to change the resolution. - elsif ($fields{'resolution'}) { - $fields{'knob'} = 'change_resolution'; - $fields{'resolution_knob_change_resolution'} = $fields{'resolution'}; - } - if ($fields{'dup_id'}) { - $fields{'knob'} = 'duplicate'; - } - - # Move @cc to @newcc as @cc is used by process_bug.cgi to remove - # users from the CC list when @removecc is set. - $fields{'newcc'} = delete $fields{'cc'} if $fields{'cc'}; - - # Make it possible to remove CCs. - if ($fields{'removecc'}) { - $fields{'cc'} = [split(',', $fields{'removecc'})]; - $fields{'removecc'} = 1; - } - - check_email_fields(\%fields); - - my $cgi = Bugzilla->cgi; - foreach my $field (keys %fields) { - $cgi->param(-name => $field, -value => $fields{$field}); - } - $cgi->param('longdesclength', scalar @{ $bug->comments }); - $cgi->param('token', issue_hash_token([$bug->id, $bug->delta_ts])); - - require 'process_bug.cgi'; - debug_print("Bug processed."); - - my $added_comment; - if (trim($fields{'comment'})) { - # The "old" bug object doesn't contain the comment we just added. - $added_comment = Bugzilla::Bug->check($bug_id)->comments->[-1]; - } - return ($bug, $added_comment); + my ($fields_in) = @_; + my %fields = %$fields_in; + + my $bug_id = $fields{'bug_id'}; + $fields{'id'} = $bug_id; + delete $fields{'bug_id'}; + + debug_print("Updating Bug $fields{id}..."); + + my $bug = Bugzilla::Bug->check($bug_id); + + if ($fields{'bug_status'}) { + $fields{'knob'} = $fields{'bug_status'}; + } + + # If no status is given, then we only want to change the resolution. + elsif ($fields{'resolution'}) { + $fields{'knob'} = 'change_resolution'; + $fields{'resolution_knob_change_resolution'} = $fields{'resolution'}; + } + if ($fields{'dup_id'}) { + $fields{'knob'} = 'duplicate'; + } + + # Move @cc to @newcc as @cc is used by process_bug.cgi to remove + # users from the CC list when @removecc is set. + $fields{'newcc'} = delete $fields{'cc'} if $fields{'cc'}; + + # Make it possible to remove CCs. + if ($fields{'removecc'}) { + $fields{'cc'} = [split(',', $fields{'removecc'})]; + $fields{'removecc'} = 1; + } + + check_email_fields(\%fields); + + my $cgi = Bugzilla->cgi; + foreach my $field (keys %fields) { + $cgi->param(-name => $field, -value => $fields{$field}); + } + $cgi->param('longdesclength', scalar @{$bug->comments}); + $cgi->param('token', issue_hash_token([$bug->id, $bug->delta_ts])); + + require 'process_bug.cgi'; + debug_print("Bug processed."); + + my $added_comment; + if (trim($fields{'comment'})) { + + # The "old" bug object doesn't contain the comment we just added. + $added_comment = Bugzilla::Bug->check($bug_id)->comments->[-1]; + } + return ($bug, $added_comment); } sub handle_attachments { - my ($bug, $attachments, $comment) = @_; - return if !$attachments; - debug_print("Handling attachments..."); - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my ($update_comment, $update_bug); - foreach my $attachment (@$attachments) { - my $data = delete $attachment->{payload}; - debug_print("Inserting Attachment: " . Dumper($attachment), 2); - $attachment->{content_type} ||= 'application/octet-stream'; - my $obj = Bugzilla::Attachment->create({ - bug => $bug, - description => $attachment->{filename}, - filename => $attachment->{filename}, - mimetype => $attachment->{content_type}, - data => $data, - }); - # If we added a comment, and our comment does not already have a type, - # and this is our first attachment, then we make the comment an - # "attachment created" comment. - if ($comment and !$comment->type and !$update_comment) { - $comment->set_all({ type => CMT_ATTACHMENT_CREATED, - extra_data => $obj->id }); - $update_comment = 1; - } - else { - $bug->add_comment('', { type => CMT_ATTACHMENT_CREATED, - extra_data => $obj->id }); - $update_bug = 1; - } + my ($bug, $attachments, $comment) = @_; + return if !$attachments; + debug_print("Handling attachments..."); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + my ($update_comment, $update_bug); + foreach my $attachment (@$attachments) { + my $data = delete $attachment->{payload}; + debug_print("Inserting Attachment: " . Dumper($attachment), 2); + $attachment->{content_type} ||= 'application/octet-stream'; + my $obj = Bugzilla::Attachment->create({ + bug => $bug, + description => $attachment->{filename}, + filename => $attachment->{filename}, + mimetype => $attachment->{content_type}, + data => $data, + }); + + # If we added a comment, and our comment does not already have a type, + # and this is our first attachment, then we make the comment an + # "attachment created" comment. + if ($comment and !$comment->type and !$update_comment) { + $comment->set_all({type => CMT_ATTACHMENT_CREATED, extra_data => $obj->id}); + $update_comment = 1; } - # We only update the comments and bugs at the end of the transaction, - # because doing so modifies bugs_fulltext, which is a non-transactional - # table. - $bug->update() if $update_bug; - $comment->update() if $update_comment; - $dbh->bz_commit_transaction(); + else { + $bug->add_comment('', {type => CMT_ATTACHMENT_CREATED, extra_data => $obj->id}); + $update_bug = 1; + } + } + + # We only update the comments and bugs at the end of the transaction, + # because doing so modifies bugs_fulltext, which is a non-transactional + # table. + $bug->update() if $update_bug; + $comment->update() if $update_comment; + $dbh->bz_commit_transaction(); } ###################### @@ -274,116 +283,124 @@ sub handle_attachments { ###################### sub debug_print { - my ($str, $level) = @_; - $level ||= 1; - print STDERR "$str\n" if $level <= $switch{'verbose'}; + my ($str, $level) = @_; + $level ||= 1; + print STDERR "$str\n" if $level <= $switch{'verbose'}; } sub get_body_and_attachments { - my ($email) = @_; + my ($email) = @_; + + my $ct = $email->content_type || 'text/plain'; + debug_print("Splitting Body and Attachments [Type: $ct]..."); + + my $body; + my $attachments = []; + if ($ct =~ /^multipart\/(alternative|signed)/i) { + $body = get_text_alternative($email); + } + else { + my $stripper + = new Email::MIME::Attachment::Stripper($email, force_filename => 1); + my $message = $stripper->message; + $body = get_text_alternative($message); + $attachments = [$stripper->attachments]; + } + + return ($body, $attachments); +} - my $ct = $email->content_type || 'text/plain'; - debug_print("Splitting Body and Attachments [Type: $ct]..."); +sub get_text_alternative { + my ($email) = @_; - my $body; - my $attachments = []; - if ($ct =~ /^multipart\/(alternative|signed)/i) { - $body = get_text_alternative($email); + my @parts = $email->parts; + my $body; + foreach my $part (@parts) { + my $ct = $part->content_type || 'text/plain'; + my $charset = 'iso-8859-1'; + + # The charset may be quoted. + if ($ct =~ /charset="?([^;"]+)/) { + $charset = $1; } - else { - my $stripper = new Email::MIME::Attachment::Stripper( - $email, force_filename => 1); - my $message = $stripper->message; - $body = get_text_alternative($message); - $attachments = [$stripper->attachments]; + debug_print("Part Content-Type: $ct", 2); + debug_print("Part Character Encoding: $charset", 2); + if (!$ct || $ct =~ /^text\/plain/i) { + $body = $part->body; + if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($body)) { + $body = Encode::decode($charset, $body); + } + last; } + } - return ($body, $attachments); -} - -sub get_text_alternative { - my ($email) = @_; - - my @parts = $email->parts; - my $body; - foreach my $part (@parts) { - my $ct = $part->content_type || 'text/plain'; - my $charset = 'iso-8859-1'; - # The charset may be quoted. - if ($ct =~ /charset="?([^;"]+)/) { - $charset= $1; - } - debug_print("Part Content-Type: $ct", 2); - debug_print("Part Character Encoding: $charset", 2); - if (!$ct || $ct =~ /^text\/plain/i) { - $body = $part->body; - if (Bugzilla->params->{'utf8'} && !utf8::is_utf8($body)) { - $body = Encode::decode($charset, $body); - } - last; - } - } + if (!defined $body) { - if (!defined $body) { - # Note that this only happens if the email does not contain any - # text/plain parts. If the email has an empty text/plain part, - # you're fine, and this message does NOT get thrown. - ThrowUserError('email_no_text_plain'); - } + # Note that this only happens if the email does not contain any + # text/plain parts. If the email has an empty text/plain part, + # you're fine, and this message does NOT get thrown. + ThrowUserError('email_no_text_plain'); + } - return $body; + return $body; } sub remove_leading_blank_lines { - my ($text) = @_; - $text =~ s/^(\s*\n)+//s; - return $text; + my ($text) = @_; + $text =~ s/^(\s*\n)+//s; + return $text; } sub html_strip { - my ($var) = @_; - # Trivial HTML tag remover (this is just for error messages, really.) - $var =~ s/<[^>]*>//g; - # And this basically reverses the Template-Toolkit html filter. - $var =~ s/\&/\&/g; - $var =~ s/\<//g; - $var =~ s/\"/\"/g; - $var =~ s/@/@/g; - # Also remove undesired newlines and consecutive spaces. - $var =~ s/[\n\s]+/ /gms; - return $var; + my ($var) = @_; + + # Trivial HTML tag remover (this is just for error messages, really.) + $var =~ s/<[^>]*>//g; + + # And this basically reverses the Template-Toolkit html filter. + $var =~ s/\&/\&/g; + $var =~ s/\<//g; + $var =~ s/\"/\"/g; + $var =~ s/@/@/g; + + # Also remove undesired newlines and consecutive spaces. + $var =~ s/[\n\s]+/ /gms; + return $var; } sub die_handler { - my ($msg) = @_; - - # In Template-Toolkit, [% RETURN %] is implemented as a call to "die". - # But of course, we really don't want to actually *die* just because - # the user-error or code-error template ended. So we don't really die. - return if blessed($msg) && $msg->isa('Template::Exception') - && $msg->type eq 'return'; - - # If this is inside an eval, then we should just act like...we're - # in an eval (instead of printing the error and exiting). - die(@_) if $^S; - - # We can't depend on the MTA to send an error message, so we have - # to generate one properly. - if ($input_email) { - $msg =~ s/at .+ line.*$//ms; - $msg =~ s/^Compilation failed in require.+$//ms; - $msg = html_strip($msg); - my $from = Bugzilla->params->{'mailfrom'}; - my $reply = reply(to => $input_email, from => $from, top_post => 1, - body => "$msg\n"); - MessageToMTA($reply->as_string); - } - print STDERR "$msg\n"; - # We exit with a successful value, because we don't want the MTA - # to *also* send a failure notice. - exit; + my ($msg) = @_; + + # In Template-Toolkit, [% RETURN %] is implemented as a call to "die". + # But of course, we really don't want to actually *die* just because + # the user-error or code-error template ended. So we don't really die. + return + if blessed($msg) + && $msg->isa('Template::Exception') + && $msg->type eq 'return'; + + # If this is inside an eval, then we should just act like...we're + # in an eval (instead of printing the error and exiting). + die(@_) if $^S; + + # We can't depend on the MTA to send an error message, so we have + # to generate one properly. + if ($input_email) { + $msg =~ s/at .+ line.*$//ms; + $msg =~ s/^Compilation failed in require.+$//ms; + $msg = html_strip($msg); + my $from = Bugzilla->params->{'mailfrom'}; + my $reply + = reply(to => $input_email, from => $from, top_post => 1, body => "$msg\n"); + MessageToMTA($reply->as_string); + } + print STDERR "$msg\n"; + + # We exit with a successful value, because we don't want the MTA + # to *also* send a failure notice. + exit; } ############### @@ -400,18 +417,19 @@ pod2usage({-verbose => 0, -exitval => 1}) if $switch{'help'}; Bugzilla->usage_mode(USAGE_MODE_EMAIL); -my @mail_lines = ; -my $mail_text = join("", @mail_lines); +my @mail_lines = ; +my $mail_text = join("", @mail_lines); my $mail_fields = parse_mail($mail_text); -Bugzilla::Hook::process('email_in_after_parse', { fields => $mail_fields }); +Bugzilla::Hook::process('email_in_after_parse', {fields => $mail_fields}); my $attachments = delete $mail_fields->{'attachments'}; my $username = $mail_fields->{'reporter'}; + # If emailsuffix is in use, we have to remove it from the email address. if (my $suffix = Bugzilla->params->{'emailsuffix'}) { - $username =~ s/\Q$suffix\E$//i; + $username =~ s/\Q$suffix\E$//i; } my $user = Bugzilla::User->check($username); @@ -419,10 +437,10 @@ Bugzilla->set_user($user); my ($bug, $comment); if ($mail_fields->{'bug_id'}) { - ($bug, $comment) = process_bug($mail_fields); + ($bug, $comment) = process_bug($mail_fields); } else { - ($bug, $comment) = post_bug($mail_fields); + ($bug, $comment) = post_bug($mail_fields); } handle_attachments($bug, $attachments, $comment); @@ -434,7 +452,7 @@ handle_attachments($bug, $attachments, $comment); # to wait for $bug->update() to be fully used in email_in.pl first. So # currently, process_bug.cgi does the mail sending for bugs, and this does # any mail sending for attachments after the first one. -Bugzilla::BugMail::Send($bug->id, { changer => Bugzilla->user }); +Bugzilla::BugMail::Send($bug->id, {changer => Bugzilla->user}); debug_print("Sent bugmail"); diff --git a/enter_bug.cgi b/enter_bug.cgi index 5b8a97dba..903ee9a01 100755 --- a/enter_bug.cgi +++ b/enter_bug.cgi @@ -44,119 +44,131 @@ my $user = Bugzilla->login(LOGIN_REQUIRED); my $cloned_bug; my $cloned_bug_id; -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; # BMO add a hook for the guided extension -Bugzilla::Hook::process('enter_bug_start', { vars => $vars }); +Bugzilla::Hook::process('enter_bug_start', {vars => $vars}); # All pages point to the same part of the documentation. $vars->{'doc_section'} = 'using/filing.html'; if (!$vars->{'disable_guided'}) { - # Purpose: force guided format for newbies - $cgi->param(-name=>'format', -value=>'guided') - if !$cgi->param('format') && !$user->in_group('canconfirm'); - $cgi->delete('format') - if ($cgi->param('format') && ($cgi->param('format') eq "__default__")); + # Purpose: force guided format for newbies + $cgi->param(-name => 'format', -value => 'guided') + if !$cgi->param('format') && !$user->in_group('canconfirm'); + + $cgi->delete('format') + if ($cgi->param('format') && ($cgi->param('format') eq "__default__")); } my $product_name = trim($cgi->param('product') || ''); + # Will contain the product object the bug is created in. my $product; if ($product_name eq '') { - # If the user cannot enter bugs in any product, stop here. - my @enterable_products = @{$user->get_enterable_products}; - ThrowUserError('no_products') unless scalar(@enterable_products); - # MOZILLA CUSTOMIZATION - # skip the classification selection page - my $classification; + # If the user cannot enter bugs in any product, stop here. + my @enterable_products = @{$user->get_enterable_products}; + ThrowUserError('no_products') unless scalar(@enterable_products); + + # MOZILLA CUSTOMIZATION + # skip the classification selection page + my $classification; + if (Bugzilla->params->{'useclassification'}) { + $classification = scalar($cgi->param('classification')) || '__all'; + } + else { + $classification = '__all'; + } + + # Unless a real classification name is given, we sort products + # by classification. + my @classifications; + + unless ($classification && $classification ne '__all') { if (Bugzilla->params->{'useclassification'}) { - $classification = scalar($cgi->param('classification')) || '__all'; - } else { - $classification = '__all'; + my $class; + + # Get all classifications with at least one enterable product. + foreach my $product (@enterable_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); + } + @classifications = sort { + $a->{'object'}->sortkey <=> $b->{'object'}->sortkey + || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) + } (values %$class); } - - # Unless a real classification name is given, we sort products - # by classification. - my @classifications; - - unless ($classification && $classification ne '__all') { - if (Bugzilla->params->{'useclassification'}) { - my $class; - # Get all classifications with at least one enterable product. - foreach my $product (@enterable_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); - } - @classifications = sort {$a->{'object'}->sortkey <=> $b->{'object'}->sortkey - || lc($a->{'object'}->name) cmp lc($b->{'object'}->name)} - (values %$class); - } - else { - @classifications = ({object => undef, products => \@enterable_products}); - } + else { + @classifications = ({object => undef, products => \@enterable_products}); } + } - unless ($classification) { - # We know there is at least one classification available, - # else we would have stopped earlier. - if (scalar(@classifications) > 1) { - # We only need classification objects. - $vars->{'classifications'} = [map {$_->{'object'}} @classifications]; - - $vars->{'target'} = "enter_bug.cgi"; - $vars->{'format'} = $cgi->param('format'); - $vars->{'cloned_bug_id'} = $cgi->param('cloned_bug_id'); - - print $cgi->header(); - $template->process("global/choose-classification.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } - # If we come here, then there is only one classification available. - $classification = $classifications[0]->{'object'}->name; - } + unless ($classification) { + + # We know there is at least one classification available, + # else we would have stopped earlier. + if (scalar(@classifications) > 1) { - # Keep only enterable products which are in the specified classification. - if ($classification ne "__all") { - my $class = new Bugzilla::Classification({'name' => $classification}); - # If the classification doesn't exist, then there is no product in it. - if ($class) { - @enterable_products - = grep {$_->classification_id == $class->id} @enterable_products; - @classifications = ({object => $class, products => \@enterable_products}); - } - else { - @enterable_products = (); - } + # We only need classification objects. + $vars->{'classifications'} = [map { $_->{'object'} } @classifications]; + + $vars->{'target'} = "enter_bug.cgi"; + $vars->{'format'} = $cgi->param('format'); + $vars->{'cloned_bug_id'} = $cgi->param('cloned_bug_id'); + + print $cgi->header(); + $template->process("global/choose-classification.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } - if (scalar(@enterable_products) == 0) { - ThrowUserError('no_products'); + # If we come here, then there is only one classification available. + $classification = $classifications[0]->{'object'}->name; + } + + # Keep only enterable products which are in the specified classification. + if ($classification ne "__all") { + my $class = new Bugzilla::Classification({'name' => $classification}); + + # If the classification doesn't exist, then there is no product in it. + if ($class) { + @enterable_products + = grep { $_->classification_id == $class->id } @enterable_products; + @classifications = ({object => $class, products => \@enterable_products}); } - elsif (scalar(@enterable_products) > 1) { - $vars->{'classifications'} = \@classifications; - $vars->{'target'} = "enter_bug.cgi"; - $vars->{'format'} = $cgi->param('format'); - $vars->{'cloned_bug_id'} = $cgi->param('cloned_bug_id'); - - print $cgi->header(); - $template->process("global/choose-product.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } else { - # Only one product exists. - $product = $enterable_products[0]; + else { + @enterable_products = (); } + } + + if (scalar(@enterable_products) == 0) { + ThrowUserError('no_products'); + } + elsif (scalar(@enterable_products) > 1) { + $vars->{'classifications'} = \@classifications; + $vars->{'target'} = "enter_bug.cgi"; + $vars->{'format'} = $cgi->param('format'); + $vars->{'cloned_bug_id'} = $cgi->param('cloned_bug_id'); + + print $cgi->header(); + $template->process("global/choose-product.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + else { + # Only one product exists. + $product = $enterable_products[0]; + } } # We need to check and make sure that the user has permission @@ -167,15 +179,15 @@ $product = $user->can_enter_product($product || $product_name, THROW_ERROR); # Useful Subroutines ############################################################################## sub formvalue { - my ($name, $default) = (@_); - return Bugzilla->cgi->param($name) || $default || ""; + my ($name, $default) = (@_); + return Bugzilla->cgi->param($name) || $default || ""; } ############################################################################## # End of subroutines ############################################################################## -my $has_editbugs = $user->in_group('editbugs', $product->id); +my $has_editbugs = $user->in_group('editbugs', $product->id); my $has_canconfirm = $user->in_group('canconfirm', $product->id); # If a user is trying to clone a bug @@ -184,153 +196,160 @@ my $has_canconfirm = $user->in_group('canconfirm', $product->id); $cloned_bug_id = $cgi->param('cloned_bug_id'); if ($cloned_bug_id) { - $cloned_bug = Bugzilla::Bug->check($cloned_bug_id); - $cloned_bug_id = $cloned_bug->id; + $cloned_bug = Bugzilla::Bug->check($cloned_bug_id); + $cloned_bug_id = $cloned_bug->id; } # If there is only one active component, choose it my @active = grep { $_->is_active } @{$product->components}; if (scalar(@active) == 1) { - $cgi->param('component', $active[0]->name); + $cgi->param('component', $active[0]->name); } # If there is only one active version, choose it @active = grep { $_->is_active } @{$product->versions}; if (scalar(@active) == 1) { - $cgi->param('version', $active[0]->name); + $cgi->param('version', $active[0]->name); } my %default; -$vars->{'product'} = $product; +$vars->{'product'} = $product; -$vars->{'priority'} = get_legal_field_values('priority'); -$vars->{'bug_severity'} = get_legal_field_values('bug_severity'); -$vars->{'rep_platform'} = get_legal_field_values('rep_platform'); -$vars->{'op_sys'} = get_legal_field_values('op_sys'); +$vars->{'priority'} = get_legal_field_values('priority'); +$vars->{'bug_severity'} = get_legal_field_values('bug_severity'); +$vars->{'rep_platform'} = get_legal_field_values('rep_platform'); +$vars->{'op_sys'} = get_legal_field_values('op_sys'); -$vars->{'assigned_to'} = formvalue('assigned_to'); -$vars->{'assigned_to_disabled'} = !$has_editbugs; -$vars->{'cc_disabled'} = 0; +$vars->{'assigned_to'} = formvalue('assigned_to'); +$vars->{'assigned_to_disabled'} = !$has_editbugs; +$vars->{'cc_disabled'} = 0; -$vars->{'qa_contact'} = formvalue('qa_contact'); -$vars->{'qa_contact_disabled'} = !$has_editbugs; +$vars->{'qa_contact'} = formvalue('qa_contact'); +$vars->{'qa_contact_disabled'} = !$has_editbugs; -$vars->{'cloned_bug_id'} = $cloned_bug_id; +$vars->{'cloned_bug_id'} = $cloned_bug_id; $vars->{'token'} = issue_session_token('create_bug'); my @enter_bug_fields = grep { $_->enter_bug } Bugzilla->active_custom_fields; foreach my $field (@enter_bug_fields) { - my $cf_name = $field->name; - my $cf_value = $cgi->param($cf_name); - if (defined $cf_value) { - if ($field->type == FIELD_TYPE_MULTI_SELECT) { - $cf_value = [$cgi->param($cf_name)]; - } - $default{$cf_name} = $vars->{$cf_name} = $cf_value; + my $cf_name = $field->name; + my $cf_value = $cgi->param($cf_name); + if (defined $cf_value) { + if ($field->type == FIELD_TYPE_MULTI_SELECT) { + $cf_value = [$cgi->param($cf_name)]; } + $default{$cf_name} = $vars->{$cf_name} = $cf_value; + } } # This allows the Field visibility and value controls to work with the # Classification and Product fields as a parent. $default{'classification'} = $product->classification->name; -$default{'product'} = $product->name; +$default{'product'} = $product->name; if ($cloned_bug_id) { - # BMO: allow form value component to override the cloned bug component - $default{'component_'} = formvalue('component') || $cloned_bug->component; - $default{'priority'} = $cloned_bug->priority; - $default{'bug_severity'} = $cloned_bug->bug_severity; - $default{'rep_platform'} = $cloned_bug->rep_platform; - $default{'op_sys'} = $cloned_bug->op_sys; - - $vars->{'short_desc'} = $cloned_bug->short_desc; - $vars->{'bug_file_loc'} = $cloned_bug->bug_file_loc; - $vars->{'keywords'} = $cloned_bug->keywords; - $vars->{'dependson'} = join (", ", $cloned_bug_id, @{$cloned_bug->dependson}); - $vars->{'blocked'} = join (", ", @{$cloned_bug->blocked}); - $vars->{'deadline'} = $cloned_bug->deadline; - $vars->{'estimated_time'} = $cloned_bug->estimated_time; - $vars->{'status_whiteboard'} = $cloned_bug->status_whiteboard; - - if (defined $cloned_bug->cc) { - $vars->{'cc'} = join (", ", @{$cloned_bug->cc}); - } else { - $vars->{'cc'} = formvalue('cc'); - } - - if ($cloned_bug->reporter->id != $user->id) { - $vars->{'cc'} = join (", ", $cloned_bug->reporter->login, $vars->{'cc'}); - } - - foreach my $field (@enter_bug_fields) { - my $field_name = $field->name; - $vars->{$field_name} = $cloned_bug->$field_name; - } - - # We need to ensure that we respect the 'insider' status of - # the first comment, if it has one. Either way, make a note - # that this bug was cloned from another bug. - my $bug_desc = $cloned_bug->comments({ order => 'oldest_to_newest' })->[0]; - my $isprivate = $bug_desc->is_private; - - $vars->{'comment'} = ""; - $vars->{'comment_is_private'} = 0; - - if (!$isprivate || Bugzilla->user->is_insider) { - # We use "body" to avoid any format_comment text, which would be - # pointless to clone. - $vars->{'comment'} = $bug_desc->body; - $vars->{'comment_is_private'} = $isprivate; - } - - # BMO Bug 1019747 - $vars->{'cloned_bug'} = $cloned_bug; - - # BMO Allow mentors to be cloned as well - $vars->{'bug_mentors'} = join(', ', map { $_->login } @{ $cloned_bug->mentors }); - -} # end of cloned bug entry form + # BMO: allow form value component to override the cloned bug component + $default{'component_'} = formvalue('component') || $cloned_bug->component; + $default{'priority'} = $cloned_bug->priority; + $default{'bug_severity'} = $cloned_bug->bug_severity; + $default{'rep_platform'} = $cloned_bug->rep_platform; + $default{'op_sys'} = $cloned_bug->op_sys; + + $vars->{'short_desc'} = $cloned_bug->short_desc; + $vars->{'bug_file_loc'} = $cloned_bug->bug_file_loc; + $vars->{'keywords'} = $cloned_bug->keywords; + $vars->{'dependson'} = join(", ", $cloned_bug_id, @{$cloned_bug->dependson}); + $vars->{'blocked'} = join(", ", @{$cloned_bug->blocked}); + $vars->{'deadline'} = $cloned_bug->deadline; + $vars->{'estimated_time'} = $cloned_bug->estimated_time; + $vars->{'status_whiteboard'} = $cloned_bug->status_whiteboard; + + if (defined $cloned_bug->cc) { + $vars->{'cc'} = join(", ", @{$cloned_bug->cc}); + } + else { + $vars->{'cc'} = formvalue('cc'); + } + + if ($cloned_bug->reporter->id != $user->id) { + $vars->{'cc'} = join(", ", $cloned_bug->reporter->login, $vars->{'cc'}); + } + + foreach my $field (@enter_bug_fields) { + my $field_name = $field->name; + $vars->{$field_name} = $cloned_bug->$field_name; + } + + # We need to ensure that we respect the 'insider' status of + # the first comment, if it has one. Either way, make a note + # that this bug was cloned from another bug. + my $bug_desc = $cloned_bug->comments({order => 'oldest_to_newest'})->[0]; + my $isprivate = $bug_desc->is_private; + + $vars->{'comment'} = ""; + $vars->{'comment_is_private'} = 0; + + if (!$isprivate || Bugzilla->user->is_insider) { + + # We use "body" to avoid any format_comment text, which would be + # pointless to clone. + $vars->{'comment'} = $bug_desc->body; + $vars->{'comment_is_private'} = $isprivate; + } + + # BMO Bug 1019747 + $vars->{'cloned_bug'} = $cloned_bug; + + # BMO Allow mentors to be cloned as well + $vars->{'bug_mentors'} = join(', ', map { $_->login } @{$cloned_bug->mentors}); + +} # end of cloned bug entry form else { - $default{'component_'} = formvalue('component'); - $default{'priority'} = formvalue('priority', Bugzilla->params->{'defaultpriority'}); - $default{'bug_severity'} = formvalue('bug_severity', Bugzilla->params->{'defaultseverity'}); - - # BMO - use per-product default hw/os - if (any { $_->NAME eq 'BMO' } @{ Bugzilla->extensions }) { - $default{'rep_platform'} = formvalue('rep_platform', $product->default_platform // detect_platform()); - $default{'op_sys'} = formvalue('op_sys', $product->default_op_sys // detect_op_sys()); - } else { - $default{'rep_platform'} = formvalue('rep_platform', detect_platform()); - $default{'op_sys'} = formvalue('op_sys', detect_op_sys()); - } - $vars->{'rep_platform'} = detect_platform(); - $vars->{'rep_op_sys'} = detect_op_sys(); - - $vars->{'alias'} = formvalue('alias'); - $vars->{'short_desc'} = formvalue('short_desc'); - $vars->{'bug_file_loc'} = formvalue('bug_file_loc', "http://"); - $vars->{'keywords'} = formvalue('keywords'); - $vars->{'dependson'} = formvalue('dependson'); - $vars->{'blocked'} = formvalue('blocked'); - $vars->{'deadline'} = formvalue('deadline'); - $vars->{'estimated_time'} = formvalue('estimated_time'); - $vars->{'bug_ignored'} = formvalue('bug_ignored'); - $vars->{'see_also'} = formvalue('see_also'); - - $vars->{'cc'} = join(', ', $cgi->param('cc')); - - $vars->{'comment'} = formvalue('comment'); - $vars->{'comment_is_private'} = formvalue('comment_is_private'); - - # BMO Add support for mentors - $vars->{'bug_mentors'} = formvalue('bug_mentors'); - -} # end of normal/bookmarked entry form + $default{'component_'} = formvalue('component'); + $default{'priority'} + = formvalue('priority', Bugzilla->params->{'defaultpriority'}); + $default{'bug_severity'} + = formvalue('bug_severity', Bugzilla->params->{'defaultseverity'}); + + # BMO - use per-product default hw/os + if (any { $_->NAME eq 'BMO' } @{Bugzilla->extensions}) { + $default{'rep_platform'} + = formvalue('rep_platform', $product->default_platform // detect_platform()); + $default{'op_sys'} + = formvalue('op_sys', $product->default_op_sys // detect_op_sys()); + } + else { + $default{'rep_platform'} = formvalue('rep_platform', detect_platform()); + $default{'op_sys'} = formvalue('op_sys', detect_op_sys()); + } + $vars->{'rep_platform'} = detect_platform(); + $vars->{'rep_op_sys'} = detect_op_sys(); + + $vars->{'alias'} = formvalue('alias'); + $vars->{'short_desc'} = formvalue('short_desc'); + $vars->{'bug_file_loc'} = formvalue('bug_file_loc', "http://"); + $vars->{'keywords'} = formvalue('keywords'); + $vars->{'dependson'} = formvalue('dependson'); + $vars->{'blocked'} = formvalue('blocked'); + $vars->{'deadline'} = formvalue('deadline'); + $vars->{'estimated_time'} = formvalue('estimated_time'); + $vars->{'bug_ignored'} = formvalue('bug_ignored'); + $vars->{'see_also'} = formvalue('see_also'); + + $vars->{'cc'} = join(', ', $cgi->param('cc')); + + $vars->{'comment'} = formvalue('comment'); + $vars->{'comment_is_private'} = formvalue('comment_is_private'); + + # BMO Add support for mentors + $vars->{'bug_mentors'} = formvalue('bug_mentors'); + +} # end of normal/bookmarked entry form # IF this is a cloned bug, @@ -350,49 +369,54 @@ $vars->{'version'} = $product->versions; my $version_cookie = $cgi->cookie("VERSION-" . $product->name); -if ( ($cloned_bug_id) && - ($product->name eq $cloned_bug->product ) ) { - $default{'version'} = $cloned_bug->version; -} elsif (formvalue('version')) { - $default{'version'} = formvalue('version'); -} elsif (defined $version_cookie - and grep { $_->name eq $version_cookie } @{ $vars->{'version'} }) +if (($cloned_bug_id) && ($product->name eq $cloned_bug->product)) { + $default{'version'} = $cloned_bug->version; +} +elsif (formvalue('version')) { + $default{'version'} = formvalue('version'); +} +elsif (defined $version_cookie + and grep { $_->name eq $version_cookie } @{$vars->{'version'}}) { - $default{'version'} = $version_cookie; -} else { - $default{'version'} = $vars->{'version'}->[$#{$vars->{'version'}}]->name; + $default{'version'} = $version_cookie; +} +else { + $default{'version'} = $vars->{'version'}->[$#{$vars->{'version'}}]->name; } # Get list of milestones. -if ( Bugzilla->params->{'usetargetmilestone'} ) { - $vars->{'target_milestone'} = $product->milestones; - if (formvalue('target_milestone')) { - $default{'target_milestone'} = formvalue('target_milestone'); - } else { - $default{'target_milestone'} = $product->default_milestone; - } +if (Bugzilla->params->{'usetargetmilestone'}) { + $vars->{'target_milestone'} = $product->milestones; + if (formvalue('target_milestone')) { + $default{'target_milestone'} = formvalue('target_milestone'); + } + else { + $default{'target_milestone'} = $product->default_milestone; + } } # Construct the list of allowable statuses. -my @statuses = @{ Bugzilla::Status->can_change_to() }; +my @statuses = @{Bugzilla::Status->can_change_to()}; + # Exclude closed states from the UI, even if the workflow allows them. # The back-end code will still accept them, though. @statuses = grep { $_->is_open } @statuses; # UNCONFIRMED is illegal if allows_unconfirmed is false. if (!$product->allows_unconfirmed) { - @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; + @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; } scalar(@statuses) || ThrowUserError('no_initial_bug_status'); # If the user has no privs... unless ($has_editbugs || $has_canconfirm) { - # ... 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]); + # ... 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]); } $vars->{'bug_status'} = \@statuses; @@ -402,32 +426,38 @@ $vars->{'bug_status'} = \@statuses; # to the first confirmed bug status on the list, if available. my $picked_status = formvalue('bug_status'); -if ( $picked_status and grep( $_->name eq $picked_status, @statuses ) ) { - $default{'bug_status'} = formvalue('bug_status'); +if ($picked_status and grep($_->name eq $picked_status, @statuses)) { + $default{'bug_status'} = formvalue('bug_status'); } -elsif ( scalar @statuses == 1 ) { - $default{'bug_status'} = $statuses[0]->name; +elsif (scalar @statuses == 1) { + $default{'bug_status'} = $statuses[0]->name; } else { - $default{'bug_status'} = ( $statuses[0]->name ne 'UNCONFIRMED' ) ? $statuses[0]->name : $statuses[1]->name; + $default{'bug_status'} + = ($statuses[0]->name ne 'UNCONFIRMED') + ? $statuses[0]->name + : $statuses[1]->name; } my @groups = $cgi->param('groups'); if ($cloned_bug) { - my @clone_groups = map { $_->name } @{ $cloned_bug->groups_in }; - # It doesn't matter if there are duplicate names, since all we check - # for in the template is whether or not the group is set. - push(@groups, @clone_groups); + my @clone_groups = map { $_->name } @{$cloned_bug->groups_in}; + + # It doesn't matter if there are duplicate names, since all we check + # for in the template is whether or not the group is set. + push(@groups, @clone_groups); } $default{'groups'} = \@groups; -Bugzilla::Hook::process('enter_bug_entrydefaultvars', { vars => $vars }); +Bugzilla::Hook::process('enter_bug_entrydefaultvars', {vars => $vars}); $vars->{'default'} = \%default; -my $format = $template->get_format("bug/create/create", - scalar $cgi->param('format'), - scalar $cgi->param('ctype')); +my $format = $template->get_format( + "bug/create/create", + scalar $cgi->param('format'), + scalar $cgi->param('ctype') +); print $cgi->header($format->{'ctype'}); $template->process($format->{'template'}, $vars) diff --git a/extensions/AntiSpam/Config.pm b/extensions/AntiSpam/Config.pm index e16add9b7..18cd3efa2 100644 --- a/extensions/AntiSpam/Config.pm +++ b/extensions/AntiSpam/Config.pm @@ -12,13 +12,8 @@ use strict; use warnings; use constant NAME => 'AntiSpam'; -use constant REQUIRED_MODULES => [ - { - package => 'Email-Address', - module => 'Email::Address', - version => 0, - }, -]; +use constant REQUIRED_MODULES => + [{package => 'Email-Address', module => 'Email::Address', version => 0,},]; use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/AntiSpam/Extension.pm b/extensions/AntiSpam/Extension.pm index 19eddb4e7..990130c8e 100644 --- a/extensions/AntiSpam/Extension.pm +++ b/extensions/AntiSpam/Extension.pm @@ -26,34 +26,29 @@ our $VERSION = '1'; # sub _project_honeypot_blocking { - my ($self, $api_key, $login) = @_; - my $ip = remote_ip(); - return unless $ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; - my $lookup = "$api_key.$4.$3.$2.$1.dnsbl.httpbl.org"; - return unless my $packed = gethostbyname($lookup); - my $honeypot = inet_ntoa($packed); - return unless $honeypot =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; - my ($status, $days, $threat, $type) = ($1, $2, $3, $4); - - return if $status != 127 - || $threat < Bugzilla->params->{honeypot_threat_threshold}; - - Bugzilla->audit(sprintf("blocked <%s> from creating %s, honeypot %s", $ip, $login, $honeypot)); - ThrowUserError('account_creation_restricted'); + my ($self, $api_key, $login) = @_; + my $ip = remote_ip(); + return unless $ip =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; + my $lookup = "$api_key.$4.$3.$2.$1.dnsbl.httpbl.org"; + return unless my $packed = gethostbyname($lookup); + my $honeypot = inet_ntoa($packed); + return unless $honeypot =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/; + my ($status, $days, $threat, $type) = ($1, $2, $3, $4); + + return + if $status != 127 || $threat < Bugzilla->params->{honeypot_threat_threshold}; + + Bugzilla->audit( + sprintf("blocked <%s> from creating %s, honeypot %s", $ip, $login, $honeypot)); + ThrowUserError('account_creation_restricted'); } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{auth}->{params} }, { - name => 'honeypot_api_key', - type => 't', - default => '', - }; - push @{ $args->{panels}->{auth}->{params} }, { - name => 'honeypot_threat_threshold', - type => 't', - default => '32', - }; + my ($self, $args) = @_; + push @{$args->{panels}->{auth}->{params}}, + {name => 'honeypot_api_key', type => 't', default => '',}; + push @{$args->{panels}->{auth}->{params}}, + {name => 'honeypot_threat_threshold', type => 't', default => '32',}; } # @@ -61,20 +56,22 @@ sub config_modify_panels { # sub _comment_blocking { - my ($self, $params) = @_; - my $user = Bugzilla->user; - return if $user->in_group('editbugs'); - - my $blocklist = Bugzilla->dbh->selectcol_arrayref( - 'SELECT word FROM antispam_comment_blocklist' - ); - return unless @$blocklist; - - my $regex = '\b(?:' . join('|', map { quotemeta } @$blocklist) . ')\b'; - if ($params->{thetext} =~ /$regex/i) { - Bugzilla->audit(sprintf("blocked <%s> %s from commenting, blacklisted phrase", remote_ip(), $user->login)); - ThrowUserError('antispam_comment_blocked'); - } + my ($self, $params) = @_; + my $user = Bugzilla->user; + return if $user->in_group('editbugs'); + + my $blocklist = Bugzilla->dbh->selectcol_arrayref( + 'SELECT word FROM antispam_comment_blocklist'); + return unless @$blocklist; + + my $regex = '\b(?:' . join('|', map {quotemeta} @$blocklist) . ')\b'; + if ($params->{thetext} =~ /$regex/i) { + Bugzilla->audit(sprintf( + "blocked <%s> %s from commenting, blacklisted phrase", + remote_ip(), $user->login + )); + ThrowUserError('antispam_comment_blocked'); + } } # @@ -82,17 +79,19 @@ sub _comment_blocking { # sub _domain_blocking { - my ($self, $login) = @_; - my $address = Email::Address->new(undef, $login); - my $blocked = Bugzilla->dbh->selectrow_array( - "SELECT 1 FROM antispam_domain_blocklist WHERE domain=?", - undef, - $address->host - ); - if ($blocked) { - Bugzilla->audit(sprintf("blocked <%s> from creating %s, blacklisted domain", remote_ip(), $login)); - ThrowUserError('account_creation_restricted'); - } + my ($self, $login) = @_; + my $address = Email::Address->new(undef, $login); + my $blocked + = Bugzilla->dbh->selectrow_array( + "SELECT 1 FROM antispam_domain_blocklist WHERE domain=?", + undef, $address->host); + if ($blocked) { + Bugzilla->audit(sprintf( + "blocked <%s> from creating %s, blacklisted domain", + remote_ip(), $login + )); + ThrowUserError('account_creation_restricted'); + } } # @@ -100,18 +99,18 @@ sub _domain_blocking { # sub _ip_blocking { - my ($self, $login) = @_; - my $ip = remote_ip(); - trick_taint($ip); - my $blocked = Bugzilla->dbh->selectrow_array( - "SELECT 1 FROM antispam_ip_blocklist WHERE ip_address=?", - undef, - $ip - ); - if ($blocked) { - Bugzilla->audit(sprintf("blocked <%s> from creating %s, blacklisted IP", $ip, $login)); - ThrowUserError('account_creation_restricted'); - } + my ($self, $login) = @_; + my $ip = remote_ip(); + trick_taint($ip); + my $blocked + = Bugzilla->dbh->selectrow_array( + "SELECT 1 FROM antispam_ip_blocklist WHERE ip_address=?", + undef, $ip); + if ($blocked) { + Bugzilla->audit( + sprintf("blocked <%s> from creating %s, blacklisted IP", $ip, $login)); + ThrowUserError('account_creation_restricted'); + } } # @@ -119,44 +118,50 @@ sub _ip_blocking { # sub _is_limited_user { - return Bugzilla->user->creation_age < Bugzilla->params->{antispam_multi_user_limit_age}; + return Bugzilla->user->creation_age + < Bugzilla->params->{antispam_multi_user_limit_age}; } sub bug_before_create { - my ($self, $args) = @_; - $self->_cc_limit($args->{params}, 'cc'); + my ($self, $args) = @_; + $self->_cc_limit($args->{params}, 'cc'); } sub bug_start_of_set_all { - my ($self, $args) = @_; - $self->_cc_limit($args->{params}, 'newcc'); + my ($self, $args) = @_; + $self->_cc_limit($args->{params}, 'newcc'); } sub _cc_limit { - my ($self, $params, $cc_field) = @_; - return unless _is_limited_user(); - return unless exists $params->{$cc_field}; - - my $cc_count = ref($params->{$cc_field}) ? scalar(@{ $params->{$cc_field} }) : 1; - if ($cc_count > Bugzilla->params->{antispam_multi_user_limit_count}) { - Bugzilla->audit(sprintf("blocked <%s> from CC'ing %s users", Bugzilla->user->login, $cc_count)); - delete $params->{$cc_field}; - if (exists $params->{cc} && exists $params->{cc}->{add}) { - delete $params->{cc}->{add}; - } + my ($self, $params, $cc_field) = @_; + return unless _is_limited_user(); + return unless exists $params->{$cc_field}; + + my $cc_count = ref($params->{$cc_field}) ? scalar(@{$params->{$cc_field}}) : 1; + if ($cc_count > Bugzilla->params->{antispam_multi_user_limit_count}) { + Bugzilla->audit( + sprintf("blocked <%s> from CC'ing %s users", Bugzilla->user->login, $cc_count)); + delete $params->{$cc_field}; + if (exists $params->{cc} && exists $params->{cc}->{add}) { + delete $params->{cc}->{add}; } + } } sub bug_set_flags { - my ($self, $args) = @_; - return unless _is_limited_user(); - - my $flag_count = @{ $args->{new_flags} }; - if ($flag_count > Bugzilla->params->{antispam_multi_user_limit_count}) { - Bugzilla->audit(sprintf("blocked <%s> from flaging %s users", Bugzilla->user->login, $flag_count)); - # empty the arrayref - $#{ $args->{new_flags} } = -1; - } + my ($self, $args) = @_; + return unless _is_limited_user(); + + my $flag_count = @{$args->{new_flags}}; + if ($flag_count > Bugzilla->params->{antispam_multi_user_limit_count}) { + Bugzilla->audit(sprintf( + "blocked <%s> from flaging %s users", + Bugzilla->user->login, $flag_count + )); + + # empty the arrayref + $#{$args->{new_flags}} = -1; + } } # @@ -164,30 +169,31 @@ sub bug_set_flags { # sub comment_after_add_tag { - my ($self, $args) = @_; - my $tag = lc($args->{tag}); - return unless $tag eq 'spam' or $tag eq 'abusive' or $tag eq 'abuse'; - my $comment = $args->{comment}; - my $author = $comment->author; - - # exclude disabled users - return if !$author->is_enabled; - - # exclude users by group - return if $author->in_group(Bugzilla->params->{antispam_spammer_exclude_group}); - - # exclude users who are no longer new - return if !$author->is_new; - - # exclude users who haven't made enough comments - my $count = $tag eq 'spam' - ? Bugzilla->params->{antispam_spammer_comment_count} - : Bugzilla->params->{antispam_abusive_comment_count}; - return if $author->comment_count < $count; - - # get user's comments - trick_taint($tag); - my $comments = Bugzilla->dbh->selectall_arrayref(" + my ($self, $args) = @_; + my $tag = lc($args->{tag}); + return unless $tag eq 'spam' or $tag eq 'abusive' or $tag eq 'abuse'; + my $comment = $args->{comment}; + my $author = $comment->author; + + # exclude disabled users + return if !$author->is_enabled; + + # exclude users by group + return if $author->in_group(Bugzilla->params->{antispam_spammer_exclude_group}); + + # exclude users who are no longer new + return if !$author->is_new; + + # exclude users who haven't made enough comments + my $count + = $tag eq 'spam' + ? Bugzilla->params->{antispam_spammer_comment_count} + : Bugzilla->params->{antispam_abusive_comment_count}; + return if $author->comment_count < $count; + + # get user's comments + trick_taint($tag); + my $comments = Bugzilla->dbh->selectall_arrayref(" SELECT longdescs.comment_id,longdescs_tags.id FROM longdescs LEFT JOIN longdescs_tags @@ -197,41 +203,39 @@ sub comment_after_add_tag { ORDER BY longdescs.bug_when ", undef, $tag, $author->id); - # this comment needs to be counted too - my $comment_id = $comment->id; - foreach my $ra (@$comments) { - if ($ra->[0] == $comment_id) { - $ra->[1] = 1; - last; - } - } - - # throw away comment id and negate bool to make it a list of not-spam/abuse - $comments = [ map { $_->[1] ? 0 : 1 } @$comments ]; - - my $reason; - - # check if the first N comments are spam/abuse - if (!scalar(grep { $_ } @$comments[0..($count - 1)])) { - $reason = "first $count comments are $tag"; - } - - # check if the last N comments are spam/abuse - elsif (!scalar(grep { $_ } @$comments[-$count..-1])) { - $reason = "last $count comments are $tag"; - } - - # disable - if ($reason) { - $author->set_disabledtext( - $tag eq 'spam' - ? Bugzilla->params->{antispam_spammer_disable_text} - : Bugzilla->params->{antispam_abusive_disable_text} - ); - $author->set_disable_mail(1); - $author->update(); - Bugzilla->audit(sprintf("antispam disabled <%s>: %s", $author->login, $reason)); + # this comment needs to be counted too + my $comment_id = $comment->id; + foreach my $ra (@$comments) { + if ($ra->[0] == $comment_id) { + $ra->[1] = 1; + last; } + } + + # throw away comment id and negate bool to make it a list of not-spam/abuse + $comments = [map { $_->[1] ? 0 : 1 } @$comments]; + + my $reason; + + # check if the first N comments are spam/abuse + if (!scalar(grep {$_} @$comments[0 .. ($count - 1)])) { + $reason = "first $count comments are $tag"; + } + + # check if the last N comments are spam/abuse + elsif (!scalar(grep {$_} @$comments[-$count .. -1])) { + $reason = "last $count comments are $tag"; + } + + # disable + if ($reason) { + $author->set_disabledtext($tag eq 'spam' + ? Bugzilla->params->{antispam_spammer_disable_text} + : Bugzilla->params->{antispam_abusive_disable_text}); + $author->set_disable_mail(1); + $author->update(); + Bugzilla->audit(sprintf("antispam disabled <%s>: %s", $author->login, $reason)); + } } # @@ -239,51 +243,54 @@ sub comment_after_add_tag { # sub object_end_of_create_validators { - my ($self, $args) = @_; - if ($args->{class} eq 'Bugzilla::Comment') { - $self->_comment_blocking($args->{params}); - } + my ($self, $args) = @_; + if ($args->{class} eq 'Bugzilla::Comment') { + $self->_comment_blocking($args->{params}); + } } sub user_verify_login { - my ($self, $args) = @_; - if (my $api_key = Bugzilla->params->{honeypot_api_key}) { - $self->_project_honeypot_blocking($api_key, $args->{login}); - } - $self->_ip_blocking($args->{login}); - $self->_domain_blocking($args->{login}); + my ($self, $args) = @_; + if (my $api_key = Bugzilla->params->{honeypot_api_key}) { + $self->_project_honeypot_blocking($api_key, $args->{login}); + } + $self->_ip_blocking($args->{login}); + $self->_domain_blocking($args->{login}); } sub editable_tables { - my ($self, $args) = @_; - my $tables = $args->{tables}; - # allow these tables to be edited with the EditTables extension - $tables->{antispam_domain_blocklist} = { - id_field => 'id', - order_by => 'domain', - blurb => 'List of fully qualified domain names to block at account creation time.', - group => 'can_configure_antispam', - }; - $tables->{antispam_comment_blocklist} = { - id_field => 'id', - order_by => 'word', - blurb => "List of whole words that will cause comments containing \\b\$word\\b to be blocked.\n" . - "This only applies to comments on bugs which the user didn't report.\n" . - "Users in the editbugs group are exempt from comment blocking.", - group => 'can_configure_antispam', - }; - $tables->{antispam_ip_blocklist} = { - id_field => 'id', - order_by => 'ip_address', - blurb => 'List of IPv4 addresses which are prevented from creating accounts.', - group => 'can_configure_antispam', - }; + my ($self, $args) = @_; + my $tables = $args->{tables}; + + # allow these tables to be edited with the EditTables extension + $tables->{antispam_domain_blocklist} = { + id_field => 'id', + order_by => 'domain', + blurb => + 'List of fully qualified domain names to block at account creation time.', + group => 'can_configure_antispam', + }; + $tables->{antispam_comment_blocklist} = { + id_field => 'id', + order_by => 'word', + blurb => + "List of whole words that will cause comments containing \\b\$word\\b to be blocked.\n" + . "This only applies to comments on bugs which the user didn't report.\n" + . "Users in the editbugs group are exempt from comment blocking.", + group => 'can_configure_antispam', + }; + $tables->{antispam_ip_blocklist} = { + id_field => 'id', + order_by => 'ip_address', + blurb => 'List of IPv4 addresses which are prevented from creating accounts.', + group => 'can_configure_antispam', + }; } sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{AntiSpam} = "Bugzilla::Extension::AntiSpam::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{AntiSpam} = "Bugzilla::Extension::AntiSpam::Config"; } # @@ -291,82 +298,43 @@ sub config_add_panels { # sub install_before_final_checks { - if (!Bugzilla::Group->new({ name => 'can_configure_antispam' })) { - Bugzilla::Group->create({ - name => 'can_configure_antispam', - description => 'Can configure Anti-Spam measures', - isbuggroup => 0, - }); - } + if (!Bugzilla::Group->new({name => 'can_configure_antispam'})) { + Bugzilla::Group->create({ + name => 'can_configure_antispam', + description => 'Can configure Anti-Spam measures', + isbuggroup => 0, + }); + } } sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'antispam_domain_blocklist'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - domain => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - comment => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - ], - INDEXES => [ - antispam_domain_blocklist_idx => { - FIELDS => [ 'domain' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'antispam_comment_blocklist'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - word => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - ], - INDEXES => [ - antispam_comment_blocklist_idx => { - FIELDS => [ 'word' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'antispam_ip_blocklist'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - ip_address => { - TYPE => 'VARCHAR(15)', - NOTNULL => 1, - }, - comment => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - ], - INDEXES => [ - antispam_ip_blocklist_idx => { - FIELDS => [ 'ip_address' ], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'antispam_domain_blocklist'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + domain => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + comment => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + ], + INDEXES => + [antispam_domain_blocklist_idx => {FIELDS => ['domain'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'antispam_comment_blocklist'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + word => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + ], + INDEXES => + [antispam_comment_blocklist_idx => {FIELDS => ['word'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'antispam_ip_blocklist'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + ip_address => {TYPE => 'VARCHAR(15)', NOTNULL => 1,}, + comment => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + ], + INDEXES => + [antispam_ip_blocklist_idx => {FIELDS => ['ip_address'], TYPE => 'UNIQUE',},], + }; } __PACKAGE__->NAME; diff --git a/extensions/AntiSpam/lib/Config.pm b/extensions/AntiSpam/lib/Config.pm index 278baea8f..3cddb77ed 100644 --- a/extensions/AntiSpam/lib/Config.pm +++ b/extensions/AntiSpam/lib/Config.pm @@ -17,66 +17,66 @@ use Bugzilla::Group; our $sortkey = 511; sub get_param_list { - my ($class) = @_; + my ($class) = @_; - my @param_list = ( - { - name => 'antispam_spammer_exclude_group', - type => 's', - choices => \&get_all_group_names, - default => 'canconfirm', - checker => \&check_group - }, - { - name => 'antispam_spammer_comment_count', - type => 't', - default => '3', - checker => \&check_numeric - }, - { - name => 'antispam_spammer_disable_text', - type => 'l', - default => - "This account has been automatically disabled as a result of " . - "a high number of spam comments.
\n
\n" . - "Please contact the address at the end of this message if " . - "you believe this to be an error." - }, - { - name => 'antispam_abusive_comment_count', - type => 't', - default => '5', - checker => \&check_numeric - }, - { - name => 'antispam_abusive_disable_text', - type => 'l', - default => - "This account has been automatically disabled as a result of " . - "a high number of comments tagged as abusive.
\n
\n" . - "All interactions on Bugzilla should follow our " . - "localconfig->{'urlbase'} . "page.cgi?id=etiquette.html\">" . - "etiquette guidelines.
\n
\n" . - "Please contact the address at the end of this message if you " . - "believe this to be an error, or if you would like your account " . - "reactivated in order to interact within our etiquette " . - "guidelines." - }, - { - name => 'antispam_multi_user_limit_age', - type => 't', - default => '2', - checker => \&check_numeric, - }, - { - name => 'antispam_multi_user_limit_count', - type => 't', - default => '5', - checker => \&check_numeric, - }, - ); + my @param_list = ( + { + name => 'antispam_spammer_exclude_group', + type => 's', + choices => \&get_all_group_names, + default => 'canconfirm', + checker => \&check_group + }, + { + name => 'antispam_spammer_comment_count', + type => 't', + default => '3', + checker => \&check_numeric + }, + { + name => 'antispam_spammer_disable_text', + type => 'l', + default => "This account has been automatically disabled as a result of " + . "a high number of spam comments.
\n
\n" + . "Please contact the address at the end of this message if " + . "you believe this to be an error." + }, + { + name => 'antispam_abusive_comment_count', + type => 't', + default => '5', + checker => \&check_numeric + }, + { + name => 'antispam_abusive_disable_text', + type => 'l', + default => "This account has been automatically disabled as a result of " + . "a high number of comments tagged as abusive.
\n
\n" + . "All interactions on Bugzilla should follow our " + . "localconfig->{'urlbase'} + . "page.cgi?id=etiquette.html\">" + . "etiquette guidelines.
\n
\n" + . "Please contact the address at the end of this message if you " + . "believe this to be an error, or if you would like your account " + . "reactivated in order to interact within our etiquette " + . "guidelines." + }, + { + name => 'antispam_multi_user_limit_age', + type => 't', + default => '2', + checker => \&check_numeric, + }, + { + name => 'antispam_multi_user_limit_count', + type => 't', + default => '5', + checker => \&check_numeric, + }, + ); - return @param_list; + return @param_list; } 1; diff --git a/extensions/BMO/Config.pm b/extensions/BMO/Config.pm index 153af24cb..e185b0b5d 100644 --- a/extensions/BMO/Config.pm +++ b/extensions/BMO/Config.pm @@ -28,24 +28,11 @@ use warnings; use constant NAME => 'BMO'; use constant REQUIRED_MODULES => [ - { - package => 'Tie-IxHash', - module => 'Tie::IxHash', - version => 0 - }, - { - package => 'Sys-Syslog', - module => 'Sys::Syslog', - version => 0 - }, - { - package => 'File-MimeInfo', - module => 'File::MimeInfo::Magic', - version => '0' - }, + {package => 'Tie-IxHash', module => 'Tie::IxHash', version => 0}, + {package => 'Sys-Syslog', module => 'Sys::Syslog', version => 0}, + {package => 'File-MimeInfo', module => 'File::MimeInfo::Magic', version => '0'}, ]; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/BMO/Extension.pm b/extensions/BMO/Extension.pm index 7499f0d1c..f4fb6fa32 100644 --- a/extensions/BMO/Extension.pm +++ b/extensions/BMO/Extension.pm @@ -72,296 +72,304 @@ our $VERSION = '0.1'; # BEGIN { - *Bugzilla::Bug::last_closed_date = \&_last_closed_date; - *Bugzilla::Bug::reporters_hw_os = \&_bug_reporters_hw_os; - *Bugzilla::Bug::is_unassigned = \&_bug_is_unassigned; - *Bugzilla::Bug::has_current_patch = \&_bug_has_current_patch; - *Bugzilla::Bug::missing_sec_approval = \&_bug_missing_sec_approval; - *Bugzilla::Product::default_security_group = \&_default_security_group; - *Bugzilla::Product::default_security_group_obj = \&_default_security_group_obj; - *Bugzilla::Product::group_always_settable = \&_group_always_settable; - *Bugzilla::Product::default_platform_id = \&_product_default_platform_id; - *Bugzilla::Product::default_op_sys_id = \&_product_default_op_sys_id; - *Bugzilla::Product::default_platform = \&_product_default_platform; - *Bugzilla::Product::default_op_sys = \&_product_default_op_sys; - *Bugzilla::check_default_product_security_group = \&_check_default_product_security_group; - *Bugzilla::Attachment::is_bounty_attachment = \&_attachment_is_bounty_attachment; - *Bugzilla::Attachment::bounty_details = \&_attachment_bounty_details; - *Bugzilla::Attachment::external_redirect = \&_attachment_external_redirect; - *Bugzilla::Attachment::can_review = \&_attachment_can_review; - *Bugzilla::Attachment::fetch_github_pr_diff = \&_attachment_fetch_github_pr_diff; + *Bugzilla::Bug::last_closed_date = \&_last_closed_date; + *Bugzilla::Bug::reporters_hw_os = \&_bug_reporters_hw_os; + *Bugzilla::Bug::is_unassigned = \&_bug_is_unassigned; + *Bugzilla::Bug::has_current_patch = \&_bug_has_current_patch; + *Bugzilla::Bug::missing_sec_approval = \&_bug_missing_sec_approval; + *Bugzilla::Product::default_security_group = \&_default_security_group; + *Bugzilla::Product::default_security_group_obj = \&_default_security_group_obj; + *Bugzilla::Product::group_always_settable = \&_group_always_settable; + *Bugzilla::Product::default_platform_id = \&_product_default_platform_id; + *Bugzilla::Product::default_op_sys_id = \&_product_default_op_sys_id; + *Bugzilla::Product::default_platform = \&_product_default_platform; + *Bugzilla::Product::default_op_sys = \&_product_default_op_sys; + *Bugzilla::check_default_product_security_group + = \&_check_default_product_security_group; + *Bugzilla::Attachment::is_bounty_attachment + = \&_attachment_is_bounty_attachment; + *Bugzilla::Attachment::bounty_details = \&_attachment_bounty_details; + *Bugzilla::Attachment::external_redirect = \&_attachment_external_redirect; + *Bugzilla::Attachment::can_review = \&_attachment_can_review; + *Bugzilla::Attachment::fetch_github_pr_diff + = \&_attachment_fetch_github_pr_diff; } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - $vars->{'cf_hidden_in_product'} = \&cf_hidden_in_product; - - if ($file =~ /^list\/list/) { - # Purpose: enable correct sorting of list table - # Matched to changes in list/table.html.tmpl - my %db_order_column_name_map = ( - 'map_components.name' => 'component', - 'map_products.name' => 'product', - 'map_reporter.login_name' => 'reporter', - 'map_assigned_to.login_name' => 'assigned_to', - 'delta_ts' => 'opendate', - 'creation_ts' => 'changeddate', - ); - - my @orderstrings = split(/,\s*/, $vars->{'order'}); - - # contains field names of the columns being used to sort the table. - my @order_columns; - foreach my $o (@orderstrings) { - $o =~ s/bugs.//; - $o = $db_order_column_name_map{$o} if - grep($_ eq $o, keys(%db_order_column_name_map)); - next if (grep($_ eq $o, @order_columns)); - push(@order_columns, $o); - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + $vars->{'cf_hidden_in_product'} = \&cf_hidden_in_product; + + if ($file =~ /^list\/list/) { + + # Purpose: enable correct sorting of list table + # Matched to changes in list/table.html.tmpl + my %db_order_column_name_map = ( + 'map_components.name' => 'component', + 'map_products.name' => 'product', + 'map_reporter.login_name' => 'reporter', + 'map_assigned_to.login_name' => 'assigned_to', + 'delta_ts' => 'opendate', + 'creation_ts' => 'changeddate', + ); - $vars->{'order_columns'} = \@order_columns; + my @orderstrings = split(/,\s*/, $vars->{'order'}); - # fields that have a custom sortkey. (So they are correctly sorted - # when using js) - my @sortkey_fields = qw(bug_status resolution bug_severity priority - rep_platform op_sys); + # contains field names of the columns being used to sort the table. + my @order_columns; + foreach my $o (@orderstrings) { + $o =~ s/bugs.//; + $o = $db_order_column_name_map{$o} + if grep($_ eq $o, keys(%db_order_column_name_map)); + next if (grep($_ eq $o, @order_columns)); + push(@order_columns, $o); + } - my %columns_sortkey; - foreach my $field (@sortkey_fields) { - $columns_sortkey{$field} = _get_field_values_sort_key($field); - } - $columns_sortkey{'target_milestone'} = _get_field_values_sort_key('milestones'); + $vars->{'order_columns'} = \@order_columns; - $vars->{'columns_sortkey'} = \%columns_sortkey; + # fields that have a custom sortkey. (So they are correctly sorted + # when using js) + my @sortkey_fields = qw(bug_status resolution bug_severity priority + rep_platform op_sys); + + my %columns_sortkey; + foreach my $field (@sortkey_fields) { + $columns_sortkey{$field} = _get_field_values_sort_key($field); } - elsif ($file =~ /^bug\/create\/create[\.-](.*)/) { - my $format = $1; - if (!$vars->{'cloned_bug_id'}) { - # Allow status whiteboard values to be bookmarked - $vars->{'status_whiteboard'} = - Bugzilla->cgi->param('status_whiteboard') || ""; - } + $columns_sortkey{'target_milestone'} = _get_field_values_sort_key('milestones'); - # Purpose: for pretty product chooser - $vars->{'format'} = Bugzilla->cgi->param('format'); + $vars->{'columns_sortkey'} = \%columns_sortkey; + } + elsif ($file =~ /^bug\/create\/create[\.-](.*)/) { + my $format = $1; + if (!$vars->{'cloned_bug_id'}) { - if ($format eq 'doc.html.tmpl') { - my $versions = Bugzilla::Product->new({ name => 'Core' })->versions; - $vars->{'versions'} = [ reverse @$versions ]; - } - } - elsif ($file eq 'bug/edit.html.tmpl' || $file eq 'bug_modal/edit.html.tmpl') { - $vars->{split_cf_crash_signature} = $self->_split_crash_signature($vars); + # Allow status whiteboard values to be bookmarked + $vars->{'status_whiteboard'} = Bugzilla->cgi->param('status_whiteboard') || ""; } + # Purpose: for pretty product chooser + $vars->{'format'} = Bugzilla->cgi->param('format'); - if ($file =~ /^list\/list/ || $file =~ /^bug\/create\/create[\.-]/) { - # hack to allow the bug entry templates to use check_can_change_field - # to see if various field values should be available to the current user. - $vars->{'default'} = Bugzilla::Extension::BMO::FakeBug->new($vars->{'default'} || {}); + if ($format eq 'doc.html.tmpl') { + my $versions = Bugzilla::Product->new({name => 'Core'})->versions; + $vars->{'versions'} = [reverse @$versions]; } + } + elsif ($file eq 'bug/edit.html.tmpl' || $file eq 'bug_modal/edit.html.tmpl') { + $vars->{split_cf_crash_signature} = $self->_split_crash_signature($vars); + } - if ($file =~ /^attachment\/diff-header\./) { - my $attachid = $vars->{attachid} ? $vars->{attachid} : $vars->{newid}; - $vars->{attachment} = Bugzilla::Attachment->new({ id => $attachid, cache => 1 }) - if $attachid; - } - if ($file =~ /^admin\/products\/(create|edit)\./) { - my $product = $vars->{product}; - my $security_groups = Bugzilla::Group->match({ isbuggroup => 1, isactive => 1 }); - if ($product) { - # If set group is not active currently, we add it into the list - if (!grep($_->name eq $product->default_security_group, @$security_groups)) { - push(@$security_groups, $product->default_security_group_obj); - @$security_groups = sort { $a->name cmp $b->name } @$security_groups; - } - } - $vars->{security_groups} = $security_groups; - } -} + if ($file =~ /^list\/list/ || $file =~ /^bug\/create\/create[\.-]/) { -sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; + # hack to allow the bug entry templates to use check_can_change_field + # to see if various field values should be available to the current user. + $vars->{'default'} + = Bugzilla::Extension::BMO::FakeBug->new($vars->{'default'} || {}); + } - if ($page eq 'user_activity.html') { - require Bugzilla::Extension::BMO::Reports::UserActivity; - Bugzilla::Extension::BMO::Reports::UserActivity::report($vars); + if ($file =~ /^attachment\/diff-header\./) { + my $attachid = $vars->{attachid} ? $vars->{attachid} : $vars->{newid}; + $vars->{attachment} = Bugzilla::Attachment->new({id => $attachid, cache => 1}) + if $attachid; + } + if ($file =~ /^admin\/products\/(create|edit)\./) { + my $product = $vars->{product}; + my $security_groups = Bugzilla::Group->match({isbuggroup => 1, isactive => 1}); + if ($product) { + + # If set group is not active currently, we add it into the list + if (!grep($_->name eq $product->default_security_group, @$security_groups)) { + push(@$security_groups, $product->default_security_group_obj); + @$security_groups = sort { $a->name cmp $b->name } @$security_groups; + } } - elsif ($page eq 'triage_reports.html') { - require Bugzilla::Extension::BMO::Reports::Triage; - Bugzilla::Extension::BMO::Reports::Triage::unconfirmed($vars); - } - elsif ($page eq 'triage_owners.html') { - require Bugzilla::Extension::BMO::Reports::Triage; - Bugzilla::Extension::BMO::Reports::Triage::owners($vars); - } - elsif ($page eq 'group_admins.html') { - require Bugzilla::Extension::BMO::Reports::Groups; - Bugzilla::Extension::BMO::Reports::Groups::admins_report($vars); - } - elsif ($page eq 'group_membership.html' or $page eq 'group_membership.txt') { - require Bugzilla::Extension::BMO::Reports::Groups; - Bugzilla::Extension::BMO::Reports::Groups::membership_report($page, $vars); - } - elsif ($page eq 'group_members.html' or $page eq 'group_members.json') { - require Bugzilla::Extension::BMO::Reports::Groups; - Bugzilla::Extension::BMO::Reports::Groups::members_report($page, $vars); - } - elsif ($page eq 'recruiting_dashboard.html') { - require Bugzilla::Extension::BMO::Reports::Recruiting; - Bugzilla::Extension::BMO::Reports::Recruiting::report($vars); - } - elsif ($page eq 'internship_dashboard.html') { - require Bugzilla::Extension::BMO::Reports::Internship; - Bugzilla::Extension::BMO::Reports::Internship::report($vars); - } - elsif ($page eq 'email_queue.html') { - print Bugzilla->cgi->redirect('view_job_queue.cgi'); - } - elsif ($page eq 'release_tracking_report.html') { - require Bugzilla::Extension::BMO::Reports::ReleaseTracking; - Bugzilla::Extension::BMO::Reports::ReleaseTracking::report($vars); - } - elsif ($page eq 'product_security_report.html') { - require Bugzilla::Extension::BMO::Reports::ProductSecurity; - Bugzilla::Extension::BMO::Reports::ProductSecurity::report($vars); - } - elsif ($page eq 'fields.html') { - # Recently global/field-descs.none.tmpl and bug/field-help.none.tmpl - # were changed for better performance and are now only loaded once. - # I have not found an easy way to allow our hook template to check if - # it is called from pages/fields.html.tmpl. So we set a value in request_cache - # that our hook template can see. - Bugzilla->request_cache->{'bmo_fields_page'} = 1; - } - elsif ($page eq 'query_database.html') { - query_database($vars); - } - elsif ($page eq 'attachment_bounty_form.html') { - bounty_attachment($vars); - } - elsif ($page eq 'triage_request.html') { - triage_request($vars); - } + $vars->{security_groups} = $security_groups; + } +} + +sub page_before_template { + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'user_activity.html') { + require Bugzilla::Extension::BMO::Reports::UserActivity; + Bugzilla::Extension::BMO::Reports::UserActivity::report($vars); + + } + elsif ($page eq 'triage_reports.html') { + require Bugzilla::Extension::BMO::Reports::Triage; + Bugzilla::Extension::BMO::Reports::Triage::unconfirmed($vars); + } + elsif ($page eq 'triage_owners.html') { + require Bugzilla::Extension::BMO::Reports::Triage; + Bugzilla::Extension::BMO::Reports::Triage::owners($vars); + } + elsif ($page eq 'group_admins.html') { + require Bugzilla::Extension::BMO::Reports::Groups; + Bugzilla::Extension::BMO::Reports::Groups::admins_report($vars); + } + elsif ($page eq 'group_membership.html' or $page eq 'group_membership.txt') { + require Bugzilla::Extension::BMO::Reports::Groups; + Bugzilla::Extension::BMO::Reports::Groups::membership_report($page, $vars); + } + elsif ($page eq 'group_members.html' or $page eq 'group_members.json') { + require Bugzilla::Extension::BMO::Reports::Groups; + Bugzilla::Extension::BMO::Reports::Groups::members_report($page, $vars); + } + elsif ($page eq 'recruiting_dashboard.html') { + require Bugzilla::Extension::BMO::Reports::Recruiting; + Bugzilla::Extension::BMO::Reports::Recruiting::report($vars); + } + elsif ($page eq 'internship_dashboard.html') { + require Bugzilla::Extension::BMO::Reports::Internship; + Bugzilla::Extension::BMO::Reports::Internship::report($vars); + } + elsif ($page eq 'email_queue.html') { + print Bugzilla->cgi->redirect('view_job_queue.cgi'); + } + elsif ($page eq 'release_tracking_report.html') { + require Bugzilla::Extension::BMO::Reports::ReleaseTracking; + Bugzilla::Extension::BMO::Reports::ReleaseTracking::report($vars); + } + elsif ($page eq 'product_security_report.html') { + require Bugzilla::Extension::BMO::Reports::ProductSecurity; + Bugzilla::Extension::BMO::Reports::ProductSecurity::report($vars); + } + elsif ($page eq 'fields.html') { + + # Recently global/field-descs.none.tmpl and bug/field-help.none.tmpl + # were changed for better performance and are now only loaded once. + # I have not found an easy way to allow our hook template to check if + # it is called from pages/fields.html.tmpl. So we set a value in request_cache + # that our hook template can see. + Bugzilla->request_cache->{'bmo_fields_page'} = 1; + } + elsif ($page eq 'query_database.html') { + query_database($vars); + } + elsif ($page eq 'attachment_bounty_form.html') { + bounty_attachment($vars); + } + elsif ($page eq 'triage_request.html') { + triage_request($vars); + } } sub bounty_attachment { - my ($vars) = @_; - - my $user = Bugzilla->user; - $user->in_group('bounty-team') - || ThrowUserError("auth_failure", { group => "bounty-team", - action => "add", - object => "bounty_attachments" }); - - my $input = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; - my $bug = Bugzilla::Bug->check({ id => $input->{bug_id}, cache => 1 }); - my $attachment = first { $_ && _attachment_is_bounty_attachment($_) } @{$bug->attachments}; - $vars->{bug} = $bug; - - if ($input->{submit}) { - ThrowUserError('bounty_attachment_missing_reporter') - unless $input->{reporter_email}; - - check_hash_token($input->{token}, ['bounty', $bug->id]); - - my @fields = qw( reporter_email amount_paid reported_date fixed_date awarded_date publish ); - my %form = map { $_ => $input->{$_} } @fields; - $form{credit} = [ grep { defined } map { $input->{"credit_$_"} } 1..3 ]; - - $dbh->bz_start_transaction(); - if ($attachment) { - $attachment->set( - description => format_bounty_attachment_description(\%form) - ); - $attachment->update; - } - else { - my $attachment = Bugzilla::Attachment->create({ - bug => $bug, - isprivate => 1, - mimetype => 'text/plain', - data => 'bounty', - filename => 'bugbounty.data', - description => format_bounty_attachment_description(\%form), - }); - } - $dbh->bz_commit_transaction(); + my ($vars) = @_; - Bugzilla::BugMail::Send($bug->id, { changer => $user }); + my $user = Bugzilla->user; + $user->in_group('bounty-team') + || ThrowUserError("auth_failure", + {group => "bounty-team", action => "add", object => "bounty_attachments"}); - print Bugzilla->cgi->redirect('show_bug.cgi?id=' . $bug->id); - exit; - } + my $input = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + my $bug = Bugzilla::Bug->check({id => $input->{bug_id}, cache => 1}); + my $attachment + = first { $_ && _attachment_is_bounty_attachment($_) } @{$bug->attachments}; + $vars->{bug} = $bug; + + if ($input->{submit}) { + ThrowUserError('bounty_attachment_missing_reporter') + unless $input->{reporter_email}; + + check_hash_token($input->{token}, ['bounty', $bug->id]); + + my @fields + = qw( reporter_email amount_paid reported_date fixed_date awarded_date publish ); + my %form = map { $_ => $input->{$_} } @fields; + $form{credit} = [grep {defined} map { $input->{"credit_$_"} } 1 .. 3]; + $dbh->bz_start_transaction(); if ($attachment) { - $vars->{form} = $attachment->bounty_details; + $attachment->set(description => format_bounty_attachment_description(\%form)); + $attachment->update; } else { - my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $vars->{form} = { - reporter_email => $bug->reporter->email, - reported_date => format_time($bug->creation_ts, "%Y-%m-%d"), - awarded_date => format_time($now, "%Y-%m-%d"), - publish => 1 - }; - if ($bug->cf_last_resolved) { - $vars->{form}{fixed_date} = format_time($bug->cf_last_resolved, "%Y-%m-%d"), - } + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + isprivate => 1, + mimetype => 'text/plain', + data => 'bounty', + filename => 'bugbounty.data', + description => format_bounty_attachment_description(\%form), + }); + } + $dbh->bz_commit_transaction(); + + Bugzilla::BugMail::Send($bug->id, {changer => $user}); + + print Bugzilla->cgi->redirect('show_bug.cgi?id=' . $bug->id); + exit; + } + + if ($attachment) { + $vars->{form} = $attachment->bounty_details; + } + else { + my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $vars->{form} = { + reporter_email => $bug->reporter->email, + reported_date => format_time($bug->creation_ts, "%Y-%m-%d"), + awarded_date => format_time($now, "%Y-%m-%d"), + publish => 1 + }; + if ($bug->cf_last_resolved) { + $vars->{form}{fixed_date} = format_time($bug->cf_last_resolved, "%Y-%m-%d"),; } - $vars->{form}{token} = issue_hash_token(['bounty', $bug->id]); + } + $vars->{form}{token} = issue_hash_token(['bounty', $bug->id]); } sub _attachment_is_bounty_attachment { - my ($attachment) = @_; + my ($attachment) = @_; - return 0 unless $attachment->filename eq 'bugbounty.data'; - return 0 unless $attachment->contenttype eq 'text/plain'; - return 0 unless $attachment->isprivate; - return 0 unless $attachment->attacher->in_group('bounty-team'); + return 0 unless $attachment->filename eq 'bugbounty.data'; + return 0 unless $attachment->contenttype eq 'text/plain'; + return 0 unless $attachment->isprivate; + return 0 unless $attachment->attacher->in_group('bounty-team'); - return $attachment->description =~ /^(?:[^,]*,)+[^,]*$/; + return $attachment->description =~ /^(?:[^,]*,)+[^,]*$/; } sub _attachment_bounty_details { - my ($attachment) = @_; - if (!exists $attachment->{bounty_details}) { - if ($attachment->is_bounty_attachment) { - $attachment->{bounty_details} = parse_bounty_attachment_description($attachment->description); - } - else { - $attachment->{bounty_details} = undef; - } + my ($attachment) = @_; + if (!exists $attachment->{bounty_details}) { + if ($attachment->is_bounty_attachment) { + $attachment->{bounty_details} + = parse_bounty_attachment_description($attachment->description); + } + else { + $attachment->{bounty_details} = undef; } - return $attachment->{bounty_details}; + } + return $attachment->{bounty_details}; } sub format_bounty_attachment_description { - my ($form) = @_; - my @fields = ( - @$form{qw( reporter_email amount_paid reported_date fixed_date awarded_date )}, - $form->{publish} ? 'true' : 'false', - @{ $form->{credit} // [] } - ); + my ($form) = @_; + my @fields = ( + @$form{qw( reporter_email amount_paid reported_date fixed_date awarded_date )}, + $form->{publish} ? 'true' : 'false', + @{$form->{credit} // []} + ); - return join(',', map { $_ // '' } @fields); + return join(',', map { $_ // '' } @fields); } sub parse_bounty_attachment_description { - my ($desc) = @_; + my ($desc) = @_; - my %map = ( true => 1, false => 0 ); - my $date = qr/\d{4}-\d{2}-\d{2}/; - $desc =~ m! + my %map = (true => 1, false => 0); + my $date = qr/\d{4}-\d{2}-\d{2}/; + $desc =~ m! ^ (? [^,]+) \s*,\s* (? [0-9]+[-+?]?) ? \s*,\s* @@ -373,1799 +381,1830 @@ sub parse_bounty_attachment_description { $ !x; - return { - reporter_email => $+{reporter_email} // '', - amount_paid => $+{amount_paid} // '', - reported_date => $+{reported_date} // '', - fixed_date => $+{fixed_date} // '', - awarded_date => $+{awarded_date} // '', - publish => $map{ $+{publish} // 'false' }, - credit => [grep { $_ } split(/\s*,\s*/, $+{credits}) ] - }; + return { + reporter_email => $+{reporter_email} // '', + amount_paid => $+{amount_paid} // '', + reported_date => $+{reported_date} // '', + fixed_date => $+{fixed_date} // '', + awarded_date => $+{awarded_date} // '', + publish => $map{$+{publish} // 'false'}, + credit => [grep {$_} split(/\s*,\s*/, $+{credits})] + }; } sub triage_request { - my ($vars) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - if (Bugzilla->input_params->{update}) { - Bugzilla->set_user(Bugzilla::User->super_user); - $user->set_groups({ add => [ 'canconfirm' ] }); - Bugzilla->set_user($user); - $user->update(); - $vars->{updated} = 1; - } + my ($vars) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + if (Bugzilla->input_params->{update}) { + Bugzilla->set_user(Bugzilla::User->super_user); + $user->set_groups({add => ['canconfirm']}); + Bugzilla->set_user($user); + $user->update(); + $vars->{updated} = 1; + } } sub _get_field_values_sort_key { - my ($field) = @_; - my $dbh = Bugzilla->dbh; - my $fields = $dbh->selectall_arrayref( - "SELECT value, sortkey FROM $field - ORDER BY sortkey, value"); - - my %field_values; - foreach my $field (@$fields) { - my ($value, $sortkey) = @$field; - $field_values{$value} = $sortkey; - } - return \%field_values; + my ($field) = @_; + my $dbh = Bugzilla->dbh; + my $fields = $dbh->selectall_arrayref( + "SELECT value, sortkey FROM $field + ORDER BY sortkey, value" + ); + + my %field_values; + foreach my $field (@$fields) { + my ($value, $sortkey) = @$field; + $field_values{$value} = $sortkey; + } + return \%field_values; } sub active_custom_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - my $params = $args->{'params'}; - my $product = $params->{'product'}; - my $component = $params->{'component'}; + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + my $params = $args->{'params'}; + my $product = $params->{'product'}; + my $component = $params->{'component'}; - return if !$product; + return if !$product; - my $product_name = blessed $product ? $product->name : $product; - my $component_name = blessed $component ? $component->name : $component; + my $product_name = blessed $product ? $product->name : $product; + my $component_name = blessed $component ? $component->name : $component; - my @tmp_fields; - foreach my $field (@$$fields) { - next if cf_hidden_in_product($field->name, $product_name, $component_name); - push(@tmp_fields, $field); - } - $$fields = \@tmp_fields; + my @tmp_fields; + foreach my $field (@$$fields) { + next if cf_hidden_in_product($field->name, $product_name, $component_name); + push(@tmp_fields, $field); + } + $$fields = \@tmp_fields; } sub cf_hidden_in_product { - my ($field_name, $product_name, $component_name, $bug) = @_; - - # check bugzilla's built-in visibility controls first - if ($bug) { - my $field = Bugzilla::Field->new({ name => $field_name, cache => 1 }); - return 1 if $field && !$field->is_visible_on_bug($bug); - } - - # If used in buglist.cgi, we pass in one_product which is a Bugzilla::Product - # elsewhere, we just pass the name of the product. - $product_name = blessed($product_name) - ? $product_name->name - : $product_name; - - # Also in buglist.cgi, we pass in a list of components instead - # of a single component name everywhere else. - my $component_list = []; - if ($component_name) { - $component_list = ref $component_name - ? $component_name - : [ $component_name ]; - } - - foreach my $field_re (keys %$cf_visible_in_products) { - if ($field_name =~ $field_re) { - # If no product given, for example more than one product - # in buglist.cgi, then hide field by default - return 1 if !$product_name; - - my $products = $cf_visible_in_products->{$field_re}; - foreach my $product (keys %$products) { - my $components = $products->{$product}; - - my $found_component = 0; - if (@$components) { - foreach my $component (@$components) { - if (ref($component) eq 'Regexp') { - if (grep($_ =~ $component, @$component_list)) { - $found_component = 1; - last; - } - } else { - if (grep($_ eq $component, @$component_list)) { - $found_component = 1; - last; - } - } - } - } - - # If product matches and at at least one component matches - # from component_list (if a matching component was required), - # we allow the field to be seen - if ($product eq $product_name && (!@$components || $found_component)) { - return 0; - } + my ($field_name, $product_name, $component_name, $bug) = @_; + + # check bugzilla's built-in visibility controls first + if ($bug) { + my $field = Bugzilla::Field->new({name => $field_name, cache => 1}); + return 1 if $field && !$field->is_visible_on_bug($bug); + } + + # If used in buglist.cgi, we pass in one_product which is a Bugzilla::Product + # elsewhere, we just pass the name of the product. + $product_name = blessed($product_name) ? $product_name->name : $product_name; + + # Also in buglist.cgi, we pass in a list of components instead + # of a single component name everywhere else. + my $component_list = []; + if ($component_name) { + $component_list = ref $component_name ? $component_name : [$component_name]; + } + + foreach my $field_re (keys %$cf_visible_in_products) { + if ($field_name =~ $field_re) { + + # If no product given, for example more than one product + # in buglist.cgi, then hide field by default + return 1 if !$product_name; + + my $products = $cf_visible_in_products->{$field_re}; + foreach my $product (keys %$products) { + my $components = $products->{$product}; + + my $found_component = 0; + if (@$components) { + foreach my $component (@$components) { + if (ref($component) eq 'Regexp') { + if (grep($_ =~ $component, @$component_list)) { + $found_component = 1; + last; + } } - return 1; + else { + if (grep($_ eq $component, @$component_list)) { + $found_component = 1; + last; + } + } + } } + + # If product matches and at at least one component matches + # from component_list (if a matching component was required), + # we allow the field to be seen + if ($product eq $product_name && (!@$components || $found_component)) { + return 0; + } + } + return 1; } + } - return 0; + return 0; } # Purpose: CC certain email addresses on bugmail when a bug is added or # removed from a particular group. sub bugmail_recipients { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - my $recipients = $args->{'recipients'}; - my $diffs = $args->{'diffs'}; - - if (@$diffs) { - # Changed bug - foreach my $ref (@$diffs) { - my $old = $ref->{old}; - my $new = $ref->{new}; - my $fieldname = $ref->{field_name}; - - if ($fieldname eq "bug_group") { - _cc_if_special_group($old, $recipients); - _cc_if_special_group($new, $recipients); - } - } - } else { - # Determine if it's a new bug, or a comment without a field change - my $comment_count = scalar @{$bug->comments}; - if ($comment_count == 1) { - # New bug - foreach my $group (@{ $bug->groups_in }) { - _cc_if_special_group($group->{'name'}, $recipients); - } - } + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $recipients = $args->{'recipients'}; + my $diffs = $args->{'diffs'}; + + if (@$diffs) { + + # Changed bug + foreach my $ref (@$diffs) { + my $old = $ref->{old}; + my $new = $ref->{new}; + my $fieldname = $ref->{field_name}; + + if ($fieldname eq "bug_group") { + _cc_if_special_group($old, $recipients); + _cc_if_special_group($new, $recipients); + } + } + } + else { + # Determine if it's a new bug, or a comment without a field change + my $comment_count = scalar @{$bug->comments}; + if ($comment_count == 1) { + + # New bug + foreach my $group (@{$bug->groups_in}) { + _cc_if_special_group($group->{'name'}, $recipients); + } } + } } sub _cc_if_special_group { - my ($group, $recipients) = @_; + my ($group, $recipients) = @_; - return if !$group; + return if !$group; - if (exists $group_change_notification{$group}) { - foreach my $login (@{ $group_change_notification{$group} }) { - my $id = login_to_id($login); - $recipients->{$id}->{+REL_CC} = Bugzilla::BugMail::BIT_DIRECT(); - } + if (exists $group_change_notification{$group}) { + foreach my $login (@{$group_change_notification{$group}}) { + my $id = login_to_id($login); + $recipients->{$id}->{+REL_CC} = Bugzilla::BugMail::BIT_DIRECT(); } + } } sub _check_trusted { - my ($field, $trusted, $priv_results) = @_; + my ($field, $trusted, $priv_results) = @_; - my $needed_group = $trusted->{'_default'} || ""; - foreach my $dfield (keys %$trusted) { - if ($field =~ $dfield) { - $needed_group = $trusted->{$dfield}; - } - } - if ($needed_group && !Bugzilla->user->in_group($needed_group)) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + my $needed_group = $trusted->{'_default'} || ""; + foreach my $dfield (keys %$trusted) { + if ($field =~ $dfield) { + $needed_group = $trusted->{$dfield}; } + } + if ($needed_group && !Bugzilla->user->in_group($needed_group)) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } } sub _is_field_set { - my $value = shift; - return $value ne '---' && $value !~ /\?$/; + my $value = shift; + return $value ne '---' && $value !~ /\?$/; } sub bug_check_can_change_field { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - my $field = $args->{'field'}; - my $new_value = $args->{'new_value'}; - my $old_value = $args->{'old_value'}; - my $priv_results = $args->{'priv_results'}; - my $user = Bugzilla->user; - - if ($field =~ /^cf/ && !@$priv_results && $new_value ne '---') { - # Cannot use the standard %cf_setter mapping as we want anyone - # to be able to set ?, just not the other values. - if ($field eq 'cf_cab_review') { - if ($new_value ne '1' - && $new_value ne '?' - && !$user->in_group('infra', $bug->product_id)) - { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } - } - # "other" custom field setters restrictions - elsif (exists $cf_setters->{$field}) { - my $in_group = 0; - foreach my $group (@{$cf_setters->{$field}}) { - if ($user->in_group($group, $bug->product_id)) { - $in_group = 1; - last; - } - } - if (!$in_group) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $field = $args->{'field'}; + my $new_value = $args->{'new_value'}; + my $old_value = $args->{'old_value'}; + my $priv_results = $args->{'priv_results'}; + my $user = Bugzilla->user; + + if ($field =~ /^cf/ && !@$priv_results && $new_value ne '---') { + + # Cannot use the standard %cf_setter mapping as we want anyone + # to be able to set ?, just not the other values. + if ($field eq 'cf_cab_review') { + if ( $new_value ne '1' + && $new_value ne '?' + && !$user->in_group('infra', $bug->product_id)) + { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + } + + # "other" custom field setters restrictions + elsif (exists $cf_setters->{$field}) { + my $in_group = 0; + foreach my $group (@{$cf_setters->{$field}}) { + if ($user->in_group($group, $bug->product_id)) { + $in_group = 1; + last; } + } + if (!$in_group) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } } - elsif ($field eq 'resolution' && $new_value eq 'EXPIRED') { - # The EXPIRED resolution should only be settable by gerv. - if ($user->login ne 'gerv@mozilla.org') { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + } + elsif ($field eq 'resolution' && $new_value eq 'EXPIRED') { - } elsif ($field eq 'resolution' && $new_value eq 'FIXED') { - # You need at least canconfirm to mark a bug as FIXED - if (!$user->in_group('canconfirm', $bug->{'product_id'})) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + # The EXPIRED resolution should only be settable by gerv. + if ($user->login ne 'gerv@mozilla.org') { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } - } elsif ( - ($field eq 'bug_status' && $old_value eq 'VERIFIED') - || ($field eq 'dup_id' && $bug->status->name eq 'VERIFIED') - || ($field eq 'resolution' && $bug->status->name eq 'VERIFIED') - ) { - # You need at least editbugs to reopen a resolved/verified bug - if (!$user->in_group('editbugs', $bug->{'product_id'})) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + } + elsif ($field eq 'resolution' && $new_value eq 'FIXED') { - } elsif ($user->in_group('canconfirm', $bug->{'product_id'})) { - # Canconfirm is really "cantriage"; users with canconfirm can also mark - # bugs as DUPLICATE, WORKSFORME, and INCOMPLETE. - if ($field eq 'bug_status' - && is_open_state($old_value) - && !is_open_state($new_value)) - { - push (@$priv_results, PRIVILEGES_REQUIRED_NONE); - } - elsif ($field eq 'resolution' && - ($new_value eq 'DUPLICATE' || - $new_value eq 'WORKSFORME' || - $new_value eq 'INCOMPLETE' || - ($old_value eq '' && $new_value eq '1'))) - { - push (@$priv_results, PRIVILEGES_REQUIRED_NONE); - } - elsif ($field eq 'dup_id') { - push (@$priv_results, PRIVILEGES_REQUIRED_NONE); - } + # You need at least canconfirm to mark a bug as FIXED + if (!$user->in_group('canconfirm', $bug->{'product_id'})) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } - } elsif ($field eq 'bug_status') { - # Disallow reopening of bugs which have been resolved for > 1 year - if (is_open_state($new_value) - && !is_open_state($old_value) - && $bug->resolution eq 'FIXED') - { - my $days_ago = DateTime->now(time_zone => Bugzilla->local_timezone); - $days_ago->subtract(days => 365); - my $last_closed = datetime_from($bug->last_closed_date); - if ($last_closed lt $days_ago) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } - } + } + elsif (($field eq 'bug_status' && $old_value eq 'VERIFIED') + || ($field eq 'dup_id' && $bug->status->name eq 'VERIFIED') + || ($field eq 'resolution' && $bug->status->name eq 'VERIFIED')) + { + # You need at least editbugs to reopen a resolved/verified bug + if (!$user->in_group('editbugs', $bug->{'product_id'})) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); } + + } + elsif ($user->in_group('canconfirm', $bug->{'product_id'})) { + + # Canconfirm is really "cantriage"; users with canconfirm can also mark + # bugs as DUPLICATE, WORKSFORME, and INCOMPLETE. + if ( $field eq 'bug_status' + && is_open_state($old_value) + && !is_open_state($new_value)) + { + push(@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + elsif ( + $field eq 'resolution' + && ( $new_value eq 'DUPLICATE' + || $new_value eq 'WORKSFORME' + || $new_value eq 'INCOMPLETE' + || ($old_value eq '' && $new_value eq '1')) + ) + { + push(@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + elsif ($field eq 'dup_id') { + push(@$priv_results, PRIVILEGES_REQUIRED_NONE); + } + + } + elsif ($field eq 'bug_status') { + + # Disallow reopening of bugs which have been resolved for > 1 year + if ( is_open_state($new_value) + && !is_open_state($old_value) + && $bug->resolution eq 'FIXED') + { + my $days_ago = DateTime->now(time_zone => Bugzilla->local_timezone); + $days_ago->subtract(days => 365); + my $last_closed = datetime_from($bug->last_closed_date); + if ($last_closed lt $days_ago) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } + } + } } # link up various Mozilla-specific strings sub bug_format_comment { - my ($self, $args) = @_; - my $regexes = $args->{'regexes'}; + my ($self, $args) = @_; + my $regexes = $args->{'regexes'}; - # link to crash-stats - # Only match if not already in an URL using the negative lookbehind (? qr/(? qr/(? sub { - my $args = shift; - my $match = html_quote($args->{matches}->[0]); - return qq{bp-$match}; - } - }); - - # link to CVE/CAN security releases - push (@$regexes, { - match => qr/(? sub { - my $args = shift; - my $match = html_quote($args->{matches}->[0]); - return qq{$match}; - } - }); - - # link to svn.m.o - push (@$regexes, { - match => qr/(^|\s)r(\d{4,})\b/, - replace => sub { - my $args = shift; - my $match = html_quote($args->{matches}->[1]); - return - $args->{matches}->[0] . - qq{r$match}; - } - }); - - # link old git.mozilla.org commit messages to github - push (@$regexes, { - match => qr#^(To\s(?:ssh://)?(?:[^\@]+\@)?git\.mozilla\.org[:/](.+?\.git)\n + replace => sub { + my $args = shift; + my $match = html_quote($args->{matches}->[0]); + return + qq{bp-$match}; + } + } + ); + + # link to CVE/CAN security releases + push( + @$regexes, + { + match => qr/(? sub { + my $args = shift; + my $match = html_quote($args->{matches}->[0]); + return + qq{$match}; + } + } + ); + + # link to svn.m.o + push( + @$regexes, + { + match => qr/(^|\s)r(\d{4,})\b/, + replace => sub { + my $args = shift; + my $match = html_quote($args->{matches}->[1]); + return $args->{matches}->[0] + . qq{r$match}; + } + } + ); + + # link old git.mozilla.org commit messages to github + push( + @$regexes, + { + match => qr#^(To\s(?:ssh://)?(?:[^\@]+\@)?git\.mozilla\.org[:/](.+?\.git)\n \s+)([0-9a-z]+\.\.([0-9a-z]+)\s+\S+\s->\s\S+)#mx, - replace => sub { - my $args = shift; - my $preamble = html_quote($args->{matches}->[0]); - my $repo = html_quote($args->{matches}->[1]); - my $text = html_quote($args->{matches}->[2]); - my $revision = html_quote($args->{matches}->[3]); - $repo = 'mozilla/webtools-bmo-bugzilla' if $repo =~ /^webtools\/bmo\/bugzilla/; - $repo = 'bugzilla/bugzilla' if $repo =~ /^bugzilla\/bugzilla\.git/; - $repo = 'bugzilla/bugzilla.org' if $repo =~ /^www\/bugzilla\.org/; - return qq#$preamble$text#; - } - }); - - # link github commit messages - push (@$regexes, { - match => qr#^(To\s(?:https://|git@)?github\.com[:/](.+?)\.git\n + replace => sub { + my $args = shift; + my $preamble = html_quote($args->{matches}->[0]); + my $repo = html_quote($args->{matches}->[1]); + my $text = html_quote($args->{matches}->[2]); + my $revision = html_quote($args->{matches}->[3]); + $repo = 'mozilla/webtools-bmo-bugzilla' if $repo =~ /^webtools\/bmo\/bugzilla/; + $repo = 'bugzilla/bugzilla' if $repo =~ /^bugzilla\/bugzilla\.git/; + $repo = 'bugzilla/bugzilla.org' if $repo =~ /^www\/bugzilla\.org/; + return + qq#$preamble$text#; + } + } + ); + + # link github commit messages + push( + @$regexes, + { + match => qr#^(To\s(?:https://|git@)?github\.com[:/](.+?)\.git\n \s+)([0-9a-z]+\.\.([0-9a-z]+)\s+\S+\s->\s\S+)#mx, - replace => sub { - my $args = shift; - my $preamble = html_quote($args->{matches}->[0]); - my $repo = html_quote($args->{matches}->[1]); - my $text = html_quote($args->{matches}->[2]); - my $revision = html_quote($args->{matches}->[3]); - return qq#$preamble$text#; - } - }); - - # link github pull requests and issues - push (@$regexes, { - match => qr/(\s)([A-Za-z0-9_\.-]+)\/([A-Za-z0-9_\.-]+)\#([0-9]+)\b/, - replace => sub { - my $args = shift; - my $owner = html_quote($args->{matches}->[1]); - my $repo = html_quote($args->{matches}->[2]); - my $number = html_quote($args->{matches}->[3]); - return qq# $owner/$repo\#$number#; + replace => sub { + my $args = shift; + my $preamble = html_quote($args->{matches}->[0]); + my $repo = html_quote($args->{matches}->[1]); + my $text = html_quote($args->{matches}->[2]); + my $revision = html_quote($args->{matches}->[3]); + return + qq#$preamble$text#; + } + } + ); + + # link github pull requests and issues + push( + @$regexes, + { + match => qr/(\s)([A-Za-z0-9_\.-]+)\/([A-Za-z0-9_\.-]+)\#([0-9]+)\b/, + replace => sub { + my $args = shift; + my $owner = html_quote($args->{matches}->[1]); + my $repo = html_quote($args->{matches}->[2]); + my $number = html_quote($args->{matches}->[3]); + return + qq# $owner/$repo\#$number#; + } + } + ); + +# Update certain links to git.mozilla.org to go to github.com instead +# https://git.mozilla.org/?p=webtools/bmo/bugzilla.git;a=blob;f=Bugzilla/WebService/Bug.pm;h=d7a1d8f9bb5fdee524f2bb342a4573a63d890f2e;hb=HEAD#l657 + push( + @$regexes, + { + match => qr#\b(https?://git\.mozilla\.org\S+)\b#mx, + replace => sub { + my $args = shift; + my $match = $args->{matches}->[0]; + my $uri = URI->new($match); + my $text = html_quote($match); + + # Only work on BMO and Bugzilla repos + my $repo = html_quote($uri->query_param_delete("p")) || ''; + if ($repo !~ /(webtools\/bmo|bugzilla)\//) { + return qq#$text#; } - }); - - # Update certain links to git.mozilla.org to go to github.com instead - # https://git.mozilla.org/?p=webtools/bmo/bugzilla.git;a=blob;f=Bugzilla/WebService/Bug.pm;h=d7a1d8f9bb5fdee524f2bb342a4573a63d890f2e;hb=HEAD#l657 - push(@$regexes, { - match => qr#\b(https?://git\.mozilla\.org\S+)\b#mx, - replace => sub { - my $args = shift; - my $match = $args->{matches}->[0]; - my $uri = URI->new($match); - my $text = html_quote($match); - - # Only work on BMO and Bugzilla repos - my $repo = html_quote($uri->query_param_delete("p")) || ''; - if ($repo !~ /(webtools\/bmo|bugzilla)\//) { - return qq#$text#; - } - my $action = html_quote($uri->query_param_delete("a")) || ''; - my $file = html_quote($uri->query_param_delete("f")) || ''; - my $frag = html_quote($uri->fragment) || ''; - my $from_rev = html_quote($uri->query_param_delete("h")) || ''; - my $to_rev = html_quote($uri->query_param_delete("hb")) || ''; + my $action = html_quote($uri->query_param_delete("a")) || ''; + my $file = html_quote($uri->query_param_delete("f")) || ''; + my $frag = html_quote($uri->fragment) || ''; + my $from_rev = html_quote($uri->query_param_delete("h")) || ''; + my $to_rev = html_quote($uri->query_param_delete("hb")) || ''; - if ($frag) { - $frag =~ tr/l/L/; - $frag = "#$frag"; - } + if ($frag) { + $frag =~ tr/l/L/; + $frag = "#$frag"; + } - $to_rev = $from_rev if !$to_rev; - $to_rev = 'master' if $to_rev eq 'HEAD'; - $to_rev =~ s#refs/heads/(.*)$#$1#; + $to_rev = $from_rev if !$to_rev; + $to_rev = 'master' if $to_rev eq 'HEAD'; + $to_rev =~ s#refs/heads/(.*)$#$1#; - $repo = 'mozilla-bteam/bmo' if $repo =~ /^webtools\/bmo\/bugzilla\.git$/; - $repo = 'bugzilla/bugzilla' if $repo =~ /^bugzilla\/bugzilla\.git$/; - $repo = 'bugzilla/bugzilla.org' if $repo =~ /^www\/bugzilla\.org\.git$/; + $repo = 'mozilla-bteam/bmo' if $repo =~ /^webtools\/bmo\/bugzilla\.git$/; + $repo = 'bugzilla/bugzilla' if $repo =~ /^bugzilla\/bugzilla\.git$/; + $repo = 'bugzilla/bugzilla.org' if $repo =~ /^www\/bugzilla\.org\.git$/; - if ($action eq 'tree') { - return $to_rev eq 'HEAD' - ? qq#$text [github]# - : qq#$text [github]#; - } - if ($action eq 'blob') { - return qq#$text [github]#; - } - if ($action eq 'shortlog' || $action eq 'log') { - return qq#$text [github]#; - } - if ($action eq 'commit' || $action eq 'commitdiff') { - return qq#$text [github]#; - } - return qq#$text#; + if ($action eq 'tree') { + return $to_rev eq 'HEAD' + ? qq#$text [github]# + : qq#$text [github]#; } - }); - - # link to hg.m.o - # Note: for grouping in this regexp, always use non-capturing parentheses. - my $hgrepos = join('|', qw!(?:releases/)?comm-[\w.]+ - (?:releases/)?mozilla-[\w.]+ - (?:releases/)?mobile-[\w.]+ - tracemonkey - tamarin-[\w.]+ - camino!); - - push (@$regexes, { - match => qr/\b(($hgrepos)\s+changeset:?\s+(?:\d+:)?([0-9a-fA-F]{12}))\b/, - replace => sub { - my $args = shift; - my $text = html_quote($args->{matches}->[0]); - my $repo = html_quote($args->{matches}->[1]); - my $id = html_quote($args->{matches}->[2]); - $repo = 'integration/mozilla-inbound' if $repo eq 'mozilla-inbound'; - return qq{$text}; + if ($action eq 'blob') { + return + qq#$text [github]#; } - }); + if ($action eq 'shortlog' || $action eq 'log') { + return + qq#$text [github]#; + } + if ($action eq 'commit' || $action eq 'commitdiff') { + return qq#$text [github]#; + } + return qq#$text#; + } + } + ); + + # link to hg.m.o + # Note: for grouping in this regexp, always use non-capturing parentheses. + my $hgrepos = join( + '|', qw!(?:releases/)?comm-[\w.]+ + (?:releases/)?mozilla-[\w.]+ + (?:releases/)?mobile-[\w.]+ + tracemonkey + tamarin-[\w.]+ + camino! + ); + + push( + @$regexes, + { + match => qr/\b(($hgrepos)\s+changeset:?\s+(?:\d+:)?([0-9a-fA-F]{12}))\b/, + replace => sub { + my $args = shift; + my $text = html_quote($args->{matches}->[0]); + my $repo = html_quote($args->{matches}->[1]); + my $id = html_quote($args->{matches}->[2]); + $repo = 'integration/mozilla-inbound' if $repo eq 'mozilla-inbound'; + return qq{$text}; + } + } + ); } sub quicksearch_map { - my ($self, $args) = @_; - my $map = $args->{'map'}; + my ($self, $args) = @_; + my $map = $args->{'map'}; - foreach my $name (keys %$map) { - if ($name =~ /cf_crash_signature$/) { - $map->{'sig'} = $name; - } + foreach my $name (keys %$map) { + if ($name =~ /cf_crash_signature$/) { + $map->{'sig'} = $name; } + } } sub object_columns { - my ($self, $args) = @_; - return unless $args->{class}->isa('Bugzilla::Product'); - push @{ $args->{columns} }, qw( - default_platform_id - default_op_sys_id - security_group_id - ); + my ($self, $args) = @_; + return unless $args->{class}->isa('Bugzilla::Product'); + push @{$args->{columns}}, qw( + default_platform_id + default_op_sys_id + security_group_id + ); } sub object_update_columns { - my ($self, $args) = @_; - return unless $args->{object}->isa('Bugzilla::Product'); - push @{ $args->{columns} }, qw( - default_platform_id - default_op_sys_id - security_group_id - ); + my ($self, $args) = @_; + return unless $args->{object}->isa('Bugzilla::Product'); + push @{$args->{columns}}, qw( + default_platform_id + default_op_sys_id + security_group_id + ); } sub object_before_create { - my ($self, $args) = @_; - return unless $args->{class}->isa('Bugzilla::Product'); + my ($self, $args) = @_; + return unless $args->{class}->isa('Bugzilla::Product'); - my $cgi = Bugzilla->cgi; - my $params = $args->{params}; - foreach my $field (qw( default_platform_id default_op_sys_id security_group_id )) { - $params->{$field} = $cgi->param($field); - } + my $cgi = Bugzilla->cgi; + my $params = $args->{params}; + foreach + my $field (qw( default_platform_id default_op_sys_id security_group_id )) + { + $params->{$field} = $cgi->param($field); + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::Product'); - - my $cgi = Bugzilla->cgi; - my $params = $args->{params}; - foreach my $field (qw( default_platform_id default_op_sys_id security_group_id )) { - my $value = $cgi->param($field); - detaint_natural($value); - $object->set($field, $value); - } + my ($self, $args) = @_; + my $object = $args->{object}; + return unless $object->isa('Bugzilla::Product'); + + my $cgi = Bugzilla->cgi; + my $params = $args->{params}; + foreach + my $field (qw( default_platform_id default_op_sys_id security_group_id )) + { + my $value = $cgi->param($field); + detaint_natural($value); + $object->set($field, $value); + } } sub object_end_of_create { - my ($self, $args) = @_; - my $class = $args->{class}; - - if ($class eq 'Bugzilla::User') { - my $user = $args->{object}; - - # Log real IP addresses for auditing - Bugzilla->audit(sprintf('<%s> created user %s', remote_ip(), $user->login)); - - # Add default searches to new user's footer - my $dbh = Bugzilla->dbh; - - my $sharer = Bugzilla::User->new({ name => Bugzilla->params->{'nobody_user'} }) - or return; - my $group = Bugzilla::Group->new({ name => 'everyone' }) - or return; - - foreach my $definition (@default_named_queries) { - my ($namedquery_id) = _get_named_query($sharer->id, $group->id, $definition); - $dbh->do( - "INSERT INTO namedqueries_link_in_footer(namedquery_id,user_id) VALUES (?,?)", - undef, - $namedquery_id, $user->id - ); - } + my ($self, $args) = @_; + my $class = $args->{class}; + + if ($class eq 'Bugzilla::User') { + my $user = $args->{object}; + + # Log real IP addresses for auditing + Bugzilla->audit(sprintf('<%s> created user %s', remote_ip(), $user->login)); + + # Add default searches to new user's footer + my $dbh = Bugzilla->dbh; - } elsif ($class eq 'Bugzilla::Bug') { - # Log real IP addresses for auditing - Bugzilla->audit(sprintf('%s <%s> created bug %s', Bugzilla->user->login, remote_ip(), $args->{object}->id)); + my $sharer = Bugzilla::User->new({name => Bugzilla->params->{'nobody_user'}}) + or return; + my $group = Bugzilla::Group->new({name => 'everyone'}) or return; + + foreach my $definition (@default_named_queries) { + my ($namedquery_id) = _get_named_query($sharer->id, $group->id, $definition); + $dbh->do( + "INSERT INTO namedqueries_link_in_footer(namedquery_id,user_id) VALUES (?,?)", + undef, $namedquery_id, $user->id); } + + } + elsif ($class eq 'Bugzilla::Bug') { + + # Log real IP addresses for auditing + Bugzilla->audit(sprintf( + '%s <%s> created bug %s', + Bugzilla->user->login, remote_ip(), $args->{object}->id + )); + } } sub _bug_reporters_hw_os { - my ($self) = @_; - return $self->{ua_hw_os} if exists $self->{ua_hw_os}; - my $memcached = Bugzilla->memcached; - my $hw_os = $memcached->get({ key => 'bug.ua.' . $self->id }); - if (!$hw_os) { - (my $ua) = Bugzilla->dbh->selectrow_array( - "SELECT user_agent FROM bug_user_agent WHERE bug_id = ?", - undef, - $self->id); - $hw_os = $ua - ? [ detect_platform($ua), detect_op_sys($ua) ] - : []; - $memcached->set({ key => 'bug.ua.' . $self->id, value => $hw_os }); - } - return $self->{ua_hw_os} = $hw_os; + my ($self) = @_; + return $self->{ua_hw_os} if exists $self->{ua_hw_os}; + my $memcached = Bugzilla->memcached; + my $hw_os = $memcached->get({key => 'bug.ua.' . $self->id}); + if (!$hw_os) { + (my $ua) + = Bugzilla->dbh->selectrow_array( + "SELECT user_agent FROM bug_user_agent WHERE bug_id = ?", + undef, $self->id); + $hw_os = $ua ? [detect_platform($ua), detect_op_sys($ua)] : []; + $memcached->set({key => 'bug.ua.' . $self->id, value => $hw_os}); + } + return $self->{ua_hw_os} = $hw_os; } sub _bug_is_unassigned { - my ($self) = @_; - my $assignee = $self->assigned_to->login; - return $assignee eq Bugzilla->params->{'nobody_user'} || $assignee =~ /\.bugs$/; + my ($self) = @_; + my $assignee = $self->assigned_to->login; + return $assignee eq Bugzilla->params->{'nobody_user'} || $assignee =~ /\.bugs$/; } sub _bug_has_current_patch { - my ($self) = @_; - foreach my $attachment (@{ $self->attachments }) { - next if $attachment->isobsolete; - return 1 if $attachment->can_review; - } - return 0; + my ($self) = @_; + foreach my $attachment (@{$self->attachments}) { + next if $attachment->isobsolete; + return 1 if $attachment->can_review; + } + return 0; } sub _bug_missing_sec_approval { - my ($self) = @_; - # see https://wiki.mozilla.org/Security/Bug_Approval_Process for the rules + my ($self) = @_; - # no need to alert once a bug is closed - return 0 if $self->resolution; + # see https://wiki.mozilla.org/Security/Bug_Approval_Process for the rules - # only bugs with sec-high or sec-critical keywords need sec-approval - return 0 unless $self->has_keyword('sec-high') || $self->has_keyword('sec-critical'); + # no need to alert once a bug is closed + return 0 if $self->resolution; - # look for patches with sec-approval set to any value - foreach my $attachment (@{ $self->attachments }) { - next if $attachment->isobsolete || !$attachment->ispatch; - foreach my $flag (@{ $attachment->flags }) { - # only one patch needs sec-approval - return 0 if $flag->name eq 'sec-approval'; - } - } + # only bugs with sec-high or sec-critical keywords need sec-approval + return 0 + unless $self->has_keyword('sec-high') || $self->has_keyword('sec-critical'); - # tracking flags - require Bugzilla::Extension::TrackingFlags::Flag; - my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $self->product, - component => $self->component, - bug_id => $self->id, - is_active => 1, - WHERE => { - 'name like ?' => 'cf_status_firefox%', - }, - }); - # set flags are added after the sql query, filter those out - $flags = [ grep { $_->name =~ /^cf_status_firefox/ } @$flags ]; - return 0 unless @$flags; - - my $nightly = last_value { $_->name !~ /_esr\d+$/ } @$flags; - my $set = 0; - foreach my $flag (@$flags) { - my $value = $flag->bug_flag($self->id)->value; - next if $value eq '---'; - $set++; - # sec-approval is required if any of the current status-firefox - # tracking flags that aren't the latest are set to 'affected' - return 1 if $flag->name ne $nightly->name && $value eq 'affected'; + # look for patches with sec-approval set to any value + foreach my $attachment (@{$self->attachments}) { + next if $attachment->isobsolete || !$attachment->ispatch; + foreach my $flag (@{$attachment->flags}) { + + # only one patch needs sec-approval + return 0 if $flag->name eq 'sec-approval'; } - # sec-approval is required if no tracking flags are set - return $set == 0; + } + + # tracking flags + require Bugzilla::Extension::TrackingFlags::Flag; + my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $self->product, + component => $self->component, + bug_id => $self->id, + is_active => 1, + WHERE => {'name like ?' => 'cf_status_firefox%',}, + }); + + # set flags are added after the sql query, filter those out + $flags = [grep { $_->name =~ /^cf_status_firefox/ } @$flags]; + return 0 unless @$flags; + + my $nightly = last_value { $_->name !~ /_esr\d+$/ } @$flags; + my $set = 0; + foreach my $flag (@$flags) { + my $value = $flag->bug_flag($self->id)->value; + next if $value eq '---'; + $set++; + + # sec-approval is required if any of the current status-firefox + # tracking flags that aren't the latest are set to 'affected' + return 1 if $flag->name ne $nightly->name && $value eq 'affected'; + } + + # sec-approval is required if no tracking flags are set + return $set == 0; } sub _product_default_platform_id { $_[0]->{default_platform_id} } -sub _product_default_op_sys_id { $_[0]->{default_op_sys_id} } +sub _product_default_op_sys_id { $_[0]->{default_op_sys_id} } sub _product_default_platform { - my ($self) = @_; - if (!exists $self->{default_platform}) { - $self->{default_platform} = $self->default_platform_id - ? Bugzilla::Field::Choice - ->type('rep_platform') - ->new($_[0]->{default_platform_id}) - ->name - : undef; - } - return $self->{default_platform}; + my ($self) = @_; + if (!exists $self->{default_platform}) { + $self->{default_platform} + = $self->default_platform_id + ? Bugzilla::Field::Choice->type('rep_platform') + ->new($_[0]->{default_platform_id})->name + : undef; + } + return $self->{default_platform}; } + sub _product_default_op_sys { - my ($self) = @_; - if (!exists $self->{default_op_sys}) { - $self->{default_op_sys} = $self->default_op_sys_id - ? Bugzilla::Field::Choice - ->type('op_sys') - ->new($_[0]->{default_op_sys_id}) - ->name - : undef; - } - return $self->{default_op_sys}; + my ($self) = @_; + if (!exists $self->{default_op_sys}) { + $self->{default_op_sys} + = $self->default_op_sys_id + ? Bugzilla::Field::Choice->type('op_sys')->new($_[0]->{default_op_sys_id}) + ->name + : undef; + } + return $self->{default_op_sys}; } sub _get_named_query { - my ($sharer_id, $group_id, $definition) = @_; - my $dbh = Bugzilla->dbh; - # find existing namedquery - my ($namedquery_id) = $dbh->selectrow_array( - "SELECT id FROM namedqueries WHERE userid=? AND name=?", - undef, - $sharer_id, $definition->{name} - ); - return $namedquery_id if $namedquery_id; - # create namedquery - $dbh->do( - "INSERT INTO namedqueries(userid,name,query) VALUES (?,?,?)", - undef, - $sharer_id, $definition->{name}, $definition->{query} - ); - $namedquery_id = $dbh->bz_last_key(); - # and share it - $dbh->do( - "INSERT INTO namedquery_group_map(namedquery_id,group_id) VALUES (?,?)", - undef, - $namedquery_id, $group_id, - ); - return $namedquery_id; + my ($sharer_id, $group_id, $definition) = @_; + my $dbh = Bugzilla->dbh; + + # find existing namedquery + my ($namedquery_id) + = $dbh->selectrow_array( + "SELECT id FROM namedqueries WHERE userid=? AND name=?", + undef, $sharer_id, $definition->{name}); + return $namedquery_id if $namedquery_id; + + # create namedquery + $dbh->do("INSERT INTO namedqueries(userid,name,query) VALUES (?,?,?)", + undef, $sharer_id, $definition->{name}, $definition->{query}); + $namedquery_id = $dbh->bz_last_key(); + + # and share it + $dbh->do( + "INSERT INTO namedquery_group_map(namedquery_id,group_id) VALUES (?,?)", + undef, $namedquery_id, $group_id,); + return $namedquery_id; } sub bug_end_of_create { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - - # automatically CC users to bugs based on group & product - foreach my $group_name (keys %group_auto_cc) { - my $group_obj = Bugzilla::Group->new({ name => $group_name }); - if ($group_obj && $bug->in_group($group_obj)) { - my $ra_logins = exists $group_auto_cc{$group_name}->{$bug->product} - ? $group_auto_cc{$group_name}->{$bug->product} - : $group_auto_cc{$group_name}->{'_default'}; - foreach my $login (@$ra_logins) { - $bug->add_cc($login); - } - } - } - - # store user-agent - if (my $ua = Bugzilla->cgi->user_agent) { - trick_taint($ua); - Bugzilla->dbh->do( - "INSERT INTO bug_user_agent (bug_id, user_agent) VALUES (?, ?)", - undef, - $bug->id, $ua - ); - } + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + + # automatically CC users to bugs based on group & product + foreach my $group_name (keys %group_auto_cc) { + my $group_obj = Bugzilla::Group->new({name => $group_name}); + if ($group_obj && $bug->in_group($group_obj)) { + my $ra_logins + = exists $group_auto_cc{$group_name}->{$bug->product} + ? $group_auto_cc{$group_name}->{$bug->product} + : $group_auto_cc{$group_name}->{'_default'}; + foreach my $login (@$ra_logins) { + $bug->add_cc($login); + } + } + } + + # store user-agent + if (my $ua = Bugzilla->cgi->user_agent) { + trick_taint($ua); + Bugzilla->dbh->do( + "INSERT INTO bug_user_agent (bug_id, user_agent) VALUES (?, ?)", + undef, $bug->id, $ua); + } } sub sanitycheck_check { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - my $status = $args->{'status'}; - $status->('bmo_check_cf_visible_in_products'); - - my $products = $dbh->selectcol_arrayref('SELECT name FROM products'); - my %product = map { $_ => 1 } @$products; - my @cf_products = map { keys %$_ } values %$cf_visible_in_products; - foreach my $cf_product (@cf_products) { - $status->('bmo_check_cf_visible_in_products_missing', - { cf_product => $cf_product }, 'alert') unless $product{$cf_product}; - } + my $dbh = Bugzilla->dbh; + my $status = $args->{'status'}; + $status->('bmo_check_cf_visible_in_products'); + + my $products = $dbh->selectcol_arrayref('SELECT name FROM products'); + my %product = map { $_ => 1 } @$products; + my @cf_products = map { keys %$_ } values %$cf_visible_in_products; + foreach my $cf_product (@cf_products) { + $status->( + 'bmo_check_cf_visible_in_products_missing', + {cf_product => $cf_product}, 'alert' + ) unless $product{$cf_product}; + } } sub db_sanitize { - print "deleting reporter's user-agents...\n"; - Bugzilla->dbh->do("TRUNCATE TABLE bug_user_agent"); + print "deleting reporter's user-agents...\n"; + Bugzilla->dbh->do("TRUNCATE TABLE bug_user_agent"); } # bugs in an ASSIGNED state must be assigned to a real person # reset bugs to NEW if the assignee is nobody/.bugs$ sub object_start_of_update { - my ($self, $args) = @_; - my ($new_bug, $old_bug) = @$args{qw( object old_object )}; - return unless $new_bug->isa('Bugzilla::Bug'); - - # if either the assignee or status has changed - return unless - $old_bug->assigned_to->id != $new_bug->assigned_to->id - || $old_bug->bug_status ne $new_bug->bug_status; - - # and the bug is now ASSIGNED - return unless - $new_bug->bug_status eq 'ASSIGNED'; - - # and the assignee isn't a real person - return unless - $new_bug->assigned_to->login eq Bugzilla->params->{'nobody_user'} - || $new_bug->assigned_to->login =~ /\.bugs$/; - - # and the user can set the status to NEW - return unless - $old_bug->check_can_change_field('bug_status', $old_bug->bug_status, 'NEW'); - - # if the user is changing the assignee, silently change the bug's status to new - if ($old_bug->assigned_to->id != $new_bug->assigned_to->id) { - $new_bug->set_bug_status('NEW'); - } + my ($self, $args) = @_; + my ($new_bug, $old_bug) = @$args{qw( object old_object )}; + return unless $new_bug->isa('Bugzilla::Bug'); - # otherwise the user is trying to set the bug's status to ASSIGNED without - # assigning a real person. throw an error. - else { - ThrowUserError('bug_status_unassigned'); - } + # if either the assignee or status has changed + return + unless $old_bug->assigned_to->id != $new_bug->assigned_to->id + || $old_bug->bug_status ne $new_bug->bug_status; + + # and the bug is now ASSIGNED + return unless $new_bug->bug_status eq 'ASSIGNED'; + + # and the assignee isn't a real person + return + unless $new_bug->assigned_to->login eq Bugzilla->params->{'nobody_user'} + || $new_bug->assigned_to->login =~ /\.bugs$/; + + # and the user can set the status to NEW + return + unless $old_bug->check_can_change_field('bug_status', $old_bug->bug_status, + 'NEW'); + + # if the user is changing the assignee, silently change the bug's status to new + if ($old_bug->assigned_to->id != $new_bug->assigned_to->id) { + $new_bug->set_bug_status('NEW'); + } + + # otherwise the user is trying to set the bug's status to ASSIGNED without + # assigning a real person. throw an error. + else { + ThrowUserError('bug_status_unassigned'); + } } # detect github pull requests and reviewboard reviews, set the content-type sub attachment_process_data { - my ($self, $args) = @_; - my $attributes = $args->{attributes}; - - # must be a text attachment - return unless $attributes->{mimetype} eq 'text/plain'; - - # check the attachment size, and get attachment content if it isn't too large - my $data = $attributes->{data}; - my $url; - if (blessed($data) && blessed($data) eq 'Fh') { - # filehandle - my $size = -s $data; - return if $size > 256; - sysread($data, $url, $size); - seek($data, 0, 0); - } else { - # string - $url = $data; - } - - if (my $detected = _detect_attached_url($url)) { - $attributes->{mimetype} = $detected->{content_type}; - $attributes->{ispatch} = 0; - } + my ($self, $args) = @_; + my $attributes = $args->{attributes}; + + # must be a text attachment + return unless $attributes->{mimetype} eq 'text/plain'; + + # check the attachment size, and get attachment content if it isn't too large + my $data = $attributes->{data}; + my $url; + if (blessed($data) && blessed($data) eq 'Fh') { + + # filehandle + my $size = -s $data; + return if $size > 256; + sysread($data, $url, $size); + seek($data, 0, 0); + } + else { + # string + $url = $data; + } + + if (my $detected = _detect_attached_url($url)) { + $attributes->{mimetype} = $detected->{content_type}; + $attributes->{ispatch} = 0; + } } sub _detect_attached_url { - my ($url) = @_; - - # trim and check for the pull request url - return unless defined $url; - return if length($url) > 256; - $url = trim($url); - # ignore urls that contain unescaped characters outside of the range mentioned in RFC 3986 section 2 - return if $url =~ m<[^A-Za-z0-9._~:/?#\[\]@!\$&'()*+,;=`.%-]>; - - foreach my $key (keys %autodetect_attach_urls) { - my $regex = $autodetect_attach_urls{$key}->{regex}; - if (ref($regex) eq 'CODE') { - $regex = $regex->(); - } - if ($url =~ $regex) { - return $autodetect_attach_urls{$key}; - } + my ($url) = @_; + + # trim and check for the pull request url + return unless defined $url; + return if length($url) > 256; + $url = trim($url); + +# ignore urls that contain unescaped characters outside of the range mentioned in RFC 3986 section 2 + return if $url =~ m<[^A-Za-z0-9._~:/?#\[\]@!\$&'()*+,;=`.%-]>; + + foreach my $key (keys %autodetect_attach_urls) { + my $regex = $autodetect_attach_urls{$key}->{regex}; + if (ref($regex) eq 'CODE') { + $regex = $regex->(); + } + if ($url =~ $regex) { + return $autodetect_attach_urls{$key}; } + } - return undef; + return undef; } sub _attachment_external_redirect { - my ($self) = @_; + my ($self) = @_; - # must be our supported content-type - return undef unless - any { $self->contenttype eq $autodetect_attach_urls{$_}->{content_type} } - keys %autodetect_attach_urls; + # must be our supported content-type + return undef + unless + any { $self->contenttype eq $autodetect_attach_urls{$_}->{content_type} } + keys %autodetect_attach_urls; - # must still be a valid url - return _detect_attached_url($self->data) + # must still be a valid url + return _detect_attached_url($self->data); } sub _attachment_can_review { - my ($self) = @_; + my ($self) = @_; - return 1 if $self->ispatch; - my $external = $self->external_redirect // return; - return $external->{can_review}; + return 1 if $self->ispatch; + my $external = $self->external_redirect // return; + return $external->{can_review}; } sub _attachment_fetch_github_pr_diff { - my ($self) = @_; + my ($self) = @_; - # must be our supported content-type - return undef unless - any { $self->contenttype eq $autodetect_attach_urls{$_}->{content_type} } - keys %autodetect_attach_urls; + # must be our supported content-type + return undef + unless + any { $self->contenttype eq $autodetect_attach_urls{$_}->{content_type} } + keys %autodetect_attach_urls; - # must still be a valid url - return undef unless _detect_attached_url($self->data); + # must still be a valid url + return undef unless _detect_attached_url($self->data); - my $ua = LWP::UserAgent->new( timeout => 10 ); - if (Bugzilla->params->{proxy_url}) { - $ua->proxy('https', Bugzilla->params->{proxy_url}); - } + my $ua = LWP::UserAgent->new(timeout => 10); + if (Bugzilla->params->{proxy_url}) { + $ua->proxy('https', Bugzilla->params->{proxy_url}); + } - my $pr_diff = $self->data . ".diff"; - my $response = $ua->get($pr_diff); - if ($response->is_error) { - warn "Github fetch error: $pr_diff, " . $response->status_line; - return "Error retrieving Github pull request diff for " . $self->data; - } - return $response->decoded_content; + my $pr_diff = $self->data . ".diff"; + my $response = $ua->get($pr_diff); + if ($response->is_error) { + warn "Github fetch error: $pr_diff, " . $response->status_line; + return "Error retrieving Github pull request diff for " . $self->data; + } + return $response->decoded_content; } # redirect automatically to github urls sub attachment_view { - my ($self, $args) = @_; - my $attachment = $args->{attachment}; - my $cgi = Bugzilla->cgi; + my ($self, $args) = @_; + my $attachment = $args->{attachment}; + my $cgi = Bugzilla->cgi; - # don't redirect if the content-type is specified explicitly - return if defined $cgi->param('content_type'); + # don't redirect if the content-type is specified explicitly + return if defined $cgi->param('content_type'); - # must be a valid redirection url - return unless defined $attachment->external_redirect; + # must be a valid redirection url + return unless defined $attachment->external_redirect; - # redirect - print $cgi->redirect(trim($attachment->data)); - exit; + # redirect + print $cgi->redirect(trim($attachment->data)); + exit; } sub install_before_final_checks { - my ($self, $args) = @_; - - # Add product chooser setting - add_setting({ - name => 'product_chooser', - options => ['pretty_product_chooser', 'full_product_chooser'], - default => 'pretty_product_chooser', - category => 'User Interface' - }); - - # Add option to inject x-bugzilla headers into the message body to work - # around gmail filtering limitations - add_setting({ - name => 'headers_in_body', - options => ['on', 'off'], - default => 'off', - category => 'Email Notifications' - }); - - # Migrate from 'gmail_threading' setting to 'bugmail_new_prefix' - my $dbh = Bugzilla->dbh; - if ($dbh->selectrow_array("SELECT 1 FROM setting WHERE name='gmail_threading'")) { - $dbh->bz_start_transaction(); - $dbh->do("UPDATE profile_setting + my ($self, $args) = @_; + + # Add product chooser setting + add_setting({ + name => 'product_chooser', + options => ['pretty_product_chooser', 'full_product_chooser'], + default => 'pretty_product_chooser', + category => 'User Interface' + }); + + # Add option to inject x-bugzilla headers into the message body to work + # around gmail filtering limitations + add_setting({ + name => 'headers_in_body', + options => ['on', 'off'], + default => 'off', + category => 'Email Notifications' + }); + + # Migrate from 'gmail_threading' setting to 'bugmail_new_prefix' + my $dbh = Bugzilla->dbh; + if ($dbh->selectrow_array("SELECT 1 FROM setting WHERE name='gmail_threading'")) + { + $dbh->bz_start_transaction(); + $dbh->do( + "UPDATE profile_setting SET setting_value='on-temp' - WHERE setting_name='gmail_threading' AND setting_value='Off'"); - $dbh->do("UPDATE profile_setting + WHERE setting_name='gmail_threading' AND setting_value='Off'" + ); + $dbh->do( + "UPDATE profile_setting SET setting_value='off' - WHERE setting_name='gmail_threading' AND setting_value='On'"); - $dbh->do("UPDATE profile_setting + WHERE setting_name='gmail_threading' AND setting_value='On'" + ); + $dbh->do( + "UPDATE profile_setting SET setting_value='on' - WHERE setting_name='gmail_threading' AND setting_value='on-temp'"); - $dbh->do("UPDATE profile_setting + WHERE setting_name='gmail_threading' AND setting_value='on-temp'" + ); + $dbh->do( + "UPDATE profile_setting SET setting_name='bugmail_new_prefix' - WHERE setting_name='gmail_threading'"); - $dbh->do("DELETE FROM setting WHERE name='gmail_threading'"); - $dbh->bz_commit_transaction(); - } + WHERE setting_name='gmail_threading'" + ); + $dbh->do("DELETE FROM setting WHERE name='gmail_threading'"); + $dbh->bz_commit_transaction(); + } } sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{schema}->{bug_user_agent} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE', - }, - }, - user_agent => { - TYPE => 'MEDIUMTEXT', - NOTNULL => 1, - }, - ], - INDEXES => [ - bug_user_agent_idx => { - FIELDS => [ 'bug_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{schema}->{job_last_run} = { - FIELDS => [ - id => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - name => { - TYPE => 'VARCHAR(100)', - NOTNULL => 1, - }, - last_run => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - ], - INDEXES => [ - job_last_run_name_idx => { - FIELDS => [ 'name' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{schema}->{secbugs_BugHistory} = { - FIELDS => [ - bugid => { TYPE => 'BIGINT', NOTNULL => 1 }, - changetime => { TYPE => 'NATIVE_DATETIME' }, - fieldname => { TYPE => 'VARCHAR(32)', NOTNULL => 1 }, - new => { TYPE => 'VARCHAR(255)' }, - old => { TYPE => 'VARCHAR(255)' }, - ], - }; - - $args->{schema}->{secbugs_Bugs} = { - FIELDS => [ - bugid => { TYPE => 'BIGINT', NOTNULL => 1, PRIMARYKEY => 1 }, - opendate => { TYPE => 'NATIVE_DATETIME' }, - closedate => { TYPE => 'NATIVE_DATETIME', NOTNULL => 1 }, - severity => { TYPE => 'VARCHAR(16)' }, - summary => { TYPE => 'VARCHAR(255)' }, - updated => { TYPE => 'NATIVE_DATETIME' }, - ], - }; - - $args->{schema}->{secbugs_Details} = { - FIELDS => [ - did => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - sid => { - TYPE => 'INT4', - }, - product => { - TYPE => 'VARCHAR(255)', - }, - component => { - TYPE => 'VARCHAR(255)', - }, - count => { TYPE => 'INT4' }, - bug_list => { TYPE => 'TEXT' }, - date => { TYPE => 'NATIVE_DATETIME' }, - avg_age_days => { TYPE => 'INT4' }, - med_age_days => { TYPE => 'INT4' }, - ] - }; - - $args->{schema}->{secbugs_Stats} = { - FIELDS => [ - sid => { TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1 }, - category => { TYPE => 'VARCHAR(32)' }, - count => { TYPE => 'INT4' }, - date => { TYPE => 'NATIVE_DATETIME' }, - ] - }; + my ($self, $args) = @_; + $args->{schema}->{bug_user_agent} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}, + }, + user_agent => {TYPE => 'MEDIUMTEXT', NOTNULL => 1,}, + ], + INDEXES => [bug_user_agent_idx => {FIELDS => ['bug_id'], TYPE => 'UNIQUE',},], + }; + $args->{schema}->{job_last_run} = { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + name => {TYPE => 'VARCHAR(100)', NOTNULL => 1,}, + last_run => {TYPE => 'DATETIME', NOTNULL => 1,}, + ], + INDEXES => [job_last_run_name_idx => {FIELDS => ['name'], TYPE => 'UNIQUE',},], + }; + $args->{schema}->{secbugs_BugHistory} = { + FIELDS => [ + bugid => {TYPE => 'BIGINT', NOTNULL => 1}, + changetime => {TYPE => 'NATIVE_DATETIME'}, + fieldname => {TYPE => 'VARCHAR(32)', NOTNULL => 1}, + new => {TYPE => 'VARCHAR(255)'}, + old => {TYPE => 'VARCHAR(255)'}, + ], + }; + + $args->{schema}->{secbugs_Bugs} = { + FIELDS => [ + bugid => {TYPE => 'BIGINT', NOTNULL => 1, PRIMARYKEY => 1}, + opendate => {TYPE => 'NATIVE_DATETIME'}, + closedate => {TYPE => 'NATIVE_DATETIME', NOTNULL => 1}, + severity => {TYPE => 'VARCHAR(16)'}, + summary => {TYPE => 'VARCHAR(255)'}, + updated => {TYPE => 'NATIVE_DATETIME'}, + ], + }; + + $args->{schema}->{secbugs_Details} = { + FIELDS => [ + did => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + sid => {TYPE => 'INT4',}, + product => {TYPE => 'VARCHAR(255)',}, + component => {TYPE => 'VARCHAR(255)',}, + count => {TYPE => 'INT4'}, + bug_list => {TYPE => 'TEXT'}, + date => {TYPE => 'NATIVE_DATETIME'}, + avg_age_days => {TYPE => 'INT4'}, + med_age_days => {TYPE => 'INT4'}, + ] + }; + + $args->{schema}->{secbugs_Stats} = { + FIELDS => [ + sid => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + category => {TYPE => 'VARCHAR(32)'}, + count => {TYPE => 'INT4'}, + date => {TYPE => 'NATIVE_DATETIME'}, + ] + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + + # per-product hw/os defaults + my $op_sys_default = _field_value('op_sys', 'Unspecified', 50); + $dbh->bz_add_column( + 'products', + 'default_op_sys_id' => { + TYPE => 'INT2', + DEFAULT => $op_sys_default->id, + REFERENCES => {TABLE => 'op_sys', COLUMN => 'id', DELETE => 'SET NULL',}, + } + ); + my $platform_default = _field_value('rep_platform', 'Unspecified', 50); + $dbh->bz_add_column( + 'products', + 'default_platform_id' => { + TYPE => 'INT2', + DEFAULT => $platform_default->id, + REFERENCES => {TABLE => 'rep_platform', COLUMN => 'id', DELETE => 'SET NULL',}, + } + ); + + # Migrate old is_active stuff to new patch (is in core in 4.2), The old + # column name was 'is_active', the new one is 'isactive' (no underscore). + if ($dbh->bz_column_info('milestones', 'is_active')) { + $dbh->do("UPDATE milestones SET isactive = 0 WHERE is_active = 0;"); + $dbh->bz_drop_column('milestones', 'is_active'); + $dbh->bz_drop_column('milestones', 'is_searchable'); + } + + # remove tables from the old TryAutoLand extension + $dbh->bz_drop_table('autoland_branches'); + $dbh->bz_drop_table('autoland_attachments'); + + unless (Bugzilla::Field->new({name => 'cf_rank'})) { + Bugzilla::Field->create({ + name => 'cf_rank', + description => 'Rank', + type => FIELD_TYPE_INTEGER, + mailhead => 0, + enter_bug => 0, + obsolete => 0, + custom => 1, + buglist => 1, + }); + } + unless (Bugzilla::Field->new({name => 'cf_crash_signature'})) { + Bugzilla::Field->create({ + name => 'cf_crash_signature', + description => 'Crash Signature', + type => FIELD_TYPE_TEXTAREA, + mailhead => 0, + enter_bug => 1, + obsolete => 0, + custom => 1, + buglist => 0, + }); + } - # per-product hw/os defaults - my $op_sys_default = _field_value('op_sys', 'Unspecified', 50); - $dbh->bz_add_column( - 'products', - 'default_op_sys_id' => { - TYPE => 'INT2', - DEFAULT => $op_sys_default->id, - REFERENCES => { - TABLE => 'op_sys', - COLUMN => 'id', - DELETE => 'SET NULL', - }, - } - ); - my $platform_default = _field_value('rep_platform', 'Unspecified', 50); + # Add default security group id column + if (!$dbh->bz_column_info('products', 'security_group_id')) { $dbh->bz_add_column( - 'products', - 'default_platform_id' => { - TYPE => 'INT2', - DEFAULT => $platform_default->id, - REFERENCES => { - TABLE => 'rep_platform', - COLUMN => 'id', - DELETE => 'SET NULL', - }, - } + 'products', + 'security_group_id' => { + TYPE => 'INT3', + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL',}, + } ); - # Migrate old is_active stuff to new patch (is in core in 4.2), The old - # column name was 'is_active', the new one is 'isactive' (no underscore). - if ($dbh->bz_column_info('milestones', 'is_active')) { - $dbh->do("UPDATE milestones SET isactive = 0 WHERE is_active = 0;"); - $dbh->bz_drop_column('milestones', 'is_active'); - $dbh->bz_drop_column('milestones', 'is_searchable'); - } - - # remove tables from the old TryAutoLand extension - $dbh->bz_drop_table('autoland_branches'); - $dbh->bz_drop_table('autoland_attachments'); - - unless (Bugzilla::Field->new({ name => 'cf_rank' })) { - Bugzilla::Field->create({ - name => 'cf_rank', - description => 'Rank', - type => FIELD_TYPE_INTEGER, - mailhead => 0, - enter_bug => 0, - obsolete => 0, - custom => 1, - buglist => 1, - }); - } - unless (Bugzilla::Field->new({ name => 'cf_crash_signature' })) { - Bugzilla::Field->create({ - name => 'cf_crash_signature', - description => 'Crash Signature', - type => FIELD_TYPE_TEXTAREA, - mailhead => 0, - enter_bug => 1, - obsolete => 0, - custom => 1, - buglist => 0, - }); - } - - # Add default security group id column - if (!$dbh->bz_column_info('products', 'security_group_id')) { - $dbh->bz_add_column( - 'products', - 'security_group_id' => { - TYPE => 'INT3', - REFERENCES => { - TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL', - }, - } - ); - - # if there are no groups, then we're creating a database from scratch - # and there's nothing to migrate - my ($group_count) = $dbh->selectrow_array("SELECT COUNT(*) FROM groups"); - if ($group_count) { - # Migrate old product_sec_group mappings from the time this change was made - my %product_sec_groups = ( - "addons.mozilla.org" => 'client-services-security', - "Air Mozilla" => 'mozilla-employee-confidential', - "Android Background Services" => 'cloud-services-security', - "Audio/Visual Infrastructure" => 'mozilla-employee-confidential', - "AUS" => 'client-services-security', - "Bugzilla" => 'bugzilla-security', - "bugzilla.mozilla.org" => 'bugzilla-security', - "Cloud Services" => 'cloud-services-security', - "Community Tools" => 'websites-security', - "Data & BI Services Team" => 'metrics-private', - "Developer Documentation" => 'websites-security', - "Developer Ecosystem" => 'client-services-security', - "Finance" => 'finance', - "Firefox Friends" => 'mozilla-employee-confidential', - "Firefox Health Report" => 'cloud-services-security', - "Infrastructure & Operations" => 'mozilla-employee-confidential', - "Input" => 'websites-security', - "Intellego" => 'intellego-team', - "Internet Public Policy" => 'mozilla-employee-confidential', - "L20n" => 'l20n-security', - "Legal" => 'legal', - "Marketing" => 'marketing-private', - "Marketplace" => 'client-services-security', - "Mozilla Communities" => 'mozilla-communities-security', - "Mozilla Corporation" => 'mozilla-employee-confidential', - "Mozilla Developer Network" => 'websites-security', - "Mozilla Foundation" => 'mozilla-employee-confidential', - "Mozilla Foundation Operations" => 'mozilla-foundation-operations', - "Mozilla Grants" => 'grants', - "mozillaignite" => 'websites-security', - "Mozilla Messaging" => 'mozilla-messaging-confidential', - "Mozilla Metrics" => 'metrics-private', - "mozilla.org" => 'mozilla-employee-confidential', - "Mozilla PR" => 'pr-private', - "Mozilla QA" => 'mozilla-employee-confidential', - "Mozilla Reps" => 'mozilla-reps', - "Popcorn" => 'websites-security', - "Privacy" => 'privacy', - "quality.mozilla.org" => 'websites-security', - "Recruiting" => 'hr', - "Release Engineering" => 'mozilla-employee-confidential', - "Snippets" => 'websites-security', - "Socorro" => 'client-services-security', - "support.mozillamessaging.com" => 'websites-security', - "support.mozilla.org" => 'websites-security', - "Talkback" => 'talkback-private', - "Tamarin" => 'tamarin-security', - "Taskcluster" => 'taskcluster-security', - "Testopia" => 'bugzilla-security', - "Tree Management" => 'mozilla-employee-confidential', - "Web Apps" => 'client-services-security', - "Webmaker" => 'websites-security', - "Websites" => 'websites-security', - "Webtools" => 'webtools-security', - "www.mozilla.org" => 'websites-security', - ); - # 1. Set all to core-security by default - my $core_sec_group = Bugzilla::Group->new({ name => Bugzilla->params->{insidergroup} }); - $dbh->do("UPDATE products SET security_group_id = ?", undef, $core_sec_group->id); - # 2. Update the ones that have explicit security groups - foreach my $prod_name (keys %product_sec_groups) { - my $group_name = $product_sec_groups{$prod_name}; - next if $group_name eq Bugzilla->params->{insidergroup}; # already done - my $group = Bugzilla::Group->new({ name => $group_name, cache => 1 }); - if (!$group) { - warn "Security group $group_name not found. Using insider group instead.\n"; - next; - } - $dbh->do("UPDATE products SET security_group_id = ? WHERE name = ?", undef, $group->id, $prod_name); - } + # if there are no groups, then we're creating a database from scratch + # and there's nothing to migrate + my ($group_count) = $dbh->selectrow_array("SELECT COUNT(*) FROM groups"); + if ($group_count) { + + # Migrate old product_sec_group mappings from the time this change was made + my %product_sec_groups = ( + "addons.mozilla.org" => 'client-services-security', + "Air Mozilla" => 'mozilla-employee-confidential', + "Android Background Services" => 'cloud-services-security', + "Audio/Visual Infrastructure" => 'mozilla-employee-confidential', + "AUS" => 'client-services-security', + "Bugzilla" => 'bugzilla-security', + "bugzilla.mozilla.org" => 'bugzilla-security', + "Cloud Services" => 'cloud-services-security', + "Community Tools" => 'websites-security', + "Data & BI Services Team" => 'metrics-private', + "Developer Documentation" => 'websites-security', + "Developer Ecosystem" => 'client-services-security', + "Finance" => 'finance', + "Firefox Friends" => 'mozilla-employee-confidential', + "Firefox Health Report" => 'cloud-services-security', + "Infrastructure & Operations" => 'mozilla-employee-confidential', + "Input" => 'websites-security', + "Intellego" => 'intellego-team', + "Internet Public Policy" => 'mozilla-employee-confidential', + "L20n" => 'l20n-security', + "Legal" => 'legal', + "Marketing" => 'marketing-private', + "Marketplace" => 'client-services-security', + "Mozilla Communities" => 'mozilla-communities-security', + "Mozilla Corporation" => 'mozilla-employee-confidential', + "Mozilla Developer Network" => 'websites-security', + "Mozilla Foundation" => 'mozilla-employee-confidential', + "Mozilla Foundation Operations" => 'mozilla-foundation-operations', + "Mozilla Grants" => 'grants', + "mozillaignite" => 'websites-security', + "Mozilla Messaging" => 'mozilla-messaging-confidential', + "Mozilla Metrics" => 'metrics-private', + "mozilla.org" => 'mozilla-employee-confidential', + "Mozilla PR" => 'pr-private', + "Mozilla QA" => 'mozilla-employee-confidential', + "Mozilla Reps" => 'mozilla-reps', + "Popcorn" => 'websites-security', + "Privacy" => 'privacy', + "quality.mozilla.org" => 'websites-security', + "Recruiting" => 'hr', + "Release Engineering" => 'mozilla-employee-confidential', + "Snippets" => 'websites-security', + "Socorro" => 'client-services-security', + "support.mozillamessaging.com" => 'websites-security', + "support.mozilla.org" => 'websites-security', + "Talkback" => 'talkback-private', + "Tamarin" => 'tamarin-security', + "Taskcluster" => 'taskcluster-security', + "Testopia" => 'bugzilla-security', + "Tree Management" => 'mozilla-employee-confidential', + "Web Apps" => 'client-services-security', + "Webmaker" => 'websites-security', + "Websites" => 'websites-security', + "Webtools" => 'webtools-security', + "www.mozilla.org" => 'websites-security', + ); + + # 1. Set all to core-security by default + my $core_sec_group + = Bugzilla::Group->new({name => Bugzilla->params->{insidergroup}}); + $dbh->do("UPDATE products SET security_group_id = ?", + undef, $core_sec_group->id); + + # 2. Update the ones that have explicit security groups + foreach my $prod_name (keys %product_sec_groups) { + my $group_name = $product_sec_groups{$prod_name}; + next if $group_name eq Bugzilla->params->{insidergroup}; # already done + my $group = Bugzilla::Group->new({name => $group_name, cache => 1}); + if (!$group) { + warn "Security group $group_name not found. Using insider group instead.\n"; + next; } + $dbh->do("UPDATE products SET security_group_id = ? WHERE name = ?", + undef, $group->id, $prod_name); + } } + } } # return the Bugzilla::Field::Choice object for the specified field and value. # if the value doesn't exist it will be created. sub _field_value { - my ($field_name, $value_name, $sort_key) = @_; - my $field = Bugzilla::Field->check({ name => $field_name }); - my $existing = Bugzilla::Field::Choice->type($field)->match({ value => $value_name }); - return $existing->[0] if $existing && @$existing; - return Bugzilla::Field::Choice->type($field)->create({ - value => $value_name, - sortkey => $sort_key, - isactive => 1, - }); + my ($field_name, $value_name, $sort_key) = @_; + my $field = Bugzilla::Field->check({name => $field_name}); + my $existing + = Bugzilla::Field::Choice->type($field)->match({value => $value_name}); + return $existing->[0] if $existing && @$existing; + return Bugzilla::Field::Choice->type($field) + ->create({value => $value_name, sortkey => $sort_key, isactive => 1,}); } sub _last_closed_date { - my ($self) = @_; - my $dbh = Bugzilla->dbh; + my ($self) = @_; + my $dbh = Bugzilla->dbh; - return $self->{'last_closed_date'} if defined $self->{'last_closed_date'}; + return $self->{'last_closed_date'} if defined $self->{'last_closed_date'}; - my $closed_statuses = "'" . join("','", map { $_->name } closed_bug_statuses()) . "'"; - my $status_field_id = get_field_id('bug_status'); + my $closed_statuses + = "'" . join("','", map { $_->name } closed_bug_statuses()) . "'"; + my $status_field_id = get_field_id('bug_status'); - $self->{'last_closed_date'} = $dbh->selectrow_array(" + $self->{'last_closed_date'} = $dbh->selectrow_array(" SELECT bugs_activity.bug_when FROM bugs_activity WHERE bugs_activity.fieldid = ? AND bugs_activity.added IN ($closed_statuses) AND bugs_activity.bug_id = ? - ORDER BY bugs_activity.bug_when DESC " . $dbh->sql_limit(1), - undef, $status_field_id, $self->id - ); + ORDER BY bugs_activity.bug_when DESC " . $dbh->sql_limit(1), undef, + $status_field_id, $self->id); - return $self->{'last_closed_date'}; + return $self->{'last_closed_date'}; } sub field_end_of_create { - my ($self, $args) = @_; - my $field = $args->{'field'}; - - # Create an IT bug so Mozilla's DBAs so they can update the grants for metrics - - if (Bugzilla->localconfig->{'urlbase'} ne 'https://bugzilla.mozilla.org/' - && Bugzilla->localconfig->{'urlbase'} ne 'https://bugzilla.allizom.org/') - { - return; - } - - my $name = $field->name; - - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - Bugzilla->set_user(Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} })); - print "Creating IT permission grant bug for new field '$name'..."; - } - - my $bug_data = { - short_desc => "Custom field '$name' added to bugzilla.mozilla.org", - product => 'Data & BI Services Team', - component => 'Database Operations', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - version => 'other', - }; - - my $comment = <{'field'}; + + # Create an IT bug so Mozilla's DBAs so they can update the grants for metrics + + if ( Bugzilla->localconfig->{'urlbase'} ne 'https://bugzilla.mozilla.org/' + && Bugzilla->localconfig->{'urlbase'} ne 'https://bugzilla.allizom.org/') + { + return; + } + + my $name = $field->name; + + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + Bugzilla->set_user(Bugzilla::User->check( + {name => Bugzilla->params->{'nobody_user'}})); + print "Creating IT permission grant bug for new field '$name'..."; + } + + my $bug_data = { + short_desc => "Custom field '$name' added to bugzilla.mozilla.org", + product => 'Data & BI Services Team', + component => 'Database Operations', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + version => 'other', + }; + + my $comment = <type == FIELD_TYPE_SINGLE_SELECT - || $field->type == FIELD_TYPE_MULTI_SELECT) { - $comment .= <type == FIELD_TYPE_SINGLE_SELECT + || $field->type == FIELD_TYPE_MULTI_SELECT) + { + $comment .= <type == FIELD_TYPE_MULTI_SELECT) { - $comment .= <type == FIELD_TYPE_MULTI_SELECT) { + $comment .= <type != FIELD_TYPE_MULTI_SELECT) { - $comment .= <type != FIELD_TYPE_MULTI_SELECT) { + $comment .= <{'comment'} = $comment; + $bug_data->{'comment'} = $comment; - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); - my $new_bug = eval { Bugzilla::Bug->create($bug_data) }; + my $new_bug = eval { Bugzilla::Bug->create($bug_data) }; - my $error = $@; - undef $@; - Bugzilla->error_mode($old_error_mode); + my $error = $@; + undef $@; + Bugzilla->error_mode($old_error_mode); - if ($error || !($new_bug && $new_bug->{'bug_id'})) { - warn "Error creating IT bug for new field $name: $error"; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "\nError: $error\n"; - } + if ($error || !($new_bug && $new_bug->{'bug_id'})) { + warn "Error creating IT bug for new field $name: $error"; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "\nError: $error\n"; } - else { - Bugzilla::BugMail::Send($new_bug->id, { changer => Bugzilla->user }); - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { - print "bug " . $new_bug->id . " created.\n"; - } + } + else { + Bugzilla::BugMail::Send($new_bug->id, {changer => Bugzilla->user}); + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE) { + print "bug " . $new_bug->id . " created.\n"; } + } } sub webservice { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{BMO} = "Bugzilla::Extension::BMO::WebService"; + my $dispatch = $args->{dispatch}; + $dispatch->{BMO} = "Bugzilla::Extension::BMO::WebService"; } sub psgi_builder { - my ($self, $args) = @_; - my $mount = $args->{mount}; - - my $ses_index = Plack::Builder::builder(sub { - my $auth_user = Bugzilla->localconfig->{ses_username}; - my $auth_pass = Bugzilla->localconfig->{ses_password}; - Plack::Builder::enable("Auth::Basic", authenticator => sub { - my ($username, $password, $env) = @_; - return ( $auth_user - && $auth_pass - && $username - && $password - && $username eq $auth_user - && $password eq $auth_pass ); - }); - compile_cgi("ses/index.cgi"); - }); + my ($self, $args) = @_; + my $mount = $args->{mount}; + + my $ses_index = Plack::Builder::builder(sub { + my $auth_user = Bugzilla->localconfig->{ses_username}; + my $auth_pass = Bugzilla->localconfig->{ses_password}; + Plack::Builder::enable( + "Auth::Basic", + authenticator => sub { + my ($username, $password, $env) = @_; + return ($auth_user + && $auth_pass + && $username + && $password + && $username eq $auth_user + && $password eq $auth_pass); + } + ); + compile_cgi("ses/index.cgi"); + }); - $mount->{'ses/index.cgi'} = $ses_index; + $mount->{'ses/index.cgi'} = $ses_index; } our $search_content_matches; + BEGIN { - $search_content_matches = \&Bugzilla::Search::_content_matches; + $search_content_matches = \&Bugzilla::Search::_content_matches; } sub search_operator_field_override { - my ($self, $args) = @_; - my $search = $args->{'search'}; - my $operators = $args->{'operators'}; - - my $cgi = Bugzilla->cgi; - my @comments = $cgi->param('comments'); - my $exclude_comments = scalar(@comments) && !grep { $_ eq '1' } @comments; - - if ($cgi->param('query_format') - && $cgi->param('query_format') eq 'specific' - && $exclude_comments - ) { - # use the non-comment operator - $operators->{'content'}->{matches} = \&_short_desc_matches; - $operators->{'content'}->{notmatches} = \&_short_desc_matches; - - } else { - # restore default content operator - $operators->{'content'}->{matches} = $search_content_matches; - $operators->{'content'}->{notmatches} = $search_content_matches; - } + my ($self, $args) = @_; + my $search = $args->{'search'}; + my $operators = $args->{'operators'}; + + my $cgi = Bugzilla->cgi; + my @comments = $cgi->param('comments'); + my $exclude_comments = scalar(@comments) && !grep { $_ eq '1' } @comments; + + if ( $cgi->param('query_format') + && $cgi->param('query_format') eq 'specific' + && $exclude_comments) + { + # use the non-comment operator + $operators->{'content'}->{matches} = \&_short_desc_matches; + $operators->{'content'}->{notmatches} = \&_short_desc_matches; + + } + else { + # restore default content operator + $operators->{'content'}->{matches} = $search_content_matches; + $operators->{'content'}->{notmatches} = $search_content_matches; + } } sub _short_desc_matches { - # copy of Bugzilla::Search::_content_matches with comment searching removed - my ($self, $args) = @_; - my ($chart_id, $joins, $fields, $operator, $value) = - @$args{qw(chart_id joins fields operator value)}; - my $dbh = Bugzilla->dbh; + # copy of Bugzilla::Search::_content_matches with comment searching removed - # Add the fulltext table to the query so we can search on it. - my $table = "bugs_fulltext_$chart_id"; - push(@$joins, { table => 'bugs_fulltext', as => $table }); + my ($self, $args) = @_; + my ($chart_id, $joins, $fields, $operator, $value) + = @$args{qw(chart_id joins fields operator value)}; + my $dbh = Bugzilla->dbh; - # Create search terms to add to the SELECT and WHERE clauses. - my ($term, $rterm) = - $dbh->sql_fulltext_search("$table.short_desc", $value, 2); - $rterm = $term if !$rterm; + # Add the fulltext table to the query so we can search on it. + my $table = "bugs_fulltext_$chart_id"; + push(@$joins, {table => 'bugs_fulltext', as => $table}); - # The term to use in the WHERE clause. - if ($operator =~ /not/i) { - $term = "NOT($term)"; - } - $args->{term} = $term; + # Create search terms to add to the SELECT and WHERE clauses. + my ($term, $rterm) = $dbh->sql_fulltext_search("$table.short_desc", $value, 2); + $rterm = $term if !$rterm; + + # The term to use in the WHERE clause. + if ($operator =~ /not/i) { + $term = "NOT($term)"; + } + $args->{term} = $term; + + my $current = $self->COLUMNS->{'relevance'}->{name}; + $current = $current ? "$current + " : ''; - my $current = $self->COLUMNS->{'relevance'}->{name}; - $current = $current ? "$current + " : ''; - # For NOT searches, we just add 0 to the relevance. - my $select_term = $operator =~ /not/ ? 0 : "($current$rterm)"; - $self->COLUMNS->{'relevance'}->{name} = $select_term; + # For NOT searches, we just add 0 to the relevance. + my $select_term = $operator =~ /not/ ? 0 : "($current$rterm)"; + $self->COLUMNS->{'relevance'}->{name} = $select_term; } sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{email}; + my ($self, $args) = @_; + my $email = $args->{email}; - _log_sent_email($email); + _log_sent_email($email); - # $bug->mentors is added by the Review extension - if (Bugzilla::Bug->can('mentors')) { - _add_mentors_header($email); - } + # $bug->mentors is added by the Review extension + if (Bugzilla::Bug->can('mentors')) { + _add_mentors_header($email); + } - # insert x-bugzilla headers into the body - _inject_headers_into_body($email); + # insert x-bugzilla headers into the body + _inject_headers_into_body($email); } # Log a summary of bugmail sent to the syslog, for auditing and monitoring sub _log_sent_email { - my $email = shift; + my $email = shift; - my $recipient = $email->header('to'); - return unless $recipient; + my $recipient = $email->header('to'); + return unless $recipient; - my $subject = $email->header('Subject'); + my $subject = $email->header('Subject'); - my $bug_id = $email->header('X-Bugzilla-ID'); - if (!$bug_id && $subject =~ /[\[\(]Bug (\d+)/i) { - $bug_id = $1; - } - $bug_id = $bug_id ? "bug-$bug_id" : '-'; - - my $message_type; - my $type = $email->header('X-Bugzilla-Type'); - my $reason = $email->header('X-Bugzilla-Reason'); - if ($type eq 'whine' || $type eq 'request' || $type eq 'admin') { - $message_type = $type; - } elsif ($reason && $reason ne 'None') { - $message_type = $reason; - } else { - $message_type = $email->header('X-Bugzilla-Watch-Reason'); - } - $message_type ||= $type || '?'; + my $bug_id = $email->header('X-Bugzilla-ID'); + if (!$bug_id && $subject =~ /[\[\(]Bug (\d+)/i) { + $bug_id = $1; + } + $bug_id = $bug_id ? "bug-$bug_id" : '-'; - $subject =~ s/[\[\(]Bug \d+[\]\)]\s*//; + my $message_type; + my $type = $email->header('X-Bugzilla-Type'); + my $reason = $email->header('X-Bugzilla-Reason'); + if ($type eq 'whine' || $type eq 'request' || $type eq 'admin') { + $message_type = $type; + } + elsif ($reason && $reason ne 'None') { + $message_type = $reason; + } + else { + $message_type = $email->header('X-Bugzilla-Watch-Reason'); + } + $message_type ||= $type || '?'; - _syslog("[bugmail] $recipient ($message_type) $bug_id $subject"); + $subject =~ s/[\[\(]Bug \d+[\]\)]\s*//; + + _syslog("[bugmail] $recipient ($message_type) $bug_id $subject"); } # Add X-Bugzilla-Mentors field to bugmail sub _add_mentors_header { - my $email = shift; - return unless my $bug_id = $email->header('X-Bugzilla-ID'); - return unless my $bug = Bugzilla::Bug->new({ id => $bug_id, cache => 1 }); - return unless my $mentors = $bug->mentors; - return unless @$mentors; - $email->header_set('X-Bugzilla-Mentors', join(', ', map { $_->login } @$mentors)); + my $email = shift; + return unless my $bug_id = $email->header('X-Bugzilla-ID'); + return unless my $bug = Bugzilla::Bug->new({id => $bug_id, cache => 1}); + return unless my $mentors = $bug->mentors; + return unless @$mentors; + $email->header_set('X-Bugzilla-Mentors', + join(', ', map { $_->login } @$mentors)); } sub _inject_headers_into_body { - my $email = shift; - my $replacement = ''; - - my $recipient = Bugzilla::User->new({ name => $email->header('To'), cache => 1 }); - if ($recipient - && $recipient->settings->{headers_in_body}->{value} eq 'on') - { - my @headers; - my $it = natatime(2, $email->header_pairs); - while (my ($name, $value) = $it->()) { - next unless $name =~ /^X-Bugzilla-(.+)/; - if ($name eq 'X-Bugzilla-Flags' || $name eq 'X-Bugzilla-Changed-Field-Names') { - # these are multi-value fields, split on space - foreach my $v (split(/\s+/, $value)) { - push @headers, "$name: $v"; - } - } - elsif ($name eq 'X-Bugzilla-Changed-Fields') { - # cannot split on space for this field, because field names contain - # spaces. instead work from a list of field names. - my @fields = - map { $_->description } - @{ Bugzilla->fields }; - # these aren't real fields, but exist in the headers - push @fields, ('Comment Created', 'Attachment Created'); - @fields = - sort { length($b) <=> length($a) } - @fields; - while ($value ne '') { - foreach my $field (@fields) { - if ($value eq $field) { - push @headers, "$name: $field"; - $value = ''; - last; - } - if (substr($value, 0, length($field) + 1) eq $field . ' ') { - push @headers, "$name: $field"; - $value = substr($value, length($field) + 1); - last; - } - } - } + my $email = shift; + my $replacement = ''; + + my $recipient = Bugzilla::User->new({name => $email->header('To'), cache => 1}); + if ($recipient && $recipient->settings->{headers_in_body}->{value} eq 'on') { + my @headers; + my $it = natatime(2, $email->header_pairs); + while (my ($name, $value) = $it->()) { + next unless $name =~ /^X-Bugzilla-(.+)/; + if ($name eq 'X-Bugzilla-Flags' || $name eq 'X-Bugzilla-Changed-Field-Names') { + + # these are multi-value fields, split on space + foreach my $v (split(/\s+/, $value)) { + push @headers, "$name: $v"; + } + } + elsif ($name eq 'X-Bugzilla-Changed-Fields') { + + # cannot split on space for this field, because field names contain + # spaces. instead work from a list of field names. + my @fields = map { $_->description } @{Bugzilla->fields}; + + # these aren't real fields, but exist in the headers + push @fields, ('Comment Created', 'Attachment Created'); + @fields = sort { length($b) <=> length($a) } @fields; + while ($value ne '') { + foreach my $field (@fields) { + if ($value eq $field) { + push @headers, "$name: $field"; + $value = ''; + last; } - else { - push @headers, "$name: $value"; + if (substr($value, 0, length($field) + 1) eq $field . ' ') { + push @headers, "$name: $field"; + $value = substr($value, length($field) + 1); + last; } + } } - $replacement = join("\n", @headers); - } - - # update the message body - if (scalar($email->parts) > 1) { - $email->walk_parts(sub { - my ($part) = @_; - - # skip top-level - return if $part->parts > 1; - - # do not filter attachments such as patches, etc. - return if - $part->header('Content-Disposition') - && $part->header('Content-Disposition') =~ /attachment/; - - # text/plain|html only - return unless $part->content_type =~ /^text\/(?:html|plain)/; - - # hide in html content - if ($replacement && $part->content_type =~ /^text\/html/) { - $replacement = '
' . $replacement . '
'; - } - - # and inject - _replace_placeholder_in_part($part, $replacement); - }); + } + else { + push @headers, "$name: $value"; + } + } + $replacement = join("\n", @headers); + } + + # update the message body + if (scalar($email->parts) > 1) { + $email->walk_parts(sub { + my ($part) = @_; + + # skip top-level + return if $part->parts > 1; + + # do not filter attachments such as patches, etc. + return + if $part->header('Content-Disposition') + && $part->header('Content-Disposition') =~ /attachment/; + + # text/plain|html only + return unless $part->content_type =~ /^text\/(?:html|plain)/; + + # hide in html content + if ($replacement && $part->content_type =~ /^text\/html/) { + $replacement + = '
' . $replacement . '
'; + } + + # and inject + _replace_placeholder_in_part($part, $replacement); + }); - # force Email::MIME to re-create all the parts. without this - # as_string() doesn't return the updated body for multi-part sub-parts. - $email->parts_set([ $email->subparts ]); - } - elsif (!$email->content_type - || $email->content_type =~ /^text\/(?:html|plain)/) - { - # text-only email - _replace_placeholder_in_part($email, $replacement); - } + # force Email::MIME to re-create all the parts. without this + # as_string() doesn't return the updated body for multi-part sub-parts. + $email->parts_set([$email->subparts]); + } + elsif (!$email->content_type || $email->content_type =~ /^text\/(?:html|plain)/) + { + # text-only email + _replace_placeholder_in_part($email, $replacement); + } } sub _replace_placeholder_in_part { - my ($part, $replacement) = @_; + my ($part, $replacement) = @_; - _fix_encoding($part); + _fix_encoding($part); - # replace - my $placeholder = quotemeta('@@body-headers@@'); - my $body = $part->body_str; - $body =~ s/$placeholder/$replacement/; - $part->body_str_set($body); + # replace + my $placeholder = quotemeta('@@body-headers@@'); + my $body = $part->body_str; + $body =~ s/$placeholder/$replacement/; + $part->body_str_set($body); } sub _fix_encoding { - my $part = shift; - - # don't touch the top-level part of multi-part mail - return if $part->parts > 1; - - # nothing to do if the part already has a charset - my $ct = parse_content_type($part->content_type); - my $charset = $ct->{attributes}{charset} - ? $ct->{attributes}{charset} - : ''; - return unless !$charset || $charset eq 'us-ascii'; - - if (Bugzilla->params->{utf8}) { - $part->charset_set('UTF-8'); - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } + my $part = shift; + + # don't touch the top-level part of multi-part mail + return if $part->parts > 1; + + # nothing to do if the part already has a charset + my $ct = parse_content_type($part->content_type); + my $charset = $ct->{attributes}{charset} ? $ct->{attributes}{charset} : ''; + return unless !$charset || $charset eq 'us-ascii'; + + if (Bugzilla->params->{utf8}) { + $part->charset_set('UTF-8'); + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); } - $part->encoding_set('quoted-printable'); + } + $part->encoding_set('quoted-printable'); } sub _syslog { - my $message = shift; - openlog('apache', 'cons,pid', 'local4'); - syslog('notice', encode_utf8($message)); - closelog(); + my $message = shift; + openlog('apache', 'cons,pid', 'local4'); + syslog('notice', encode_utf8($message)); + closelog(); } sub post_bug_after_creation { - my ($self, $args) = @_; - return unless my $format = Bugzilla->input_params->{format}; - my $bug = $args->{vars}->{bug}; - - if ($format eq 'employee-incident' - && $bug->component eq 'Server Operations: Desktop Issues') - { - $self->_post_employee_incident_bug($args); - } - elsif ($format eq 'swag') { - $self->_post_gear_bug($args); - } - elsif ($format eq 'mozpr') { - $self->_post_mozpr_bug($args); - } - elsif ($format eq 'dev-engagement-event') { - $self->_post_dev_engagement($args); - } - elsif ($format eq 'shield-studies') { - $self->_post_shield_studies($args); - } + my ($self, $args) = @_; + return unless my $format = Bugzilla->input_params->{format}; + my $bug = $args->{vars}->{bug}; + + if ( $format eq 'employee-incident' + && $bug->component eq 'Server Operations: Desktop Issues') + { + $self->_post_employee_incident_bug($args); + } + elsif ($format eq 'swag') { + $self->_post_gear_bug($args); + } + elsif ($format eq 'mozpr') { + $self->_post_mozpr_bug($args); + } + elsif ($format eq 'dev-engagement-event') { + $self->_post_dev_engagement($args); + } + elsif ($format eq 'shield-studies') { + $self->_post_shield_studies($args); + } } sub _post_employee_incident_bug { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $bug = $vars->{bug}; - - my $error_mode_cache = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $bug = $vars->{bug}; + + my $error_mode_cache = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + + my ($investigate_bug, $ssh_key_bug); + my $old_user = Bugzilla->user; + eval { + Bugzilla->set_user(Bugzilla::User->new( + {name => Bugzilla->params->{'nobody_user'}})); + my $new_user = Bugzilla->user; + + # HACK: User needs to be in the editbugs and primary bug's group to allow + # setting of dependencies. + $new_user->{'groups'} = [ + Bugzilla::Group->new({name => 'editbugs'}), + Bugzilla::Group->new({name => 'infra'}), + Bugzilla::Group->new({name => 'infrasec'}) + ]; - my ($investigate_bug, $ssh_key_bug); - my $old_user = Bugzilla->user; - eval { - Bugzilla->set_user(Bugzilla::User->new({ name => Bugzilla->params->{'nobody_user'} })); - my $new_user = Bugzilla->user; - - # HACK: User needs to be in the editbugs and primary bug's group to allow - # setting of dependencies. - $new_user->{'groups'} = [ Bugzilla::Group->new({ name => 'editbugs' }), - Bugzilla::Group->new({ name => 'infra' }), - Bugzilla::Group->new({ name => 'infrasec' }) ]; - - my $recipients = { changer => $new_user }; - $vars->{original_reporter} = $old_user; - - my $comment; - $cgi->param('display_action', ''); - $template->process('bug/create/comment-employee-incident.txt.tmpl', $vars, \$comment) - || ThrowTemplateError($template->error()); - - $investigate_bug = Bugzilla::Bug->create({ - short_desc => 'Investigate Lost Device', - product => 'mozilla.org', - component => 'Security Assurance: Incident', - status_whiteboard => '[infrasec:incident]', - bug_severity => 'critical', - cc => [ 'jstevensen@mozilla.com' ], - groups => [ 'infrasec' ], - comment => $comment, - op_sys => 'All', - rep_platform => 'All', - version => 'other', - dependson => $bug->bug_id, - }); - $bug->set_all({ blocked => { add => [ $investigate_bug->bug_id ] }}); - Bugzilla::BugMail::Send($investigate_bug->id, $recipients); - - Bugzilla->set_user($old_user); - $vars->{original_reporter} = ''; - $comment = ''; - $cgi->param('display_action', 'ssh'); - $template->process('bug/create/comment-employee-incident.txt.tmpl', $vars, \$comment) - || ThrowTemplateError($template->error()); - - $ssh_key_bug = Bugzilla::Bug->create({ - short_desc => 'Disable/Regenerate SSH Key', - product => $bug->product, - component => $bug->component, - bug_severity => 'critical', - cc => $bug->cc, - groups => [ map { $_->{name} } @{ $bug->groups } ], - comment => $comment, - op_sys => 'All', - rep_platform => 'All', - version => 'other', - dependson => $bug->bug_id, - }); - $bug->set_all({ blocked => { add => [ $ssh_key_bug->bug_id ] }}); - Bugzilla::BugMail::Send($ssh_key_bug->id, $recipients); - }; - my $error = $@; + my $recipients = {changer => $new_user}; + $vars->{original_reporter} = $old_user; + + my $comment; + $cgi->param('display_action', ''); + $template->process('bug/create/comment-employee-incident.txt.tmpl', + $vars, \$comment) + || ThrowTemplateError($template->error()); + + $investigate_bug = Bugzilla::Bug->create({ + short_desc => 'Investigate Lost Device', + product => 'mozilla.org', + component => 'Security Assurance: Incident', + status_whiteboard => '[infrasec:incident]', + bug_severity => 'critical', + cc => ['jstevensen@mozilla.com'], + groups => ['infrasec'], + comment => $comment, + op_sys => 'All', + rep_platform => 'All', + version => 'other', + dependson => $bug->bug_id, + }); + $bug->set_all({blocked => {add => [$investigate_bug->bug_id]}}); + Bugzilla::BugMail::Send($investigate_bug->id, $recipients); Bugzilla->set_user($old_user); - Bugzilla->error_mode($error_mode_cache); + $vars->{original_reporter} = ''; + $comment = ''; + $cgi->param('display_action', 'ssh'); + $template->process('bug/create/comment-employee-incident.txt.tmpl', + $vars, \$comment) + || ThrowTemplateError($template->error()); + + $ssh_key_bug = Bugzilla::Bug->create({ + short_desc => 'Disable/Regenerate SSH Key', + product => $bug->product, + component => $bug->component, + bug_severity => 'critical', + cc => $bug->cc, + groups => [map { $_->{name} } @{$bug->groups}], + comment => $comment, + op_sys => 'All', + rep_platform => 'All', + version => 'other', + dependson => $bug->bug_id, + }); + $bug->set_all({blocked => {add => [$ssh_key_bug->bug_id]}}); + Bugzilla::BugMail::Send($ssh_key_bug->id, $recipients); + }; + my $error = $@; - if ($error || !$investigate_bug || !$ssh_key_bug) { - warn "Failed to create additional employee-incident bug: $error" if $error; - $vars->{'message'} = 'employee_incident_creation_failed'; - } + Bugzilla->set_user($old_user); + Bugzilla->error_mode($error_mode_cache); + + if ($error || !$investigate_bug || !$ssh_key_bug) { + warn "Failed to create additional employee-incident bug: $error" if $error; + $vars->{'message'} = 'employee_incident_creation_failed'; + } } sub _post_gear_bug { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $bug = $vars->{bug}; - my $input = Bugzilla->input_params; - - my ($team, $code) = $input->{teamcode} =~ /^(.+?) \((\d+)\)$/; - my @request = ( - "Date Required: $input->{date_required}", - "$input->{firstname} $input->{lastname}", - $input->{email}, - $input->{mozspace}, - $team, - $code, - $input->{purpose}, - ); - my @recipient = ( - "$input->{shiptofirstname} $input->{shiptolastname}", - $input->{shiptoemail}, - $input->{shiptoaddress1}, - $input->{shiptoaddress2}, - $input->{shiptocity}, - $input->{shiptostate}, - $input->{shiptopostcode}, - $input->{shiptocountry}, - "Phone: $input->{shiptophone}", - $input->{shiptoidrut}, - ); - - # the csv has 14 item fields - my @items = map { trim($_) } split(/\n/, $input->{items}); - my @csv; - while (@items) { - my @batch; - if (scalar(@items) > 14) { - @batch = splice(@items, 0, 14); - } - else { - @batch = @items; - push @batch, '' for scalar(@items)..13; - @items = (); - } - push @csv, [ @request, @batch, @recipient ]; + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $bug = $vars->{bug}; + my $input = Bugzilla->input_params; + + my ($team, $code) = $input->{teamcode} =~ /^(.+?) \((\d+)\)$/; + my @request = ( + "Date Required: $input->{date_required}", + "$input->{firstname} $input->{lastname}", + $input->{email}, $input->{mozspace}, $team, $code, $input->{purpose}, + ); + my @recipient = ( + "$input->{shiptofirstname} $input->{shiptolastname}", + $input->{shiptoemail}, + $input->{shiptoaddress1}, + $input->{shiptoaddress2}, + $input->{shiptocity}, + $input->{shiptostate}, + $input->{shiptopostcode}, + $input->{shiptocountry}, + "Phone: $input->{shiptophone}", + $input->{shiptoidrut}, + ); + + # the csv has 14 item fields + my @items = map { trim($_) } split(/\n/, $input->{items}); + my @csv; + while (@items) { + my @batch; + if (scalar(@items) > 14) { + @batch = splice(@items, 0, 14); + } + else { + @batch = @items; + push @batch, '' for scalar(@items) .. 13; + @items = (); } + push @csv, [@request, @batch, @recipient]; + } - # csv quoting and concat - foreach my $line (@csv) { - foreach my $field (@$line) { - if ($field =~ s/"/""/g || $field =~ /,/) { - $field = qq#"$field"#; - } - } - $line = join(',', @$line); + # csv quoting and concat + foreach my $line (@csv) { + foreach my $field (@$line) { + if ($field =~ s/"/""/g || $field =~ /,/) { + $field = qq#"$field"#; + } } + $line = join(',', @$line); + } - $self->_add_attachment($args, { - data => join("\n", @csv), - description => "Items (CSV)", - filename => "gear_" . $bug->id . ".csv", - mimetype => "text/csv", - }); - $bug->update($bug->creation_ts); + $self->_add_attachment( + $args, + { + data => join("\n", @csv), + description => "Items (CSV)", + filename => "gear_" . $bug->id . ".csv", + mimetype => "text/csv", + } + ); + $bug->update($bug->creation_ts); } sub _post_mozpr_bug { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $bug = $vars->{bug}; - my $input = Bugzilla->input_params; - - if ($input->{proj_mat_file}) { - $self->_add_attachment($args, { - data => $input->{proj_mat_file_attach}, - description => $input->{proj_mat_file_desc}, - filename => scalar $input->{proj_mat_file_attach}, - }); - } - if ($input->{pr_mat_file}) { - $self->_add_attachment($args, { - data => $input->{pr_mat_file_attach}, - description => $input->{pr_mat_file_desc}, - filename => scalar $input->{pr_mat_file_attach}, - }); - } - $bug->update($bug->creation_ts); + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $bug = $vars->{bug}; + my $input = Bugzilla->input_params; + + if ($input->{proj_mat_file}) { + $self->_add_attachment( + $args, + { + data => $input->{proj_mat_file_attach}, + description => $input->{proj_mat_file_desc}, + filename => scalar $input->{proj_mat_file_attach}, + } + ); + } + if ($input->{pr_mat_file}) { + $self->_add_attachment( + $args, + { + data => $input->{pr_mat_file_attach}, + description => $input->{pr_mat_file_desc}, + filename => scalar $input->{pr_mat_file_attach}, + } + ); + } + $bug->update($bug->creation_ts); } sub _post_dev_engagement { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $parent_bug = $vars->{bug}; - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $params = Bugzilla->input_params; - my $old_user = Bugzilla->user; - - my $error_mode_cache = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - eval { - # Add attachment containing tab delimited field values for - # spreadsheet import. - my @columns = qw(event start_date end_date location attendees - audience desc mozilla_attending_list); - my @attach_values; - foreach my $column(@columns) { - my $value = $params->{$column} || ""; - $value =~ s/"/""/g; - push(@attach_values, qq{"$value"}); - } - - my @requested; - foreach my $param (grep(/^request_/, keys %$params)) { - next if !$params->{$param} || $param eq 'request_other_text'; - $param =~ s/^request_//; - push(@requested, ucfirst($param)); - } - push(@attach_values, '"' . join(",", @requested) . '"'); - - # we wrap the data inside a textarea to allow for the delimited data to - # be pasted directly into google docs. - - my $values = html_quote(join("\t", @attach_values)); - my $data = <{vars}; + my $parent_bug = $vars->{bug}; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $params = Bugzilla->input_params; + my $old_user = Bugzilla->user; + + my $error_mode_cache = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + eval { + # Add attachment containing tab delimited field values for + # spreadsheet import. + my @columns = qw(event start_date end_date location attendees + audience desc mozilla_attending_list); + my @attach_values; + foreach my $column (@columns) { + my $value = $params->{$column} || ""; + $value =~ s/"/""/g; + push(@attach_values, qq{"$value"}); + } + + my @requested; + foreach my $param (grep(/^request_/, keys %$params)) { + next if !$params->{$param} || $param eq 'request_other_text'; + $param =~ s/^request_//; + push(@requested, ucfirst($param)); + } + push(@attach_values, '"' . join(",", @requested) . '"'); + + # we wrap the data inside a textarea to allow for the delimited data to + # be pasted directly into google docs. + + my $values = html_quote(join("\t", @attach_values)); + my $data = < @@ -2196,651 +2235,728 @@ sub _post_dev_engagement { EOF - $self->_add_attachment($args, { - data => $data, - description => 'Spreadsheet Data', - filename => 'dev_engagement_submission.html', - mimetype => 'text/html', - }); - }; + $self->_add_attachment( + $args, + { + data => $data, + description => 'Spreadsheet Data', + filename => 'dev_engagement_submission.html', + mimetype => 'text/html', + } + ); + }; - $parent_bug->update($parent_bug->creation_ts); + $parent_bug->update($parent_bug->creation_ts); } sub _post_shield_studies { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $parent_bug = $vars->{bug}; - my $params = Bugzilla->input_params; - my (@dep_comment, @dep_errors, @send_mail); - - # Common parameters always passed to _file_child_bug - # bug_data and template_suffix will be different for each bug - my $child_params = { - parent_bug => $parent_bug, - template_vars => $vars, - dep_comment => \@dep_comment, - dep_errors => \@dep_errors, - send_mail => \@send_mail, - }; - - # Study Validation Review - $child_params->{'bug_data'} = { - short_desc => '[SHIELD] Study Validation Review for ' . $params->{hypothesis}, - product => 'Shield', - component => 'Shield Study', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $parent_bug->bug_id, - }; - $child_params->{'template_suffix'} = 'validation-review'; - _file_child_bug($child_params); - - # Shipping Status - $child_params->{'bug_data'} = { - short_desc => '[SHIELD] Shipping Status for ' . $params->{hypothesis}, - product => 'Shield', - component => 'Shield Study', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $parent_bug->bug_id, - }; - $child_params->{'template_suffix'} = 'shipping-status'; - - # Data Review - _file_child_bug($child_params); - $child_params->{'bug_data'} = { - short_desc => '[SHIELD] Data Review for ' . $params->{hypothesis}, - product => 'Shield', - component => 'Shield Study', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $parent_bug->bug_id, - }; - $child_params->{'template_suffix'} = 'data-review'; - _file_child_bug($child_params); - - # Legal Review - $child_params->{'bug_data'} = { - short_desc => '[SHIELD] Legal Review for ' . $params->{hypothesis}, - product => 'Legal', - component => 'Firefox', - bug_severity => 'normal', - op_sys => 'All', - rep_platform => 'All', - groups => [ 'mozilla-employee-confidential' ], - version => 'unspecified', - blocked => $parent_bug->bug_id, - }; - $child_params->{'template_suffix'} = 'legal'; - _file_child_bug($child_params); - + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $parent_bug = $vars->{bug}; + my $params = Bugzilla->input_params; + my (@dep_comment, @dep_errors, @send_mail); + + # Common parameters always passed to _file_child_bug + # bug_data and template_suffix will be different for each bug + my $child_params = { + parent_bug => $parent_bug, + template_vars => $vars, + dep_comment => \@dep_comment, + dep_errors => \@dep_errors, + send_mail => \@send_mail, + }; + + # Study Validation Review + $child_params->{'bug_data'} = { + short_desc => '[SHIELD] Study Validation Review for ' . $params->{hypothesis}, + product => 'Shield', + component => 'Shield Study', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $parent_bug->bug_id, + }; + $child_params->{'template_suffix'} = 'validation-review'; + _file_child_bug($child_params); + + # Shipping Status + $child_params->{'bug_data'} = { + short_desc => '[SHIELD] Shipping Status for ' . $params->{hypothesis}, + product => 'Shield', + component => 'Shield Study', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $parent_bug->bug_id, + }; + $child_params->{'template_suffix'} = 'shipping-status'; + + # Data Review + _file_child_bug($child_params); + $child_params->{'bug_data'} = { + short_desc => '[SHIELD] Data Review for ' . $params->{hypothesis}, + product => 'Shield', + component => 'Shield Study', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $parent_bug->bug_id, + }; + $child_params->{'template_suffix'} = 'data-review'; + _file_child_bug($child_params); + + # Legal Review + $child_params->{'bug_data'} = { + short_desc => '[SHIELD] Legal Review for ' . $params->{hypothesis}, + product => 'Legal', + component => 'Firefox', + bug_severity => 'normal', + op_sys => 'All', + rep_platform => 'All', + groups => ['mozilla-employee-confidential'], + version => 'unspecified', + blocked => $parent_bug->bug_id, + }; + $child_params->{'template_suffix'} = 'legal'; + _file_child_bug($child_params); + + if (scalar @dep_errors) { + warn "[Bug " + . $parent_bug->id + . "] Failed to create additional moz-project-review bugs:\n" + . join("\n", @dep_errors); + $vars->{'message'} = 'moz_project_review_creation_failed'; + } + + if (scalar @dep_comment) { + my $comment = join("\n", @dep_comment); if (scalar @dep_errors) { - warn "[Bug " . $parent_bug->id . "] Failed to create additional moz-project-review bugs:\n" . - join("\n", @dep_errors); - $vars->{'message'} = 'moz_project_review_creation_failed'; - } - - if (scalar @dep_comment) { - my $comment = join("\n", @dep_comment); - if (scalar @dep_errors) { - $comment .= "\n\nSome errors occurred creating dependent bugs and have been recorded"; - } - $parent_bug->add_comment($comment); - $parent_bug->update($parent_bug->creation_ts); + $comment + .= "\n\nSome errors occurred creating dependent bugs and have been recorded"; } + $parent_bug->add_comment($comment); + $parent_bug->update($parent_bug->creation_ts); + } - foreach my $bug_id (@send_mail) { - Bugzilla::BugMail::Send($bug_id, { changer => Bugzilla->user }); - } + foreach my $bug_id (@send_mail) { + Bugzilla::BugMail::Send($bug_id, {changer => Bugzilla->user}); + } } sub _file_child_bug { - my ($params) = @_; - my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, $dep_errors, $send_mail) - = @$params{qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail)}; - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - my $new_bug; - eval { - my $comment; - my $full_template = "bug/create/comment-shield-studies-$template_suffix.txt.tmpl"; - Bugzilla->template->process($full_template, $template_vars, \$comment) - || ThrowTemplateError(Bugzilla->template->error()); - $bug_data->{'comment'} = $comment; - if ($new_bug = Bugzilla::Bug->create($bug_data)) { - my $set_all = { - dependson => { add => [ $new_bug->bug_id ] } - }; - $parent_bug->set_all($set_all); - $parent_bug->update($parent_bug->creation_ts); - } + my ($params) = @_; + my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, + $dep_errors, $send_mail) + = @$params{ + qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail) }; - - if ($@ || !($new_bug && $new_bug->{'bug_id'})) { - push(@$dep_comment, "Error creating $template_suffix review bug"); - push(@$dep_errors, "$template_suffix : $@") if $@; - # Since we performed Bugzilla::Bug::create in an eval block, we - # need to manually rollback the commit as this is not done - # in Bugzilla::Error automatically for eval'ed code. - Bugzilla->dbh->bz_rollback_transaction(); - } - else { - push(@$send_mail, $new_bug->id); - push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc); + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + my $new_bug; + eval { + my $comment; + my $full_template + = "bug/create/comment-shield-studies-$template_suffix.txt.tmpl"; + Bugzilla->template->process($full_template, $template_vars, \$comment) + || ThrowTemplateError(Bugzilla->template->error()); + $bug_data->{'comment'} = $comment; + if ($new_bug = Bugzilla::Bug->create($bug_data)) { + my $set_all = {dependson => {add => [$new_bug->bug_id]}}; + $parent_bug->set_all($set_all); + $parent_bug->update($parent_bug->creation_ts); } + }; + + if ($@ || !($new_bug && $new_bug->{'bug_id'})) { + push(@$dep_comment, "Error creating $template_suffix review bug"); + push(@$dep_errors, "$template_suffix : $@") if $@; + + # Since we performed Bugzilla::Bug::create in an eval block, we + # need to manually rollback the commit as this is not done + # in Bugzilla::Error automatically for eval'ed code. + Bugzilla->dbh->bz_rollback_transaction(); + } + else { + push(@$send_mail, $new_bug->id); + push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc); + } - undef $@; - Bugzilla->error_mode($old_error_mode); + undef $@; + Bugzilla->error_mode($old_error_mode); } sub _pre_fxos_feature { - my ($self, $args) = @_; - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - my $params = $args->{params}; + my ($self, $args) = @_; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + my $params = $args->{params}; - $params->{keywords} = 'foxfood'; - $params->{keywords} .= ',feature' if ($cgi->param('feature_type') // '') eq 'new'; - $params->{bug_status} = $user->in_group('canconfirm') ? 'NEW' : 'UNCONFIRMED'; + $params->{keywords} = 'foxfood'; + $params->{keywords} .= ',feature' + if ($cgi->param('feature_type') // '') eq 'new'; + $params->{bug_status} = $user->in_group('canconfirm') ? 'NEW' : 'UNCONFIRMED'; } sub _add_attachment { - my ($self, $args, $attachment_args) = @_; - - my $bug = $args->{vars}->{bug}; - $attachment_args->{bug} = $bug; - $attachment_args->{creation_ts} = $bug->creation_ts; - $attachment_args->{ispatch} = 0 unless exists $attachment_args->{ispatch}; - $attachment_args->{isprivate} = 0 unless exists $attachment_args->{isprivate}; - $attachment_args->{mimetype} ||= $self->_detect_content_type($attachment_args->{data}); - - # If the attachment cannot be successfully added to the bug, - # we notify the user, but we don't interrupt the bug creation process. - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $attachment; - eval { - $attachment = Bugzilla::Attachment->create($attachment_args); - }; - warn "$@" if $@; - Bugzilla->error_mode($old_error_mode); - - if ($attachment) { - # Insert comment for attachment - $bug->add_comment('', { isprivate => 0, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - delete $bug->{attachments}; - } - else { - $args->{vars}->{'message'} = 'attachment_creation_failed'; - } + my ($self, $args, $attachment_args) = @_; + + my $bug = $args->{vars}->{bug}; + $attachment_args->{bug} = $bug; + $attachment_args->{creation_ts} = $bug->creation_ts; + $attachment_args->{ispatch} = 0 unless exists $attachment_args->{ispatch}; + $attachment_args->{isprivate} = 0 unless exists $attachment_args->{isprivate}; + $attachment_args->{mimetype} + ||= $self->_detect_content_type($attachment_args->{data}); + + # If the attachment cannot be successfully added to the bug, + # we notify the user, but we don't interrupt the bug creation process. + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $attachment; + eval { $attachment = Bugzilla::Attachment->create($attachment_args); }; + warn "$@" if $@; + Bugzilla->error_mode($old_error_mode); + + if ($attachment) { + + # Insert comment for attachment + $bug->add_comment('', + {isprivate => 0, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id} + ); + delete $bug->{attachments}; + } + else { + $args->{vars}->{'message'} = 'attachment_creation_failed'; + } - # Note: you must call $bug->update($bug->creation_ts) after adding all attachments +# Note: you must call $bug->update($bug->creation_ts) after adding all attachments } # bugzilla's content_type detection makes assumptions about form fields, which # means we can't use it here. this code is lifted from # Bugzilla::Attachment::get_content_type and the TypeSniffer extension. sub _detect_content_type { - my ($self, $data) = @_; - my $cgi = Bugzilla->cgi; - - # browser provided content-type - my $content_type = $cgi->uploadInfo($data)->{'Content-Type'}; - $content_type = 'image/png' if $content_type eq 'image/x-png'; - - if ($content_type eq 'application/octet-stream') { - # detect from filename - my $filename = scalar($data); - if (my $from_filename = mimetype($filename)) { - return $from_filename; - } + my ($self, $data) = @_; + my $cgi = Bugzilla->cgi; + + # browser provided content-type + my $content_type = $cgi->uploadInfo($data)->{'Content-Type'}; + $content_type = 'image/png' if $content_type eq 'image/x-png'; + + if ($content_type eq 'application/octet-stream') { + + # detect from filename + my $filename = scalar($data); + if (my $from_filename = mimetype($filename)) { + return $from_filename; } + } - return $content_type || 'application/octet-stream'; + return $content_type || 'application/octet-stream'; } sub buglist_columns { - my ($self, $args) = @_; - my $columns = $args->{columns}; - $columns->{'cc_count'} = { - name => '(SELECT COUNT(*) FROM cc WHERE cc.bug_id = bugs.bug_id)', - title => 'CC Count', - }; - $columns->{'dupe_count'} = { - name => '(SELECT COUNT(*) FROM duplicates WHERE duplicates.dupe_of = bugs.bug_id)', - title => 'Duplicate Count', - }; + my ($self, $args) = @_; + my $columns = $args->{columns}; + $columns->{'cc_count'} = { + name => '(SELECT COUNT(*) FROM cc WHERE cc.bug_id = bugs.bug_id)', + title => 'CC Count', + }; + $columns->{'dupe_count'} = { + name => + '(SELECT COUNT(*) FROM duplicates WHERE duplicates.dupe_of = bugs.bug_id)', + title => 'Duplicate Count', + }; } sub enter_bug_start { - my ($self, $args) = @_; - # if configured with create_bug_formats, force users into a custom bug - # format (can be overridden with a __standard__ format) - my $cgi = Bugzilla->cgi; - if ($cgi->param('format')) { - if ($cgi->param('format') eq '__standard__') { - $cgi->delete('format'); - $cgi->param('format_forced', 1); - } - } elsif (my $format = forced_format($cgi->param('product'))) { - $cgi->param('format', $format); - } - - # If product eq 'mozilla.org' and format eq 'itrequest', then - # switch to the new 'Infrastructure & Operations' product. - if ($cgi->param('product') && $cgi->param('product') eq 'mozilla.org' - && $cgi->param('format') && $cgi->param('format') eq 'itrequest') - { - $cgi->param('product', 'Infrastructure & Operations'); - } - - # map renamed groups - $cgi->param('groups', _map_groups($cgi->param('groups'))); + my ($self, $args) = @_; + + # if configured with create_bug_formats, force users into a custom bug + # format (can be overridden with a __standard__ format) + my $cgi = Bugzilla->cgi; + if ($cgi->param('format')) { + if ($cgi->param('format') eq '__standard__') { + $cgi->delete('format'); + $cgi->param('format_forced', 1); + } + } + elsif (my $format = forced_format($cgi->param('product'))) { + $cgi->param('format', $format); + } + + # If product eq 'mozilla.org' and format eq 'itrequest', then + # switch to the new 'Infrastructure & Operations' product. + if ( $cgi->param('product') + && $cgi->param('product') eq 'mozilla.org' + && $cgi->param('format') + && $cgi->param('format') eq 'itrequest') + { + $cgi->param('product', 'Infrastructure & Operations'); + } + + # map renamed groups + $cgi->param('groups', _map_groups($cgi->param('groups'))); } sub bug_before_create { - my ($self, $args) = @_; - my $params = $args->{params}; - if (exists $params->{groups}) { - # map renamed groups - $params->{groups} = [ _map_groups($params->{groups}) ]; - } - if ((Bugzilla->cgi->param('format') // '') eq 'fxos-feature') { - $self->_pre_fxos_feature($args); - } + my ($self, $args) = @_; + my $params = $args->{params}; + if (exists $params->{groups}) { + + # map renamed groups + $params->{groups} = [_map_groups($params->{groups})]; + } + if ((Bugzilla->cgi->param('format') // '') eq 'fxos-feature') { + $self->_pre_fxos_feature($args); + } } sub _map_groups { - my (@groups) = @_; - return unless @groups; - @groups = @{ $groups[0] } if ref($groups[0]); - return map { - # map mozilla-corporation-confidential => mozilla-employee-confidential - $_ eq 'mozilla-corporation-confidential' - ? 'mozilla-employee-confidential' - : $_ - } @groups; + my (@groups) = @_; + return unless @groups; + @groups = @{$groups[0]} if ref($groups[0]); + return map { + + # map mozilla-corporation-confidential => mozilla-employee-confidential + $_ eq 'mozilla-corporation-confidential' ? 'mozilla-employee-confidential' : $_ + } @groups; } sub forced_format { - # note: this is also called from the guided bug entry extension - my ($product) = @_; - return undef unless defined $product; - - # always work on the correct product name - $product = Bugzilla::Product->new({ name => $product, cache => 1 }) - unless blessed($product); - return undef unless $product; - - # check for a forced-format entry - my $forced = $create_bug_formats{$product->name} - || return; - - # should this user be included? - my $user = Bugzilla->user; - my $include = ref($forced->{include}) ? $forced->{include} : [ $forced->{include} ]; - foreach my $inc (@$include) { - return $forced->{format} if $user->in_group($inc); - } - return undef; + # note: this is also called from the guided bug entry extension + my ($product) = @_; + return undef unless defined $product; + + # always work on the correct product name + $product = Bugzilla::Product->new({name => $product, cache => 1}) + unless blessed($product); + return undef unless $product; + + # check for a forced-format entry + my $forced = $create_bug_formats{$product->name} || return; + + # should this user be included? + my $user = Bugzilla->user; + my $include + = ref($forced->{include}) ? $forced->{include} : [$forced->{include}]; + foreach my $inc (@$include) { + return $forced->{format} if $user->in_group($inc); + } + + return undef; } sub query_database { - my ($vars) = @_; - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - my $template = Bugzilla->template; - - # validate group membership - $user->in_group('query_database') - || ThrowUserError('auth_failure', { group => 'query_database', - action => 'access', - object => 'query_database' }); - - # read query - my $input = Bugzilla->input_params; - my $query = $input->{query}; - $vars->{query} = $query; - - if ($query) { - # Only allow POST requests - if ($cgi->request_method ne 'POST') { - ThrowCodeError('illegal_request_method', - { method => $cgi->request_method, accepted => ['POST'] }); - } + my ($vars) = @_; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + my $template = Bugzilla->template; - check_hash_token($input->{token}, ['query_database']); - trick_taint($query); - $vars->{executed} = 1; + # validate group membership + $user->in_group('query_database') + || ThrowUserError('auth_failure', + {group => 'query_database', action => 'access', object => 'query_database'}); - # add limit if missing - if ($query !~ /\sLIMIT\s+\d+\s*$/si) { - $query .= ' LIMIT 1000'; - $vars->{query} = $query; - } + # read query + my $input = Bugzilla->input_params; + my $query = $input->{query}; + $vars->{query} = $query; - # log query - _syslog(sprintf("[db_query] %s %s", $user->login, $query)); - - # connect to database and execute - # switching to the shadow db gives us a read-only connection - my $dbh = Bugzilla->switch_to_shadow_db(); - my $sth; - eval { - $sth = $dbh->prepare($query); - $sth->execute(); - }; - if ($@) { - $vars->{sql_error} = $@; - return; - } + if ($query) { - # build result - my $columns = $sth->{NAME}; - my $rows; - while (my @row = $sth->fetchrow_array) { - push @$rows, \@row; - } + # Only allow POST requests + if ($cgi->request_method ne 'POST') { + ThrowCodeError('illegal_request_method', + {method => $cgi->request_method, accepted => ['POST']}); + } - # return results - $vars->{columns} = $columns; - $vars->{rows} = $rows; + check_hash_token($input->{token}, ['query_database']); + trick_taint($query); + $vars->{executed} = 1; - if ($input->{csv}) { - print $cgi->header(-type=> 'text/csv', - -content_disposition=> "attachment; filename=\"query_database.csv\""); - $template->process("pages/query_database.csv.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + # add limit if missing + if ($query !~ /\sLIMIT\s+\d+\s*$/si) { + $query .= ' LIMIT 1000'; + $vars->{query} = $query; } + + # log query + _syslog(sprintf("[db_query] %s %s", $user->login, $query)); + + # connect to database and execute + # switching to the shadow db gives us a read-only connection + my $dbh = Bugzilla->switch_to_shadow_db(); + my $sth; + eval { + $sth = $dbh->prepare($query); + $sth->execute(); + }; + if ($@) { + $vars->{sql_error} = $@; + return; + } + + # build result + my $columns = $sth->{NAME}; + my $rows; + while (my @row = $sth->fetchrow_array) { + push @$rows, \@row; + } + + # return results + $vars->{columns} = $columns; + $vars->{rows} = $rows; + + if ($input->{csv}) { + print $cgi->header( + -type => 'text/csv', + -content_disposition => "attachment; filename=\"query_database.csv\"" + ); + $template->process("pages/query_database.csv.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + } } # you can always file bugs into a product's default security group, as well as # into any of the groups in @always_fileable_groups sub _group_always_settable { - my ($self, $group) = @_; - return - $group->name eq $self->default_security_group - || ((grep { $_ eq $group->name } @always_fileable_groups) ? 1 : 0); + my ($self, $group) = @_; + return $group->name eq $self->default_security_group + || ((grep { $_ eq $group->name } @always_fileable_groups) ? 1 : 0); } sub _default_security_group { - return $_[0]->default_security_group_obj->name; + return $_[0]->default_security_group_obj->name; } sub _default_security_group_obj { - my $group_id = $_[0]->{security_group_id}; - if (!$group_id) { - return Bugzilla::Group->new({ name => Bugzilla->params->{insidergroup}, cache => 1 }); - } - return Bugzilla::Group->new({ id => $group_id, cache => 1 }); + my $group_id = $_[0]->{security_group_id}; + if (!$group_id) { + return Bugzilla::Group->new( + {name => Bugzilla->params->{insidergroup}, cache => 1}); + } + return Bugzilla::Group->new({id => $group_id, cache => 1}); } # called from the verify version, component, and group page. # if we're making a group invalid, stuff the default group into the cgi param # to make it checked by default. sub _check_default_product_security_group { - my ($self, $product, $invalid_groups, $optional_group_controls) = @_; - return unless my $group = $product->default_security_group_obj; - if (@$invalid_groups) { - my $cgi = Bugzilla->cgi; - my @groups = $cgi->param('groups'); - push @groups, $group->name unless grep { $_ eq $group->name } @groups; - $cgi->param('groups', @groups); - } + my ($self, $product, $invalid_groups, $optional_group_controls) = @_; + return unless my $group = $product->default_security_group_obj; + if (@$invalid_groups) { + my $cgi = Bugzilla->cgi; + my @groups = $cgi->param('groups'); + push @groups, $group->name unless grep { $_ eq $group->name } @groups; + $cgi->param('groups', @groups); + } } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{files}; - my $create_files = $args->{create_files}; - my $extensions_dir = bz_locations()->{extensionsdir}; - $create_files->{__lbheartbeat__} = { - perms => Bugzilla::Install::Filesystem::WS_SERVE, - overwrite => 1, # the original value for this was wrong, overwrite it - contents => 'httpd OK', - }; - - - # version.json needs to have a source attribute pointing to - # our repository. We already have this information in the (static) - # contribute.json file, so parse that in - my $json = JSON::XS->new->pretty->utf8->canonical(); - my $contribute = eval { - $json->decode(scalar read_file(bz_locations()->{cgi_path} . "/contribute.json")); - }; - - if (!$contribute) { - die "Missing or invalid contribute.json file"; - } - - my $version_obj = { - source => $contribute->{repository}{url}, - version => BUGZILLA_VERSION, - commit => $ENV{CIRCLE_SHA1} // 'unknown', - build => $ENV{CIRCLE_BUILD_URL} // 'unknown', - }; - - $create_files->{'version.json'} = { - overwrite => 1, - perms => Bugzilla::Install::Filesystem::WS_SERVE, - contents => $json->encode($version_obj), - }; - - $files->{"$extensions_dir/BMO/bin/migrate-github-pull-requests.pl"} = { - perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{files}; + my $create_files = $args->{create_files}; + my $extensions_dir = bz_locations()->{extensionsdir}; + $create_files->{__lbheartbeat__} = { + perms => Bugzilla::Install::Filesystem::WS_SERVE, + overwrite => 1, # the original value for this was wrong, overwrite it + contents => 'httpd OK', + }; + + + # version.json needs to have a source attribute pointing to + # our repository. We already have this information in the (static) + # contribute.json file, so parse that in + my $json = JSON::XS->new->pretty->utf8->canonical(); + my $contribute = eval { + $json->decode( + scalar read_file(bz_locations()->{cgi_path} . "/contribute.json")); + }; + + if (!$contribute) { + die "Missing or invalid contribute.json file"; + } + + my $version_obj = { + source => $contribute->{repository}{url}, + version => BUGZILLA_VERSION, + commit => $ENV{CIRCLE_SHA1} // 'unknown', + build => $ENV{CIRCLE_BUILD_URL} // 'unknown', + }; + + $create_files->{'version.json'} = { + overwrite => 1, + perms => Bugzilla::Install::Filesystem::WS_SERVE, + contents => $json->encode($version_obj), + }; + + $files->{"$extensions_dir/BMO/bin/migrate-github-pull-requests.pl"} + = {perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE}; } # "deleted" comment tag sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{groupsecurity}->{params} }, { - name => 'delete_comments_group', - type => 's', - choices => \&get_all_group_names, - default => 'admin', - checker => \&check_group + my ($self, $args) = @_; + push @{$args->{panels}->{groupsecurity}->{params}}, + { + name => 'delete_comments_group', + type => 's', + choices => \&get_all_group_names, + default => 'admin', + checker => \&check_group }; } sub comment_after_add_tag { - my ($self, $args) = @_; - my $tag = $args->{tag}; - return unless lc($tag) eq 'deleted'; - - my $group_name = Bugzilla->params->{delete_comments_group}; - if (!$group_name || !Bugzilla->user->in_group($group_name)) { - ThrowUserError('auth_failure', { group => $group_name, - action => 'delete', - object => 'comments' }); - } + my ($self, $args) = @_; + my $tag = $args->{tag}; + return unless lc($tag) eq 'deleted'; + + my $group_name = Bugzilla->params->{delete_comments_group}; + if (!$group_name || !Bugzilla->user->in_group($group_name)) { + ThrowUserError('auth_failure', + {group => $group_name, action => 'delete', object => 'comments'}); + } } sub comment_after_remove_tag { - my ($self, $args) = @_; - my $tag = $args->{tag}; - return unless lc($tag) eq 'deleted'; - - my $group_name = Bugzilla->params->{delete_comments_group}; - if (!$group_name || !Bugzilla->user->in_group($group_name)) { - ThrowUserError('auth_failure', { group => $group_name, - action => 'delete', - object => 'comments' }); - } + my ($self, $args) = @_; + my $tag = $args->{tag}; + return unless lc($tag) eq 'deleted'; + + my $group_name = Bugzilla->params->{delete_comments_group}; + if (!$group_name || !Bugzilla->user->in_group($group_name)) { + ThrowUserError('auth_failure', + {group => $group_name, action => 'delete', object => 'comments'}); + } } BEGIN { - *Bugzilla::Comment::has_tag = \&_comment_has_tag; + *Bugzilla::Comment::has_tag = \&_comment_has_tag; } sub _comment_has_tag { - my ($self, $test_tag) = @_; - $test_tag = lc($test_tag); - foreach my $tag (@{ $self->tags }) { - return 1 if lc($tag) eq $test_tag; - } - return 0; + my ($self, $test_tag) = @_; + $test_tag = lc($test_tag); + foreach my $tag (@{$self->tags}) { + return 1 if lc($tag) eq $test_tag; + } + return 0; } sub bug_comments { - my ($self, $args) = @_; - my $can_delete = Bugzilla->user->in_group(Bugzilla->params->{delete_comments_group}); - my $comments = $args->{comments}; - my @deleted = grep { $_->has_tag('deleted') } @$comments; - while (my $comment = pop @deleted) { - for (my $i = scalar(@$comments) - 1; $i >= 0; $i--) { - if ($comment == $comments->[$i]) { - if ($can_delete) { - # don't remove comment from users who can "delete" them - # just collapse it instead - $comment->{collapsed} = 1; - } - else { - # otherwise, remove it from the array - splice(@$comments, $i, 1); - } - last; - } + my ($self, $args) = @_; + my $can_delete + = Bugzilla->user->in_group(Bugzilla->params->{delete_comments_group}); + my $comments = $args->{comments}; + my @deleted = grep { $_->has_tag('deleted') } @$comments; + while (my $comment = pop @deleted) { + for (my $i = scalar(@$comments) - 1; $i >= 0; $i--) { + if ($comment == $comments->[$i]) { + if ($can_delete) { + + # don't remove comment from users who can "delete" them + # just collapse it instead + $comment->{collapsed} = 1; } + else { + # otherwise, remove it from the array + splice(@$comments, $i, 1); + } + last; + } } + } } sub _split_crash_signature { - my ($self, $vars) = @_; - my $bug = $vars->{bug} // return; - my $crash_signature = $bug->cf_crash_signature // return; - return [ - grep { /\S/ } - extract_multiple($crash_signature, [ sub { extract_bracketed($_[0], '[]') } ]) - ]; + my ($self, $vars) = @_; + my $bug = $vars->{bug} // return; + my $crash_signature = $bug->cf_crash_signature // return; + return [grep {/\S/} + extract_multiple($crash_signature, [sub { extract_bracketed($_[0], '[]') }])]; } sub enter_bug_entrydefaultvars { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $cgi = Bugzilla->cgi; - return unless my $format = $cgi->param('format'); - - if ($format eq 'fxos-feature') { - $vars->{feature_type} = $cgi->param('feature_type'); - $vars->{description} = $cgi->param('description'); - $vars->{discussion} = $cgi->param('discussion'); - } + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $cgi = Bugzilla->cgi; + return unless my $format = $cgi->param('format'); + + if ($format eq 'fxos-feature') { + $vars->{feature_type} = $cgi->param('feature_type'); + $vars->{description} = $cgi->param('description'); + $vars->{discussion} = $cgi->param('discussion'); + } } sub app_startup { - my ($self, $args) = @_; - my $app = $args->{app}; - my $r = $app->routes; - - $r->get( - '/favicon.ico' => sub { - my $c = shift; - $c->reply->file( - $c->app->home->child('extensions/BMO/web/images/favicon.ico') - ); - } + my ($self, $args) = @_; + my $app = $args->{app}; + my $r = $app->routes; + + $r->get( + '/favicon.ico' => sub { + my $c = shift; + $c->reply->file($c->app->home->child('extensions/BMO/web/images/favicon.ico')); + } + ); + + $r->any('/:REWRITE_itrequest' => [REWRITE_itrequest => qr{form[\.:]itrequest}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Infrastructure & Operations', 'format' => 'itrequest'}); + $r->any('/:REWRITE_mozlist' => [REWRITE_mozlist => qr{form[\.:]mozlist}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'mozlist'}); + $r->any('/:REWRITE_poweredby' => [REWRITE_poweredby => qr{form[\.:]poweredby}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'poweredby'}); + $r->any( + '/:REWRITE_presentation' => [REWRITE_presentation => qr{form[\.:]presentation}]) + ->to( + 'cgi#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'presentation'} ); - - $r->any( '/:REWRITE_itrequest' => [ REWRITE_itrequest => qr{form[\.:]itrequest} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Infrastructure & Operations', 'format' => 'itrequest' } ); - $r->any( '/:REWRITE_mozlist' => [ REWRITE_mozlist => qr{form[\.:]mozlist} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'mozlist' } ); - $r->any( '/:REWRITE_poweredby' => [ REWRITE_poweredby => qr{form[\.:]poweredby} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'poweredby' } ); - $r->any( '/:REWRITE_presentation' => [ REWRITE_presentation => qr{form[\.:]presentation} ] ) - ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'presentation' } ); - $r->any( '/:REWRITE_trademark' => [ REWRITE_trademark => qr{form[\.:]trademark} ] ) - ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'trademark' } ); - $r->any( '/:REWRITE_recoverykey' => [ REWRITE_recoverykey => qr{form[\.:]recoverykey} ] ) - ->to( 'cgi#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'recoverykey' } ); - $r->any( '/:REWRITE_legal' => [ REWRITE_legal => qr{form[\.:]legal} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Legal', 'format' => 'legal' }, ); - $r->any( '/:REWRITE_recruiting' => [ REWRITE_recruiting => qr{form[\.:]recruiting} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Recruiting', 'format' => 'recruiting' } ); - $r->any( '/:REWRITE_intern' => [ REWRITE_intern => qr{form[\.:]intern} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Recruiting', 'format' => 'intern' } ); - $r->any( '/:REWRITE_mozpr' => [ REWRITE_mozpr => qr{form[\.:]mozpr} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla PR', 'format' => 'mozpr' }, ); - $r->any( '/:REWRITE_reps_mentorship' => [ REWRITE_reps_mentorship => qr{form[\.:]reps[\.:]mentorship} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'mozreps' }, ); - $r->any( '/:REWRITE_reps_budget' => [ REWRITE_reps_budget => qr{form[\.:]reps[\.:]budget} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-budget' } ); - $r->any( '/:REWRITE_reps_swag' => [ REWRITE_reps_swag => qr{form[\.:]reps[\.:]swag} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-swag' } ); - $r->any( '/:REWRITE_reps_it' => [ REWRITE_reps_it => qr{form[\.:]reps[\.:]it} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Mozilla Reps', 'format' => 'remo-it' } ); - $r->any( '/:REWRITE_reps_payment' => [ REWRITE_reps_payment => qr{form[\.:]reps[\.:]payment} ] ) - ->to( 'CGI#page_cgi' => { 'id' => 'remo-form-payment.html' } ); - $r->any( '/:REWRITE_csa_discourse' => [ REWRITE_csa_discourse => qr{form[\.:]csa[\.:]discourse} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Infrastructure & Operations', 'format' => 'csa-discourse' } ); - $r->any( '/:REWRITE_employee_incident' => [ REWRITE_employee_incident => qr{form[\.:]employee[\.\-:]incident} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'employee-incident' } ); - $r->any( '/:REWRITE_brownbag' => [ REWRITE_brownbag => qr{form[\.:]brownbag} ] ) - ->to( 'CGI#https_air_mozilla_org_requests' => {} ); - $r->any( '/:REWRITE_finance' => [ REWRITE_finance => qr{form[\.:]finance} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Finance', 'format' => 'finance' } ); - $r->any( - '/:REWRITE_moz_project_review' => [ REWRITE_moz_project_review => qr{form[\.:]moz[\.\-:]project[\.\-:]review} ] - )->to( 'CGI#enter_bug_cgi' => { 'product' => 'mozilla.org', 'format' => 'moz-project-review' } ); - $r->any( '/:REWRITE_docs' => [ REWRITE_docs => qr{form[\.:]docs?} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Developer Documentation', 'format' => 'doc' } ); - $r->any( '/:REWRITE_mdn' => [ REWRITE_mdn => qr{form[\.:]mdn?} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'mdn', 'product' => 'developer.mozilla.org' } ); - $r->any( '/:REWRITE_swag_gear' => [ REWRITE_swag_gear => qr{form[\.:](swag|gear)} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'swag', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_costume' => [ REWRITE_costume => qr{form[\.:]costume} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Marketing', 'format' => 'costume' } ); - $r->any( '/:REWRITE_ipp' => [ REWRITE_ipp => qr{form[\.:]ipp} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Internet Public Policy', 'format' => 'ipp' } ); - $r->any( '/:REWRITE_creative' => [ REWRITE_creative => qr{form[\.:]creative} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'creative', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_user_engagement' => [ REWRITE_user_engagement => qr{form[\.:]user[\.\-:]engagement} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'user-engagement', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_dev_engagement_event' => - [ REWRITE_dev_engagement_event => qr{form[\.:]dev[\.\-:]engagement[\.\-\:]event} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Developer Engagement', 'format' => 'dev-engagement-event' } ); - $r->any( '/:REWRITE_mobile_compat' => [ REWRITE_mobile_compat => qr{form[\.:]mobile[\.\-:]compat} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Tech Evangelism', 'format' => 'mobile-compat' } ); - $r->any( '/:REWRITE_web_bounty' => [ REWRITE_web_bounty => qr{form[\.:]web[\.:]bounty} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'web-bounty', 'product' => 'mozilla.org' } ); - $r->any( '/:REWRITE_automative' => [ REWRITE_automative => qr{form[\.:]automative} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Testing', 'format' => 'automative' } ); - $r->any( '/:REWRITE_comm_newsletter' => [ REWRITE_comm_newsletter => qr{form[\.:]comm[\.:]newsletter} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'comm-newsletter', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_screen_share_whitelist' => - [ REWRITE_screen_share_whitelist => qr{form[\.:]screen[\.:]share[\.:]whitelist} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'screen-share-whitelist', 'product' => 'Firefox' } ); - $r->any( '/:REWRITE_data_compliance' => [ REWRITE_data_compliance => qr{form[\.:]data[\.\-:]compliance} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Data Compliance', 'format' => 'data-compliance' } ); - $r->any( '/:REWRITE_fsa_budget' => [ REWRITE_fsa_budget => qr{form[\.:]fsa[\.:]budget} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'FSA', 'format' => 'fsa-budget' } ); - $r->any( '/:REWRITE_triage_request' => [ REWRITE_triage_request => qr{form[\.:]triage[\.\-]request} ] ) - ->to( 'CGI#page_cgi' => { 'id' => 'triage_request.html' } ); - $r->any( '/:REWRITE_crm_CRM' => [ REWRITE_crm_CRM => qr{form[\.:](crm|CRM)} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'crm', 'product' => 'Marketing' } ); - $r->any( '/:REWRITE_nda' => [ REWRITE_nda => qr{form[\.:]nda} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Legal', 'format' => 'nda' } ); - $r->any( '/:REWRITE_name_clearance' => [ REWRITE_name_clearance => qr{form[\.:]name[\.:]clearance} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'format' => 'name-clearance', 'product' => 'Legal' } ); - $r->any( '/:REWRITE_shield_studies' => [ REWRITE_shield_studies => qr{form[\.:]shield[\.:]studies} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Shield', 'format' => 'shield-studies' } ); - $r->any( '/:REWRITE_client_bounty' => [ REWRITE_client_bounty => qr{form[\.:]client[\.:]bounty} ] ) - ->to( 'CGI#enter_bug_cgi' => { 'product' => 'Firefox', 'format' => 'client-bounty' } ); + $r->any('/:REWRITE_trademark' => [REWRITE_trademark => qr{form[\.:]trademark}]) + ->to( + 'cgi#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'trademark'}); + $r->any( + '/:REWRITE_recoverykey' => [REWRITE_recoverykey => qr{form[\.:]recoverykey}]) + ->to( + 'cgi#enter_bug_cgi' => {'product' => 'mozilla.org', 'format' => 'recoverykey'}); + $r->any('/:REWRITE_legal' => [REWRITE_legal => qr{form[\.:]legal}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'Legal', 'format' => 'legal'},); + $r->any( + '/:REWRITE_recruiting' => [REWRITE_recruiting => qr{form[\.:]recruiting}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Recruiting', 'format' => 'recruiting'}); + $r->any('/:REWRITE_intern' => [REWRITE_intern => qr{form[\.:]intern}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Recruiting', 'format' => 'intern'}); + $r->any('/:REWRITE_mozpr' => [REWRITE_mozpr => qr{form[\.:]mozpr}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'Mozilla PR', 'format' => 'mozpr'}, + ); + $r->any('/:REWRITE_reps_mentorship' => + [REWRITE_reps_mentorship => qr{form[\.:]reps[\.:]mentorship}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Mozilla Reps', 'format' => 'mozreps'},); + $r->any('/:REWRITE_reps_budget' => + [REWRITE_reps_budget => qr{form[\.:]reps[\.:]budget}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Mozilla Reps', 'format' => 'remo-budget'} + ); + $r->any( + '/:REWRITE_reps_swag' => [REWRITE_reps_swag => qr{form[\.:]reps[\.:]swag}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Mozilla Reps', 'format' => 'remo-swag'}); + $r->any('/:REWRITE_reps_it' => [REWRITE_reps_it => qr{form[\.:]reps[\.:]it}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Mozilla Reps', 'format' => 'remo-it'}); + $r->any('/:REWRITE_reps_payment' => + [REWRITE_reps_payment => qr{form[\.:]reps[\.:]payment}]) + ->to('CGI#page_cgi' => {'id' => 'remo-form-payment.html'}); + $r->any('/:REWRITE_csa_discourse' => + [REWRITE_csa_discourse => qr{form[\.:]csa[\.:]discourse}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Infrastructure & Operations', 'format' => 'csa-discourse'}); + $r->any('/:REWRITE_employee_incident' => + [REWRITE_employee_incident => qr{form[\.:]employee[\.\-:]incident}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'mozilla.org', 'format' => 'employee-incident'}); + $r->any('/:REWRITE_brownbag' => [REWRITE_brownbag => qr{form[\.:]brownbag}]) + ->to('CGI#https_air_mozilla_org_requests' => {}); + $r->any('/:REWRITE_finance' => [REWRITE_finance => qr{form[\.:]finance}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'Finance', 'format' => 'finance'}); + $r->any('/:REWRITE_moz_project_review' => + [REWRITE_moz_project_review => qr{form[\.:]moz[\.\-:]project[\.\-:]review}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'mozilla.org', 'format' => 'moz-project-review'}); + $r->any('/:REWRITE_docs' => [REWRITE_docs => qr{form[\.:]docs?}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Developer Documentation', 'format' => 'doc'}); + $r->any('/:REWRITE_mdn' => [REWRITE_mdn => qr{form[\.:]mdn?}]) + ->to('CGI#enter_bug_cgi' => + {'format' => 'mdn', 'product' => 'developer.mozilla.org'}); + $r->any( + '/:REWRITE_swag_gear' => [REWRITE_swag_gear => qr{form[\.:](swag|gear)}]) + ->to('CGI#enter_bug_cgi' => {'format' => 'swag', 'product' => 'Marketing'}); + $r->any('/:REWRITE_costume' => [REWRITE_costume => qr{form[\.:]costume}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Marketing', 'format' => 'costume'}); + $r->any('/:REWRITE_ipp' => [REWRITE_ipp => qr{form[\.:]ipp}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Internet Public Policy', 'format' => 'ipp'}); + $r->any('/:REWRITE_creative' => [REWRITE_creative => qr{form[\.:]creative}]) + ->to( + 'CGI#enter_bug_cgi' => {'format' => 'creative', 'product' => 'Marketing'}); + $r->any('/:REWRITE_user_engagement' => + [REWRITE_user_engagement => qr{form[\.:]user[\.\-:]engagement}]) + ->to('CGI#enter_bug_cgi' => + {'format' => 'user-engagement', 'product' => 'Marketing'}); + $r->any( + '/:REWRITE_dev_engagement_event' => [ + REWRITE_dev_engagement_event => qr{form[\.:]dev[\.\-:]engagement[\.\-\:]event} + ] + ) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Developer Engagement', 'format' => 'dev-engagement-event'}); + $r->any('/:REWRITE_mobile_compat' => + [REWRITE_mobile_compat => qr{form[\.:]mobile[\.\-:]compat}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Tech Evangelism', 'format' => 'mobile-compat'}); + $r->any( + '/:REWRITE_web_bounty' => [REWRITE_web_bounty => qr{form[\.:]web[\.:]bounty}]) + ->to( + 'CGI#enter_bug_cgi' => {'format' => 'web-bounty', 'product' => 'mozilla.org'}); + $r->any( + '/:REWRITE_automative' => [REWRITE_automative => qr{form[\.:]automative}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Testing', 'format' => 'automative'}); + $r->any('/:REWRITE_comm_newsletter' => + [REWRITE_comm_newsletter => qr{form[\.:]comm[\.:]newsletter}]) + ->to('CGI#enter_bug_cgi' => + {'format' => 'comm-newsletter', 'product' => 'Marketing'}); + $r->any( + '/:REWRITE_screen_share_whitelist' => [ + REWRITE_screen_share_whitelist => qr{form[\.:]screen[\.:]share[\.:]whitelist} + ] + ) + ->to('CGI#enter_bug_cgi' => + {'format' => 'screen-share-whitelist', 'product' => 'Firefox'}); + $r->any('/:REWRITE_data_compliance' => + [REWRITE_data_compliance => qr{form[\.:]data[\.\-:]compliance}]) + ->to('CGI#enter_bug_cgi' => + {'product' => 'Data Compliance', 'format' => 'data-compliance'}); + $r->any( + '/:REWRITE_fsa_budget' => [REWRITE_fsa_budget => qr{form[\.:]fsa[\.:]budget}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'FSA', 'format' => 'fsa-budget'}); + $r->any('/:REWRITE_triage_request' => + [REWRITE_triage_request => qr{form[\.:]triage[\.\-]request}]) + ->to('CGI#page_cgi' => {'id' => 'triage_request.html'}); + $r->any('/:REWRITE_crm_CRM' => [REWRITE_crm_CRM => qr{form[\.:](crm|CRM)}]) + ->to('CGI#enter_bug_cgi' => {'format' => 'crm', 'product' => 'Marketing'}); + $r->any('/:REWRITE_nda' => [REWRITE_nda => qr{form[\.:]nda}]) + ->to('CGI#enter_bug_cgi' => {'product' => 'Legal', 'format' => 'nda'}); + $r->any('/:REWRITE_name_clearance' => + [REWRITE_name_clearance => qr{form[\.:]name[\.:]clearance}]) + ->to( + 'CGI#enter_bug_cgi' => {'format' => 'name-clearance', 'product' => 'Legal'}); + $r->any('/:REWRITE_shield_studies' => + [REWRITE_shield_studies => qr{form[\.:]shield[\.:]studies}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Shield', 'format' => 'shield-studies'}); + $r->any('/:REWRITE_client_bounty' => + [REWRITE_client_bounty => qr{form[\.:]client[\.:]bounty}]) + ->to( + 'CGI#enter_bug_cgi' => {'product' => 'Firefox', 'format' => 'client-bounty'}); } __PACKAGE__->NAME; diff --git a/extensions/BMO/bin/bug_1022707.pl b/extensions/BMO/bin/bug_1022707.pl index 4d48db01d..31afa7d05 100755 --- a/extensions/BMO/bin/bug_1022707.pl +++ b/extensions/BMO/bin/bug_1022707.pl @@ -37,8 +37,10 @@ print "About to fix $total flags\n"; print "Press to start, or ^C to cancel...\n"; readline; -my $update_fsa_sql= "UPDATE flag_state_activity SET type_id = 4 WHERE " . $dbh->sql_in('flag_id', $flag_ids); -my $update_flags_sql = "UPDATE flags SET type_id = 4 WHERE " . $dbh->sql_in('id', $flag_ids); +my $update_fsa_sql = "UPDATE flag_state_activity SET type_id = 4 WHERE " + . $dbh->sql_in('flag_id', $flag_ids); +my $update_flags_sql + = "UPDATE flags SET type_id = 4 WHERE " . $dbh->sql_in('id', $flag_ids); $dbh->bz_start_transaction(); $dbh->do($update_fsa_sql); diff --git a/extensions/BMO/bin/bug_1093952.pl b/extensions/BMO/bin/bug_1093952.pl index fd891f4ae..f52427284 100755 --- a/extensions/BMO/bin/bug_1093952.pl +++ b/extensions/BMO/bin/bug_1093952.pl @@ -23,14 +23,17 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my $dbh = Bugzilla->dbh; -my $infra = Bugzilla::Product->check({ name => 'Infrastructure & Operations' }); -my $relops_id = Bugzilla::Component->check({ product => $infra, name => 'RelOps' })->id; -my $puppet_id = Bugzilla::Component->check({ product => $infra, name => 'RelOps: Puppet' })->id; -my $infra_id = $infra->id; -my $components = $dbh->sql_in('component_id', [ $relops_id, $puppet_id ]); +my $infra = Bugzilla::Product->check({name => 'Infrastructure & Operations'}); +my $relops_id + = Bugzilla::Component->check({product => $infra, name => 'RelOps'})->id; +my $puppet_id + = Bugzilla::Component->check({product => $infra, name => 'RelOps: Puppet'}) + ->id; +my $infra_id = $infra->id; +my $components = $dbh->sql_in('component_id', [$relops_id, $puppet_id]); print "Searching for bugs..\n"; -my $bugs = $dbh->selectall_arrayref(< {} }); +my $bugs = $dbh->selectall_arrayref(< {}}); SELECT bug_id, product_id, @@ -52,9 +55,9 @@ printf "About to fix %s bugs\n", scalar(@$bugs); print "Press to stop or to continue...\n"; getc(); -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); -my $field = Bugzilla::Field->check({ name => 'status_whiteboard' }); -my $when = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); +my $field = Bugzilla::Field->check({name => 'status_whiteboard'}); +my $when = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); my $sth_bugs = $dbh->prepare(" UPDATE bugs @@ -70,22 +73,25 @@ my $sth_activity = $dbh->prepare(" $dbh->bz_start_transaction(); foreach my $bug (@$bugs) { - my $bug_id = $bug->{bug_id}; - my $whiteboard = $bug->{status_whiteboard}; - print "bug $bug_id\n $whiteboard\n"; + my $bug_id = $bug->{bug_id}; + my $whiteboard = $bug->{status_whiteboard}; + print "bug $bug_id\n $whiteboard\n"; - my $updated = $whiteboard; - $updated =~ s#\[kanban:engops:https://kanbanize\.com/ctrl_board/6/[^\]]*\]\s*##g; - if ($bug->{product_id} == $infra->id - && $bug->{component_id} != $relops_id - && $bug->{component_id} != $puppet_id - ) { - $updated =~ s#\[kanban:engops:https://mozilla\.kanbanize\.com/ctrl_board/6/[^\]]*\]\s*##g; - } - print " $updated\n"; + my $updated = $whiteboard; + $updated + =~ s#\[kanban:engops:https://kanbanize\.com/ctrl_board/6/[^\]]*\]\s*##g; + if ( $bug->{product_id} == $infra->id + && $bug->{component_id} != $relops_id + && $bug->{component_id} != $puppet_id) + { + $updated + =~ s#\[kanban:engops:https://mozilla\.kanbanize\.com/ctrl_board/6/[^\]]*\]\s*##g; + } + print " $updated\n"; - $sth_bugs->execute($updated, $when, $when, $bug_id); - $sth_activity->execute($bug_id, $nobody->id, $when, $field->id, $whiteboard, $updated); + $sth_bugs->execute($updated, $when, $when, $bug_id); + $sth_activity->execute($bug_id, $nobody->id, $when, $field->id, $whiteboard, + $updated); } $dbh->bz_commit_transaction(); diff --git a/extensions/BMO/bin/bug_1141452.pl b/extensions/BMO/bin/bug_1141452.pl index 155c4704c..56b63db91 100755 --- a/extensions/BMO/bin/bug_1141452.pl +++ b/extensions/BMO/bin/bug_1141452.pl @@ -13,8 +13,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Constants qw( USAGE_MODE_CMDLINE ); @@ -24,14 +24,17 @@ use Bugzilla::User; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my $dbh = Bugzilla->dbh; -my $blocking_b2g = Bugzilla::Extension::TrackingFlags::Flag->check({ name => 'cf_blocking_b2g' }); -my $tracking_b2g = Bugzilla::Extension::TrackingFlags::Flag->check({ name => 'cf_tracking_b2g' }); +my $blocking_b2g = Bugzilla::Extension::TrackingFlags::Flag->check( + {name => 'cf_blocking_b2g'}); +my $tracking_b2g = Bugzilla::Extension::TrackingFlags::Flag->check( + {name => 'cf_tracking_b2g'}); die "tracking-b2g does not have a 'backlog' value\n" - unless grep { $_->value eq 'backlog' } @{ $tracking_b2g->values }; + unless grep { $_->value eq 'backlog' } @{$tracking_b2g->values}; print "Searching for bugs..\n"; -my $flags = $dbh->selectall_arrayref(< {} }, $blocking_b2g->flag_id, $tracking_b2g->flag_id); +my $flags = $dbh->selectall_arrayref( + < {}}, $blocking_b2g->flag_id, $tracking_b2g->flag_id); SELECT bugs.bug_id, blocking_b2g.id id, @@ -50,54 +53,59 @@ printf "About to fix %s bugs\n", scalar(@$flags); print "Press to stop or to continue...\n"; getc(); -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); -my $when = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); +my $when = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); $dbh->bz_start_transaction(); foreach my $flag (@$flags) { - if (!$flag->{value}) { - print $flag->{bug_id}, ": changing blocking_b2g:backlog -> tracking_b2g:backlog\n"; - # no tracking_b2g value, change blocking_b2g:backlog -> tracking_b2g:backlog - $dbh->do( - "UPDATE tracking_flags_bugs SET tracking_flag_id = ? WHERE id = ?", - undef, - $tracking_b2g->flag_id, $flag->{id}, - ); - $dbh->do( - "UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", - undef, - $when, $when, $flag->{bug_id}, - ); - $dbh->do( - "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", - undef, - $flag->{bug_id}, $nobody->id, $when, $blocking_b2g->id, 'backlog', '---', - ); - $dbh->do( - "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", - undef, - $flag->{bug_id}, $nobody->id, $when, $tracking_b2g->id, '---', 'backlog', - ); - } - elsif ($flag->{value}) { - print $flag->{bug_id}, ": deleting blocking_b2g:backlog\n"; - # tracking_b2g already has a value, just delete blocking_b2g:backlog - $dbh->do( - "DELETE FROM tracking_flags_bugs WHERE id = ?", - undef, - $flag->{id}, - ); - $dbh->do( - "UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", - undef, - $when, $when, $flag->{bug_id}, - ); - $dbh->do( - "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", - undef, - $flag->{bug_id}, $nobody->id, $when, $blocking_b2g->id, 'backlog', '---', - ); - } + if (!$flag->{value}) { + print $flag->{bug_id}, + ": changing blocking_b2g:backlog -> tracking_b2g:backlog\n"; + + # no tracking_b2g value, change blocking_b2g:backlog -> tracking_b2g:backlog + $dbh->do("UPDATE tracking_flags_bugs SET tracking_flag_id = ? WHERE id = ?", + undef, $tracking_b2g->flag_id, $flag->{id},); + $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", + undef, $when, $when, $flag->{bug_id},); + $dbh->do( + "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", + undef, + $flag->{bug_id}, + $nobody->id, + $when, + $blocking_b2g->id, + 'backlog', + '---', + ); + $dbh->do( + "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", + undef, + $flag->{bug_id}, + $nobody->id, + $when, + $tracking_b2g->id, + '---', + 'backlog', + ); + } + elsif ($flag->{value}) { + print $flag->{bug_id}, ": deleting blocking_b2g:backlog\n"; + + # tracking_b2g already has a value, just delete blocking_b2g:backlog + $dbh->do("DELETE FROM tracking_flags_bugs WHERE id = ?", undef, $flag->{id},); + $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", + undef, $when, $when, $flag->{bug_id},); + $dbh->do( + "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) VALUES (?, ?, ?, ?, ?, ?)", + undef, + $flag->{bug_id}, + $nobody->id, + $when, + $blocking_b2g->id, + 'backlog', + '---', + ); + } } $dbh->bz_commit_transaction(); diff --git a/extensions/BMO/bin/migrate-github-pull-requests.pl b/extensions/BMO/bin/migrate-github-pull-requests.pl index c39778a4a..c8afdedfb 100755 --- a/extensions/BMO/bin/migrate-github-pull-requests.pl +++ b/extensions/BMO/bin/migrate-github-pull-requests.pl @@ -22,9 +22,9 @@ use Bugzilla::Install::Util qw(indicate_progress); use Bugzilla::User; use Bugzilla::Util qw(trim); -my $dbh = Bugzilla->dbh; -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); -my $field = Bugzilla::Field->check({ name => 'attachments.mimetype' }); +my $dbh = Bugzilla->dbh; +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); +my $field = Bugzilla::Field->check({name => 'attachments.mimetype'}); # grab list of suitable attachments @@ -42,7 +42,7 @@ SELECT attachments.attach_id, AND LENGTH(thedata) <= 256 EOF print "Searching for suitable attachments..\n"; -my $attachments = $dbh->selectall_arrayref($sql, { Slice => {} }); +my $attachments = $dbh->selectall_arrayref($sql, {Slice => {}}); my ($current, $total, $updated) = (1, scalar(@$attachments), 0); die "No suitable attachments found\n" unless $total; @@ -52,39 +52,32 @@ print "Press to start, or ^C to cancel...\n"; <>; foreach my $attachment (@$attachments) { - indicate_progress({ current => $current++, total => $total, every => 25 }); - - # check payload - my $url = trim($attachment->{thedata}); - next if $url =~ /\s/; - next unless $url =~ m#^https://github\.com/[^/]+/[^/]+/pull/\d+\/?$#i; - - $dbh->bz_start_transaction; - - # set content-type - $dbh->do( - "UPDATE attachments SET mimetype = ? WHERE attach_id = ?", - undef, - 'text/x-github-pull-request', $attachment->{attach_id} - ); - - # insert into bugs_activity - my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $dbh->do( - "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) - VALUES (?, ?, ?, ?, ?, ?)", - undef, - $attachment->{bug_id}, $nobody->id, $timestamp, $field->id, - $attachment->{mimetype}, 'text/x-github-pull-request' - ); - $dbh->do( - "UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", - undef, - $timestamp, $timestamp, $attachment->{bug_id} - ); - - $dbh->bz_commit_transaction; - $updated++; + indicate_progress({current => $current++, total => $total, every => 25}); + + # check payload + my $url = trim($attachment->{thedata}); + next if $url =~ /\s/; + next unless $url =~ m#^https://github\.com/[^/]+/[^/]+/pull/\d+\/?$#i; + + $dbh->bz_start_transaction; + + # set content-type + $dbh->do("UPDATE attachments SET mimetype = ? WHERE attach_id = ?", + undef, 'text/x-github-pull-request', $attachment->{attach_id}); + + # insert into bugs_activity + my $timestamp = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $dbh->do( + "INSERT INTO bugs_activity(bug_id, who, bug_when, fieldid, removed, added) + VALUES (?, ?, ?, ?, ?, ?)", undef, $attachment->{bug_id}, + $nobody->id, $timestamp, $field->id, $attachment->{mimetype}, + 'text/x-github-pull-request' + ); + $dbh->do("UPDATE bugs SET delta_ts = ?, lastdiffed = ? WHERE bug_id = ?", + undef, $timestamp, $timestamp, $attachment->{bug_id}); + + $dbh->bz_commit_transaction; + $updated++; } print "Attachments updated: $updated\n"; diff --git a/extensions/BMO/lib/Constants.pm b/extensions/BMO/lib/Constants.pm index 8227208c8..7ec92befb 100644 --- a/extensions/BMO/lib/Constants.pm +++ b/extensions/BMO/lib/Constants.pm @@ -13,8 +13,8 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - REQUEST_MAX_ATTACH_LINES - DEV_ENGAGE_DISCUSS_NEEDINFO + REQUEST_MAX_ATTACH_LINES + DEV_ENGAGE_DISCUSS_NEEDINFO ); # Maximum attachment size in lines that will be sent with a @@ -24,7 +24,7 @@ use constant REQUEST_MAX_ATTACH_LINES => 1000; # Requestees who need a needinfo flag set for the dev engagement # discussion bug use constant DEV_ENGAGE_DISCUSS_NEEDINFO => qw( - spersing@mozilla.com + spersing@mozilla.com ); 1; diff --git a/extensions/BMO/lib/Data.pm b/extensions/BMO/lib/Data.pm index 349f88093..a1e010346 100644 --- a/extensions/BMO/lib/Data.pm +++ b/extensions/BMO/lib/Data.pm @@ -15,13 +15,13 @@ use base qw(Exporter); use Tie::IxHash; our @EXPORT = qw( $cf_visible_in_products - %group_change_notification - $cf_setters - @always_fileable_groups - %group_auto_cc - %create_bug_formats - @default_named_queries - %autodetect_attach_urls ); + %group_change_notification + $cf_setters + @always_fileable_groups + %group_auto_cc + %create_bug_formats + @default_named_queries + %autodetect_attach_urls ); # Creating an attachment whose contents is a URL matching one of these regexes # will result in the user being redirected to that URL when viewing the @@ -43,35 +43,37 @@ my $mozreview_url_re = qr{ }ix; sub phabricator_url_re { - my $phab_uri = Bugzilla->params->{phabricator_base_uri} || 'https://example.com'; - return qr/^\Q${phab_uri}\ED\d+$/i; + my $phab_uri + = Bugzilla->params->{phabricator_base_uri} || 'https://example.com'; + return qr/^\Q${phab_uri}\ED\d+$/i; } our %autodetect_attach_urls = ( - github_pr => { - title => 'GitHub Pull Request', - regex => qr#^https://github\.com/[^/]+/[^/]+/pull/\d+/?$#i, - content_type => 'text/x-github-pull-request', - can_review => 1, - }, - reviewboard => { - title => 'MozReview', - regex => $mozreview_url_re, - content_type => 'text/x-review-board-request', - can_review => 0, - }, - Phabricator => { - title => 'Phabricator', - regex => \&phabricator_url_re, - content_type => 'text/x-phabricator-request', - can_review => 1, - }, - google_docs => { - title => 'Google Doc', - regex => qr#^https://docs\.google\.com/(?:document|spreadsheets|presentation)/d/#i, - content_type => 'text/x-google-doc', - can_review => 0, - }, + github_pr => { + title => 'GitHub Pull Request', + regex => qr#^https://github\.com/[^/]+/[^/]+/pull/\d+/?$#i, + content_type => 'text/x-github-pull-request', + can_review => 1, + }, + reviewboard => { + title => 'MozReview', + regex => $mozreview_url_re, + content_type => 'text/x-review-board-request', + can_review => 0, + }, + Phabricator => { + title => 'Phabricator', + regex => \&phabricator_url_re, + content_type => 'text/x-phabricator-request', + can_review => 1, + }, + google_docs => { + title => 'Google Doc', + regex => + qr#^https://docs\.google\.com/(?:document|spreadsheets|presentation)/d/#i, + content_type => 'text/x-google-doc', + can_review => 0, + }, ); # Which custom fields are visible in which products and components. @@ -83,209 +85,184 @@ our %autodetect_attach_urls = ( # # IxHash keeps them in insertion order, and so we get regexp priorities right. our $cf_visible_in_products; -tie(%$cf_visible_in_products, "Tie::IxHash", - qr/^cf_colo_site$/ => { - "mozilla.org" => [ - "Server Operations", - "Server Operations: DCOps", - "Server Operations: Projects", - "Server Operations: RelEng", - "Server Operations: Security", - ], - "Infrastructure & Operations" => [ - "RelOps", - "RelOps: Puppet", - "DCOps", - ], - }, - qr/^cf_office$/ => { - "mozilla.org" => ["Server Operations: Desktop Issues"], - }, - qr/^cf_crash_signature$/ => { - "Add-on SDK" => [], - "addons.mozilla.org" => [], - "Android Background Services" => [], - "B2GDroid" => [], - "Calendar" => [], - "Composer" => [], - "Core" => [], - "DevTools" => [], - "Directory" => [], - "External Software Affecting Firefox" => [], - "Firefox" => [], - "Firefox for Android" => [], - "GeckoView" => [], - "JSS" => [], - "MailNews Core" => [], - "Mozilla Labs" => [], - "Mozilla Localizations" => [], - "mozilla.org" => [], - "Cloud Services" => [], - "NSPR" => [], - "NSS" => [], - "Other Applications" => [], - "Penelope" => [], - "Release Engineering" => [], - "Rhino" => [], - "SeaMonkey" => [], - "Tamarin" => [], - "Tech Evangelism" => [], - "Testing" => [], - "Thunderbird" => [], - "Toolkit" => [], - "WebExtensions" => [], - }, - qr/^cf_due_date$/ => { - "bugzilla.mozilla.org" => [], - "Community Building" => [], - "Data & BI Services Team" => [], - "Data Compliance" => [], - "Developer Engagement" => [], - "Firefox" => ["Security: Review Requests"], - "Infrastructure & Operations" => [], - "Marketing" => [], - "mozilla.org" => ["Security Assurance: Review Request"], - "Mozilla Metrics" => [], - "Mozilla PR" => [], - "Mozilla Reps" => [], - }, - qr/^cf_locale$/ => { - "Mozilla Localizations" => ['Other'], - "www.mozilla.org" => [], - }, - qr/^cf_mozilla_project$/ => { - "Data & BI Services Team" => [], - }, - qr/^cf_machine_state$/ => { - "Release Engineering" => ["Buildduty"], - }, - qr/^cf_rank$/ => { - "Core" => [], - "Firefox for Android" => [], - "Firefox for iOS" => [], - "Firefox" => [], - "GeckoView" => [], - "Hello (Loop)" => [], - "Cloud Services" => [], - "Tech Evangelism" => [], - "Toolkit" => [], - }, - qr/^cf_has_regression_range$/ => { - "Core" => [], - "Firefox for Android" => [], - "Firefox for iOS" => [], - "Firefox" => [], - "GeckoView" => [], - "Toolkit" => [], - }, - qr/^cf_has_str$/ => { - "Core" => [], - "Firefox for Android" => [], - "Firefox for iOS" => [], - "Firefox" => [], - "GeckoView" => [], - "Toolkit" => [], - }, - qr/^cf_cab_review$/ => { - "Infrastructure & Operations Graveyard" => [], - "Infrastructure & Operations" => [], - "Data & BI Services Team" => [], - } +tie( + %$cf_visible_in_products, + "Tie::IxHash", + qr/^cf_colo_site$/ => { + "mozilla.org" => [ + "Server Operations", + "Server Operations: DCOps", + "Server Operations: Projects", + "Server Operations: RelEng", + "Server Operations: Security", + ], + "Infrastructure & Operations" => ["RelOps", "RelOps: Puppet", "DCOps",], + }, + qr/^cf_office$/ => {"mozilla.org" => ["Server Operations: Desktop Issues"],}, + qr/^cf_crash_signature$/ => { + "Add-on SDK" => [], + "addons.mozilla.org" => [], + "Android Background Services" => [], + "B2GDroid" => [], + "Calendar" => [], + "Composer" => [], + "Core" => [], + "DevTools" => [], + "Directory" => [], + "External Software Affecting Firefox" => [], + "Firefox" => [], + "Firefox for Android" => [], + "GeckoView" => [], + "JSS" => [], + "MailNews Core" => [], + "Mozilla Labs" => [], + "Mozilla Localizations" => [], + "mozilla.org" => [], + "Cloud Services" => [], + "NSPR" => [], + "NSS" => [], + "Other Applications" => [], + "Penelope" => [], + "Release Engineering" => [], + "Rhino" => [], + "SeaMonkey" => [], + "Tamarin" => [], + "Tech Evangelism" => [], + "Testing" => [], + "Thunderbird" => [], + "Toolkit" => [], + "WebExtensions" => [], + }, + qr/^cf_due_date$/ => { + "bugzilla.mozilla.org" => [], + "Community Building" => [], + "Data & BI Services Team" => [], + "Data Compliance" => [], + "Developer Engagement" => [], + "Firefox" => ["Security: Review Requests"], + "Infrastructure & Operations" => [], + "Marketing" => [], + "mozilla.org" => ["Security Assurance: Review Request"], + "Mozilla Metrics" => [], + "Mozilla PR" => [], + "Mozilla Reps" => [], + }, + qr/^cf_locale$/ => + {"Mozilla Localizations" => ['Other'], "www.mozilla.org" => [],}, + qr/^cf_mozilla_project$/ => {"Data & BI Services Team" => [],}, + qr/^cf_machine_state$/ => {"Release Engineering" => ["Buildduty"],}, + qr/^cf_rank$/ => { + "Core" => [], + "Firefox for Android" => [], + "Firefox for iOS" => [], + "Firefox" => [], + "GeckoView" => [], + "Hello (Loop)" => [], + "Cloud Services" => [], + "Tech Evangelism" => [], + "Toolkit" => [], + }, + qr/^cf_has_regression_range$/ => { + "Core" => [], + "Firefox for Android" => [], + "Firefox for iOS" => [], + "Firefox" => [], + "GeckoView" => [], + "Toolkit" => [], + }, + qr/^cf_has_str$/ => { + "Core" => [], + "Firefox for Android" => [], + "Firefox for iOS" => [], + "Firefox" => [], + "GeckoView" => [], + "Toolkit" => [], + }, + qr/^cf_cab_review$/ => { + "Infrastructure & Operations Graveyard" => [], + "Infrastructure & Operations" => [], + "Data & BI Services Team" => [], + } ); # Who to CC on particular bugmails when certain groups are added or removed. our %group_change_notification = ( - 'addons-security' => ['amo-editors@mozilla.org'], - 'b2g-core-security' => ['security@mozilla.org'], - 'bugzilla-security' => ['security@bugzilla.org'], - 'client-services-security' => ['amo-admins@mozilla.org', 'web-security@mozilla.org'], - 'cloud-services-security' => ['web-security@mozilla.org'], - 'core-security' => ['security@mozilla.org'], - 'crypto-core-security' => ['security@mozilla.org'], - 'dom-core-security' => ['security@mozilla.org'], - 'firefox-core-security' => ['security@mozilla.org'], - 'gfx-core-security' => ['security@mozilla.org'], - 'javascript-core-security' => ['security@mozilla.org'], - 'layout-core-security' => ['security@mozilla.org'], - 'mail-core-security' => ['security@mozilla.org'], - 'media-core-security' => ['security@mozilla.org'], - 'network-core-security' => ['security@mozilla.org'], - 'core-security-release' => ['security@mozilla.org'], - 'tamarin-security' => ['tamarinsecurity@adobe.com'], - 'toolkit-core-security' => ['security@mozilla.org'], - 'websites-security' => ['web-security@mozilla.org'], - 'webtools-security' => ['web-security@mozilla.org'], + 'addons-security' => ['amo-editors@mozilla.org'], + 'b2g-core-security' => ['security@mozilla.org'], + 'bugzilla-security' => ['security@bugzilla.org'], + 'client-services-security' => + ['amo-admins@mozilla.org', 'web-security@mozilla.org'], + 'cloud-services-security' => ['web-security@mozilla.org'], + 'core-security' => ['security@mozilla.org'], + 'crypto-core-security' => ['security@mozilla.org'], + 'dom-core-security' => ['security@mozilla.org'], + 'firefox-core-security' => ['security@mozilla.org'], + 'gfx-core-security' => ['security@mozilla.org'], + 'javascript-core-security' => ['security@mozilla.org'], + 'layout-core-security' => ['security@mozilla.org'], + 'mail-core-security' => ['security@mozilla.org'], + 'media-core-security' => ['security@mozilla.org'], + 'network-core-security' => ['security@mozilla.org'], + 'core-security-release' => ['security@mozilla.org'], + 'tamarin-security' => ['tamarinsecurity@adobe.com'], + 'toolkit-core-security' => ['security@mozilla.org'], + 'websites-security' => ['web-security@mozilla.org'], + 'webtools-security' => ['web-security@mozilla.org'], ); # Who can set custom flags (use full field names only, not regex's) -our $cf_setters = { - 'cf_colo_site' => [ 'infra', 'build' ], - 'cf_rank' => [ 'rank-setters' ], -}; +our $cf_setters + = {'cf_colo_site' => ['infra', 'build'], 'cf_rank' => ['rank-setters'],}; # Groups in which you can always file a bug, regardless of product or user. our @always_fileable_groups = qw( - addons-security - bugzilla-security - client-services-security - consulting - core-security - finance - infra - infrasec - l20n-security - marketing-private - mozilla-confidential - mozilla-employee-confidential - mozilla-foundation-confidential - mozilla-engagement - mozilla-messaging-confidential - partner-confidential - payments-confidential - tamarin-security - websites-security - webtools-security + addons-security + bugzilla-security + client-services-security + consulting + core-security + finance + infra + infrasec + l20n-security + marketing-private + mozilla-confidential + mozilla-employee-confidential + mozilla-foundation-confidential + mozilla-engagement + mozilla-messaging-confidential + partner-confidential + payments-confidential + tamarin-security + websites-security + webtools-security ); # Automatically CC users to bugs filed into configured groups and products our %group_auto_cc = ( - 'partner-confidential' => { - 'Marketing' => ['jbalaco@mozilla.com'], - '_default' => ['mbest@mozilla.com'], - }, + 'partner-confidential' => { + 'Marketing' => ['jbalaco@mozilla.com'], + '_default' => ['mbest@mozilla.com'], + }, ); # Force create-bug template by product # Users in 'include' group will be forced into using the form. our %create_bug_formats = ( - 'Data Compliance' => { - 'format' => 'data-compliance', - 'include' => 'everyone', - }, - 'developer.mozilla.org' => { - 'format' => 'mdn', - 'include' => 'everyone', - }, - 'Legal' => { - 'format' => 'legal', - 'include' => 'everyone', - }, - 'Recruiting' => { - 'format' => 'recruiting', - 'include' => 'everyone', - }, - 'Internet Public Policy' => { - 'format' => 'ipp', - 'include' => 'everyone', - }, + 'Data Compliance' => {'format' => 'data-compliance', 'include' => 'everyone',}, + 'developer.mozilla.org' => {'format' => 'mdn', 'include' => 'everyone',}, + 'Legal' => {'format' => 'legal', 'include' => 'everyone',}, + 'Recruiting' => {'format' => 'recruiting', 'include' => 'everyone',}, + 'Internet Public Policy' => {'format' => 'ipp', 'include' => 'everyone',}, ); # List of named queries which will be added to new users' footer our @default_named_queries = ( - { - name => 'Bugs Filed Today', - query => 'query_format=advanced&chfieldto=Now&chfield=[Bug creation]&chfieldfrom=-24h&order=bug_id', - }, + { + name => 'Bugs Filed Today', + query => + 'query_format=advanced&chfieldto=Now&chfield=[Bug creation]&chfieldfrom=-24h&order=bug_id', + }, ); 1; diff --git a/extensions/BMO/lib/FakeBug.pm b/extensions/BMO/lib/FakeBug.pm index f84835ddd..5b5395619 100644 --- a/extensions/BMO/lib/FakeBug.pm +++ b/extensions/BMO/lib/FakeBug.pm @@ -12,32 +12,32 @@ use Bugzilla::Bug; our $AUTOLOAD; sub new { - my $class = shift; - my $self = shift; - bless $self, $class; - return $self; + my $class = shift; + my $self = shift; + bless $self, $class; + return $self; } sub AUTOLOAD { - my $self = shift; - my $name = $AUTOLOAD; - $name =~ s/.*://; - return exists $self->{$name} ? $self->{$name} : undef; + my $self = shift; + my $name = $AUTOLOAD; + $name =~ s/.*://; + return exists $self->{$name} ? $self->{$name} : undef; } sub check_can_change_field { - my $self = shift; - return Bugzilla::Bug::check_can_change_field($self, @_) + my $self = shift; + return Bugzilla::Bug::check_can_change_field($self, @_); } sub _changes_everconfirmed { - my $self = shift; - return Bugzilla::Bug::_changes_everconfirmed($self, @_) + my $self = shift; + return Bugzilla::Bug::_changes_everconfirmed($self, @_); } sub everconfirmed { - my $self = shift; - return ($self->{'status'} == 'UNCONFIRMED') ? 0 : 1; + my $self = shift; + return ($self->{'status'} == 'UNCONFIRMED') ? 0 : 1; } 1; diff --git a/extensions/BMO/lib/Reports/Groups.pm b/extensions/BMO/lib/Reports/Groups.pm index ce7df767c..7b395aca9 100644 --- a/extensions/BMO/lib/Reports/Groups.pm +++ b/extensions/BMO/lib/Reports/Groups.pm @@ -19,25 +19,24 @@ use Bugzilla::Util qw(trim datetime_from); use JSON qw(encode_json); sub admins_report { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - ($user->in_group('editbugs')) - || ThrowUserError('auth_failure', { group => 'editbugs', - action => 'run', - object => 'group_admins' }); + ($user->in_group('editbugs')) + || ThrowUserError('auth_failure', + {group => 'editbugs', action => 'run', object => 'group_admins'}); - my @grouplist = - ($user->in_group('editusers') || $user->in_group('infrasec')) - ? map { lc($_->name) } Bugzilla::Group->get_all - : _get_permitted_membership_groups(); + my @grouplist + = ($user->in_group('editusers') || $user->in_group('infrasec')) + ? map { lc($_->name) } Bugzilla::Group->get_all + : _get_permitted_membership_groups(); - my $groups = join(',', map { $dbh->quote($_) } @grouplist); + my $groups = join(',', map { $dbh->quote($_) } @grouplist); - my $query = " - SELECT groups.id, " . - $dbh->sql_group_concat('profiles.userid', "','", 1) . " + my $query = " + SELECT groups.id, " + . $dbh->sql_group_concat('profiles.userid', "','", 1) . " FROM groups LEFT JOIN user_group_map ON user_group_map.group_id = groups.id @@ -49,271 +48,275 @@ sub admins_report { AND groups.name IN ($groups) GROUP BY groups.name"; - my @groups; - foreach my $row (@{ $dbh->selectall_arrayref($query) }) { - my $group = Bugzilla::Group->new({ id => shift @$row, cache => 1}); - my @admins; - if (my $admin_ids = shift @$row) { - foreach my $uid (split(/,/, $admin_ids)) { - push(@admins, Bugzilla::User->new({ id => $uid, cache => 1 })); - } - } - push(@groups, { name => $group->name, - description => $group->description, - owner => $group->owner, - admins => \@admins }); + my @groups; + foreach my $row (@{$dbh->selectall_arrayref($query)}) { + my $group = Bugzilla::Group->new({id => shift @$row, cache => 1}); + my @admins; + if (my $admin_ids = shift @$row) { + foreach my $uid (split(/,/, $admin_ids)) { + push(@admins, Bugzilla::User->new({id => $uid, cache => 1})); + } } + push( + @groups, + { + name => $group->name, + description => $group->description, + owner => $group->owner, + admins => \@admins + } + ); + } - $vars->{'groups'} = \@groups; + $vars->{'groups'} = \@groups; } sub membership_report { - my ($page, $vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $cgi = Bugzilla->cgi; - - ($user->in_group('editusers') || $user->in_group('infrasec')) - || ThrowUserError('auth_failure', { group => 'editusers', - action => 'run', - object => 'group_admins' }); - - my $who = $cgi->param('who'); - if (!defined($who) || $who eq '') { - if ($page eq 'group_membership.txt') { - print $cgi->redirect("page.cgi?id=group_membership.html&output=txt"); - exit; - } - $vars->{'output'} = $cgi->param('output'); - return; + my ($page, $vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + + ($user->in_group('editusers') || $user->in_group('infrasec')) + || ThrowUserError('auth_failure', + {group => 'editusers', action => 'run', object => 'group_admins'}); + + my $who = $cgi->param('who'); + if (!defined($who) || $who eq '') { + if ($page eq 'group_membership.txt') { + print $cgi->redirect("page.cgi?id=group_membership.html&output=txt"); + exit; } + $vars->{'output'} = $cgi->param('output'); + return; + } - Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} }); - $who = Bugzilla->input_params->{'who'}; - $who = ref($who) ? $who : [ $who ]; + Bugzilla::User::match_field({'who' => {'type' => 'multi'}}); + $who = Bugzilla->input_params->{'who'}; + $who = ref($who) ? $who : [$who]; - my @users; - foreach my $login (@$who) { - my $u = Bugzilla::User->new(login_to_id($login, 1)); + my @users; + foreach my $login (@$who) { + my $u = Bugzilla::User->new(login_to_id($login, 1)); - # this is lifted from $user->groups() - # we need to show which groups are direct and which are inherited + # this is lifted from $user->groups() + # we need to show which groups are direct and which are inherited - my $groups_to_check = $dbh->selectcol_arrayref( - q{SELECT DISTINCT group_id + my $groups_to_check = $dbh->selectcol_arrayref( + q{SELECT DISTINCT group_id FROM user_group_map - WHERE user_id = ? AND isbless = 0}, undef, $u->id); + WHERE user_id = ? AND isbless = 0}, undef, $u->id + ); - my $rows = $dbh->selectall_arrayref( - "SELECT DISTINCT grantor_id, member_id + my $rows = $dbh->selectall_arrayref( + "SELECT DISTINCT grantor_id, member_id FROM group_group_map - WHERE grant_type = " . GROUP_MEMBERSHIP); + WHERE grant_type = " . GROUP_MEMBERSHIP + ); - my %group_membership; - foreach my $row (@$rows) { - my ($grantor_id, $member_id) = @$row; - push (@{ $group_membership{$member_id} }, $grantor_id); - } + my %group_membership; + foreach my $row (@$rows) { + my ($grantor_id, $member_id) = @$row; + push(@{$group_membership{$member_id}}, $grantor_id); + } - my %checked_groups; - my %direct_groups; - my %indirect_groups; - my %groups; + my %checked_groups; + my %direct_groups; + my %indirect_groups; + my %groups; - foreach my $member_id (@$groups_to_check) { - $direct_groups{$member_id} = 1; - } + foreach my $member_id (@$groups_to_check) { + $direct_groups{$member_id} = 1; + } - while (scalar(@$groups_to_check) > 0) { - my $member_id = shift @$groups_to_check; - if (!$checked_groups{$member_id}) { - $checked_groups{$member_id} = 1; - my $members = $group_membership{$member_id}; - my @new_to_check = grep(!$checked_groups{$_}, @$members); - push(@$groups_to_check, @new_to_check); - foreach my $id (@new_to_check) { - $indirect_groups{$id} = $member_id; - } - $groups{$member_id} = 1; - } + while (scalar(@$groups_to_check) > 0) { + my $member_id = shift @$groups_to_check; + if (!$checked_groups{$member_id}) { + $checked_groups{$member_id} = 1; + my $members = $group_membership{$member_id}; + my @new_to_check = grep(!$checked_groups{$_}, @$members); + push(@$groups_to_check, @new_to_check); + foreach my $id (@new_to_check) { + $indirect_groups{$id} = $member_id; } + $groups{$member_id} = 1; + } + } - my @groups; - my $ra_groups = Bugzilla::Group->new_from_list([keys %groups]); - foreach my $group (@$ra_groups) { - my $via; - if ($direct_groups{$group->id}) { - $via = ''; - } else { - foreach my $g (@$ra_groups) { - if ($g->id == $indirect_groups{$group->id}) { - $via = $g->name; - last; - } - } - } - push @groups, { - name => $group->name, - desc => $group->description, - via => $via, - }; + my @groups; + my $ra_groups = Bugzilla::Group->new_from_list([keys %groups]); + foreach my $group (@$ra_groups) { + my $via; + if ($direct_groups{$group->id}) { + $via = ''; + } + else { + foreach my $g (@$ra_groups) { + if ($g->id == $indirect_groups{$group->id}) { + $via = $g->name; + last; + } } - - push @users, { - user => $u, - groups => \@groups, - }; + } + push @groups, {name => $group->name, desc => $group->description, via => $via,}; } - $vars->{'who'} = $who; - $vars->{'users'} = \@users; + push @users, {user => $u, groups => \@groups,}; + } + + $vars->{'who'} = $who; + $vars->{'users'} = \@users; } sub members_report { - my ($page, $vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $cgi = Bugzilla->cgi; - - ($user->in_group('editbugs')) - || ThrowUserError('auth_failure', { group => 'editbugs', - action => 'run', - object => 'group_admins' }); - - my $privileged = $user->in_group('editusers') || $user->in_group('infrasec'); - $vars->{privileged} = $privileged; - - my @grouplist = $privileged - ? map { lc($_->name) } Bugzilla::Group->get_all - : _get_permitted_membership_groups(); - - my $include_disabled = $cgi->param('include_disabled') ? 1 : 0; - $vars->{'include_disabled'} = $include_disabled; - - # don't allow all groups, to avoid putting pain on the servers - my @group_names = - sort - grep { !/^(?:bz_.+|canconfirm|editbugs|editbugs-team|everyone)$/ } - @grouplist; - unshift(@group_names, ''); - $vars->{'groups'} = \@group_names; - - # load selected group - my $group = lc(trim($cgi->param('group') || '')); - $group = '' unless grep { $_ eq $group } @group_names; - return if $group eq ''; - my $group_obj = Bugzilla::Group->new({ name => $group }); - $vars->{'group'} = $group_obj; - - $vars->{'privileged'} = 1 if ($group_obj->owner && $group_obj->owner->id == $user->id); - - my @types; - my $members = $group_obj->members_complete(); - foreach my $name (sort keys %$members) { - push @types, { - name => ($name eq '_direct' ? 'direct' : $name), - members => _filter_userlist($members->{$name}), + my ($page, $vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + + ($user->in_group('editbugs')) + || ThrowUserError('auth_failure', + {group => 'editbugs', action => 'run', object => 'group_admins'}); + + my $privileged = $user->in_group('editusers') || $user->in_group('infrasec'); + $vars->{privileged} = $privileged; + + my @grouplist + = $privileged + ? map { lc($_->name) } Bugzilla::Group->get_all + : _get_permitted_membership_groups(); + + my $include_disabled = $cgi->param('include_disabled') ? 1 : 0; + $vars->{'include_disabled'} = $include_disabled; + + # don't allow all groups, to avoid putting pain on the servers + my @group_names + = sort grep { !/^(?:bz_.+|canconfirm|editbugs|editbugs-team|everyone)$/ } + @grouplist; + unshift(@group_names, ''); + $vars->{'groups'} = \@group_names; + + # load selected group + my $group = lc(trim($cgi->param('group') || '')); + $group = '' unless grep { $_ eq $group } @group_names; + return if $group eq ''; + my $group_obj = Bugzilla::Group->new({name => $group}); + $vars->{'group'} = $group_obj; + + $vars->{'privileged'} = 1 + if ($group_obj->owner && $group_obj->owner->id == $user->id); + + my @types; + my $members = $group_obj->members_complete(); + foreach my $name (sort keys %$members) { + push @types, + { + name => ($name eq '_direct' ? 'direct' : $name), + members => _filter_userlist($members->{$name}), + }; + } + + # make it easy for the template to detect an empty group + my $has_members = 0; + foreach my $type (@types) { + $has_members += scalar(@{$type->{members}}); + last if $has_members; + } + @types = () unless $has_members; + + if ($page eq 'group_members.json') { + my %users; + foreach my $rh (@types) { + foreach my $member (@{$rh->{members}}) { + my $login = $member->login; + if (exists $users{$login}) { + push @{$users{$login}->{groups}}, $rh->{name} if $privileged; } - } - - # make it easy for the template to detect an empty group - my $has_members = 0; - foreach my $type (@types) { - $has_members += scalar(@{ $type->{members} }); - last if $has_members; - } - @types = () unless $has_members; - - if ($page eq 'group_members.json') { - my %users; - foreach my $rh (@types) { - foreach my $member (@{ $rh->{members} }) { - my $login = $member->login; - if (exists $users{$login}) { - push @{ $users{$login}->{groups} }, $rh->{name} if $privileged; - } - else { - my $rh_user = { - login => $login, - membership => $rh->{name} eq 'direct' ? 'direct' : 'indirect', - rh_name => $rh->{name}, - }; - if ($privileged) { - $rh_user->{group} = $rh->{name}; - $rh_user->{groups} = [ $rh->{name} ]; - $rh_user->{lastseeon} = $member->last_seen_date; - $rh_user->{mfa} = $member->mfa; - $rh_user->{api_key_only} = $member->settings->{api_key_only}->{value} eq 'on' - ? JSON::true : JSON::false; - } - $users{$login} = $rh_user; - } - } + else { + my $rh_user = { + login => $login, + membership => $rh->{name} eq 'direct' ? 'direct' : 'indirect', + rh_name => $rh->{name}, + }; + if ($privileged) { + $rh_user->{group} = $rh->{name}; + $rh_user->{groups} = [$rh->{name}]; + $rh_user->{lastseeon} = $member->last_seen_date; + $rh_user->{mfa} = $member->mfa; + $rh_user->{api_key_only} + = $member->settings->{api_key_only}->{value} eq 'on' + ? JSON::true + : JSON::false; + } + $users{$login} = $rh_user; } - $vars->{types_json} = JSON->new->pretty->canonical->utf8->encode([ values %users ]); + } } - else { - my %users; - foreach my $rh (@types) { - foreach my $member (@{ $rh->{members} }) { - $users{$member->login} = 1 unless exists $users{$member->login}; - } - } - $vars->{types} = \@types; - $vars->{count} = scalar(keys %users); + $vars->{types_json} + = JSON->new->pretty->canonical->utf8->encode([values %users]); + } + else { + my %users; + foreach my $rh (@types) { + foreach my $member (@{$rh->{members}}) { + $users{$member->login} = 1 unless exists $users{$member->login}; + } } + $vars->{types} = \@types; + $vars->{count} = scalar(keys %users); + } } sub _filter_userlist { - my ($list, $include_disabled) = @_; - $list = [ grep { $_->is_enabled } @$list ] unless $include_disabled; - my $now = DateTime->now(); - my $never = DateTime->from_epoch( epoch => 0 ); - foreach my $user (@$list) { - my $last_seen = $user->last_seen_date ? datetime_from($user->last_seen_date) : $never; - $user->{last_seen_days} = sprintf( - '%.0f', - $now->subtract_datetime_absolute($last_seen)->delta_seconds / (28 * 60 * 60)); - } - return [ sort { lc($a->identity) cmp lc($b->identity) } @$list ]; + my ($list, $include_disabled) = @_; + $list = [grep { $_->is_enabled } @$list] unless $include_disabled; + my $now = DateTime->now(); + my $never = DateTime->from_epoch(epoch => 0); + foreach my $user (@$list) { + my $last_seen + = $user->last_seen_date ? datetime_from($user->last_seen_date) : $never; + $user->{last_seen_days} = sprintf('%.0f', + $now->subtract_datetime_absolute($last_seen)->delta_seconds / (28 * 60 * 60)); + } + return [sort { lc($a->identity) cmp lc($b->identity) } @$list]; } # Groups that any user with editbugs can see the membership or admin lists for. # Transparency FTW. sub _get_permitted_membership_groups { - my $user = Bugzilla->user; - - # Default publicly viewable groups - my %default_public_groups = map { $_ => 1 } qw( - bugzilla-approvers - bugzilla-reviewers - can_restrict_comments - community-it-team - mozilla-employee-confidential - mozilla-foundation-confidential - mozilla-reps - qa-approvers - ); - - # We add the group to the permitted list if: - # 1. it is a drivers group - this gives us a little - # future-proofing - # 2. it is a one of the default public groups - # 3. the user is the group's owner - # 4. or the user can bless others into the group - my @permitted_groups; - foreach my $group (Bugzilla::Group->get_all) { - my $name = $group->name; - if ($name =~ /-drivers$/ - || exists $default_public_groups{$name} - || ($group->owner && $group->owner->id == $user->id) - || $user->can_bless($group->id)) - { - push(@permitted_groups, $name); - } + my $user = Bugzilla->user; + + # Default publicly viewable groups + my %default_public_groups = map { $_ => 1 } qw( + bugzilla-approvers + bugzilla-reviewers + can_restrict_comments + community-it-team + mozilla-employee-confidential + mozilla-foundation-confidential + mozilla-reps + qa-approvers + ); + + # We add the group to the permitted list if: + # 1. it is a drivers group - this gives us a little + # future-proofing + # 2. it is a one of the default public groups + # 3. the user is the group's owner + # 4. or the user can bless others into the group + my @permitted_groups; + foreach my $group (Bugzilla::Group->get_all) { + my $name = $group->name; + if ( $name =~ /-drivers$/ + || exists $default_public_groups{$name} + || ($group->owner && $group->owner->id == $user->id) + || $user->can_bless($group->id)) + { + push(@permitted_groups, $name); } + } - return @permitted_groups; + return @permitted_groups; } 1; diff --git a/extensions/BMO/lib/Reports/Internship.pm b/extensions/BMO/lib/Reports/Internship.pm index 2dfa583a6..f9ad1a578 100644 --- a/extensions/BMO/lib/Reports/Internship.pm +++ b/extensions/BMO/lib/Reports/Internship.pm @@ -17,33 +17,30 @@ use Bugzilla::Product; use Bugzilla::Component; sub report { - my ($vars) = @_; - my $user = Bugzilla->user; - - $user->in_group('hr') - || ThrowUserError('auth_failure', { group => 'hr', - action => 'run', - object => 'internship_dashboard' }); - - my $product = Bugzilla::Product->check({ name => 'Recruiting', cache => 1 }); - my $component = Bugzilla::Component->new({ product => $product, name => 'Intern', cache => 1 }); - - # find all open internship bugs - my $bugs = Bugzilla::Bug->match({ - product_id => $product->id, - component_id => $component->id, - resolution => '', - }); - - # filter bugs based on visibility and re-bless - $user->visible_bugs($bugs); - $bugs = [ - map { bless($_, 'InternshipBug') } - grep { $user->can_see_bug($_->id) } - @$bugs - ]; - - $vars->{bugs} = $bugs; + my ($vars) = @_; + my $user = Bugzilla->user; + + $user->in_group('hr') + || ThrowUserError('auth_failure', + {group => 'hr', action => 'run', object => 'internship_dashboard'}); + + my $product = Bugzilla::Product->check({name => 'Recruiting', cache => 1}); + my $component = Bugzilla::Component->new( + {product => $product, name => 'Intern', cache => 1}); + + # find all open internship bugs + my $bugs = Bugzilla::Bug->match({ + product_id => $product->id, + component_id => $component->id, + resolution => '', + }); + + # filter bugs based on visibility and re-bless + $user->visible_bugs($bugs); + $bugs = [map { bless($_, 'InternshipBug') } + grep { $user->can_see_bug($_->id) } @$bugs]; + + $vars->{bugs} = $bugs; } 1; @@ -58,64 +55,62 @@ use Bugzilla::Comment; use Bugzilla::Util qw(trim); sub _extract { - my ($self) = @_; - return if exists $self->{internship_data}; - $self->{internship_data} = {}; - - # we only need the first comment - my $comment = Bugzilla::Comment->match({ - bug_id => $self->id, - LIMIT => 1, - })->[0]->body; - - # extract just what we need - # changing the comment will break this - - if ($comment =~ /Hiring Manager:\s+(.+)\nTeam:\n/s) { - $self->{internship_data}->{hiring_manager} = trim($1); - } - if ($comment =~ /\nVP Authority:\s+(.+)\nProduct Line:\n/s) { - $self->{internship_data}->{scvp} = trim($1); - } - if ($comment =~ /\nProduct Line:\s+(.+)\nLevel 1/s) { - $self->{internship_data}->{product_line} = trim($1); - } - if ($comment =~ /\nBusiness Need:\s+(.+)\nPotential Project:\n/s) { - $self->{internship_data}->{business_need} = trim($1); - } - if ($comment =~ /\nName:\s+(.+)$/s) { - $self->{internship_data}->{intern_name} = trim($1); - } + my ($self) = @_; + return if exists $self->{internship_data}; + $self->{internship_data} = {}; + + # we only need the first comment + my $comment + = Bugzilla::Comment->match({bug_id => $self->id, LIMIT => 1,})->[0]->body; + + # extract just what we need + # changing the comment will break this + + if ($comment =~ /Hiring Manager:\s+(.+)\nTeam:\n/s) { + $self->{internship_data}->{hiring_manager} = trim($1); + } + if ($comment =~ /\nVP Authority:\s+(.+)\nProduct Line:\n/s) { + $self->{internship_data}->{scvp} = trim($1); + } + if ($comment =~ /\nProduct Line:\s+(.+)\nLevel 1/s) { + $self->{internship_data}->{product_line} = trim($1); + } + if ($comment =~ /\nBusiness Need:\s+(.+)\nPotential Project:\n/s) { + $self->{internship_data}->{business_need} = trim($1); + } + if ($comment =~ /\nName:\s+(.+)$/s) { + $self->{internship_data}->{intern_name} = trim($1); + } } sub hiring_manager { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{hiring_manager}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{hiring_manager}; } sub scvp { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{scvp}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{scvp}; } sub business_need { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{business_need}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{business_need}; } sub product_line { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{product_line}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{product_line}; } sub intern_name { - my ($self) = @_; - $self->_extract(); - return $self->{internship_data}->{intern_name}; + my ($self) = @_; + $self->_extract(); + return $self->{internship_data}->{intern_name}; } 1; diff --git a/extensions/BMO/lib/Reports/ProductSecurity.pm b/extensions/BMO/lib/Reports/ProductSecurity.pm index e7ccda171..fb773cd93 100644 --- a/extensions/BMO/lib/Reports/ProductSecurity.pm +++ b/extensions/BMO/lib/Reports/ProductSecurity.pm @@ -16,54 +16,54 @@ use Bugzilla::Error; use Bugzilla::Product; sub report { - my ($vars) = @_; - my $user = Bugzilla->user; + my ($vars) = @_; + my $user = Bugzilla->user; - ($user->in_group('admin') || $user->in_group('infrasec')) - || ThrowUserError('auth_failure', { group => 'admin', - action => 'run', - object => 'product_security' }); + ($user->in_group('admin') || $user->in_group('infrasec')) + || ThrowUserError('auth_failure', + {group => 'admin', action => 'run', object => 'product_security'}); - my $moco = Bugzilla::Group->new({ name => 'mozilla-employee-confidential' }) - or return; + my $moco = Bugzilla::Group->new({name => 'mozilla-employee-confidential'}) + or return; - my $products = []; - foreach my $product (@{ Bugzilla::Product->match({}) }) { - my $default_group = $product->default_security_group_obj; - my $group_controls = $product->group_controls(); + my $products = []; + foreach my $product (@{Bugzilla::Product->match({})}) { + my $default_group = $product->default_security_group_obj; + my $group_controls = $product->group_controls(); - my $item = { - name => $product->name, - default_security_group => $product->default_security_group, - group_visibility => 'None/None', - moco => exists $group_controls->{$moco->id}, - }; + my $item = { + name => $product->name, + default_security_group => $product->default_security_group, + group_visibility => 'None/None', + moco => exists $group_controls->{$moco->id}, + }; - if ($default_group) { - if (my $control = $group_controls->{$default_group->id}) { - $item->{group_visibility} = control_to_string($control->{membercontrol}) . - '/' . control_to_string($control->{othercontrol}); - } - } + if ($default_group) { + if (my $control = $group_controls->{$default_group->id}) { + $item->{group_visibility} = control_to_string($control->{membercontrol}) . '/' + . control_to_string($control->{othercontrol}); + } + } - $item->{group_problem} = $default_group ? '' : "Invalid group " . $product->default_security_group; - $item->{visibility_problem} = 'Default security group should be Shown/Shown' - if ($item->{group_visibility} ne 'Shown/Shown') - && ($item->{group_visibility} ne 'Mandatory/Mandatory') - && ($item->{group_visibility} ne 'Default/Default'); + $item->{group_problem} + = $default_group ? '' : "Invalid group " . $product->default_security_group; + $item->{visibility_problem} = 'Default security group should be Shown/Shown' + if ($item->{group_visibility} ne 'Shown/Shown') + && ($item->{group_visibility} ne 'Mandatory/Mandatory') + && ($item->{group_visibility} ne 'Default/Default'); - push @$products, $item; - } - $vars->{products} = $products; + push @$products, $item; + } + $vars->{products} = $products; } sub control_to_string { - my ($control) = @_; - return 'NA' if $control == CONTROLMAPNA; - return 'Shown' if $control == CONTROLMAPSHOWN; - return 'Default' if $control == CONTROLMAPDEFAULT; - return 'Mandatory' if $control == CONTROLMAPMANDATORY; - return ''; + my ($control) = @_; + return 'NA' if $control == CONTROLMAPNA; + return 'Shown' if $control == CONTROLMAPSHOWN; + return 'Default' if $control == CONTROLMAPDEFAULT; + return 'Mandatory' if $control == CONTROLMAPMANDATORY; + return ''; } 1; diff --git a/extensions/BMO/lib/Reports/Recruiting.pm b/extensions/BMO/lib/Reports/Recruiting.pm index 39eb8327d..c35b0cbff 100644 --- a/extensions/BMO/lib/Reports/Recruiting.pm +++ b/extensions/BMO/lib/Reports/Recruiting.pm @@ -17,33 +17,30 @@ use Bugzilla::Product; use Bugzilla::Component; sub report { - my ($vars) = @_; - my $user = Bugzilla->user; - - $user->in_group('hr') - || ThrowUserError('auth_failure', { group => 'hr', - action => 'run', - object => 'recruiting_dashboard' }); - - my $product = Bugzilla::Product->check({ name => 'Recruiting', cache => 1 }); - my $component = Bugzilla::Component->new({ product => $product, name => 'General', cache => 1 }); - - # find all open recruiting bugs - my $bugs = Bugzilla::Bug->match({ - product_id => $product->id, - component_id => $component->id, - resolution => '', - }); - - # filter bugs based on visibility and re-bless - $user->visible_bugs($bugs); - $bugs = [ - map { bless($_, 'RecruitingBug') } - grep { $user->can_see_bug($_->id) } - @$bugs - ]; - - $vars->{bugs} = $bugs; + my ($vars) = @_; + my $user = Bugzilla->user; + + $user->in_group('hr') + || ThrowUserError('auth_failure', + {group => 'hr', action => 'run', object => 'recruiting_dashboard'}); + + my $product = Bugzilla::Product->check({name => 'Recruiting', cache => 1}); + my $component = Bugzilla::Component->new( + {product => $product, name => 'General', cache => 1}); + + # find all open recruiting bugs + my $bugs = Bugzilla::Bug->match({ + product_id => $product->id, + component_id => $component->id, + resolution => '', + }); + + # filter bugs based on visibility and re-bless + $user->visible_bugs($bugs); + $bugs = [map { bless($_, 'RecruitingBug') } + grep { $user->can_see_bug($_->id) } @$bugs]; + + $vars->{bugs} = $bugs; } 1; @@ -58,55 +55,56 @@ use Bugzilla::Comment; use Bugzilla::Util qw(trim); sub _extract { - my ($self) = @_; - return if exists $self->{recruitment_data}; - $self->{recruitment_data} = {}; - - # we only need the first comment - my $comment = Bugzilla::Comment->match({ - bug_id => $self->id, - LIMIT => 1, - })->[0]->body; - - # extract just what we need - # changing the comment will break this - - if ($comment =~ /\nHiring Manager:\s+(.+)VP Authority:\n/s) { - $self->{recruitment_data}->{hiring_manager} = trim($1); - } - if ($comment =~ /\nVP Authority:\s+(.+)HRBP:\n/s) { - $self->{recruitment_data}->{scvp} = trim($1); - } - if ($comment =~ /\nWhat part of your strategic plan does this role impact\?\s+(.+)Why is this critical for success\?\n/s) { - $self->{recruitment_data}->{strategic_plan} = trim($1); - } - if ($comment =~ /\nWhy is this critical for success\?\s+(.+)$/s) { - $self->{recruitment_data}->{why_critical} = trim($1); - } + my ($self) = @_; + return if exists $self->{recruitment_data}; + $self->{recruitment_data} = {}; + + # we only need the first comment + my $comment + = Bugzilla::Comment->match({bug_id => $self->id, LIMIT => 1,})->[0]->body; + + # extract just what we need + # changing the comment will break this + + if ($comment =~ /\nHiring Manager:\s+(.+)VP Authority:\n/s) { + $self->{recruitment_data}->{hiring_manager} = trim($1); + } + if ($comment =~ /\nVP Authority:\s+(.+)HRBP:\n/s) { + $self->{recruitment_data}->{scvp} = trim($1); + } + if ($comment + =~ /\nWhat part of your strategic plan does this role impact\?\s+(.+)Why is this critical for success\?\n/s + ) + { + $self->{recruitment_data}->{strategic_plan} = trim($1); + } + if ($comment =~ /\nWhy is this critical for success\?\s+(.+)$/s) { + $self->{recruitment_data}->{why_critical} = trim($1); + } } sub hiring_manager { - my ($self) = @_; - $self->_extract(); - return $self->{recruitment_data}->{hiring_manager}; + my ($self) = @_; + $self->_extract(); + return $self->{recruitment_data}->{hiring_manager}; } sub scvp { - my ($self) = @_; - $self->_extract(); - return $self->{recruitment_data}->{scvp}; + my ($self) = @_; + $self->_extract(); + return $self->{recruitment_data}->{scvp}; } sub strategic_plan { - my ($self) = @_; - $self->_extract(); - return $self->{recruitment_data}->{strategic_plan}; + my ($self) = @_; + $self->_extract(); + return $self->{recruitment_data}->{strategic_plan}; } sub why_critical { - my ($self) = @_; - $self->_extract(); - return $self->{recruitment_data}->{why_critical}; + my ($self) = @_; + $self->_extract(); + return $self->{recruitment_data}->{why_critical}; } 1; diff --git a/extensions/BMO/lib/Reports/ReleaseTracking.pm b/extensions/BMO/lib/Reports/ReleaseTracking.pm index 9fba1e14b..38a07aee7 100644 --- a/extensions/BMO/lib/Reports/ReleaseTracking.pm +++ b/extensions/BMO/lib/Reports/ReleaseTracking.pm @@ -21,496 +21,381 @@ use JSON qw(-convert_blessed_universally); use List::MoreUtils qw(uniq); use constant DATE_RANGES => [ - { - value => '20160126-20160307', - label => '2016-01-26 and 2016-03-07' - }, - { - value => '20151215-20160125', - label => '2015-12-15 and 2016-01-25' - }, - { - value => '20151103-20151214', - label => '2015-11-03 and 2015-12-14' - }, - { - value => '20150922-20151102', - label => '2015-09-22 and 2015-11-02' - }, - { - value => '20150811-20150921', - label => '2015-08-11 and 2015-09-21' - }, - { - value => '20150630-20150810', - label => '2015-06-30 and 2015-08-10' - }, - { - value => '20150512-20150629', - label => '2015-05-12 and 2015-06-29' - }, - { - value => '20150331-20150511', - label => '2015-03-31 and 2015-05-11' - }, - { - value => '20150224-20150330', - label => '2015-02-24 and 2015-03-30' - }, - { - value => '20150113-20150223', - label => '2015-01-13 and 2015-02-23' - }, - { - value => '20141111-20141222', - label => '2014-11-11 and 2014-12-22' - }, - { - value => '20140930-20141110', - label => '2014-09-30 and 2014-11-10' - }, - { - value => '20140819-20140929', - label => '2014-08-19 and 2014-09-29' - }, - { - value => '20140708-20140818', - label => '2014-07-08 and 2014-08-18' - }, - { - value => '20140527-20140707', - label => '2014-05-27 and 2014-07-07' - }, - { - value => '20140415-20140526', - label => '2014-04-15 and 2014-05-26' - }, - { - value => '20140304-20140414', - label => '2014-03-04 and 2014-04-14' - }, - { - value => '20140121-20140303', - label => '2014-01-21 and 2014-03-03' - }, - { - value => '20131210-20140120', - label => '2013-12-10 and 2014-01-20' - }, - { - value => '20131029-20131209', - label => '2013-10-29 and 2013-12-09' - }, - { - value => '20130917-20131028', - label => '2013-09-17 and 2013-10-28' - }, - { - value => '20130806-20130916', - label => '2013-08-06 and 2013-09-16' - }, - { - value => '20130625-20130805', - label => '2013-06-25 and 2013-08-05' - }, - { - value => '20130514-20130624', - label => '2013-05-14 and 2013-06-24' - }, - { - value => '20130402-20130513', - label => '2013-04-02 and 2013-05-13' - }, - { - value => '20130219-20130401', - label => '2013-02-19 and 2013-04-01' - }, - { - value => '20130108-20130218', - label => '2013-01-08 and 2013-02-18' - }, - { - value => '20121120-20130107', - label => '2012-11-20 and 2013-01-07' - }, - { - value => '20121009-20121119', - label => '2012-10-09 and 2012-11-19' - }, - { - value => '20120828-20121008', - label => '2012-08-28 and 2012-10-08' - }, - { - value => '20120717-20120827', - label => '2012-07-17 and 2012-08-27' - }, - { - value => '20120605-20120716', - label => '2012-06-05 and 2012-07-16' - }, - { - value => '20120424-20120604', - label => '2012-04-24 and 2012-06-04' - }, - { - value => '20120313-20120423', - label => '2012-03-13 and 2012-04-23' - }, - { - value => '20120131-20120312', - label => '2012-01-31 and 2012-03-12' - }, - { - value => '20111220-20120130', - label => '2011-12-20 and 2012-01-30' - }, - { - value => '20111108-20111219', - label => '2011-11-08 and 2011-12-19' - }, - { - value => '20110927-20111107', - label => '2011-09-27 and 2011-11-07' - }, - { - value => '20110816-20110926', - label => '2011-08-16 and 2011-09-26' - }, - { - value => '*', - label => 'Anytime' - } + {value => '20160126-20160307', label => '2016-01-26 and 2016-03-07'}, + {value => '20151215-20160125', label => '2015-12-15 and 2016-01-25'}, + {value => '20151103-20151214', label => '2015-11-03 and 2015-12-14'}, + {value => '20150922-20151102', label => '2015-09-22 and 2015-11-02'}, + {value => '20150811-20150921', label => '2015-08-11 and 2015-09-21'}, + {value => '20150630-20150810', label => '2015-06-30 and 2015-08-10'}, + {value => '20150512-20150629', label => '2015-05-12 and 2015-06-29'}, + {value => '20150331-20150511', label => '2015-03-31 and 2015-05-11'}, + {value => '20150224-20150330', label => '2015-02-24 and 2015-03-30'}, + {value => '20150113-20150223', label => '2015-01-13 and 2015-02-23'}, + {value => '20141111-20141222', label => '2014-11-11 and 2014-12-22'}, + {value => '20140930-20141110', label => '2014-09-30 and 2014-11-10'}, + {value => '20140819-20140929', label => '2014-08-19 and 2014-09-29'}, + {value => '20140708-20140818', label => '2014-07-08 and 2014-08-18'}, + {value => '20140527-20140707', label => '2014-05-27 and 2014-07-07'}, + {value => '20140415-20140526', label => '2014-04-15 and 2014-05-26'}, + {value => '20140304-20140414', label => '2014-03-04 and 2014-04-14'}, + {value => '20140121-20140303', label => '2014-01-21 and 2014-03-03'}, + {value => '20131210-20140120', label => '2013-12-10 and 2014-01-20'}, + {value => '20131029-20131209', label => '2013-10-29 and 2013-12-09'}, + {value => '20130917-20131028', label => '2013-09-17 and 2013-10-28'}, + {value => '20130806-20130916', label => '2013-08-06 and 2013-09-16'}, + {value => '20130625-20130805', label => '2013-06-25 and 2013-08-05'}, + {value => '20130514-20130624', label => '2013-05-14 and 2013-06-24'}, + {value => '20130402-20130513', label => '2013-04-02 and 2013-05-13'}, + {value => '20130219-20130401', label => '2013-02-19 and 2013-04-01'}, + {value => '20130108-20130218', label => '2013-01-08 and 2013-02-18'}, + {value => '20121120-20130107', label => '2012-11-20 and 2013-01-07'}, + {value => '20121009-20121119', label => '2012-10-09 and 2012-11-19'}, + {value => '20120828-20121008', label => '2012-08-28 and 2012-10-08'}, + {value => '20120717-20120827', label => '2012-07-17 and 2012-08-27'}, + {value => '20120605-20120716', label => '2012-06-05 and 2012-07-16'}, + {value => '20120424-20120604', label => '2012-04-24 and 2012-06-04'}, + {value => '20120313-20120423', label => '2012-03-13 and 2012-04-23'}, + {value => '20120131-20120312', label => '2012-01-31 and 2012-03-12'}, + {value => '20111220-20120130', label => '2011-12-20 and 2012-01-30'}, + {value => '20111108-20111219', label => '2011-11-08 and 2011-12-19'}, + {value => '20110927-20111107', label => '2011-09-27 and 2011-11-07'}, + {value => '20110816-20110926', label => '2011-08-16 and 2011-09-26'}, + {value => '*', label => 'Anytime'} ]; sub report { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - my $user = Bugzilla->user; - - my @flag_names = qw( - approval-mozilla-release - approval-mozilla-beta - approval-mozilla-aurora - approval-mozilla-central - approval-comm-release - approval-comm-beta - approval-comm-aurora - approval-calendar-release - approval-calendar-beta - approval-calendar-aurora - approval-mozilla-esr10 - ); - - my @flags_json; - my @fields_json; - my @products_json; - - # - # tracking flags - # - - my $all_products = $user->get_selectable_products; - my @usable_products; - - # build list of flags and their matching products - - my @invalid_flag_names; - foreach my $flag_name (@flag_names) { - # grab all matching flag_types - my @flag_types = @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })}; - - # remove invalid flags - if (!@flag_types) { - push @invalid_flag_names, $flag_name; - next; - } + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + my $user = Bugzilla->user; + + my @flag_names = qw( + approval-mozilla-release + approval-mozilla-beta + approval-mozilla-aurora + approval-mozilla-central + approval-comm-release + approval-comm-beta + approval-comm-aurora + approval-calendar-release + approval-calendar-beta + approval-calendar-aurora + approval-mozilla-esr10 + ); + + my @flags_json; + my @fields_json; + my @products_json; + + # + # tracking flags + # + + my $all_products = $user->get_selectable_products; + my @usable_products; + + # build list of flags and their matching products + + my @invalid_flag_names; + foreach my $flag_name (@flag_names) { + + # grab all matching flag_types + my @flag_types + = @{Bugzilla::FlagType::match({name => $flag_name, is_active => 1})}; + + # remove invalid flags + if (!@flag_types) { + push @invalid_flag_names, $flag_name; + next; + } - # we need a list of products, based on inclusions/exclusions - my @products; - my %flag_types; - foreach my $flag_type (@flag_types) { - $flag_types{$flag_type->name} = $flag_type->id; - my $has_all = 0; - my @exclusion_ids; - my @inclusion_ids; - foreach my $flag_type (@flag_types) { - if (scalar keys %{$flag_type->inclusions}) { - my $inclusions = $flag_type->inclusions; - foreach my $key (keys %$inclusions) { - push @inclusion_ids, ($inclusions->{$key} =~ /^(\d+)/); - } - } elsif (scalar keys %{$flag_type->exclusions}) { - my $exclusions = $flag_type->exclusions; - foreach my $key (keys %$exclusions) { - push @exclusion_ids, ($exclusions->{$key} =~ /^(\d+)/); - } - } else { - $has_all = 1; - last; - } - } - - if ($has_all) { - push @products, @$all_products; - } elsif (scalar @exclusion_ids) { - push @products, @$all_products; - foreach my $exclude_id (uniq @exclusion_ids) { - @products = grep { $_->id != $exclude_id } @products; - } - } else { - foreach my $include_id (uniq @inclusion_ids) { - push @products, grep { $_->id == $include_id } @$all_products; - } - } + # we need a list of products, based on inclusions/exclusions + my @products; + my %flag_types; + foreach my $flag_type (@flag_types) { + $flag_types{$flag_type->name} = $flag_type->id; + my $has_all = 0; + my @exclusion_ids; + my @inclusion_ids; + foreach my $flag_type (@flag_types) { + if (scalar keys %{$flag_type->inclusions}) { + my $inclusions = $flag_type->inclusions; + foreach my $key (keys %$inclusions) { + push @inclusion_ids, ($inclusions->{$key} =~ /^(\d+)/); + } } - @products = uniq @products; - push @usable_products, @products; - my @product_ids = map { $_->id } sort { lc($a->name) cmp lc($b->name) } @products; - - push @flags_json, { - name => $flag_name, - id => $flag_types{$flag_name} || 0, - products => \@product_ids, - fields => [], - }; - } - foreach my $flag_name (@invalid_flag_names) { - @flag_names = grep { $_ ne $flag_name } @flag_names; - } - @usable_products = uniq @usable_products; - - # build a list of tracking flags for each product - # also build the list of all fields - - my @unlink_products; - foreach my $product (@usable_products) { - my @fields = - sort { $a->sortkey <=> $b->sortkey } - grep { is_active_status_field($_) } - Bugzilla->active_custom_fields({ product => $product }); - my @field_ids = map { $_->id } @fields; - if (!scalar @fields) { - push @unlink_products, $product; - next; + elsif (scalar keys %{$flag_type->exclusions}) { + my $exclusions = $flag_type->exclusions; + foreach my $key (keys %$exclusions) { + push @exclusion_ids, ($exclusions->{$key} =~ /^(\d+)/); + } } - - # product - push @products_json, { - name => $product->name, - id => $product->id, - fields => \@field_ids, - }; - - # add fields to flags - foreach my $rh (@flags_json) { - if (grep { $_ eq $product->id } @{$rh->{products}}) { - push @{$rh->{fields}}, @field_ids; - } + else { + $has_all = 1; + last; } - - # add fields to fields_json - foreach my $field (@fields) { - my $existing = 0; - foreach my $rh (@fields_json) { - if ($rh->{id} == $field->id) { - $existing = 1; - last; - } - } - if (!$existing) { - push @fields_json, { - name => $field->name, - desc => $field->description, - id => $field->id, - }; - } + } + + if ($has_all) { + push @products, @$all_products; + } + elsif (scalar @exclusion_ids) { + push @products, @$all_products; + foreach my $exclude_id (uniq @exclusion_ids) { + @products = grep { $_->id != $exclude_id } @products; } + } + else { + foreach my $include_id (uniq @inclusion_ids) { + push @products, grep { $_->id == $include_id } @$all_products; + } + } } - foreach my $rh (@flags_json) { - my @fields = uniq @{$rh->{fields}}; - $rh->{fields} = \@fields; + @products = uniq @products; + push @usable_products, @products; + my @product_ids + = map { $_->id } sort { lc($a->name) cmp lc($b->name) } @products; + + push @flags_json, + { + name => $flag_name, + id => $flag_types{$flag_name} || 0, + products => \@product_ids, + fields => [], + }; + } + foreach my $flag_name (@invalid_flag_names) { + @flag_names = grep { $_ ne $flag_name } @flag_names; + } + @usable_products = uniq @usable_products; + + # build a list of tracking flags for each product + # also build the list of all fields + + my @unlink_products; + foreach my $product (@usable_products) { + my @fields + = sort { $a->sortkey <=> $b->sortkey } + grep { is_active_status_field($_) } + Bugzilla->active_custom_fields({product => $product}); + my @field_ids = map { $_->id } @fields; + if (!scalar @fields) { + push @unlink_products, $product; + next; } - # remove products which aren't linked with status fields + # product + push @products_json, + {name => $product->name, id => $product->id, fields => \@field_ids,}; + # add fields to flags foreach my $rh (@flags_json) { - my @product_ids; - foreach my $id (@{$rh->{products}}) { - unless (grep { $_->id == $id } @unlink_products) { - push @product_ids, $id; - } - $rh->{products} = \@product_ids; + if (grep { $_ eq $product->id } @{$rh->{products}}) { + push @{$rh->{fields}}, @field_ids; + } + } + + # add fields to fields_json + foreach my $field (@fields) { + my $existing = 0; + foreach my $rh (@fields_json) { + if ($rh->{id} == $field->id) { + $existing = 1; + last; } + } + if (!$existing) { + push @fields_json, + {name => $field->name, desc => $field->description, id => $field->id,}; + } } + } + foreach my $rh (@flags_json) { + my @fields = uniq @{$rh->{fields}}; + $rh->{fields} = \@fields; + } + + # remove products which aren't linked with status fields + + foreach my $rh (@flags_json) { + my @product_ids; + foreach my $id (@{$rh->{products}}) { + unless (grep { $_->id == $id } @unlink_products) { + push @product_ids, $id; + } + $rh->{products} = \@product_ids; + } + } - # - # run report - # + # + # run report + # - if ($input->{q} && !$input->{edit}) { - my $q = _parse_query($input->{q}); + if ($input->{q} && !$input->{edit}) { + my $q = _parse_query($input->{q}); - my @where; - my @params; - my $query = " + my @where; + my @params; + my $query = " SELECT DISTINCT b.bug_id FROM bugs b INNER JOIN flags f ON f.bug_id = b.bug_id\n"; - if ($q->{start_date}) { - $query .= "INNER JOIN bugs_activity a ON a.bug_id = b.bug_id\n"; - } + if ($q->{start_date}) { + $query .= "INNER JOIN bugs_activity a ON a.bug_id = b.bug_id\n"; + } - $query .= "WHERE "; + $query .= "WHERE "; - if ($q->{start_date}) { - push @where, "(a.fieldid = ?)"; - push @params, $q->{field_id}; + if ($q->{start_date}) { + push @where, "(a.fieldid = ?)"; + push @params, $q->{field_id}; - push @where, "(CONVERT_TZ(a.bug_when, 'UTC', 'America/Los_Angeles') >= ?)"; - push @params, $q->{start_date} . ' 00:00:00'; - push @where, "(CONVERT_TZ(a.bug_when, 'UTC', 'America/Los_Angeles') <= ?)"; - push @params, $q->{end_date} . ' 23:59:59'; + push @where, "(CONVERT_TZ(a.bug_when, 'UTC', 'America/Los_Angeles') >= ?)"; + push @params, $q->{start_date} . ' 00:00:00'; + push @where, "(CONVERT_TZ(a.bug_when, 'UTC', 'America/Los_Angeles') <= ?)"; + push @params, $q->{end_date} . ' 23:59:59'; - push @where, "(a.added LIKE ?)"; - push @params, '%' . $q->{flag_name} . $q->{flag_status} . '%'; - } + push @where, "(a.added LIKE ?)"; + push @params, '%' . $q->{flag_name} . $q->{flag_status} . '%'; + } - my ($type_id) = $dbh->selectrow_array( - "SELECT id FROM flagtypes WHERE name = ?", - undef, - $q->{flag_name} - ); - push @where, "(f.type_id = ?)"; - push @params, $type_id; + my ($type_id) = $dbh->selectrow_array("SELECT id FROM flagtypes WHERE name = ?", + undef, $q->{flag_name}); + push @where, "(f.type_id = ?)"; + push @params, $type_id; - push @where, "(f.status = ?)"; - push @params, $q->{flag_status}; + push @where, "(f.status = ?)"; + push @params, $q->{flag_status}; - if ($q->{product_id}) { - push @where, "(b.product_id = ?)"; - push @params, $q->{product_id}; - } + if ($q->{product_id}) { + push @where, "(b.product_id = ?)"; + push @params, $q->{product_id}; + } - if (scalar @{$q->{fields}}) { - my @fields; - foreach my $field (@{$q->{fields}}) { - my $field_sql = "("; - if ($field->{type} == FIELD_TYPE_EXTENSION) { - $field_sql .= " + if (scalar @{$q->{fields}}) { + my @fields; + foreach my $field (@{$q->{fields}}) { + my $field_sql = "("; + if ($field->{type} == FIELD_TYPE_EXTENSION) { + $field_sql .= " COALESCE( (SELECT tracking_flags_bugs.value FROM tracking_flags_bugs LEFT JOIN tracking_flags ON tracking_flags.id = tracking_flags_bugs.tracking_flag_id WHERE tracking_flags_bugs.bug_id = b.bug_id - AND tracking_flags.name = " . $dbh->quote($field->{name}) . ") + AND tracking_flags.name = " + . $dbh->quote($field->{name}) . ") , '') "; - } - else { - $field_sql .= "b." . $field->{name}; - } - $field_sql .= " " . ($field->{value} eq '+' ? '' : 'NOT ') . "IN ('fixed','verified'))"; - push(@fields, $field_sql); - } - my $join = uc $q->{join}; - push @where, '(' . join(" $join ", @fields) . ')'; } - - $query .= join("\nAND ", @where); - - my $bugs = $dbh->selectcol_arrayref($query, undef, @params); - push @$bugs, 0 unless @$bugs; - - my $urlbase = Bugzilla->localconfig->{urlbase}; - my $cgi = Bugzilla->cgi; - print $cgi->redirect( - -url => "${urlbase}buglist.cgi?bug_id=" . join(',', @$bugs) - ); - exit; + else { + $field_sql .= "b." . $field->{name}; + } + $field_sql + .= " " . ($field->{value} eq '+' ? '' : 'NOT ') . "IN ('fixed','verified'))"; + push(@fields, $field_sql); + } + my $join = uc $q->{join}; + push @where, '(' . join(" $join ", @fields) . ')'; } - # - # set template vars - # - - my $json = JSON->new()->shrink(1); - $vars->{flags_json} = $json->encode(\@flags_json); - $vars->{products_json} = $json->encode(\@products_json); - $vars->{fields_json} = $json->encode(\@fields_json); - $vars->{flag_names} = \@flag_names; - $vars->{ranges} = DATE_RANGES; - $vars->{default_query} = $input->{q}; - $vars->{is_custom} = $input->{is_custom}; - foreach my $field (qw(product flags range)) { - $vars->{$field} = $input->{$field}; - } + $query .= join("\nAND ", @where); + + my $bugs = $dbh->selectcol_arrayref($query, undef, @params); + push @$bugs, 0 unless @$bugs; + + my $urlbase = Bugzilla->localconfig->{urlbase}; + my $cgi = Bugzilla->cgi; + print $cgi->redirect( + -url => "${urlbase}buglist.cgi?bug_id=" . join(',', @$bugs)); + exit; + } + + # + # set template vars + # + + my $json = JSON->new()->shrink(1); + $vars->{flags_json} = $json->encode(\@flags_json); + $vars->{products_json} = $json->encode(\@products_json); + $vars->{fields_json} = $json->encode(\@fields_json); + $vars->{flag_names} = \@flag_names; + $vars->{ranges} = DATE_RANGES; + $vars->{default_query} = $input->{q}; + $vars->{is_custom} = $input->{is_custom}; + foreach my $field (qw(product flags range)) { + $vars->{$field} = $input->{$field}; + } } sub _parse_query { - my $q = shift; - my @query = split(/:/, $q); - my $query; - - # field_id for flag changes - $query->{field_id} = get_field_id('flagtypes.name'); - - # flag_name - my $flag_name = shift @query; - @{Bugzilla::FlagType::match({ name => $flag_name, is_active => 1 })} - or ThrowUserError('report_invalid_parameter', { name => 'flag_name' }); - trick_taint($flag_name); - $query->{flag_name} = $flag_name; - - # flag_status - my $flag_status = shift @query; - $flag_status =~ /^([\?\-\+])$/ - or ThrowUserError('report_invalid_parameter', { name => 'flag_status' }); - $query->{flag_status} = $1; - - # date_range -> from_ymd to_ymd - my $date_range = shift @query; - if ($date_range ne '*') { - $date_range =~ /^(\d\d\d\d)(\d\d)(\d\d)-(\d\d\d\d)(\d\d)(\d\d)$/ - or ThrowUserError('report_invalid_parameter', { name => 'date_range' }); - $query->{start_date} = "$1-$2-$3"; - $query->{end_date} = "$4-$5-$6"; - validate_date($query->{start_date}) - || ThrowUserError('illegal_date', { date => $query->{start_date}, - format => 'YYYY-MM-DD' }); - validate_date($query->{end_date}) - || ThrowUserError('illegal_date', { date => $query->{end_date}, - format => 'YYYY-MM-DD' }); - } - - # product_id - my $product_id = shift @query; - $product_id =~ /^(\d+)$/ - or ThrowUserError('report_invalid_parameter', { name => 'product_id' }); - $query->{product_id} = $1; - - # join - my $join = shift @query; - $join =~ /^(and|or)$/ - or ThrowUserError('report_invalid_parameter', { name => 'join' }); - $query->{join} = $1; - - # fields - my @fields; - foreach my $field (@query) { - $field =~ /^(\d+)([\-\+])$/ - or ThrowUserError('report_invalid_parameter', { name => 'fields' }); - my ($id, $value) = ($1, $2); - my $field_obj = Bugzilla::Field->new($id) - or ThrowUserError('report_invalid_parameter', { name => 'field_id' }); - push @fields, { id => $id, value => $value, - name => $field_obj->name, type => $field_obj->type }; - } - $query->{fields} = \@fields; - - return $query; + my $q = shift; + my @query = split(/:/, $q); + my $query; + + # field_id for flag changes + $query->{field_id} = get_field_id('flagtypes.name'); + + # flag_name + my $flag_name = shift @query; + @{Bugzilla::FlagType::match({name => $flag_name, is_active => 1})} + or ThrowUserError('report_invalid_parameter', {name => 'flag_name'}); + trick_taint($flag_name); + $query->{flag_name} = $flag_name; + + # flag_status + my $flag_status = shift @query; + $flag_status =~ /^([\?\-\+])$/ + or ThrowUserError('report_invalid_parameter', {name => 'flag_status'}); + $query->{flag_status} = $1; + + # date_range -> from_ymd to_ymd + my $date_range = shift @query; + if ($date_range ne '*') { + $date_range =~ /^(\d\d\d\d)(\d\d)(\d\d)-(\d\d\d\d)(\d\d)(\d\d)$/ + or ThrowUserError('report_invalid_parameter', {name => 'date_range'}); + $query->{start_date} = "$1-$2-$3"; + $query->{end_date} = "$4-$5-$6"; + validate_date($query->{start_date}) + || ThrowUserError('illegal_date', + {date => $query->{start_date}, format => 'YYYY-MM-DD'}); + validate_date($query->{end_date}) + || ThrowUserError('illegal_date', + {date => $query->{end_date}, format => 'YYYY-MM-DD'}); + } + + # product_id + my $product_id = shift @query; + $product_id =~ /^(\d+)$/ + or ThrowUserError('report_invalid_parameter', {name => 'product_id'}); + $query->{product_id} = $1; + + # join + my $join = shift @query; + $join =~ /^(and|or)$/ + or ThrowUserError('report_invalid_parameter', {name => 'join'}); + $query->{join} = $1; + + # fields + my @fields; + foreach my $field (@query) { + $field =~ /^(\d+)([\-\+])$/ + or ThrowUserError('report_invalid_parameter', {name => 'fields'}); + my ($id, $value) = ($1, $2); + my $field_obj = Bugzilla::Field->new($id) + or ThrowUserError('report_invalid_parameter', {name => 'field_id'}); + push @fields, + { + id => $id, + value => $value, + name => $field_obj->name, + type => $field_obj->type + }; + } + $query->{fields} = \@fields; + + return $query; } 1; diff --git a/extensions/BMO/lib/Reports/Triage.pm b/extensions/BMO/lib/Reports/Triage.pm index 55eeb17eb..0ccbbee6e 100644 --- a/extensions/BMO/lib/Reports/Triage.pm +++ b/extensions/BMO/lib/Reports/Triage.pm @@ -25,261 +25,279 @@ use List::MoreUtils qw(any); # set an upper limit on the *unfiltered* number of bugs to process use constant MAX_NUMBER_BUGS => 4000; -use constant DEFAULT_OWNER_PRODUCTS => ( - 'Core', - 'Firefox', - 'Firefox for Android', - 'Firefox for iOS', - 'Toolkit', -); +use constant DEFAULT_OWNER_PRODUCTS => + ('Core', 'Firefox', 'Firefox for Android', 'Firefox for iOS', 'Toolkit',); sub unconfirmed { - my ($vars, $filter) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - my $user = Bugzilla->user; - - if (exists $input->{'action'} && $input->{'action'} eq 'run' && $input->{'product'}) { - - # load product and components from input - - my $product = Bugzilla::Product->new({ name => $input->{'product'} }) - || ThrowUserError('invalid_object', { object => 'Product', value => $input->{'product'} }); - - my @component_ids; - if ($input->{'component'} ne '') { - my $ra_components = ref($input->{'component'}) - ? $input->{'component'} : [ $input->{'component'} ]; - foreach my $component_name (@$ra_components) { - my $component = Bugzilla::Component->new({ name => $component_name, product => $product }) - || ThrowUserError('invalid_object', { object => 'Component', value => $component_name }); - push @component_ids, $component->id; - } - } + my ($vars, $filter) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + my $user = Bugzilla->user; - # determine which comment filters to run + if ( exists $input->{'action'} + && $input->{'action'} eq 'run' + && $input->{'product'}) + { - my $filter_commenter = $input->{'filter_commenter'}; - my $filter_commenter_on = $input->{'commenter'}; - my $filter_last = $input->{'filter_last'}; - my $filter_last_period = $input->{'last'}; + # load product and components from input - if (!$filter_commenter || $filter_last) { - $filter_commenter = '1'; - $filter_commenter_on = 'reporter'; - } + my $product + = Bugzilla::Product->new({name => $input->{'product'}}) + || ThrowUserError('invalid_object', + {object => 'Product', value => $input->{'product'}}); - my $filter_commenter_id; - if ($filter_commenter && $filter_commenter_on eq 'is') { - Bugzilla::User::match_field({ 'commenter_is' => {'type' => 'single'} }); - my $user = Bugzilla::User->new({ name => $input->{'commenter_is'} }) - || ThrowUserError('invalid_object', { object => 'User', value => $input->{'commenter_is'} }); - $filter_commenter_id = $user ? $user->id : 0; - } + my @component_ids; + if ($input->{'component'} ne '') { + my $ra_components + = ref($input->{'component'}) + ? $input->{'component'} + : [$input->{'component'}]; + foreach my $component_name (@$ra_components) { + my $component + = Bugzilla::Component->new({name => $component_name, product => $product}) + || ThrowUserError('invalid_object', + {object => 'Component', value => $component_name}); + push @component_ids, $component->id; + } + } - my $filter_last_time; - if ($filter_last) { - if ($filter_last_period eq 'is') { - $filter_last_period = -1; - $filter_last_time = str2time($input->{'last_is'} . " 00:00:00") || 0; - } else { - detaint_natural($filter_last_period); - $filter_last_period = 14 if $filter_last_period < 14; - } - } + # determine which comment filters to run + + my $filter_commenter = $input->{'filter_commenter'}; + my $filter_commenter_on = $input->{'commenter'}; + my $filter_last = $input->{'filter_last'}; + my $filter_last_period = $input->{'last'}; + + if (!$filter_commenter || $filter_last) { + $filter_commenter = '1'; + $filter_commenter_on = 'reporter'; + } + + my $filter_commenter_id; + if ($filter_commenter && $filter_commenter_on eq 'is') { + Bugzilla::User::match_field({'commenter_is' => {'type' => 'single'}}); + my $user + = Bugzilla::User->new({name => $input->{'commenter_is'}}) + || ThrowUserError('invalid_object', + {object => 'User', value => $input->{'commenter_is'}}); + $filter_commenter_id = $user ? $user->id : 0; + } - # form sql queries + my $filter_last_time; + if ($filter_last) { + if ($filter_last_period eq 'is') { + $filter_last_period = -1; + $filter_last_time = str2time($input->{'last_is'} . " 00:00:00") || 0; + } + else { + detaint_natural($filter_last_period); + $filter_last_period = 14 if $filter_last_period < 14; + } + } + + # form sql queries - my $now = (time); - my $bugs_sql = " + my $now = (time); + my $bugs_sql = " SELECT bug_id, short_desc, reporter, creation_ts FROM bugs WHERE product_id = ? AND bug_status = 'UNCONFIRMED'"; - if (@component_ids) { - $bugs_sql .= " AND component_id IN (" . join(',', @component_ids) . ")"; - } - $bugs_sql .= " + if (@component_ids) { + $bugs_sql .= " AND component_id IN (" . join(',', @component_ids) . ")"; + } + $bugs_sql .= " ORDER BY creation_ts "; - my $comment_count_sql = " + my $comment_count_sql = " SELECT COUNT(*) FROM longdescs WHERE bug_id = ? "; - my $comment_sql = " + my $comment_sql = " SELECT who, bug_when, type, thetext, extra_data FROM longdescs WHERE bug_id = ? "; - if (!Bugzilla->user->is_insider) { - $comment_sql .= " AND isprivate = 0 "; - } - $comment_sql .= " + if (!Bugzilla->user->is_insider) { + $comment_sql .= " AND isprivate = 0 "; + } + $comment_sql .= " ORDER BY bug_when DESC LIMIT 1 "; - my $attach_sql = " + my $attach_sql = " SELECT description, isprivate FROM attachments WHERE attach_id = ? "; - # work on an initial list of bugs + # work on an initial list of bugs - my $list = $dbh->selectall_arrayref($bugs_sql, undef, $product->id); - my @bugs; + my $list = $dbh->selectall_arrayref($bugs_sql, undef, $product->id); + my @bugs; - # this can be slow to process, resulting in 'service unavailable' errors from zeus - # so if too many bugs are returned, throw an error + # this can be slow to process, resulting in 'service unavailable' errors from zeus + # so if too many bugs are returned, throw an error - if (scalar(@$list) > MAX_NUMBER_BUGS) { - ThrowUserError('report_too_many_bugs'); - } + if (scalar(@$list) > MAX_NUMBER_BUGS) { + ThrowUserError('report_too_many_bugs'); + } - foreach my $entry (@$list) { - my ($bug_id, $summary, $reporter_id, $creation_ts) = @$entry; - - next unless $user->can_see_bug($bug_id); - - # get last comment information - - my ($comment_count) = $dbh->selectrow_array($comment_count_sql, undef, $bug_id); - my ($commenter_id, $comment_ts, $type, $comment, $extra) - = $dbh->selectrow_array($comment_sql, undef, $bug_id); - my $commenter = 0; - - # apply selected filters - - if ($filter_commenter) { - next if $comment_count <= 1; - - if ($filter_commenter_on eq 'reporter') { - next if $commenter_id != $reporter_id; - - } elsif ($filter_commenter_on eq 'noconfirm') { - $commenter = Bugzilla::User->new({ id => $commenter_id, cache => 1 }); - next if $commenter_id != $reporter_id - || $commenter->in_group('canconfirm'); - - } elsif ($filter_commenter_on eq 'is') { - next if $commenter_id != $filter_commenter_id; - } - } else { - $input->{'commenter'} = ''; - $input->{'commenter_is'} = ''; - } - - if ($filter_last) { - my $comment_time = str2time($comment_ts) - or next; - if ($filter_last_period == -1) { - next if $comment_time >= $filter_last_time; - } else { - next if $now - $comment_time <= 60 * 60 * 24 * $filter_last_period; - } - } else { - $input->{'last'} = ''; - $input->{'last_is'} = ''; - } - - # get data for attachment comments - - if ($comment eq '' && $type == CMT_ATTACHMENT_CREATED) { - my ($description, $is_private) = $dbh->selectrow_array($attach_sql, undef, $extra); - next if $is_private && !Bugzilla->user->is_insider; - $comment = "(Attachment) " . $description; - } - - # truncate long comments - - if (length($comment) > 80) { - $comment = substr($comment, 0, 80) . '...'; - } - - # build bug hash for template - - my $bug = {}; - $bug->{id} = $bug_id; - $bug->{summary} = $summary; - $bug->{reporter} = Bugzilla::User->new({ id => $reporter_id, cache => 1 }); - $bug->{creation_ts} = $creation_ts; - $bug->{commenter} = $commenter || Bugzilla::User->new({ id => $commenter_id, cache => 1 }); - $bug->{comment_ts} = $comment_ts; - $bug->{comment} = $comment; - $bug->{comment_count} = $comment_count; - push @bugs, $bug; - } + foreach my $entry (@$list) { + my ($bug_id, $summary, $reporter_id, $creation_ts) = @$entry; - @bugs = sort { $b->{comment_ts} cmp $a->{comment_ts} } @bugs; + next unless $user->can_see_bug($bug_id); - $vars->{bugs} = \@bugs; - } else { - $input->{action} = ''; - } + # get last comment information - if (!$input->{filter_commenter} && !$input->{filter_last}) { - $input->{filter_commenter} = 1; - } + my ($comment_count) = $dbh->selectrow_array($comment_count_sql, undef, $bug_id); + my ($commenter_id, $comment_ts, $type, $comment, $extra) + = $dbh->selectrow_array($comment_sql, undef, $bug_id); + my $commenter = 0; - $vars->{'input'} = $input; -} + # apply selected filters -sub owners { - my ($vars, $filter) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - my $user = Bugzilla->user; + if ($filter_commenter) { + next if $comment_count <= 1; - Bugzilla::User::match_field({ 'owner' => {'type' => 'multi'} }); + if ($filter_commenter_on eq 'reporter') { + next if $commenter_id != $reporter_id; - my @products; - if (!$input->{product} && $input->{owner}) { - @products = @{ $user->get_selectable_products }; - } - else { - my @product_names = $input->{product} ? ($input->{product}) : DEFAULT_OWNER_PRODUCTS; - foreach my $name (@product_names) { - push(@products, Bugzilla::Product->check({ name => $name })); } - } + elsif ($filter_commenter_on eq 'noconfirm') { + $commenter = Bugzilla::User->new({id => $commenter_id, cache => 1}); + next if $commenter_id != $reporter_id || $commenter->in_group('canconfirm'); - my @component_ids; - if (@products == 1 && $input->{'component'}) { - my $ra_components = ref($input->{'component'}) - ? $input->{'component'} - : [ $input->{'component'} ]; - foreach my $component_name (@$ra_components) { - my $component = Bugzilla::Component->check({ name => $component_name, product => $products[0] }); - push @component_ids, $component->id; } + elsif ($filter_commenter_on eq 'is') { + next if $commenter_id != $filter_commenter_id; + } + } + else { + $input->{'commenter'} = ''; + $input->{'commenter_is'} = ''; + } + + if ($filter_last) { + my $comment_time = str2time($comment_ts) or next; + if ($filter_last_period == -1) { + next if $comment_time >= $filter_last_time; + } + else { + next if $now - $comment_time <= 60 * 60 * 24 * $filter_last_period; + } + } + else { + $input->{'last'} = ''; + $input->{'last_is'} = ''; + } + + # get data for attachment comments + + if ($comment eq '' && $type == CMT_ATTACHMENT_CREATED) { + my ($description, $is_private) + = $dbh->selectrow_array($attach_sql, undef, $extra); + next if $is_private && !Bugzilla->user->is_insider; + $comment = "(Attachment) " . $description; + } + + # truncate long comments + + if (length($comment) > 80) { + $comment = substr($comment, 0, 80) . '...'; + } + + # build bug hash for template + + my $bug = {}; + $bug->{id} = $bug_id; + $bug->{summary} = $summary; + $bug->{reporter} = Bugzilla::User->new({id => $reporter_id, cache => 1}); + $bug->{creation_ts} = $creation_ts; + $bug->{commenter} + = $commenter || Bugzilla::User->new({id => $commenter_id, cache => 1}); + $bug->{comment_ts} = $comment_ts; + $bug->{comment} = $comment; + $bug->{comment_count} = $comment_count; + push @bugs, $bug; } - my @owner_names = split(/[,;]+/, $input->{owner}) if $input->{owner}; - my @owner_ids; - foreach my $name (@owner_names) { - $name = trim($name); - next unless $name; - push(@owner_ids, login_to_id($name, THROW_ERROR)); - } + @bugs = sort { $b->{comment_ts} cmp $a->{comment_ts} } @bugs; - my $sql = "SELECT products.name, components.name, components.id, components.triage_owner_id - FROM components JOIN products ON components.product_id = products.id - WHERE products.id IN (" . join(',', map { $_->id } @products) . ")"; - if (@component_ids) { - $sql .= " AND components.id IN (" . join(',', @component_ids) . ")"; - } - if (@owner_ids) { - $sql .= " AND components.triage_owner_id IN (" . join(',', @owner_ids) . ")"; - } - $sql .= " ORDER BY products.name, components.name"; + $vars->{bugs} = \@bugs; + } + else { + $input->{action} = ''; + } - my $rows = $dbh->selectall_arrayref($sql); + if (!$input->{filter_commenter} && !$input->{filter_last}) { + $input->{filter_commenter} = 1; + } - my $bug_count_sth = $dbh->prepare(" + $vars->{'input'} = $input; +} + +sub owners { + my ($vars, $filter) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + my $user = Bugzilla->user; + + Bugzilla::User::match_field({'owner' => {'type' => 'multi'}}); + + my @products; + if (!$input->{product} && $input->{owner}) { + @products = @{$user->get_selectable_products}; + } + else { + my @product_names + = $input->{product} ? ($input->{product}) : DEFAULT_OWNER_PRODUCTS; + foreach my $name (@product_names) { + push(@products, Bugzilla::Product->check({name => $name})); + } + } + + my @component_ids; + if (@products == 1 && $input->{'component'}) { + my $ra_components + = ref($input->{'component'}) + ? $input->{'component'} + : [$input->{'component'}]; + foreach my $component_name (@$ra_components) { + my $component = Bugzilla::Component->check( + {name => $component_name, product => $products[0]}); + push @component_ids, $component->id; + } + } + + my @owner_names = split(/[,;]+/, $input->{owner}) if $input->{owner}; + my @owner_ids; + foreach my $name (@owner_names) { + $name = trim($name); + next unless $name; + push(@owner_ids, login_to_id($name, THROW_ERROR)); + } + + my $sql + = "SELECT products.name, components.name, components.id, components.triage_owner_id + FROM components JOIN products ON components.product_id = products.id + WHERE products.id IN (" + . join(',', map { $_->id } @products) . ")"; + if (@component_ids) { + $sql .= " AND components.id IN (" . join(',', @component_ids) . ")"; + } + if (@owner_ids) { + $sql .= " AND components.triage_owner_id IN (" . join(',', @owner_ids) . ")"; + } + $sql .= " ORDER BY products.name, components.name"; + + my $rows = $dbh->selectall_arrayref($sql); + + my $bug_count_sth = $dbh->prepare(" SELECT COUNT(bugs.bug_id) FROM bugs INNER JOIN components AS map_component ON bugs.component_id = map_component.id INNER JOIN bug_status AS map_bug_status ON bugs.bug_status = map_bug_status.value @@ -296,55 +314,57 @@ sub owners { WHERE bugs_1.bug_id = bugs.bug_id AND CONCAT(flagtypes_1.name, flags_1.status) = 'needinfo?'))) AND bugs.component_id = ?"); - my @results; - foreach my $row (@$rows) { - my ($product_name, $component_name, $component_id, $triage_owner_id) = @$row; - my $triage_owner = $triage_owner_id - ? Bugzilla::User->new({ id => $triage_owner_id, cache => 1 }) - : ""; - my $data = { - product => $product_name, - component => $component_name, - owner => $triage_owner, - }; - $data->{buglist_url} = 'priority=--&resolution=---&f1=creation_ts&o1=greaterthaneq&v1=2016-06-01'. - '&f2=flagtypes.name&o2=notequals&v2=needinfo%3F'; - if ($triage_owner) { - $data->{buglist_url} .= '&f3=triage_owner&o3=equals&v3=' . url_quote($triage_owner->login); - } - $bug_count_sth->execute($component_id); - ($data->{bug_count}) = $bug_count_sth->fetchrow_array(); - push @results, $data; + my @results; + foreach my $row (@$rows) { + my ($product_name, $component_name, $component_id, $triage_owner_id) = @$row; + my $triage_owner + = $triage_owner_id + ? Bugzilla::User->new({id => $triage_owner_id, cache => 1}) + : ""; + my $data = { + product => $product_name, + component => $component_name, + owner => $triage_owner, + }; + $data->{buglist_url} + = 'priority=--&resolution=---&f1=creation_ts&o1=greaterthaneq&v1=2016-06-01' + . '&f2=flagtypes.name&o2=notequals&v2=needinfo%3F'; + if ($triage_owner) { + $data->{buglist_url} + .= '&f3=triage_owner&o3=equals&v3=' . url_quote($triage_owner->login); } - $vars->{results} = \@results; - - my $json_data = { products => [] }; - foreach my $product (@{ $user->get_selectable_products }) { - my $prod_data = { - name => $product->name, - components => [], - }; - foreach my $component (@{ $product->components }) { - my $selected = 0; - if ($input->{product} - && $input->{product} eq $product->name - && $input->{component}) - { - $selected = 1 if (ref $input->{component} && any { $_ eq $component->name } @{ $input->{component} }); - $selected = 1 if (!ref $input->{componet} && $input->{component} eq $component->name); - } - my $comp_data = { - name => $component->name, - selected => $selected - }; - push(@{ $prod_data->{components} }, $comp_data); - } - push(@{ $json_data->{products} }, $prod_data); + $bug_count_sth->execute($component_id); + ($data->{bug_count}) = $bug_count_sth->fetchrow_array(); + push @results, $data; + } + $vars->{results} = \@results; + + my $json_data = {products => []}; + foreach my $product (@{$user->get_selectable_products}) { + my $prod_data = {name => $product->name, components => [],}; + foreach my $component (@{$product->components}) { + my $selected = 0; + if ( $input->{product} + && $input->{product} eq $product->name + && $input->{component}) + { + $selected = 1 + if ( + ref $input->{component} && any { $_ eq $component->name } + @{$input->{component}} + ); + $selected = 1 + if (!ref $input->{componet} && $input->{component} eq $component->name); + } + my $comp_data = {name => $component->name, selected => $selected}; + push(@{$prod_data->{components}}, $comp_data); } + push(@{$json_data->{products}}, $prod_data); + } - $vars->{product} = $input->{product}; - $vars->{owner} = $input->{owner}; - $vars->{json_data} = encode_json($json_data); + $vars->{product} = $input->{product}; + $vars->{owner} = $input->{owner}; + $vars->{json_data} = encode_json($json_data); } 1; diff --git a/extensions/BMO/lib/Reports/UserActivity.pm b/extensions/BMO/lib/Reports/UserActivity.pm index 8dfe0c5cd..3be6f74c9 100644 --- a/extensions/BMO/lib/Reports/UserActivity.pm +++ b/extensions/BMO/lib/Reports/UserActivity.pm @@ -18,104 +18,104 @@ use Bugzilla::Util qw(trim); use DateTime; sub report { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; - - my @who = (); - my $from = trim($input->{'from'} || ''); - my $to = trim($input->{'to'} || ''); - my $action = $input->{'action'} || ''; - - # fix non-breaking hyphens - $from =~ s/\N{U+2011}/-/g; - $to =~ s/\N{U+2011}/-/g; - - if ($from eq '') { - my $dt = DateTime->now()->subtract('weeks' => 1); - $from = $dt->ymd('-'); + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; + + my @who = (); + my $from = trim($input->{'from'} || ''); + my $to = trim($input->{'to'} || ''); + my $action = $input->{'action'} || ''; + + # fix non-breaking hyphens + $from =~ s/\N{U+2011}/-/g; + $to =~ s/\N{U+2011}/-/g; + + if ($from eq '') { + my $dt = DateTime->now()->subtract('weeks' => 1); + $from = $dt->ymd('-'); + } + if ($to eq '') { + my $dt = DateTime->now(); + $to = $dt->ymd('-'); + } + + if ($action eq 'run') { + if (!exists $input->{'who'} || $input->{'who'} eq '') { + ThrowUserError('user_activity_missing_username'); } - if ($to eq '') { - my $dt = DateTime->now(); - $to = $dt->ymd('-'); - } - - if ($action eq 'run') { - if (!exists $input->{'who'} || $input->{'who'} eq '') { - ThrowUserError('user_activity_missing_username'); - } - Bugzilla::User::match_field({ 'who' => {'type' => 'multi'} }); + Bugzilla::User::match_field({'who' => {'type' => 'multi'}}); - my $from_dt = string_to_datetime($from); - $from = $from_dt->ymd(); + my $from_dt = string_to_datetime($from); + $from = $from_dt->ymd(); - my $to_dt = string_to_datetime($to); - $to = $to_dt->ymd(); + my $to_dt = string_to_datetime($to); + $to = $to_dt->ymd(); - my ($activity_joins, $activity_where) = ('', ''); - my ($attachments_joins, $attachments_where) = ('', ''); - my ($tags_activity_joins, $tags_activity_where) = ('', ''); - if (Bugzilla->params->{"insidergroup"} - && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'})) - { - $activity_joins = "LEFT JOIN attachments + my ($activity_joins, $activity_where) = ('', ''); + my ($attachments_joins, $attachments_where) = ('', ''); + my ($tags_activity_joins, $tags_activity_where) = ('', ''); + if (Bugzilla->params->{"insidergroup"} + && !Bugzilla->user->in_group(Bugzilla->params->{'insidergroup'})) + { + $activity_joins = "LEFT JOIN attachments ON attachments.attach_id = bugs_activity.attach_id"; - $activity_where = "AND COALESCE(attachments.isprivate, 0) = 0"; - $attachments_where = $activity_where; + $activity_where = "AND COALESCE(attachments.isprivate, 0) = 0"; + $attachments_where = $activity_where; - $tags_activity_joins = 'LEFT JOIN longdescs + $tags_activity_joins = 'LEFT JOIN longdescs ON longdescs_tags_activity.comment_id = longdescs.comment_id'; - $tags_activity_where = 'AND COALESCE(longdescs.isprivate, 0) = 0'; - } + $tags_activity_where = 'AND COALESCE(longdescs.isprivate, 0) = 0'; + } - my @who_bits; - foreach my $who ( - ref $input->{'who'} - ? @{$input->{'who'}} - : $input->{'who'} - ) { - push @who, $who; - push @who_bits, '?'; - } - my $who_bits = join(',', @who_bits); - - if (!@who) { - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $vars = {}; - $vars->{'script'} = $cgi->url(-relative => 1); - $vars->{'fields'} = {}; - $vars->{'matches'} = []; - $vars->{'matchsuccess'} = 0; - $vars->{'matchmultiple'} = 1; - print $cgi->header(); - $template->process("global/confirm-user-match.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } + my @who_bits; + foreach my $who (ref $input->{'who'} ? @{$input->{'who'}} : $input->{'who'}) { + push @who, $who; + push @who_bits, '?'; + } + my $who_bits = join(',', @who_bits); + + if (!@who) { + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $vars = {}; + $vars->{'script'} = $cgi->url(-relative => 1); + $vars->{'fields'} = {}; + $vars->{'matches'} = []; + $vars->{'matchsuccess'} = 0; + $vars->{'matchmultiple'} = 1; + print $cgi->header(); + $template->process("global/confirm-user-match.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } - $from_dt = $from_dt->ymd() . ' 00:00:00'; - $to_dt = $to_dt->ymd() . ' 23:59:59'; - my @params; - for (1..5) { - push @params, @who; - push @params, ($from_dt, $to_dt); - } + $from_dt = $from_dt->ymd() . ' 00:00:00'; + $to_dt = $to_dt->ymd() . ' 23:59:59'; + my @params; + for (1 .. 5) { + push @params, @who; + push @params, ($from_dt, $to_dt); + } - my $order = ($input->{'group'} && $input->{'group'} eq 'bug') - ? 'bug_id, bug_when' : 'bug_when'; + my $order + = ($input->{'group'} && $input->{'group'} eq 'bug') + ? 'bug_id, bug_when' + : 'bug_when'; - my $comment_filter = ''; - if (!Bugzilla->user->is_insider) { - $comment_filter = 'AND longdescs.isprivate = 0'; - } + my $comment_filter = ''; + if (!Bugzilla->user->is_insider) { + $comment_filter = 'AND longdescs.isprivate = 0'; + } - my $query = " + my $query = " SELECT fielddefs.name, bugs_activity.bug_id, bugs_activity.attach_id, - ".$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s')." AS ts, + " + . $dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') + . " AS ts, bugs_activity.removed, bugs_activity.added, profiles.login_name, @@ -138,8 +138,10 @@ sub report { 'comment_tag' AS name, longdescs_tags_activity.bug_id, NULL as attach_id, - ".$dbh->sql_date_format('longdescs_tags_activity.bug_when', - '%Y.%m.%d %H:%i:%s') . " AS bug_when, + " + . $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, @@ -160,7 +162,8 @@ sub report { 'bug_id' AS name, bugs.bug_id, NULL AS attach_id, - ".$dbh->sql_date_format('bugs.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts, + " + . $dbh->sql_date_format('bugs.creation_ts', '%Y.%m.%d %H:%i:%s') . " AS ts, '(new bug)' AS removed, bugs.short_desc AS added, profiles.login_name, @@ -199,7 +202,9 @@ sub report { 'attachments.description' AS name, attachments.bug_id, attachments.attach_id, - ".$dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i:%s')." AS ts, + " + . $dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i:%s') + . " AS ts, '(new attachment)' AS removed, attachments.description AS added, profiles.login_name, @@ -215,119 +220,118 @@ sub report { ORDER BY $order "; - my $list = $dbh->selectall_arrayref($query, undef, @params); + my $list = $dbh->selectall_arrayref($query, undef, @params); - if ($input->{debug}) { - while (my $param = shift @params) { - $query =~ s/\?/$dbh->quote($param)/e; - } - $vars->{debug_sql} = $query; + if ($input->{debug}) { + while (my $param = shift @params) { + $query =~ s/\?/$dbh->quote($param)/e; + } + $vars->{debug_sql} = $query; + } + + my @operations; + my $operation = {}; + my $changes = []; + my $incomplete_data = 0; + my %bug_ids; + + foreach my $entry (@$list) { + my ($fieldname, $bugid, $attachid, $when, $removed, $added, $who, $comment_id) + = @$entry; + my %change; + my $activity_visible = 1; + + next unless Bugzilla->user->can_see_bug($bugid); + + # check if the user should see this field's activity + if ( $fieldname eq 'remaining_time' + || $fieldname eq 'estimated_time' + || $fieldname eq 'work_time' + || $fieldname eq 'deadline') + { + $activity_visible = Bugzilla->user->is_timetracker; + } + elsif ($fieldname eq 'longdescs.isprivate' + && !Bugzilla->user->is_insider + && $added) + { + $activity_visible = 0; + } + else { + $activity_visible = 1; + } + + if ($activity_visible) { + + # Check for the results of an old Bugzilla data corruption bug + if ( ($added eq '?' && $removed eq '?') + || ($added =~ /^\? / || $removed =~ /^\? /)) + { + $incomplete_data = 1; } - my @operations; - my $operation = {}; - my $changes = []; - my $incomplete_data = 0; - my %bug_ids; - - foreach my $entry (@$list) { - my ($fieldname, $bugid, $attachid, $when, $removed, $added, $who, - $comment_id) = @$entry; - my %change; - my $activity_visible = 1; - - next unless Bugzilla->user->can_see_bug($bugid); - - # check if the user should see this field's activity - if ($fieldname eq 'remaining_time' - || $fieldname eq 'estimated_time' - || $fieldname eq 'work_time' - || $fieldname eq 'deadline') - { - $activity_visible = Bugzilla->user->is_timetracker; - } - elsif ($fieldname eq 'longdescs.isprivate' - && !Bugzilla->user->is_insider - && $added) - { - $activity_visible = 0; - } - else { - $activity_visible = 1; - } - - if ($activity_visible) { - # Check for the results of an old Bugzilla data corruption bug - if (($added eq '?' && $removed eq '?') - || ($added =~ /^\? / || $removed =~ /^\? /)) { - $incomplete_data = 1; - } - - # Start a new changeset if required (depends on the grouping type) - my $is_new_changeset; - if ($order eq 'bug_when') { - $is_new_changeset = - $operation->{'who'} && - ( - $who ne $operation->{'who'} - || $when ne $operation->{'when'} - || $bugid != $operation->{'bug'} - ); - } else { - $is_new_changeset = - $operation->{'bug'} && - $bugid != $operation->{'bug'}; - } - if ($is_new_changeset) { - $operation->{'changes'} = $changes; - push (@operations, $operation); - $operation = {}; - $changes = []; - } - - $bug_ids{$bugid} = 1; - - $operation->{'bug'} = $bugid; - $operation->{'who'} = $who; - $operation->{'when'} = $when; - - $change{'fieldname'} = $fieldname; - $change{'attachid'} = $attachid; - $change{'removed'} = $removed; - $change{'added'} = $added; - $change{'when'} = $when; - - if ($comment_id) { - $change{'comment'} = Bugzilla::Comment->new($comment_id); - next if $change{'comment'}->count == 0; - } - - if ($attachid) { - $change{'attach'} = Bugzilla::Attachment->new($attachid); - } - - push (@$changes, \%change); - } + # Start a new changeset if required (depends on the grouping type) + my $is_new_changeset; + if ($order eq 'bug_when') { + $is_new_changeset + = $operation->{'who'} + && ($who ne $operation->{'who'} + || $when ne $operation->{'when'} + || $bugid != $operation->{'bug'}); + } + else { + $is_new_changeset = $operation->{'bug'} && $bugid != $operation->{'bug'}; } + if ($is_new_changeset) { + $operation->{'changes'} = $changes; + push(@operations, $operation); + $operation = {}; + $changes = []; + } + + $bug_ids{$bugid} = 1; + + $operation->{'bug'} = $bugid; + $operation->{'who'} = $who; + $operation->{'when'} = $when; - if ($operation->{'who'}) { - $operation->{'changes'} = $changes; - push (@operations, $operation); + $change{'fieldname'} = $fieldname; + $change{'attachid'} = $attachid; + $change{'removed'} = $removed; + $change{'added'} = $added; + $change{'when'} = $when; + + if ($comment_id) { + $change{'comment'} = Bugzilla::Comment->new($comment_id); + next if $change{'comment'}->count == 0; + } + + if ($attachid) { + $change{'attach'} = Bugzilla::Attachment->new($attachid); } - $vars->{'incomplete_data'} = $incomplete_data; - $vars->{'operations'} = \@operations; + push(@$changes, \%change); + } + } - my @bug_ids = sort { $a <=> $b } keys %bug_ids; - $vars->{'bug_ids'} = \@bug_ids; + if ($operation->{'who'}) { + $operation->{'changes'} = $changes; + push(@operations, $operation); } - $vars->{'action'} = $action; - $vars->{'who'} = join(',', @who); - $vars->{'who_count'} = scalar @who; - $vars->{'from'} = $from; - $vars->{'to'} = $to; - $vars->{'group'} = $input->{'group'}; + $vars->{'incomplete_data'} = $incomplete_data; + $vars->{'operations'} = \@operations; + + my @bug_ids = sort { $a <=> $b } keys %bug_ids; + $vars->{'bug_ids'} = \@bug_ids; + } + + $vars->{'action'} = $action; + $vars->{'who'} = join(',', @who); + $vars->{'who_count'} = scalar @who; + $vars->{'from'} = $from; + $vars->{'to'} = $to; + $vars->{'group'} = $input->{'group'}; } 1; diff --git a/extensions/BMO/lib/Util.pm b/extensions/BMO/lib/Util.pm index dc9c904f9..4d376aecb 100644 --- a/extensions/BMO/lib/Util.pm +++ b/extensions/BMO/lib/Util.pm @@ -19,74 +19,77 @@ use DateTime; use base qw(Exporter); our @EXPORT = qw( string_to_datetime - time_to_datetime - parse_date - is_active_status_field ); + time_to_datetime + parse_date + is_active_status_field ); sub string_to_datetime { - my $input = shift; - my $time = parse_date($input) - or ThrowUserError('report_invalid_date', { date => $input }); - return time_to_datetime($time); + my $input = shift; + my $time = parse_date($input) + or ThrowUserError('report_invalid_date', {date => $input}); + return time_to_datetime($time); } sub time_to_datetime { - my $time = shift; - return DateTime->from_epoch(epoch => $time) - ->set_time_zone('local') - ->truncate(to => 'day'); + my $time = shift; + return DateTime->from_epoch(epoch => $time)->set_time_zone('local') + ->truncate(to => 'day'); } sub parse_date { - my ($str) = @_; - if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) { - # relative date - my ($sign, $amount, $unit, $date) = ($1, $2, lc $3, time); - my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); - $amount = -$amount if $sign && $sign eq '+'; - if ($unit eq 'w') { - # convert weeks to days - $amount = 7 * $amount + $wday; - $unit = 'd'; - } - if ($unit eq 'd') { - $date -= $sec + 60 * $min + 3600 * $hour + 24 * 3600 * $amount; - return $date; - } - elsif ($unit eq 'y') { - return str2time(sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount)); - } - elsif ($unit eq 'm') { - $month -= $amount; - while ($month < 0) { $year--; $month += 12; } - return str2time(sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1)); - } - elsif ($unit eq 'h') { - # Special case 0h for 'beginning of this hour' - if ($amount == 0) { - $date -= $sec + 60 * $min; - } else { - $date -= 3600 * $amount; - } - return $date; - } - return undef; + my ($str) = @_; + if ($str =~ /^(-|\+)?(\d+)([hHdDwWmMyY])$/) { + + # relative date + my ($sign, $amount, $unit, $date) = ($1, $2, lc $3, time); + my ($sec, $min, $hour, $mday, $month, $year, $wday) = localtime($date); + $amount = -$amount if $sign && $sign eq '+'; + if ($unit eq 'w') { + + # convert weeks to days + $amount = 7 * $amount + $wday; + $unit = 'd'; + } + if ($unit eq 'd') { + $date -= $sec + 60 * $min + 3600 * $hour + 24 * 3600 * $amount; + return $date; + } + elsif ($unit eq 'y') { + return str2time(sprintf("%4d-01-01 00:00:00", $year + 1900 - $amount)); } - return str2time($str); + elsif ($unit eq 'm') { + $month -= $amount; + while ($month < 0) { $year--; $month += 12; } + return str2time(sprintf("%4d-%02d-01 00:00:00", $year + 1900, $month + 1)); + } + elsif ($unit eq 'h') { + + # Special case 0h for 'beginning of this hour' + if ($amount == 0) { + $date -= $sec + 60 * $min; + } + else { + $date -= 3600 * $amount; + } + return $date; + } + return undef; + } + return str2time($str); } sub is_active_status_field { - my ($field) = @_; + my ($field) = @_; - if ($field->type == FIELD_TYPE_EXTENSION - && $field->isa('Bugzilla::Extension::TrackingFlags::Flag') - && $field->flag_type eq 'tracking' - && $field->name =~ /_status_/ - ) { - return $field->is_active; - } + if ( $field->type == FIELD_TYPE_EXTENSION + && $field->isa('Bugzilla::Extension::TrackingFlags::Flag') + && $field->flag_type eq 'tracking' + && $field->name =~ /_status_/) + { + return $field->is_active; + } - return 0; + return 0; } 1; diff --git a/extensions/BMO/lib/WebService.pm b/extensions/BMO/lib/WebService.pm index 327c8563f..4c9187254 100644 --- a/extensions/BMO/lib/WebService.pm +++ b/extensions/BMO/lib/WebService.pm @@ -32,26 +32,26 @@ use Bugzilla::WebService::Util qw(validate); use Bugzilla::Field; use constant PUBLIC_METHODS => qw( - getBugsConfirmer - getBugsVerifier + getBugsConfirmer + getBugsVerifier ); sub getBugsConfirmer { - my ($self, $params) = validate(@_, 'names'); - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'names'); + my $dbh = Bugzilla->dbh; - defined($params->{names}) - || ThrowCodeError('params_required', - { function => 'BMO.getBugsConfirmer', params => ['names'] }); + defined($params->{names}) + || ThrowCodeError('params_required', + {function => 'BMO.getBugsConfirmer', params => ['names']}); - my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }; + my @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}}; - # start filtering to remove duplicate user ids - @user_objects = values %{{ map { $_->id => $_ } @user_objects }}; + # start filtering to remove duplicate user ids + @user_objects = values %{{map { $_->id => $_ } @user_objects}}; - my $fieldid = get_field_id('bug_status'); + my $fieldid = get_field_id('bug_status'); - my $query = "SELECT DISTINCT bugs_activity.bug_id + my $query = "SELECT DISTINCT bugs_activity.bug_id FROM bugs_activity LEFT JOIN bug_group_map ON bugs_activity.bug_id = bug_group_map.bug_id @@ -62,31 +62,31 @@ sub getBugsConfirmer { AND bug_group_map.bug_id IS NULL ORDER BY bugs_activity.bug_id"; - my %users; - foreach my $user (@user_objects) { - my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); - $users{$user->login} = $bugs; - } + my %users; + foreach my $user (@user_objects) { + my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); + $users{$user->login} = $bugs; + } - return \%users; + return \%users; } sub getBugsVerifier { - my ($self, $params) = validate(@_, 'names'); - my $dbh = Bugzilla->dbh; + my ($self, $params) = validate(@_, 'names'); + my $dbh = Bugzilla->dbh; - defined($params->{names}) - || ThrowCodeError('params_required', - { function => 'BMO.getBugsVerifier', params => ['names'] }); + defined($params->{names}) + || ThrowCodeError('params_required', + {function => 'BMO.getBugsVerifier', params => ['names']}); - my @user_objects = map { Bugzilla::User->check($_) } @{ $params->{names} }; + my @user_objects = map { Bugzilla::User->check($_) } @{$params->{names}}; - # start filtering to remove duplicate user ids - @user_objects = values %{{ map { $_->id => $_ } @user_objects }}; + # start filtering to remove duplicate user ids + @user_objects = values %{{map { $_->id => $_ } @user_objects}}; - my $fieldid = get_field_id('bug_status'); + my $fieldid = get_field_id('bug_status'); - my $query = "SELECT DISTINCT bugs_activity.bug_id + my $query = "SELECT DISTINCT bugs_activity.bug_id FROM bugs_activity LEFT JOIN bug_group_map ON bugs_activity.bug_id = bug_group_map.bug_id @@ -97,13 +97,13 @@ sub getBugsVerifier { AND bug_group_map.bug_id IS NULL ORDER BY bugs_activity.bug_id"; - my %users; - foreach my $user (@user_objects) { - my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); - $users{$user->login} = $bugs; - } + my %users; + foreach my $user (@user_objects) { + my $bugs = $dbh->selectcol_arrayref($query, undef, $fieldid, $user->id); + $users{$user->login} = $bugs; + } - return \%users; + return \%users; } 1; diff --git a/extensions/BMO/t/bounty_attachment.t b/extensions/BMO/t/bounty_attachment.t index 6e596eeba..61552c074 100644 --- a/extensions/BMO/t/bounty_attachment.t +++ b/extensions/BMO/t/bounty_attachment.t @@ -13,46 +13,69 @@ use Test::More; use Bugzilla; BEGIN { Bugzilla->extensions } -my $class = 'Bugzilla::Extension::BMO'; +my $class = 'Bugzilla::Extension::BMO'; my $parse = $class->can('parse_bounty_attachment_description'); my $format = $class->can('format_bounty_attachment_description'); ok($parse, "got the function"); my $bughunter = $parse->('bughunter@hacker.org, , 2014-06-25, , ,false'); -is_deeply({ reporter_email => 'bughunter@hacker.org', - amount_paid => '', - reported_date => '2014-06-25', - fixed_date => '', - awarded_date => '', - publish => 0, - credit => []}, $bughunter); - -my $hfli = $parse->('hfli@fortinet.com, 1000, 2010-07-16, 2010-08-04, 2011-06-15, true, Fortiguard Labs'); -is_deeply({ reporter_email => 'hfli@fortinet.com', - amount_paid => '1000', - reported_date => '2010-07-16', - fixed_date => '2010-08-04', - awarded_date => '2011-06-15', - publish => 1, - credit => ['Fortiguard Labs']}, $hfli); - -is('batman@justiceleague.america,1000,2015-01-01,2015-02-02,2015-03-03,true,JLA,Wayne Industries,Test', - $format->({ reporter_email => 'batman@justiceleague.america', - amount_paid => 1000, - reported_date => '2015-01-01', - fixed_date => '2015-02-02', - awarded_date => '2015-03-03', - publish => 1, - credit => ['JLA', 'Wayne Industries', 'Test'] })); - -my $dylan = $parse->('dylan@hardison.net,2,2014-09-23,2014-09-24,2014-09-25,true,Foo bar,Bork,'); -is_deeply({ reporter_email => 'dylan@hardison.net', - amount_paid => 2, - reported_date => '2014-09-23', - fixed_date => '2014-09-24', - awarded_date => '2014-09-25', - publish => 1, - credit => ['Foo bar', 'Bork']}, $dylan); +is_deeply( + { + reporter_email => 'bughunter@hacker.org', + amount_paid => '', + reported_date => '2014-06-25', + fixed_date => '', + awarded_date => '', + publish => 0, + credit => [] + }, + $bughunter +); + +my $hfli + = $parse->( + 'hfli@fortinet.com, 1000, 2010-07-16, 2010-08-04, 2011-06-15, true, Fortiguard Labs' + ); +is_deeply( + { + reporter_email => 'hfli@fortinet.com', + amount_paid => '1000', + reported_date => '2010-07-16', + fixed_date => '2010-08-04', + awarded_date => '2011-06-15', + publish => 1, + credit => ['Fortiguard Labs'] + }, + $hfli +); + +is( + 'batman@justiceleague.america,1000,2015-01-01,2015-02-02,2015-03-03,true,JLA,Wayne Industries,Test', + $format->({ + reporter_email => 'batman@justiceleague.america', + amount_paid => 1000, + reported_date => '2015-01-01', + fixed_date => '2015-02-02', + awarded_date => '2015-03-03', + publish => 1, + credit => ['JLA', 'Wayne Industries', 'Test'] + }) +); + +my $dylan = $parse->( + 'dylan@hardison.net,2,2014-09-23,2014-09-24,2014-09-25,true,Foo bar,Bork,'); +is_deeply( + { + reporter_email => 'dylan@hardison.net', + amount_paid => 2, + reported_date => '2014-09-23', + fixed_date => '2014-09-24', + awarded_date => '2014-09-25', + publish => 1, + credit => ['Foo bar', 'Bork'] + }, + $dylan +); done_testing; diff --git a/extensions/Bitly/Config.pm b/extensions/Bitly/Config.pm index 7e46a6ad8..ce27508c7 100644 --- a/extensions/Bitly/Config.pm +++ b/extensions/Bitly/Config.pm @@ -16,6 +16,6 @@ use constant NAME => 'Bitly'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; -use constant API_VERSION_MAP => { '1_0' => '1_0' }; +use constant API_VERSION_MAP => {'1_0' => '1_0'}; __PACKAGE__->NAME; diff --git a/extensions/Bitly/Extension.pm b/extensions/Bitly/Extension.pm index 82f17bc2a..31f4b7438 100644 --- a/extensions/Bitly/Extension.pm +++ b/extensions/Bitly/Extension.pm @@ -17,17 +17,14 @@ our $VERSION = '1'; use Bugzilla; sub webservice { - my ($self, $args) = @_; - $args->{dispatch}->{Bitly} = "Bugzilla::Extension::Bitly::WebService"; + my ($self, $args) = @_; + $args->{dispatch}->{Bitly} = "Bugzilla::Extension::Bitly::WebService"; } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{advanced}->{params} }, { - name => 'bitly_token', - type => 't', - default => '', - }; + my ($self, $args) = @_; + push @{$args->{panels}->{advanced}->{params}}, + {name => 'bitly_token', type => 't', default => '',}; } __PACKAGE__->NAME; diff --git a/extensions/Bitly/lib/WebService.pm b/extensions/Bitly/lib/WebService.pm index 4b44faa0e..b900e17ee 100644 --- a/extensions/Bitly/lib/WebService.pm +++ b/extensions/Bitly/lib/WebService.pm @@ -27,121 +27,115 @@ use URI::Escape; use URI::QueryParam; use constant PUBLIC_METHODS => qw( - list - shorten + list + shorten ); sub _validate_uri { - my ($self, $params) = @_; - - # extract url from params - if (!defined $params->{url}) { - ThrowCodeError( - 'param_required', - { function => 'Bitly.shorten', param => 'url' } - ); - } - my $url = ref($params->{url}) ? $params->{url}->[0] : $params->{url}; - - # only allow buglist queries for this bugzilla install - my $uri = URI->new($url); - $uri->query(undef); - $uri->fragment(undef); - if ($uri->as_string ne Bugzilla->localconfig->{urlbase} . 'buglist.cgi') { - ThrowUserError('bitly_unsupported'); - } - - return URI->new($url); + my ($self, $params) = @_; + + # extract url from params + if (!defined $params->{url}) { + ThrowCodeError('param_required', {function => 'Bitly.shorten', param => 'url'}); + } + my $url = ref($params->{url}) ? $params->{url}->[0] : $params->{url}; + + # only allow buglist queries for this bugzilla install + my $uri = URI->new($url); + $uri->query(undef); + $uri->fragment(undef); + if ($uri->as_string ne Bugzilla->localconfig->{urlbase} . 'buglist.cgi') { + ThrowUserError('bitly_unsupported'); + } + + return URI->new($url); } sub shorten { - my ($self) = shift; - my $uri = $self->_validate_uri(@_); + my ($self) = shift; + my $uri = $self->_validate_uri(@_); - # the list_id is user-specific, remove it - $uri->query_param_delete('list_id'); + # the list_id is user-specific, remove it + $uri->query_param_delete('list_id'); - return $self->_bitly($uri); + return $self->_bitly($uri); } sub list { - my ($self) = shift; - my $uri = $self->_validate_uri(@_); - - # map params to cgi vars, converting quicksearch if required - my $params = $uri->query_param('quicksearch') - ? Bugzilla::CGI->new(quicksearch($uri->query_param('quicksearch')))->Vars - : Bugzilla::CGI->new($uri->query)->Vars; - - # execute the search - my $search = Bugzilla::Search->new( - params => $params, - fields => ['bug_id'], - limit => Bugzilla->params->{max_search_results}, - ); - my $data = $search->data; - - # form a bug_id only url, sanity check the length - $uri = URI->new(Bugzilla->localconfig->{urlbase} . 'buglist.cgi?bug_id=' . join(',', map { $_->[0] } @$data)); - if (length($uri->as_string) > CGI_URI_LIMIT) { - ThrowUserError('bitly_failure', { message => "Too many bugs returned by search" }); - } - - # shorten - return $self->_bitly($uri); + my ($self) = shift; + my $uri = $self->_validate_uri(@_); + + # map params to cgi vars, converting quicksearch if required + my $params + = $uri->query_param('quicksearch') + ? Bugzilla::CGI->new(quicksearch($uri->query_param('quicksearch')))->Vars + : Bugzilla::CGI->new($uri->query)->Vars; + + # execute the search + my $search = Bugzilla::Search->new( + params => $params, + fields => ['bug_id'], + limit => Bugzilla->params->{max_search_results}, + ); + my $data = $search->data; + + # form a bug_id only url, sanity check the length + $uri + = URI->new(Bugzilla->localconfig->{urlbase} + . 'buglist.cgi?bug_id=' + . join(',', map { $_->[0] } @$data)); + if (length($uri->as_string) > CGI_URI_LIMIT) { + ThrowUserError('bitly_failure', + {message => "Too many bugs returned by search"}); + } + + # shorten + return $self->_bitly($uri); } sub _bitly { - my ($self, $uri) = @_; - - # form request url - # http://dev.bitly.com/links.html#v3_shorten - my $bitly_url = sprintf( - 'https://api-ssl.bitly.com/v3/shorten?access_token=%s&longUrl=%s', - Bugzilla->params->{bitly_token}, - uri_escape($uri->as_string) - ); - - # is Mozilla::CA isn't installed, skip certificate verification - eval { require Mozilla::CA }; - $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = $@ ? 0 : 1; - - # request - my $ua = LWP::UserAgent->new(agent => 'Bugzilla'); - $ua->timeout(10); - $ua->protocols_allowed(['http', 'https']); - if (my $proxy_url = Bugzilla->params->{proxy_url}) { - $ua->proxy(['http', 'https'], $proxy_url); - } - else { - $ua->env_proxy(); - } - my $response = $ua->get($bitly_url); - if ($response->is_error) { - ThrowUserError('bitly_failure', { message => $response->message }); - } - my $result = decode_json($response->decoded_content); - if ($result->{status_code} != 200) { - ThrowUserError('bitly_failure', { message => $result->{status_txt} }); - } - - # return just the short url - return { url => $result->{data}->{url} }; + my ($self, $uri) = @_; + + # form request url + # http://dev.bitly.com/links.html#v3_shorten + my $bitly_url = sprintf( + 'https://api-ssl.bitly.com/v3/shorten?access_token=%s&longUrl=%s', + Bugzilla->params->{bitly_token}, + uri_escape($uri->as_string) + ); + + # is Mozilla::CA isn't installed, skip certificate verification + eval { require Mozilla::CA }; + $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = $@ ? 0 : 1; + + # request + my $ua = LWP::UserAgent->new(agent => 'Bugzilla'); + $ua->timeout(10); + $ua->protocols_allowed(['http', 'https']); + if (my $proxy_url = Bugzilla->params->{proxy_url}) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy(); + } + my $response = $ua->get($bitly_url); + if ($response->is_error) { + ThrowUserError('bitly_failure', {message => $response->message}); + } + my $result = decode_json($response->decoded_content); + if ($result->{status_code} != 200) { + ThrowUserError('bitly_failure', {message => $result->{status_txt}}); + } + + # return just the short url + return {url => $result->{data}->{url}}; } sub rest_resources { - return [ - qr{^/bitly/shorten$}, { - GET => { - method => 'shorten', - }, - }, - qr{^/bitly/list$}, { - GET => { - method => 'list', - }, - }, - ] + return [ + qr{^/bitly/shorten$}, {GET => {method => 'shorten',},}, + qr{^/bitly/list$}, {GET => {method => 'list',},}, + ]; } 1; diff --git a/extensions/BugModal/Config.pm b/extensions/BugModal/Config.pm index 25a864e6e..bd15c075c 100644 --- a/extensions/BugModal/Config.pm +++ b/extensions/BugModal/Config.pm @@ -10,8 +10,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'BugModal'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'BugModal'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/BugModal/Extension.pm b/extensions/BugModal/Extension.pm index ef9c93a37..8b72bb757 100644 --- a/extensions/BugModal/Extension.pm +++ b/extensions/BugModal/Extension.pm @@ -26,322 +26,321 @@ use JSON::XS qw(encode_json); our $VERSION = '1'; use constant READABLE_BUG_STATUS_PRODUCTS => ( - 'Core', - 'Toolkit', - 'Firefox', - 'Firefox for Android', - 'Firefox for iOS', - 'Bugzilla', - 'bugzilla.mozilla.org' + 'Core', 'Toolkit', + 'Firefox', 'Firefox for Android', + 'Firefox for iOS', 'Bugzilla', + 'bugzilla.mozilla.org' ); sub show_bug_format { - my ($self, $args) = @_; - $args->{format} = _alternative_show_bug_format(); + my ($self, $args) = @_; + $args->{format} = _alternative_show_bug_format(); } sub edit_bug_format { - my ($self, $args) = @_; - $args->{format} = _alternative_show_bug_format(); + my ($self, $args) = @_; + $args->{format} = _alternative_show_bug_format(); } sub _alternative_show_bug_format { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - if (my $ctype = $cgi->param('ctype')) { - return '' if $ctype ne 'html'; - } - if (my $format = $cgi->param('format')) { - return ($format eq '__default__' || $format eq 'default') ? '' : $format; - } - return $user->setting('ui_experiments') eq 'on' ? 'modal' : ''; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + if (my $ctype = $cgi->param('ctype')) { + return '' if $ctype ne 'html'; + } + if (my $format = $cgi->param('format')) { + return ($format eq '__default__' || $format eq 'default') ? '' : $format; + } + return $user->setting('ui_experiments') eq 'on' ? 'modal' : ''; } sub template_after_create { - my ($self, $args) = @_; - my $context = $args->{template}->context; - - # wrapper around time_ago() - $context->define_filter( - time_duration => sub { - my ($context) = @_; - return sub { - my ($timestamp) = @_; - my $datetime = datetime_from($timestamp) - // return $timestamp; - return time_ago($datetime); - }; - }, 1 - ); - - # morph a string into one which is suitable to use as an element's id - $context->define_filter( - id => sub { - my ($context) = @_; - return sub { - my ($id) = @_; - $id //= ''; - $id = lc($id); - while ($id ne '' && $id !~ /^[a-z]/) { - $id = substr($id, 1); - } - $id =~ tr/ /-/; - $id =~ s/[^a-z\d\-_:\.]/_/g; - return $id; - }; - }, 1 - ); - - # parse date string and output epoch - $context->define_filter( - epoch => sub { - my ($context) = @_; - return sub { - my ($date_str) = @_; - return date_str_to_time($date_str); - }; - }, 1 - ); - - # flatten a list of hashrefs to a list of values - # eg. logins = users.pluck("login") - $context->define_vmethod( - list => pluck => sub { - my ($list, $field) = @_; - return [ map { $_->$field } @$list ]; - } - ); - - # returns array where the value in $field does not equal $value - # opposite of "only" - # eg. not_byron = users.skip("name", "Byron") - $context->define_vmethod( - list => skip => sub { - my ($list, $field, $value) = @_; - return [ grep { $_->$field ne $value } @$list ]; - } - ); - - # returns array where the value in $field equals $value - # opposite of "skip" - # eg. byrons_only = users.only("name", "Byron") - $context->define_vmethod( - list => only => sub { - my ($list, $field, $value) = @_; - return [ grep { $_->$field eq $value } @$list ]; - } - ); - - # returns boolean indicating if the value exists in the list - # eg. has_byron = user_names.exists("byron") - $context->define_vmethod( - list => exists => sub { - my ($list, $value) = @_; - return any { $_ eq $value } @$list; - } - ); - - # ucfirst is only available in new template::toolkit versions - $context->define_vmethod( - item => ucfirst => sub { - my ($text) = @_; - return ucfirst($text); - } - ); -} - -sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{file}; - my $vars = $args->{vars}; - - if ($file eq 'bug/process/header.html.tmpl' - || $file eq 'bug/create/created.html.tmpl' - || $file eq 'attachment/created.html.tmpl' - || $file eq 'attachment/updated.html.tmpl') - { - if (_alternative_show_bug_format() eq 'modal') { - $vars->{alt_ui_header} = 'bug_modal/header.html.tmpl'; - $vars->{alt_ui_show} = 'bug/show-modal.html.tmpl'; - $vars->{alt_ui_edit} = 'bug_modal/edit.html.tmpl'; + my ($self, $args) = @_; + my $context = $args->{template}->context; + + # wrapper around time_ago() + $context->define_filter( + time_duration => sub { + my ($context) = @_; + return sub { + my ($timestamp) = @_; + my $datetime = datetime_from($timestamp) // return $timestamp; + return time_ago($datetime); + }; + }, + 1 + ); + + # morph a string into one which is suitable to use as an element's id + $context->define_filter( + id => sub { + my ($context) = @_; + return sub { + my ($id) = @_; + $id //= ''; + $id = lc($id); + while ($id ne '' && $id !~ /^[a-z]/) { + $id = substr($id, 1); } - return; + $id =~ tr/ /-/; + $id =~ s/[^a-z\d\-_:\.]/_/g; + return $id; + }; + }, + 1 + ); + + # parse date string and output epoch + $context->define_filter( + epoch => sub { + my ($context) = @_; + return sub { + my ($date_str) = @_; + return date_str_to_time($date_str); + }; + }, + 1 + ); + + # flatten a list of hashrefs to a list of values + # eg. logins = users.pluck("login") + $context->define_vmethod( + list => pluck => sub { + my ($list, $field) = @_; + return [map { $_->$field } @$list]; } - - if ($file =~ m#^bug/show-([^\.]+)\.html\.tmpl$#) { - my $format = $1; - return unless _alternative_show_bug_format() eq $format; + ); + + # returns array where the value in $field does not equal $value + # opposite of "only" + # eg. not_byron = users.skip("name", "Byron") + $context->define_vmethod( + list => skip => sub { + my ($list, $field, $value) = @_; + return [grep { $_->$field ne $value } @$list]; } - elsif ($file ne 'bug_modal/edit.html.tmpl') { - return; + ); + + # returns array where the value in $field equals $value + # opposite of "skip" + # eg. byrons_only = users.only("name", "Byron") + $context->define_vmethod( + list => only => sub { + my ($list, $field, $value) = @_; + return [grep { $_->$field eq $value } @$list]; } - - if ($vars->{bug} && !$vars->{bugs}) { - $vars->{bugs} = [$vars->{bug}]; + ); + + # returns boolean indicating if the value exists in the list + # eg. has_byron = user_names.exists("byron") + $context->define_vmethod( + list => exists => sub { + my ($list, $value) = @_; + return any { $_ eq $value } @$list; } + ); - return unless - $vars->{bugs} - && ref($vars->{bugs}) eq 'ARRAY' - && scalar(@{ $vars->{bugs} }) == 1; - my $bug = $vars->{bugs}->[0]; - return if exists $bug->{error}; - - # trigger loading of tracking flags - if (Bugzilla->has_extension('TrackingFlags')) { - Bugzilla::Extension::TrackingFlags->template_before_process({ - file => 'bug/edit.html.tmpl', - vars => $vars, - }); + # ucfirst is only available in new template::toolkit versions + $context->define_vmethod( + item => ucfirst => sub { + my ($text) = @_; + return ucfirst($text); } + ); +} - if (any { $bug->product eq $_ } READABLE_BUG_STATUS_PRODUCTS) { - my @flags = map { { name => $_->name, status => $_->status } } @{$bug->flags}; - $vars->{readable_bug_status_json} = encode_json({ - dupe_of => $bug->dup_id, - id => $bug->id, - keywords => [ map { $_->name } @{$bug->keyword_objects} ], - priority => $bug->priority, - resolution => $bug->resolution, - status => $bug->bug_status, - flags => \@flags, - target_milestone => $bug->target_milestone, - map { $_->name => $_->bug_flag($bug->id)->value } @{$vars->{tracking_flags}}, - }); - # HTML4 attributes cannot be longer than this, so just skip it in this case. - if (length($vars->{readable_bug_status_json}) > 65536) { - delete $vars->{readable_bug_status_json}; - } +sub template_before_process { + my ($self, $args) = @_; + my $file = $args->{file}; + my $vars = $args->{vars}; + + if ( $file eq 'bug/process/header.html.tmpl' + || $file eq 'bug/create/created.html.tmpl' + || $file eq 'attachment/created.html.tmpl' + || $file eq 'attachment/updated.html.tmpl') + { + if (_alternative_show_bug_format() eq 'modal') { + $vars->{alt_ui_header} = 'bug_modal/header.html.tmpl'; + $vars->{alt_ui_show} = 'bug/show-modal.html.tmpl'; + $vars->{alt_ui_edit} = 'bug_modal/edit.html.tmpl'; } + return; + } + + if ($file =~ m#^bug/show-([^\.]+)\.html\.tmpl$#) { + my $format = $1; + return unless _alternative_show_bug_format() eq $format; + } + elsif ($file ne 'bug_modal/edit.html.tmpl') { + return; + } + + if ($vars->{bug} && !$vars->{bugs}) { + $vars->{bugs} = [$vars->{bug}]; + } + + return + unless $vars->{bugs} + && ref($vars->{bugs}) eq 'ARRAY' + && scalar(@{$vars->{bugs}}) == 1; + my $bug = $vars->{bugs}->[0]; + return if exists $bug->{error}; + + # trigger loading of tracking flags + if (Bugzilla->has_extension('TrackingFlags')) { + Bugzilla::Extension::TrackingFlags->template_before_process({ + file => 'bug/edit.html.tmpl', vars => $vars, + }); + } + + if (any { $bug->product eq $_ } READABLE_BUG_STATUS_PRODUCTS) { + my @flags = map { {name => $_->name, status => $_->status} } @{$bug->flags}; + $vars->{readable_bug_status_json} = encode_json({ + dupe_of => $bug->dup_id, + id => $bug->id, + keywords => [map { $_->name } @{$bug->keyword_objects}], + priority => $bug->priority, + resolution => $bug->resolution, + status => $bug->bug_status, + flags => \@flags, + target_milestone => $bug->target_milestone, + map { $_->name => $_->bug_flag($bug->id)->value } @{$vars->{tracking_flags}}, + }); - # bug->choices loads a lot of data that we want to lazy-load - # just load the status and resolutions and perform extra checks here - # upstream does these checks in the bug/fields template - my $perms = $bug->user; - my @resolutions; - foreach my $r (@{ Bugzilla::Field->new({ name => 'resolution', cache => 1 })->legal_values }) { - my $resolution = $r->name; - next unless $resolution; - - # always allow the current value - if ($resolution eq $bug->resolution) { - push @resolutions, $r; - next; - } - - # never allow inactive values - next unless $r->is_active; + # HTML4 attributes cannot be longer than this, so just skip it in this case. + if (length($vars->{readable_bug_status_json}) > 65536) { + delete $vars->{readable_bug_status_json}; + } + } + + # bug->choices loads a lot of data that we want to lazy-load + # just load the status and resolutions and perform extra checks here + # upstream does these checks in the bug/fields template + my $perms = $bug->user; + my @resolutions; + foreach my $r ( + @{Bugzilla::Field->new({name => 'resolution', cache => 1})->legal_values}) + { + my $resolution = $r->name; + next unless $resolution; + + # always allow the current value + if ($resolution eq $bug->resolution) { + push @resolutions, $r; + next; + } - # ensure the user has basic rights to change this field - next unless $bug->check_can_change_field('resolution', '---', $resolution); + # never allow inactive values + next unless $r->is_active; - # canconfirm users can only set the resolution to WFM, INCOMPLETE or DUPE - if ($perms->{canconfirm} - && !($perms->{canedit} || $perms->{isreporter})) - { - next if - $resolution ne 'WORKSFORME' - && $resolution ne 'INCOMPLETE' - && $resolution ne 'DUPLICATE'; - } + # ensure the user has basic rights to change this field + next unless $bug->check_can_change_field('resolution', '---', $resolution); - # reporters can set it to anything, except INCOMPLETE - if ($perms->{isreporter} - && !($perms->{canconfirm} || $perms->{canedit})) - { - next if $resolution eq 'INCOMPLETE'; - } + # canconfirm users can only set the resolution to WFM, INCOMPLETE or DUPE + if ($perms->{canconfirm} && !($perms->{canedit} || $perms->{isreporter})) { + next + if $resolution ne 'WORKSFORME' + && $resolution ne 'INCOMPLETE' + && $resolution ne 'DUPLICATE'; + } - # expired has, uh, expired - next if $resolution eq 'EXPIRED'; + # reporters can set it to anything, except INCOMPLETE + if ($perms->{isreporter} && !($perms->{canconfirm} || $perms->{canedit})) { + next if $resolution eq 'INCOMPLETE'; + } - push @resolutions, $r; + # expired has, uh, expired + next if $resolution eq 'EXPIRED'; + + push @resolutions, $r; + } + $bug->{choices} = { + bug_status => [ + grep { $_->is_active || $_->name eq $bug->bug_status } + @{$bug->statuses_available} + ], + resolution => \@resolutions, + }; + + # group tracking flags by version to allow for a better tabular output + my @tracking_table; + my $tracking_flags = $vars->{tracking_flags}; + foreach my $flag (@$tracking_flags) { + my $flag_type = $flag->flag_type; + my $type = 'status'; + my $name = $flag->description; + if ($flag_type eq 'tracking' && $name =~ /^(tracking|status)-(.+)/) { + ($type, $name) = ($1, $2); } - $bug->{choices} = { - bug_status => [ - grep { $_->is_active || $_->name eq $bug->bug_status } - @{ $bug->statuses_available } - ], - resolution => \@resolutions, - }; - - # group tracking flags by version to allow for a better tabular output - my @tracking_table; - my $tracking_flags = $vars->{tracking_flags}; - foreach my $flag (@$tracking_flags) { - my $flag_type = $flag->flag_type; - my $type = 'status'; - my $name = $flag->description; - if ($flag_type eq 'tracking' && $name =~ /^(tracking|status)-(.+)/) { - ($type, $name) = ($1, $2); - } - my ($existing) = grep { $_->{type} eq $flag_type && $_->{name} eq $name } @tracking_table; - if ($existing) { - $existing->{$type} = $flag; - } - else { - push @tracking_table, { - $type => $flag, - name => $name, - type => $flag_type, - }; - } + my ($existing) + = grep { $_->{type} eq $flag_type && $_->{name} eq $name } @tracking_table; + if ($existing) { + $existing->{$type} = $flag; } - $vars->{tracking_flags_table} = \@tracking_table; - - # for the "view -> hide treeherder comments" menu item - my $treeherder_id = Bugzilla->treeherder_user->id; - foreach my $change_set (@{ $bug->activity_stream }) { - if ($change_set->{comment} && $change_set->{comment}->author->id == $treeherder_id) { - $vars->{treeherder} = Bugzilla->treeherder_user; - last; - } + else { + push @tracking_table, {$type => $flag, name => $name, type => $flag_type,}; + } + } + $vars->{tracking_flags_table} = \@tracking_table; + + # for the "view -> hide treeherder comments" menu item + my $treeherder_id = Bugzilla->treeherder_user->id; + foreach my $change_set (@{$bug->activity_stream}) { + if ( $change_set->{comment} + && $change_set->{comment}->author->id == $treeherder_id) + { + $vars->{treeherder} = Bugzilla->treeherder_user; + last; } + } } sub bug_start_of_set_all { - my ($self, $args) = @_; - my $bug = $args->{bug}; - my $params = $args->{params}; - - # reset to the component defaults if not supplied - if (exists $params->{assigned_to} && (!defined $params->{assigned_to} || $params->{assigned_to} eq '')) { - $params->{assigned_to} = $bug->component_obj->default_assignee->login; - } - if (exists $params->{qa_contact} && (!defined $params->{qa_contact} || $params->{qa_contact} eq '') - && $bug->component_obj->default_qa_contact->id) - { - $params->{qa_contact} = $bug->component_obj->default_qa_contact->login; - } + my ($self, $args) = @_; + my $bug = $args->{bug}; + my $params = $args->{params}; + + # reset to the component defaults if not supplied + if (exists $params->{assigned_to} + && (!defined $params->{assigned_to} || $params->{assigned_to} eq '')) + { + $params->{assigned_to} = $bug->component_obj->default_assignee->login; + } + if ( exists $params->{qa_contact} + && (!defined $params->{qa_contact} || $params->{qa_contact} eq '') + && $bug->component_obj->default_qa_contact->id) + { + $params->{qa_contact} = $bug->component_obj->default_qa_contact->login; + } } sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{bug_modal} = 'Bugzilla::Extension::BugModal::WebService'; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{bug_modal} = 'Bugzilla::Extension::BugModal::WebService'; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'ui_experiments', - options => ['on', 'off'], - default => 'on', - category => 'User Interface' - }); - add_setting({ - name => 'ui_remember_collapsed', - options => ['on', 'off'], - default => 'off', - category => 'User Interface' - }); - add_setting({ - name => 'ui_use_absolute_time', - options => ['on', 'off'], - default => 'off', - category => 'User Interface', - }); + my ($self, $args) = @_; + add_setting({ + name => 'ui_experiments', + options => ['on', 'off'], + default => 'on', + category => 'User Interface' + }); + add_setting({ + name => 'ui_remember_collapsed', + options => ['on', 'off'], + default => 'off', + category => 'User Interface' + }); + add_setting({ + name => 'ui_use_absolute_time', + options => ['on', 'off'], + default => 'off', + category => 'User Interface', + }); } __PACKAGE__->NAME; diff --git a/extensions/BugModal/lib/ActivityStream.pm b/extensions/BugModal/lib/ActivityStream.pm index 098c5df33..a7983e85c 100644 --- a/extensions/BugModal/lib/ActivityStream.pm +++ b/extensions/BugModal/lib/ActivityStream.pm @@ -49,310 +49,324 @@ use Bugzilla::Constants; # ] sub activity_stream { - my ($self) = @_; - if (!$self->{activity_stream}) { - my $stream = []; - _add_comments_to_stream($self, $stream); - _add_activities_to_stream($self, $stream); - _add_duplicates_to_stream($self, $stream); - - my $base_time = date_str_to_time($self->creation_ts); - foreach my $change_set (@$stream) { - $change_set->{id} = $change_set->{comment} - ? 'c' . $change_set->{comment}->count - : 'a' . ($change_set->{time} - $base_time) . '_' . $change_set->{user_id}; - foreach my $activity (@{ $change_set->{activity} }) { - $activity->{changes} = [ - sort { $a->{fieldname} cmp $b->{fieldname} } - @{ $activity->{changes} } - ]; - } - } - my $order = Bugzilla->user->setting('comment_sort_order'); - if ($order eq 'oldest_to_newest') { - $self->{activity_stream} = [ sort { $a->{time} <=> $b->{time} } @$stream ]; - } - elsif ($order eq 'newest_to_oldest') { - $self->{activity_stream} = [ sort { $b->{time} <=> $a->{time} } @$stream ]; - } - elsif ($order eq 'newest_to_oldest_desc_first') { - my $desc = shift @$stream; - $self->{activity_stream} = [ $desc, sort { $b->{time} <=> $a->{time} } @$stream ]; - } + my ($self) = @_; + if (!$self->{activity_stream}) { + my $stream = []; + _add_comments_to_stream($self, $stream); + _add_activities_to_stream($self, $stream); + _add_duplicates_to_stream($self, $stream); + + my $base_time = date_str_to_time($self->creation_ts); + foreach my $change_set (@$stream) { + $change_set->{id} + = $change_set->{comment} + ? 'c' . $change_set->{comment}->count + : 'a' . ($change_set->{time} - $base_time) . '_' . $change_set->{user_id}; + foreach my $activity (@{$change_set->{activity}}) { + $activity->{changes} + = [sort { $a->{fieldname} cmp $b->{fieldname} } @{$activity->{changes}}]; + } + } + my $order = Bugzilla->user->setting('comment_sort_order'); + if ($order eq 'oldest_to_newest') { + $self->{activity_stream} = [sort { $a->{time} <=> $b->{time} } @$stream]; } - return $self->{activity_stream}; + elsif ($order eq 'newest_to_oldest') { + $self->{activity_stream} = [sort { $b->{time} <=> $a->{time} } @$stream]; + } + elsif ($order eq 'newest_to_oldest_desc_first') { + my $desc = shift @$stream; + $self->{activity_stream} = [$desc, sort { $b->{time} <=> $a->{time} } @$stream]; + } + } + return $self->{activity_stream}; } sub find_activity_id_for_attachment { - my ($self, $attachment) = @_; - my $attach_id = $attachment->id; - my $stream = $self->activity_stream; - foreach my $change_set (@$stream) { - next unless exists $change_set->{attach_id}; - return $change_set->{id} if $change_set->{attach_id} == $attach_id; - } - return undef; + my ($self, $attachment) = @_; + my $attach_id = $attachment->id; + my $stream = $self->activity_stream; + foreach my $change_set (@$stream) { + next unless exists $change_set->{attach_id}; + return $change_set->{id} if $change_set->{attach_id} == $attach_id; + } + return undef; } sub find_activity_id_for_flag { - my ($self, $flag) = @_; - my $flagtype_name = $flag->type->name; - my $date = $flag->modification_date; - my $setter_id = $flag->setter->id; - my $stream = $self->activity_stream; - - # unfortunately bugs_activity treats all flag changes as the same field, so - # we don't have an object_id to match on - - if (!exists $self->{activity_cache}->{flag}->{$flag->id}) { - foreach my $change_set (reverse @$stream) { - foreach my $activity (@{ $change_set->{activity} }) { - # match by user, timestamp, and flag-type name - next unless - $activity->{who}->id == $setter_id - && $activity->{when} eq $date; - foreach my $change (@{ $activity->{changes} }) { - next unless - $change->{fieldname} eq 'flagtypes.name' - && $change->{flagtype_name} eq $flagtype_name; - $self->{activity_cache}->{flag}->{$flag->id} = $change_set->{id}; - return $change_set->{id}; - } - } + my ($self, $flag) = @_; + my $flagtype_name = $flag->type->name; + my $date = $flag->modification_date; + my $setter_id = $flag->setter->id; + my $stream = $self->activity_stream; + + # unfortunately bugs_activity treats all flag changes as the same field, so + # we don't have an object_id to match on + + if (!exists $self->{activity_cache}->{flag}->{$flag->id}) { + foreach my $change_set (reverse @$stream) { + foreach my $activity (@{$change_set->{activity}}) { + + # match by user, timestamp, and flag-type name + next unless $activity->{who}->id == $setter_id && $activity->{when} eq $date; + foreach my $change (@{$activity->{changes}}) { + next + unless $change->{fieldname} eq 'flagtypes.name' + && $change->{flagtype_name} eq $flagtype_name; + $self->{activity_cache}->{flag}->{$flag->id} = $change_set->{id}; + return $change_set->{id}; } - # if we couldn't find the flag in bugs_activity it means it was set - # during bug creation - $self->{activity_cache}->{flag}->{$flag->id} = 'c0'; + } } - return $self->{activity_cache}->{flag}->{$flag->id}; + + # if we couldn't find the flag in bugs_activity it means it was set + # during bug creation + $self->{activity_cache}->{flag}->{$flag->id} = 'c0'; + } + return $self->{activity_cache}->{flag}->{$flag->id}; } # comments are processed first, so there's no need to merge into existing entries sub _add_comment_to_stream { - my ($stream, $time, $user_id, $comment) = @_; - my $rh = { - time => $time, - user_id => $user_id, - comment => $comment, - activity => [], - }; - if ($comment->type == CMT_ATTACHMENT_CREATED || $comment->type == CMT_ATTACHMENT_UPDATED) { - $rh->{attach_id} = $comment->extra_data; - } - push @$stream, $rh; + my ($stream, $time, $user_id, $comment) = @_; + my $rh + = {time => $time, user_id => $user_id, comment => $comment, activity => [],}; + if ( $comment->type == CMT_ATTACHMENT_CREATED + || $comment->type == CMT_ATTACHMENT_UPDATED) + { + $rh->{attach_id} = $comment->extra_data; + } + push @$stream, $rh; } sub _add_activity_to_stream { - my ($stream, $time, $user_id, $data) = @_; - foreach my $entry (@$stream) { - next unless $entry->{time} == $time && $entry->{user_id} == $user_id; - $entry->{cc_only} = $entry->{cc_only} && $data->{cc_only}; - push @{ $entry->{activity} }, $data; - return; - } - push @$stream, { - time => $time, - user_id => $user_id, - comment => undef, - cc_only => $data->{cc_only}, - activity => [ $data ], + my ($stream, $time, $user_id, $data) = @_; + foreach my $entry (@$stream) { + next unless $entry->{time} == $time && $entry->{user_id} == $user_id; + $entry->{cc_only} = $entry->{cc_only} && $data->{cc_only}; + push @{$entry->{activity}}, $data; + return; + } + push @$stream, + { + time => $time, + user_id => $user_id, + comment => undef, + cc_only => $data->{cc_only}, + activity => [$data], }; } sub _add_comments_to_stream { - my ($bug, $stream) = @_; - my $user = Bugzilla->user; - my $treeherder_id = Bugzilla->treeherder_user->id; - - my $raw_comments = $bug->comments(); - foreach my $comment (@$raw_comments) { - next if $comment->type == CMT_HAS_DUPE; - my $author_id = $comment->author->id; - next if $comment->is_private && !($user->is_insider || $user->id == $author_id); - next if $comment->body eq '' && ($comment->work_time - 0) != 0 && $user->is_timetracker; - - # treeherder is so spammy we hide its comments by default - if ($author_id == $treeherder_id) { - $comment->{collapsed} = 1; - $comment->{collapsed_reason} = $comment->author->name; - } - if ($comment->type != CMT_ATTACHMENT_CREATED && $comment->count == 0 && length($comment->body) == 0) { - $comment->{collapsed} = 1; - $comment->{collapsed_reason} = 'empty'; - } - # If comment type is resolved as duplicate, do not add '...marked as duplicate...' string to comment body - if ($comment->type == CMT_DUPE_OF) { - $comment->set_type(0); - # Skip if user did not supply comment also - next if $comment->body eq ''; - } - - _add_comment_to_stream($stream, date_str_to_time($comment->creation_ts), $comment->author->id, $comment); + my ($bug, $stream) = @_; + my $user = Bugzilla->user; + my $treeherder_id = Bugzilla->treeherder_user->id; + + my $raw_comments = $bug->comments(); + foreach my $comment (@$raw_comments) { + next if $comment->type == CMT_HAS_DUPE; + my $author_id = $comment->author->id; + next if $comment->is_private && !($user->is_insider || $user->id == $author_id); + next + if $comment->body eq '' + && ($comment->work_time - 0) != 0 + && $user->is_timetracker; + + # treeherder is so spammy we hide its comments by default + if ($author_id == $treeherder_id) { + $comment->{collapsed} = 1; + $comment->{collapsed_reason} = $comment->author->name; } -} - -sub _add_activities_to_stream { - my ($bug, $stream) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # build bug activity - my ($raw_activity) = $bug->can('get_activity') - ? $bug->get_activity() - : Bugzilla::Bug::GetBugActivity($bug->id); - - # allow other extensions to alter history - Bugzilla::Hook::process('inline_history_activitiy', { activity => $raw_activity }); - - my %attachment_cache; - foreach my $attachment (@{$bug->attachments}) { - $attachment_cache{$attachment->id} = $attachment; + if ( $comment->type != CMT_ATTACHMENT_CREATED + && $comment->count == 0 + && length($comment->body) == 0) + { + $comment->{collapsed} = 1; + $comment->{collapsed_reason} = 'empty'; } - # build a list of bugs we need to check visibility of, so we can check with a single query - my %visible_bug_ids; +# If comment type is resolved as duplicate, do not add '...marked as duplicate...' string to comment body + if ($comment->type == CMT_DUPE_OF) { + $comment->set_type(0); - # envelope, augment and tweak - foreach my $operation (@$raw_activity) { - - # make operation.who an object - $operation->{who} = Bugzilla::User->new({ name => $operation->{who}, cache => 1 }); - - # we need to track operations which are just cc changes - $operation->{cc_only} = 1; - - for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { - my $change = $operation->{changes}->[$i]; - - # make an attachment object - if ($change->{attachid}) { - $change->{attach} = $attachment_cache{$change->{attachid}}; - } - - # empty resolutions are displayed as --- by default - # make it explicit here to enable correct display of the change - if ($change->{fieldname} eq 'resolution') { - $change->{removed} = '---' if $change->{removed} eq ''; - $change->{added} = '---' if $change->{added} eq ''; - } - - # make boolean fields true/false instead of 1/0 - my ($table, $field) = ('bugs', $change->{fieldname}); - if ($field =~ /^([^\.]+)\.(.+)$/) { - ($table, $field) = ($1, $2); - } - my $column = $dbh->bz_column_info($table, $field); - if ($column && $column->{TYPE} eq 'BOOLEAN') { - $change->{removed} = ''; - $change->{added} = $change->{added} ? 'true' : 'false'; - } - - # load field object (only required for custom fields), and set the - # field type for custom fields - my $field_obj; - if ($change->{fieldname} =~ /^cf_/) { - $field_obj = Bugzilla::Field->new({ name => $change->{fieldname}, cache => 1 }); - $change->{fieldtype} = $field_obj->type; - } + # Skip if user did not supply comment also + next if $comment->body eq ''; + } - # identify buglist changes - if ($change->{fieldname} eq 'blocked' || - $change->{fieldname} eq 'dependson' || - $change->{fieldname} eq 'dupe' || - ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID) - ) { - $change->{buglist} = 1; - foreach my $what (qw(removed added)) { - my @buglist = split(/[\s,]+/, $change->{$what}); - foreach my $id (@buglist) { - if ($id && $id =~ /^\d+$/) { - $visible_bug_ids{$id} = 1; - } - } - } - } + _add_comment_to_stream($stream, date_str_to_time($comment->creation_ts), + $comment->author->id, $comment); + } +} - # split see-also - if ($change->{fieldname} eq 'see_also') { - my $url_base = Bugzilla->localconfig->{urlbase}; - foreach my $f (qw( added removed )) { - my @values; - foreach my $value (split(/, /, $change->{$f})) { - my ($bug_id) = substr($value, 0, length($url_base)) eq $url_base - ? $value =~ /id=(\d+)$/ - : undef; - push @values, { - url => $value, - bug_id => $bug_id, - }; - } - $change->{$f} = \@values; - } +sub _add_activities_to_stream { + my ($bug, $stream) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # build bug activity + my ($raw_activity) + = $bug->can('get_activity') + ? $bug->get_activity() + : Bugzilla::Bug::GetBugActivity($bug->id); + + # allow other extensions to alter history + Bugzilla::Hook::process('inline_history_activitiy', + {activity => $raw_activity}); + + my %attachment_cache; + foreach my $attachment (@{$bug->attachments}) { + $attachment_cache{$attachment->id} = $attachment; + } + +# build a list of bugs we need to check visibility of, so we can check with a single query + my %visible_bug_ids; + + # envelope, augment and tweak + foreach my $operation (@$raw_activity) { + + # make operation.who an object + $operation->{who} + = Bugzilla::User->new({name => $operation->{who}, cache => 1}); + + # we need to track operations which are just cc changes + $operation->{cc_only} = 1; + + for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { + my $change = $operation->{changes}->[$i]; + + # make an attachment object + if ($change->{attachid}) { + $change->{attach} = $attachment_cache{$change->{attachid}}; + } + + # empty resolutions are displayed as --- by default + # make it explicit here to enable correct display of the change + if ($change->{fieldname} eq 'resolution') { + $change->{removed} = '---' if $change->{removed} eq ''; + $change->{added} = '---' if $change->{added} eq ''; + } + + # make boolean fields true/false instead of 1/0 + my ($table, $field) = ('bugs', $change->{fieldname}); + if ($field =~ /^([^\.]+)\.(.+)$/) { + ($table, $field) = ($1, $2); + } + my $column = $dbh->bz_column_info($table, $field); + if ($column && $column->{TYPE} eq 'BOOLEAN') { + $change->{removed} = ''; + $change->{added} = $change->{added} ? 'true' : 'false'; + } + + # load field object (only required for custom fields), and set the + # field type for custom fields + my $field_obj; + if ($change->{fieldname} =~ /^cf_/) { + $field_obj = Bugzilla::Field->new({name => $change->{fieldname}, cache => 1}); + $change->{fieldtype} = $field_obj->type; + } + + # identify buglist changes + if ( $change->{fieldname} eq 'blocked' + || $change->{fieldname} eq 'dependson' + || $change->{fieldname} eq 'dupe' + || ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID)) + { + $change->{buglist} = 1; + foreach my $what (qw(removed added)) { + my @buglist = split(/[\s,]+/, $change->{$what}); + foreach my $id (@buglist) { + if ($id && $id =~ /^\d+$/) { + $visible_bug_ids{$id} = 1; } + } + } + } + + # split see-also + if ($change->{fieldname} eq 'see_also') { + my $url_base = Bugzilla->localconfig->{urlbase}; + foreach my $f (qw( added removed )) { + my @values; + foreach my $value (split(/, /, $change->{$f})) { + my ($bug_id) + = substr($value, 0, length($url_base)) eq $url_base + ? $value =~ /id=(\d+)$/ + : undef; + push @values, {url => $value, bug_id => $bug_id,}; + } + $change->{$f} = \@values; + } + } + + # track cc-only + if ($change->{fieldname} ne 'cc') { + $operation->{cc_only} = 0; + } + + # split multiple flag changes (must be processed last) + # set $change->{flagtype_name} to make searching the activity + # stream for flag changes easier and quicker + if ($change->{fieldname} eq 'flagtypes.name') { + my @added = split(/, /, $change->{added}); + my @removed = split(/, /, $change->{removed}); + if (scalar(@added) <= 1 && scalar(@removed) <= 1) { + $change->{flagtype_name} = _extract_flagtype($added[0] || $removed[0]); + next; + } - # track cc-only - if ($change->{fieldname} ne 'cc') { - $operation->{cc_only} = 0; - } + # remove current change + splice(@{$operation->{changes}}, $i, 1); - # split multiple flag changes (must be processed last) - # set $change->{flagtype_name} to make searching the activity - # stream for flag changes easier and quicker - if ($change->{fieldname} eq 'flagtypes.name') { - my @added = split(/, /, $change->{added}); - my @removed = split(/, /, $change->{removed}); - if (scalar(@added) <= 1 && scalar(@removed) <= 1) { - $change->{flagtype_name} = _extract_flagtype($added[0] || $removed[0]); - next; - } - # remove current change - splice(@{$operation->{changes}}, $i, 1); - # restructure into added/removed for each flag - my %flags; - foreach my $flag (@added) { - $flags{$flag}{added} = $flag; - $flags{$flag}{removed} = ''; - } - foreach my $flag (@removed) { - $flags{$flag}{added} = ''; - $flags{$flag}{removed} = $flag; - } - # clone current change, modify and insert - foreach my $flag (sort keys %flags) { - my $flag_change = {}; - foreach my $key (keys %$change) { - $flag_change->{$key} = $change->{$key}; - } - $flag_change->{removed} = $flags{$flag}{removed}; - $flag_change->{added} = $flags{$flag}{added}; - $flag_change->{flagtype_name} = _extract_flagtype($flag); - splice(@{$operation->{changes}}, $i, 0, $flag_change); - } - $i--; - } + # restructure into added/removed for each flag + my %flags; + foreach my $flag (@added) { + $flags{$flag}{added} = $flag; + $flags{$flag}{removed} = ''; + } + foreach my $flag (@removed) { + $flags{$flag}{added} = ''; + $flags{$flag}{removed} = $flag; } - _add_activity_to_stream($stream, date_str_to_time($operation->{when}), $operation->{who}->id, $operation); + # clone current change, modify and insert + foreach my $flag (sort keys %flags) { + my $flag_change = {}; + foreach my $key (keys %$change) { + $flag_change->{$key} = $change->{$key}; + } + $flag_change->{removed} = $flags{$flag}{removed}; + $flag_change->{added} = $flags{$flag}{added}; + $flag_change->{flagtype_name} = _extract_flagtype($flag); + splice(@{$operation->{changes}}, $i, 0, $flag_change); + } + $i--; + } } - # prime the visible-bugs cache - $user->visible_bugs([keys %visible_bug_ids]); + _add_activity_to_stream( + $stream, + date_str_to_time($operation->{when}), + $operation->{who}->id, $operation + ); + } + + # prime the visible-bugs cache + $user->visible_bugs([keys %visible_bug_ids]); } sub _extract_flagtype { - my ($value) = @_; - return $value =~ /^(.+)[\?\-\+]/ ? $1 : undef; + my ($value) = @_; + return $value =~ /^(.+)[\?\-\+]/ ? $1 : undef; } # display 'duplicate of this bug' as an activity entry, not a comment sub _add_duplicates_to_stream { - my ($bug, $stream) = @_; - my $dbh = Bugzilla->dbh; + my ($bug, $stream) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT longdescs.who, - UNIX_TIMESTAMP(bug_when), " . - $dbh->sql_date_format('bug_when') . ", + UNIX_TIMESTAMP(bug_when), " . $dbh->sql_date_format('bug_when') . ", type, extra_data FROM longdescs @@ -360,19 +374,22 @@ sub _add_duplicates_to_stream { WHERE bug_id = ? AND (type = ? OR type = ?) ORDER BY bug_when "); - $sth->execute($bug->id, CMT_HAS_DUPE, CMT_DUPE_OF); - - while (my($who, $time, $when, $type, $dupe_id) = $sth->fetchrow_array) { - _add_activity_to_stream($stream, $time, $who, { - who => Bugzilla::User->new({ id => $who, cache => 1 }), - when => $when, - changes => [{ - fieldname => ($type == CMT_HAS_DUPE ? 'has_dupe' : 'dupe_of'), - added => $dupe_id, - buglist => 1, - }], - }); - } + $sth->execute($bug->id, CMT_HAS_DUPE, CMT_DUPE_OF); + + while (my ($who, $time, $when, $type, $dupe_id) = $sth->fetchrow_array) { + _add_activity_to_stream( + $stream, $time, $who, + { + who => Bugzilla::User->new({id => $who, cache => 1}), + when => $when, + changes => [{ + fieldname => ($type == CMT_HAS_DUPE ? 'has_dupe' : 'dupe_of'), + added => $dupe_id, + buglist => 1, + }], + } + ); + } } 1; diff --git a/extensions/BugModal/lib/MonkeyPatches.pm b/extensions/BugModal/lib/MonkeyPatches.pm index 54bd6e560..042dabc38 100644 --- a/extensions/BugModal/lib/MonkeyPatches.pm +++ b/extensions/BugModal/lib/MonkeyPatches.pm @@ -17,10 +17,10 @@ use warnings; use Bugzilla::User; sub treeherder_user { - return Bugzilla->process_cache->{treeherder_user} //= - Bugzilla::User->new({ name => 'tbplbot@gmail.com', cache => 1 }) - || Bugzilla::User->new({ name => 'orangefactor@bots.tld', cache => 1 }) - || Bugzilla::User->new(); + return Bugzilla->process_cache->{treeherder_user} + //= Bugzilla::User->new({name => 'tbplbot@gmail.com', cache => 1}) + || Bugzilla::User->new({name => 'orangefactor@bots.tld', cache => 1}) + || Bugzilla::User->new(); } package Bugzilla::Bug; @@ -32,10 +32,11 @@ use warnings; use Bugzilla::Attachment; sub active_attachments { - my ($self) = @_; - return [] if $self->{error}; - return $self->{active_attachments} //= Bugzilla::Attachment->get_attachments_by_bug( - $self, { exclude_obsolete => 1, preload => 1 }); + my ($self) = @_; + return [] if $self->{error}; + return $self->{active_attachments} + //= Bugzilla::Attachment->get_attachments_by_bug($self, + {exclude_obsolete => 1, preload => 1}); } 1; @@ -47,8 +48,8 @@ use strict; use warnings; sub is_image { - my ($self) = @_; - return substr($self->contenttype, 0, 6) eq 'image/'; + my ($self) = @_; + return substr($self->contenttype, 0, 6) eq 'image/'; } 1; diff --git a/extensions/BugModal/lib/Util.pm b/extensions/BugModal/lib/Util.pm index 6a453159e..b1d7068d8 100644 --- a/extensions/BugModal/lib/Util.pm +++ b/extensions/BugModal/lib/Util.pm @@ -21,19 +21,21 @@ use DateTime::TimeZone; use Time::Local qw(timelocal); sub date_str_to_time { - my ($date) = @_; - # avoid creating a DateTime object - if ($date =~ /^(\d{4})[\.\-](\d{2})[\.\-](\d{2}) (\d{2}):(\d{2}):(\d{2})$/) { - return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); - } - state $tz //= DateTime::TimeZone->new( name => 'local' ); - my $dt = datetime_from($date, $tz); - if (!$dt) { - # this should never happen - warn("invalid datetime '$date'"); - return undef; - } - return $dt->epoch; + my ($date) = @_; + + # avoid creating a DateTime object + if ($date =~ /^(\d{4})[\.\-](\d{2})[\.\-](\d{2}) (\d{2}):(\d{2}):(\d{2})$/) { + return timelocal($6, $5, $4, $3, $2 - 1, $1 - 1900); + } + state $tz //= DateTime::TimeZone->new(name => 'local'); + my $dt = datetime_from($date, $tz); + if (!$dt) { + + # this should never happen + warn("invalid datetime '$date'"); + return undef; + } + return $dt->epoch; } 1; diff --git a/extensions/BugModal/lib/WebService.pm b/extensions/BugModal/lib/WebService.pm index b69d609dd..5f3308327 100644 --- a/extensions/BugModal/lib/WebService.pm +++ b/extensions/BugModal/lib/WebService.pm @@ -27,353 +27,357 @@ use Taint::Util qw(untaint); # these methods are much lighter than our public API calls sub rest_resources { - return [ - # return all the products accessible by the user. - # required by new-bug - qr{^/bug_modal/initial_field_values}, { - GET => { - method => 'initial_field_values' - }, + return [ + # return all the products accessible by the user. + # required by new-bug + qr{^/bug_modal/initial_field_values}, + {GET => {method => 'initial_field_values'},}, + + # return all the components pertaining to the product. + # required by new-bug + qr{^/bug_modal/product_info}, + { + GET => { + method => 'product_info', + params => sub { + return {product_name => Bugzilla->input_params->{product}}; }, + }, + }, - # return all the components pertaining to the product. - # required by new-bug - qr{^/bug_modal/product_info}, { - GET => { - method => 'product_info', - params => sub { - return { product_name => Bugzilla->input_params->{product} } - }, - }, + # return all the lazy-loaded data; kept in sync with the UI's + # requirements. + qr{^/bug_modal/edit/(\d+)$}, + { + GET => { + method => 'edit', + params => sub { + return {id => $_[0]}; }, + }, + }, - # return all the lazy-loaded data; kept in sync with the UI's - # requirements. - qr{^/bug_modal/edit/(\d+)$}, { - GET => { - method => 'edit', - params => sub { - return { id => $_[0] } - }, - }, + # returns pre-formatted html, enabling reuse of the user template + qr{^/bug_modal/cc/(\d+)$}, + { + GET => { + method => 'cc', + params => sub { + return {id => $_[0]}; }, + }, + }, - # returns pre-formatted html, enabling reuse of the user template - qr{^/bug_modal/cc/(\d+)$}, { - GET => { - method => 'cc', - params => sub { - return { id => $_[0] } - }, - }, - }, + # returns fields that require touching when the product is changed + qw{^/bug_modal/new_product/(\d+)$}, + { + GET => { + method => 'new_product', + params => sub { - # returns fields that require touching when the product is changed - qw{^/bug_modal/new_product/(\d+)$}, { - GET => { - method => 'new_product', - params => sub { - # products with slashes in their name means we have to grab - # the product from the query-string instead of the path - return { id => $_[0], product_name => Bugzilla->input_params->{product} } - }, - }, + # products with slashes in their name means we have to grab + # the product from the query-string instead of the path + return {id => $_[0], product_name => Bugzilla->input_params->{product}}; }, - ] + }, + }, + ]; } sub initial_field_values { - my $user = Bugzilla->user; - return { - products => _name($user->get_enterable_products), - keywords => _name([Bugzilla::Keyword->get_all()]), - }; + my $user = Bugzilla->user; + return { + products => _name($user->get_enterable_products), + keywords => _name([Bugzilla::Keyword->get_all()]), + }; } sub product_info { - my ( $self, $params ) = @_; - if ( !ref $params->{product_name} ) { - untaint( $params->{product_name} ); - } - else { - ThrowCodeError( 'params_required', { function => 'BugModal.components', params => ['product'] } ); - } - my $product = Bugzilla::Product->check( { name => $params->{product_name}, cache => 1 } ); - $product = Bugzilla->user->can_enter_product( $product, 1 ); - my @components = map { - { - name => $_->name, - description => $_->description, - } - } @{ $product->components }; - return { - components => \@components, - versions => _name($product->versions), - }; + my ($self, $params) = @_; + if (!ref $params->{product_name}) { + untaint($params->{product_name}); + } + else { + ThrowCodeError('params_required', + {function => 'BugModal.components', params => ['product']}); + } + my $product + = Bugzilla::Product->check({name => $params->{product_name}, cache => 1}); + $product = Bugzilla->user->can_enter_product($product, 1); + my @components = map { {name => $_->name, description => $_->description,} } + @{$product->components}; + return {components => \@components, versions => _name($product->versions),}; } # everything we need for edit mode in a single call, returning just the fields # that the ui requires. sub edit { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - - # the keys of the options hash must match the field id in the ui - my %options; - - my @products = @{ $user->get_enterable_products }; - unless (grep { $_->id == $bug->product_id } @products) { - unshift @products, $bug->product_obj; - } - $options{product} = [ map { { name => $_->name } } @products ]; - - $options{component} = _name($bug->product_obj->components, $bug->component); - $options{version} = _name($bug->product_obj->versions, $bug->version); - $options{target_milestone} = _name($bug->product_obj->milestones, $bug->target_milestone); - $options{priority} = _name('priority', $bug->priority); - $options{bug_severity} = _name('bug_severity', $bug->bug_severity); - $options{rep_platform} = _name('rep_platform', $bug->rep_platform); - $options{op_sys} = _name('op_sys', $bug->op_sys); - - # custom select fields - my @custom_fields = - grep { $_->type == FIELD_TYPE_SINGLE_SELECT || $_->type == FIELD_TYPE_MULTI_SELECT } - Bugzilla->active_custom_fields({ product => $bug->product_obj, component => $bug->component_obj }); - foreach my $field (@custom_fields) { - my $field_name = $field->name; - my @values = map { { name => $_->name } } - grep { $bug->$field_name eq $_->name - || ($_->is_active - && $bug->check_can_change_field($field_name, $bug->$field_name, $_->name)) } - @{ $field->legal_values }; - $options{$field_name} = \@values; - } - - # keywords - my @keywords = grep { $_->is_active } Bugzilla::Keyword->get_all(); - - # results - return { - options => \%options, - keywords => [ map { $_->name } @keywords ], - }; + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + + # the keys of the options hash must match the field id in the ui + my %options; + + my @products = @{$user->get_enterable_products}; + unless (grep { $_->id == $bug->product_id } @products) { + unshift @products, $bug->product_obj; + } + $options{product} = [map { {name => $_->name} } @products]; + + $options{component} = _name($bug->product_obj->components, $bug->component); + $options{version} = _name($bug->product_obj->versions, $bug->version); + $options{target_milestone} + = _name($bug->product_obj->milestones, $bug->target_milestone); + $options{priority} = _name('priority', $bug->priority); + $options{bug_severity} = _name('bug_severity', $bug->bug_severity); + $options{rep_platform} = _name('rep_platform', $bug->rep_platform); + $options{op_sys} = _name('op_sys', $bug->op_sys); + + # custom select fields + my @custom_fields = grep { + $_->type == FIELD_TYPE_SINGLE_SELECT + || $_->type == FIELD_TYPE_MULTI_SELECT + } Bugzilla->active_custom_fields( + {product => $bug->product_obj, component => $bug->component_obj}); + foreach my $field (@custom_fields) { + my $field_name = $field->name; + my @values = map { {name => $_->name} } grep { + $bug->$field_name eq $_->name + || ($_->is_active + && $bug->check_can_change_field($field_name, $bug->$field_name, $_->name)) + } @{$field->legal_values}; + $options{$field_name} = \@values; + } + + # keywords + my @keywords = grep { $_->is_active } Bugzilla::Keyword->get_all(); + + # results + return {options => \%options, keywords => [map { $_->name } @keywords],}; } sub _name { - my ($values, $current) = @_; - # values can either be an array-ref of values, or a field name, which - # result in that field's legal-values being used. - if (!ref($values)) { - $values = Bugzilla::Field->new({ name => $values, cache => 1 })->legal_values; - } - return [ - map { { name => $_->name } } - grep { (defined $current && $_->name eq $current) || $_->is_active } - @$values - ]; + my ($values, $current) = @_; + + # values can either be an array-ref of values, or a field name, which + # result in that field's legal-values being used. + if (!ref($values)) { + $values = Bugzilla::Field->new({name => $values, cache => 1})->legal_values; + } + return [map { {name => $_->name} } + grep { (defined $current && $_->name eq $current) || $_->is_active } + @$values]; } sub cc { - my ($self, $params) = @_; - my $template = Bugzilla->template; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - my $vars = { - bug => $bug, - cc_list => [ - sort { lc($a->identity) cmp lc($b->identity) } - @{ $bug->cc_users } - ] - }; - - my $html = ''; - $template->process('bug_modal/cc_list.html.tmpl', $vars, \$html) - || ThrowTemplateError($template->error); - return { html => $html }; + my ($self, $params) = @_; + my $template = Bugzilla->template; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + my $vars = { + bug => $bug, + cc_list => [sort { lc($a->identity) cmp lc($b->identity) } @{$bug->cc_users}] + }; + + my $html = ''; + $template->process('bug_modal/cc_list.html.tmpl', $vars, \$html) + || ThrowTemplateError($template->error); + return {html => $html}; } sub new_product { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $bug = Bugzilla::Bug->check({ id => $params->{id} }); - my $product = Bugzilla::Product->check({ name => $params->{product_name}, cache => 1 }); - my $true = $self->type('boolean', 1); - my %result; - - # components - - my $components = _name($product->components); - my $current_component = $bug->component; - if (my $component = first_value { $_->{name} eq $current_component} @$components) { - # identical component in both products - $component->{selected} = $true; - } - else { - # default to a blank value - unshift @$components, { - name => '', - selected => $true, - }; - } - $result{component} = $components; - - # milestones - - my $milestones = _name($product->milestones); - my $current_milestone = $bug->target_milestone; - if ($bug->check_can_change_field('target_milestone', 0, 1) - && (my $milestone = first_value { $_->{name} eq $current_milestone} @$milestones)) - { - # identical milestone in both products - $milestone->{selected} = $true; - } - else { - # use default milestone - my $default_milestone = $product->default_milestone; - my $milestone = first_value { $_->{name} eq $default_milestone } @$milestones; - $milestone->{selected} = $true; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $bug = Bugzilla::Bug->check({id => $params->{id}}); + my $product + = Bugzilla::Product->check({name => $params->{product_name}, cache => 1}); + my $true = $self->type('boolean', 1); + my %result; + + # components + + my $components = _name($product->components); + my $current_component = $bug->component; + if (my $component + = first_value { $_->{name} eq $current_component } @$components) + { + # identical component in both products + $component->{selected} = $true; + } + else { + # default to a blank value + unshift @$components, {name => '', selected => $true,}; + } + $result{component} = $components; + + # milestones + + my $milestones = _name($product->milestones); + my $current_milestone = $bug->target_milestone; + if ($bug->check_can_change_field('target_milestone', 0, 1) + && (my $milestone + = first_value { $_->{name} eq $current_milestone } @$milestones)) + { + # identical milestone in both products + $milestone->{selected} = $true; + } + else { + # use default milestone + my $default_milestone = $product->default_milestone; + my $milestone = first_value { $_->{name} eq $default_milestone } @$milestones; + $milestone->{selected} = $true; + } + $result{target_milestone} = $milestones; + + # versions + + my $versions = _name($product->versions); + my $current_version = $bug->version; + my $selected_version; + if (my $version = first_value { $_->{name} eq $current_version } @$versions) { + + # identical version in both products + $version->{selected} = $true; + $selected_version = $version; + } + elsif ($current_version =~ /^(\d+) Branch$/ + || $current_version =~ /^Firefox (\d+)$/ + || $current_version =~ /^(\d+)$/) + { + # firefox, with its three version naming schemes + my $branch = $1; + foreach my $test_version ("$branch Branch", "Firefox $branch", $branch) { + if (my $version = first_value { $_->{name} eq $test_version } @$versions) { + $version->{selected} = $true; + $selected_version = $version; + last; + } } - $result{target_milestone} = $milestones; - - # versions + } + if (!$selected_version) { - my $versions = _name($product->versions); - my $current_version = $bug->version; - my $selected_version; - if (my $version = first_value { $_->{name} eq $current_version } @$versions) { - # identical version in both products + # "unspecified", "other" + foreach my $test_version ("unspecified", "other") { + if (my $version = first_value { lc($_->{name}) eq $test_version } @$versions) { $version->{selected} = $true; $selected_version = $version; + last; + } } - elsif ( - $current_version =~ /^(\d+) Branch$/ - || $current_version =~ /^Firefox (\d+)$/ - || $current_version =~ /^(\d+)$/) + } + if (!$selected_version) { + + # default to a blank value + unshift @$versions, {name => '', selected => $true,}; + } + $result{version} = $versions; + + # groups + + my @groups; + + # find invalid groups + push @groups, + map { {type => 'invalid', group => $_, checked => 0,} } + @{Bugzilla::Bug->get_invalid_groups( + {bug_ids => [$bug->id], product => $product})}; + + # logic lifted from bug/process/verify-new-product.html.tmpl + my $current_groups = $bug->groups_in; + my $group_controls = $product->group_controls; + foreach my $group_id (keys %$group_controls) { + my $group_control = $group_controls->{$group_id}; + if ( + $group_control->{membercontrol} == CONTROLMAPMANDATORY + || ($group_control->{othercontrol} == CONTROLMAPMANDATORY + && !$user->in_group($group_control->{name})) + ) { - # firefox, with its three version naming schemes - my $branch = $1; - foreach my $test_version ("$branch Branch", "Firefox $branch", $branch) { - if (my $version = first_value { $_->{name} eq $test_version } @$versions) { - $version->{selected} = $true; - $selected_version = $version; - last; - } - } - } - if (!$selected_version) { - # "unspecified", "other" - foreach my $test_version ("unspecified", "other") { - if (my $version = first_value { lc($_->{name}) eq $test_version } @$versions) { - $version->{selected} = $true; - $selected_version = $version; - last; - } - } - } - if (!$selected_version) { - # default to a blank value - unshift @$versions, { - name => '', - selected => $true, - }; - } - $result{version} = $versions; - - # groups - - my @groups; - - # find invalid groups - push @groups, - map {{ - type => 'invalid', - group => $_, - checked => 0, - }} - @{ Bugzilla::Bug->get_invalid_groups({ bug_ids => [ $bug->id ], product => $product }) }; - - # logic lifted from bug/process/verify-new-product.html.tmpl - my $current_groups = $bug->groups_in; - my $group_controls = $product->group_controls; - foreach my $group_id (keys %$group_controls) { - my $group_control = $group_controls->{$group_id}; - if ($group_control->{membercontrol} == CONTROLMAPMANDATORY - || ($group_control->{othercontrol} == CONTROLMAPMANDATORY && !$user->in_group($group_control->{name}))) - { - # mandatory, always checked - push @groups, { - type => 'mandatory', - group => $group_control->{group}, - checked => 1, - }; - } - elsif ( - ($group_control->{membercontrol} != CONTROLMAPNA && $user->in_group($group_control->{name})) - || $group_control->{othercontrol} != CONTROLMAPNA) - { - # optional, checked if.. - my $group = $group_control->{group}; - my $checked = - # same group as current product - (any { $_->id == $group->id } @$current_groups) - # member default - || $group_control->{membercontrol} == CONTROLMAPDEFAULT && $user->in_group($group_control->{name}) - # or other default - || $group_control->{othercontrol} == CONTROLMAPDEFAULT && !$user->in_group($group_control->{name}) - ; - push @groups, { - type => 'optional', - group => $group_control->{group}, - checked => $checked || 0, - }; - } + # mandatory, always checked + push @groups, + {type => 'mandatory', group => $group_control->{group}, checked => 1,}; } + elsif ( + ( + $group_control->{membercontrol} != CONTROLMAPNA + && $user->in_group($group_control->{name}) + ) + || $group_control->{othercontrol} != CONTROLMAPNA + ) + { + # optional, checked if.. + my $group = $group_control->{group}; + my $checked = - my $default_group_name = $product->default_security_group; - if (my $default_group = first_value { $_->{group}->name eq $default_group_name } @groups) { - # because we always allow the default product group to be selected, it's never invalid - $default_group->{type} = 'optional' if $default_group->{type} eq 'invalid'; - } - else { - # add the product's default group if it's missing - unshift @groups, { - type => 'optional', - group => $product->default_security_group_obj, - checked => 0, - }; - } + # same group as current product + (any { $_->id == $group->id } @$current_groups) - # if the bug is currently in a group, ensure a group is checked by default - # by checking the product's default group if no other groups apply - if (@$current_groups && !any { $_->{checked} } @groups) { - foreach my $g (@groups) { - next unless $g->{group}->name eq $default_group_name; - $g->{checked} = 1; - last; - } - } + # member default + || $group_control->{membercontrol} == CONTROLMAPDEFAULT + && $user->in_group($group_control->{name}) - # group by type and flatten - my $vars = { - product => $product, - groups => { invalid => [], mandatory => [], optional => [] }, - }; - foreach my $g (@groups) { - push @{ $vars->{groups}->{$g->{type}} }, { - id => $g->{group}->id, - name => $g->{group}->name, - description => $g->{group}->description, - checked => $g->{checked}, + # or other default + || $group_control->{othercontrol} == CONTROLMAPDEFAULT + && !$user->in_group($group_control->{name}); + push @groups, + { + type => 'optional', + group => $group_control->{group}, + checked => $checked || 0, }; } - - # build group selection html - my $template = Bugzilla->template; - $template->process('bug_modal/new_product_groups.html.tmpl', $vars, \$result{groups}) - || ThrowTemplateError($template->error); - - return \%result; + } + + my $default_group_name = $product->default_security_group; + if (my $default_group + = first_value { $_->{group}->name eq $default_group_name } @groups) + { +# because we always allow the default product group to be selected, it's never invalid + $default_group->{type} = 'optional' if $default_group->{type} eq 'invalid'; + } + else { + # add the product's default group if it's missing + unshift @groups, + { + type => 'optional', + group => $product->default_security_group_obj, + checked => 0, + }; + } + + # if the bug is currently in a group, ensure a group is checked by default + # by checking the product's default group if no other groups apply + if (@$current_groups && !any { $_->{checked} } @groups) { + foreach my $g (@groups) { + next unless $g->{group}->name eq $default_group_name; + $g->{checked} = 1; + last; + } + } + + # group by type and flatten + my $vars = { + product => $product, + groups => {invalid => [], mandatory => [], optional => []}, + }; + foreach my $g (@groups) { + push @{$vars->{groups}->{$g->{type}}}, + { + id => $g->{group}->id, + name => $g->{group}->name, + description => $g->{group}->description, + checked => $g->{checked}, + }; + } + + # build group selection html + my $template = Bugzilla->template; + $template->process('bug_modal/new_product_groups.html.tmpl', + $vars, \$result{groups}) + || ThrowTemplateError($template->error); + + return \%result; } 1; diff --git a/extensions/BugmailFilter/Config.pm b/extensions/BugmailFilter/Config.pm index 5948c3b64..5b9585cee 100644 --- a/extensions/BugmailFilter/Config.pm +++ b/extensions/BugmailFilter/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'BugmailFilter'; +use constant NAME => 'BugmailFilter'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/BugmailFilter/Extension.pm b/extensions/BugmailFilter/Extension.pm index d4d7bb790..063cb273d 100644 --- a/extensions/BugmailFilter/Extension.pm +++ b/extensions/BugmailFilter/Extension.pm @@ -34,185 +34,172 @@ use Sys::Syslog qw(:DEFAULT); # sub user_preferences { - my ($self, $args) = @_; - return unless $args->{current_tab} eq 'bugmail_filter'; - - if ($args->{save_changes}) { - my $input = Bugzilla->input_params; - - if ($input->{add_filter}) { - - # add a new filter - - my $params = { - user_id => Bugzilla->user->id, - }; - $params->{field_name} = $input->{field} || IS_NULL; - if ($params->{field_name} eq '~') { - $params->{field_name} = '~' . $input->{field_contains}; - } - $params->{relationship} = $input->{relationship} || IS_NULL; - if ($input->{changer}) { - Bugzilla::User::match_field({ changer => { type => 'single'} }); - $params->{changer_id} = Bugzilla::User->check({ - name => $input->{changer}, - cache => 1, - })->id; - } - else { - $params->{changer_id} = IS_NULL; - } - if (my $product_name = $input->{product}) { - my $product = Bugzilla::Product->check({ - name => $product_name, cache => 1 - }); - $params->{product_id} = $product->id; - - if (my $component_name = $input->{component}) { - $params->{component_id} = Bugzilla::Component->check({ - name => $component_name, product => $product, - cache => 1 - })->id; - } - else { - $params->{component_id} = IS_NULL; - } - } - else { - $params->{product_id} = IS_NULL; - $params->{component_id} = IS_NULL; - } - - if (@{ Bugzilla::Extension::BugmailFilter::Filter->match($params) }) { - ThrowUserError('bugmail_filter_exists'); - } - $params->{action} = $input->{action} eq 'Exclude' ? 1 : 0; - foreach my $name (keys %$params) { - $params->{$name} = undef - if $params->{$name} eq IS_NULL; - } - Bugzilla::Extension::BugmailFilter::Filter->create($params); + my ($self, $args) = @_; + return unless $args->{current_tab} eq 'bugmail_filter'; + + if ($args->{save_changes}) { + my $input = Bugzilla->input_params; + + if ($input->{add_filter}) { + + # add a new filter + + my $params = {user_id => Bugzilla->user->id,}; + $params->{field_name} = $input->{field} || IS_NULL; + if ($params->{field_name} eq '~') { + $params->{field_name} = '~' . $input->{field_contains}; + } + $params->{relationship} = $input->{relationship} || IS_NULL; + if ($input->{changer}) { + Bugzilla::User::match_field({changer => {type => 'single'}}); + $params->{changer_id} + = Bugzilla::User->check({name => $input->{changer}, cache => 1,})->id; + } + else { + $params->{changer_id} = IS_NULL; + } + if (my $product_name = $input->{product}) { + my $product = Bugzilla::Product->check({name => $product_name, cache => 1}); + $params->{product_id} = $product->id; + + if (my $component_name = $input->{component}) { + $params->{component_id} + = Bugzilla::Component->check({ + name => $component_name, product => $product, cache => 1 + })->id; } - - elsif ($input->{remove_filter}) { - - # remove filter(s) - - my $ids = ref($input->{remove}) ? $input->{remove} : [ $input->{remove} ]; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - my $filters = Bugzilla::Extension::BugmailFilter::Filter->match({ id => $ids, user_id => $user->id }); - $dbh->bz_start_transaction; - foreach my $filter (@$filters) { - $filter->remove_from_db(); - } - $dbh->bz_commit_transaction; + else { + $params->{component_id} = IS_NULL; } + } + else { + $params->{product_id} = IS_NULL; + $params->{component_id} = IS_NULL; + } + + if (@{Bugzilla::Extension::BugmailFilter::Filter->match($params)}) { + ThrowUserError('bugmail_filter_exists'); + } + $params->{action} = $input->{action} eq 'Exclude' ? 1 : 0; + foreach my $name (keys %$params) { + $params->{$name} = undef if $params->{$name} eq IS_NULL; + } + Bugzilla::Extension::BugmailFilter::Filter->create($params); } - my $vars = $args->{vars}; - my $field_descs = template_var('field_descs'); + elsif ($input->{remove_filter}) { - # load all fields into a hash for easy manipulation - my %fields = - map { $_->name => $field_descs->{$_->name} } - @{ Bugzilla->fields({ obsolete => 0 }) }; + # remove filter(s) - # remove time trackinger fields - if (!Bugzilla->user->is_timetracker) { - foreach my $field (TIMETRACKING_FIELDS) { - delete $fields{$field}; - } - } + my $ids = ref($input->{remove}) ? $input->{remove} : [$input->{remove}]; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - # remove fields which don't make any sense to filter on - foreach my $field (IGNORE_FIELDS) { - delete $fields{$field}; + my $filters = Bugzilla::Extension::BugmailFilter::Filter->match( + {id => $ids, user_id => $user->id}); + $dbh->bz_start_transaction; + foreach my $filter (@$filters) { + $filter->remove_from_db(); + } + $dbh->bz_commit_transaction; } + } - # remove all tracking flag fields. these change too frequently to be of - # value, so they only add noise to the list. - foreach my $field (Bugzilla->tracking_flag_names) { - delete $fields{$field}; - } + my $vars = $args->{vars}; + my $field_descs = template_var('field_descs'); - # add tracking flag types instead - foreach my $field ( - @{ Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields() } - ) { - $fields{$field->name} = $field->description; - } + # load all fields into a hash for easy manipulation + my %fields = map { $_->name => $field_descs->{$_->name} } + @{Bugzilla->fields({obsolete => 0})}; - # adjust the description for selected fields - foreach my $field (keys %{ FIELD_DESCRIPTION_OVERRIDE() }) { - $fields{$field} = FIELD_DESCRIPTION_OVERRIDE->{$field}; + # remove time trackinger fields + if (!Bugzilla->user->is_timetracker) { + foreach my $field (TIMETRACKING_FIELDS) { + delete $fields{$field}; } - - # some fields are present in the changed-fields x-header but are not real - # bugzilla fields - foreach my $field ( - @{ Bugzilla::Extension::BugmailFilter::FakeField->fake_fields() } - ) { - $fields{$field->name} = $field->description; + } + + # remove fields which don't make any sense to filter on + foreach my $field (IGNORE_FIELDS) { + delete $fields{$field}; + } + + # remove all tracking flag fields. these change too frequently to be of + # value, so they only add noise to the list. + foreach my $field (Bugzilla->tracking_flag_names) { + delete $fields{$field}; + } + + # add tracking flag types instead + foreach my $field ( + @{Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields()}) + { + $fields{$field->name} = $field->description; + } + + # adjust the description for selected fields + foreach my $field (keys %{FIELD_DESCRIPTION_OVERRIDE()}) { + $fields{$field} = FIELD_DESCRIPTION_OVERRIDE->{$field}; + } + + # some fields are present in the changed-fields x-header but are not real + # bugzilla fields + foreach + my $field (@{Bugzilla::Extension::BugmailFilter::FakeField->fake_fields()}) + { + $fields{$field->name} = $field->description; + } + + $vars->{fields} = \%fields; + $vars->{field_list} = [sort { lc($a->{description}) cmp lc($b->{description}) } + map { {name => $_, description => $fields{$_}} } keys %fields]; + + $vars->{relationships} = FILTER_RELATIONSHIPS(); + + $vars->{filters} = [ + sort { + $a->product_name cmp $b->product_name + || $a->component_name cmp $b->component_name + || $a->field_name cmp $b->field_name + } @{Bugzilla::Extension::BugmailFilter::Filter->match({ + user_id => Bugzilla->user->id, + }) } + ]; - $vars->{fields} = \%fields; - $vars->{field_list} = [ - sort { lc($a->{description}) cmp lc($b->{description}) } - map { { name => $_, description => $fields{$_} } } - keys %fields - ]; - - $vars->{relationships} = FILTER_RELATIONSHIPS(); - - $vars->{filters} = [ - sort { - $a->product_name cmp $b->product_name - || $a->component_name cmp $b->component_name - || $a->field_name cmp $b->field_name - } - @{ Bugzilla::Extension::BugmailFilter::Filter->match({ - user_id => Bugzilla->user->id, - }) } - ]; - - # set field_description - foreach my $filter (@{ $vars->{filters} }) { - my $field_name = $filter->field_name; - if (!$field_name) { - $filter->field_description('Any'); - } - elsif (substr($field_name, 0, 1) eq '~') { - $filter->field_description('~ ' . substr($field_name, 1)); - } - else { - $filter->field_description($fields{$field_name} || $filter->field->description); - } + # set field_description + foreach my $filter (@{$vars->{filters}}) { + my $field_name = $filter->field_name; + if (!$field_name) { + $filter->field_description('Any'); } - - # build a list of tracking-flags, grouped by type - require Bugzilla::Extension::TrackingFlags::Constants; - require Bugzilla::Extension::TrackingFlags::Flag; - my %flag_types = - map { $_->{name} => $_->{description} } - @{ Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES() }; - my %tracking_flags_by_type; - foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all) { - my $type = $flag_types{$flag->flag_type}; - $tracking_flags_by_type{$type} //= []; - push @{ $tracking_flags_by_type{$type} }, $flag; + elsif (substr($field_name, 0, 1) eq '~') { + $filter->field_description('~ ' . substr($field_name, 1)); } - my @tracking_flags_by_type; - foreach my $type (sort keys %tracking_flags_by_type) { - push @tracking_flags_by_type, { - name => $type, - flags => $tracking_flags_by_type{$type}, - }; + else { + $filter->field_description($fields{$field_name} || $filter->field->description); } - $vars->{tracking_flags_by_type} = \@tracking_flags_by_type; - - ${ $args->{handled} } = 1; + } + + # build a list of tracking-flags, grouped by type + require Bugzilla::Extension::TrackingFlags::Constants; + require Bugzilla::Extension::TrackingFlags::Flag; + my %flag_types = map { $_->{name} => $_->{description} } + @{Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES()}; + my %tracking_flags_by_type; + foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all) { + my $type = $flag_types{$flag->flag_type}; + $tracking_flags_by_type{$type} //= []; + push @{$tracking_flags_by_type{$type}}, $flag; + } + my @tracking_flags_by_type; + foreach my $type (sort keys %tracking_flags_by_type) { + push @tracking_flags_by_type, + {name => $type, flags => $tracking_flags_by_type{$type},}; + } + $vars->{tracking_flags_by_type} = \@tracking_flags_by_type; + + ${$args->{handled}} = 1; } # @@ -220,219 +207,212 @@ sub user_preferences { # sub user_wants_mail { - my ($self, $args) = @_; - my ($user, $wants_mail, $diffs, $comments) - = @$args{qw( user wants_mail fieldDiffs comments )}; - - # already filtered by email prefs - return unless $$wants_mail; + my ($self, $args) = @_; + my ($user, $wants_mail, $diffs, $comments) + = @$args{qw( user wants_mail fieldDiffs comments )}; + + # already filtered by email prefs + return unless $$wants_mail; + + # avoid recursion + my $depth = 0; + for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { + $depth++ if $sub eq 'Bugzilla::User::wants_bug_mail'; + } + return if $depth > 1; + + my $cache = Bugzilla->request_cache->{bugmail_filters} //= {}; + my $filters = $cache->{$user->id} + //= Bugzilla::Extension::BugmailFilter::Filter->match({user_id => $user->id}); + return unless @$filters; + + my $fields = [ + map { { + filter_field => $_->{field_name}, # filter's field_name + field_name => $_->{field_name}, # raw bugzilla field_name + } } grep { + + # flags are added later + $_->{field_name} ne 'flagtypes.name' + } @$diffs + ]; + + # if more than one field was changed we need to check if the normal email + # preferences would have excluded the field. + if (@$fields > 1) { + + # check each field individually and create filter objects if required + my @arg_list + = @$args{qw( bug relationship fieldDiffs comments dep_mail changer )}; + foreach my $field (@$fields) { - # avoid recursion - my $depth = 0; - for (my $stack = 1; my $sub = (caller($stack))[3]; $stack++) { - $depth++ if $sub eq 'Bugzilla::User::wants_bug_mail'; - } - return if $depth > 1; - - my $cache = Bugzilla->request_cache->{bugmail_filters} //= {}; - my $filters = $cache->{$user->id} //= - Bugzilla::Extension::BugmailFilter::Filter->match({ - user_id => $user->id - }); - return unless @$filters; - - my $fields = [ - map { { - filter_field => $_->{field_name}, # filter's field_name - field_name => $_->{field_name}, # raw bugzilla field_name - } } - grep { - # flags are added later - $_->{field_name} ne 'flagtypes.name' - } - @$diffs - ]; - - # if more than one field was changed we need to check if the normal email - # preferences would have excluded the field. - if (@$fields > 1) { - # check each field individually and create filter objects if required - my @arg_list = @$args{qw( bug relationship fieldDiffs comments dep_mail changer )}; - foreach my $field (@$fields) { - # just a single diff - foreach my $diff (@$diffs) { - next unless $diff->{field_name} eq $field->{field_name}; - $arg_list[2] = [ $diff ]; - last; - } - if (!$user->wants_bug_mail(@arg_list)) { - # changes to just this field would have been dropped by email - # preferences. build a corresponding filter object so we - # interact with email preferences correctly. - push @$filters, Bugzilla::Extension::BugmailFilter::Filter->new_from_hash({ - field_name => $field->{field_name}, - action => 1, - }); - } - } + # just a single diff + foreach my $diff (@$diffs) { + next unless $diff->{field_name} eq $field->{field_name}; + $arg_list[2] = [$diff]; + last; + } + if (!$user->wants_bug_mail(@arg_list)) { + + # changes to just this field would have been dropped by email + # preferences. build a corresponding filter object so we + # interact with email preferences correctly. + push @$filters, + Bugzilla::Extension::BugmailFilter::Filter->new_from_hash({ + field_name => $field->{field_name}, action => 1, + }); + } } + } - # insert fake fields for new attachments and comments - if (@$comments) { - if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { - push @$fields, { field_name => 'attachment.created', - filter_field => 'attachment.created' }; - } - if (grep { $_->type != CMT_ATTACHMENT_CREATED } @$comments) { - push @$fields, { field_name => 'comment.created', - filter_field => 'comment.created' }; - } + # insert fake fields for new attachments and comments + if (@$comments) { + if (grep { $_->type == CMT_ATTACHMENT_CREATED } @$comments) { + push @$fields, + {field_name => 'attachment.created', filter_field => 'attachment.created'}; } - - # insert fake fields for flags - foreach my $diff (@$diffs) { - next unless $diff->{field_name} eq 'flagtypes.name'; - foreach my $change (split(/, /, join(', ', ($diff->{old}, $diff->{new})))) { - next unless $change =~ /^(.+)[\?\-+]/; - push @$fields, { - filter_field => $1, - field_name => 'flagtypes.name', - }; - } + if (grep { $_->type != CMT_ATTACHMENT_CREATED } @$comments) { + push @$fields, + {field_name => 'comment.created', filter_field => 'comment.created'}; } - - # set filter_field on tracking flags to tracking.$type - require Bugzilla::Extension::TrackingFlags::Flag; - my @tracking_flags = Bugzilla->tracking_flags; - foreach my $field (@$fields) { - next unless my $field_name = $field->{field_name}; - foreach my $tracking_flag (@tracking_flags) { - if ($field_name eq $tracking_flag->name) { - $field->{filter_field} = 'tracking.'. $tracking_flag->flag_type; - } - } + } + + # insert fake fields for flags + foreach my $diff (@$diffs) { + next unless $diff->{field_name} eq 'flagtypes.name'; + foreach my $change (split(/, /, join(', ', ($diff->{old}, $diff->{new})))) { + next unless $change =~ /^(.+)[\?\-+]/; + push @$fields, {filter_field => $1, field_name => 'flagtypes.name',}; } - - if (_should_drop($fields, $filters, $args)) { - $$wants_mail = 0; - openlog('apache', 'cons,pid', 'local4'); - syslog('notice', encode_utf8(sprintf( - '[bugmail] %s (filtered) bug-%s %s', - $args->{user}->login, - $args->{bug}->id, - $args->{bug}->short_desc, - ))); - closelog(); + } + + # set filter_field on tracking flags to tracking.$type + require Bugzilla::Extension::TrackingFlags::Flag; + my @tracking_flags = Bugzilla->tracking_flags; + foreach my $field (@$fields) { + next unless my $field_name = $field->{field_name}; + foreach my $tracking_flag (@tracking_flags) { + if ($field_name eq $tracking_flag->name) { + $field->{filter_field} = 'tracking.' . $tracking_flag->flag_type; + } } + } + + if (_should_drop($fields, $filters, $args)) { + $$wants_mail = 0; + openlog('apache', 'cons,pid', 'local4'); + syslog( + 'notice', + encode_utf8(sprintf( + '[bugmail] %s (filtered) bug-%s %s', + $args->{user}->login, + $args->{bug}->id, $args->{bug}->short_desc, + )) + ); + closelog(); + } } sub _should_drop { - my ($fields, $filters, $args) = @_; - - # calculate relationships - - my ($user, $bug, $relationship, $changer) = @$args{qw( user bug relationship changer )}; - my ($user_id, $login) = ($user->id, $user->login); - my $bit_direct = Bugzilla::BugMail::BIT_DIRECT; - my $bit_watching = Bugzilla::BugMail::BIT_WATCHING; - my $bit_compwatch = 15; # from Bugzilla::Extension::ComponentWatching - - # the index of $rel_map corresponds to the values in FILTER_RELATIONSHIPS - my @rel_map; - $rel_map[1] = $bug->assigned_to->id == $user_id; - $rel_map[2] = !$rel_map[1]; - $rel_map[3] = $bug->reporter->id == $user_id; - $rel_map[4] = !$rel_map[3]; - if ($bug->qa_contact) { - $rel_map[5] = $bug->qa_contact->id == $user_id; - $rel_map[6] = !$rel_map[6]; + my ($fields, $filters, $args) = @_; + + # calculate relationships + + my ($user, $bug, $relationship, $changer) + = @$args{qw( user bug relationship changer )}; + my ($user_id, $login) = ($user->id, $user->login); + my $bit_direct = Bugzilla::BugMail::BIT_DIRECT; + my $bit_watching = Bugzilla::BugMail::BIT_WATCHING; + my $bit_compwatch = 15; # from Bugzilla::Extension::ComponentWatching + + # the index of $rel_map corresponds to the values in FILTER_RELATIONSHIPS + my @rel_map; + $rel_map[1] = $bug->assigned_to->id == $user_id; + $rel_map[2] = !$rel_map[1]; + $rel_map[3] = $bug->reporter->id == $user_id; + $rel_map[4] = !$rel_map[3]; + if ($bug->qa_contact) { + $rel_map[5] = $bug->qa_contact->id == $user_id; + $rel_map[6] = !$rel_map[6]; + } + $rel_map[7] = $bug->cc ? grep { $_ eq $login } @{$bug->cc} : 0; + $rel_map[8] = !$rel_map[8]; + $rel_map[9] = ($relationship & $bit_watching or $relationship & $bit_compwatch); + $rel_map[10] = !$rel_map[9]; + $rel_map[11] = $bug->is_mentor($user); + $rel_map[12] = !$rel_map[11]; + foreach my $bool (@rel_map) { + $bool = $bool ? 1 : 0; + } + + # exclusions + # drop email where we are excluding all changed fields + + my $params = { + product_id => $bug->product_id, + component_id => $bug->component_id, + rel_map => \@rel_map, + changer_id => $changer->id, + }; + + foreach my $field (@$fields) { + $params->{field} = $field; + foreach my $filter (grep { $_->is_exclude } @$filters) { + if ($filter->matches($params)) { + $field->{exclude} = 1; + last; + } } - $rel_map[7] = $bug->cc - ? grep { $_ eq $login } @{ $bug->cc } - : 0; - $rel_map[8] = !$rel_map[8]; - $rel_map[9] = ( - $relationship & $bit_watching - or $relationship & $bit_compwatch - ); - $rel_map[10] = !$rel_map[9]; - $rel_map[11] = $bug->is_mentor($user); - $rel_map[12] = !$rel_map[11]; - foreach my $bool (@rel_map) { - $bool = $bool ? 1 : 0; - } - - # exclusions - # drop email where we are excluding all changed fields - - my $params = { - product_id => $bug->product_id, - component_id => $bug->component_id, - rel_map => \@rel_map, - changer_id => $changer->id, - }; - - foreach my $field (@$fields) { - $params->{field} = $field; - foreach my $filter (grep { $_->is_exclude } @$filters) { - if ($filter->matches($params)) { - $field->{exclude} = 1; - last; - } - } + } + + # no need to process includes if nothing was excluded + if (!grep { $_->{exclude} } @$fields) { + return 0; + } + + # inclusions + # flip the bit for fields that should be included + + foreach my $field (@$fields) { + $params->{field} = $field; + foreach my $filter (grep { $_->is_include } @$filters) { + if ($filter->matches($params)) { + $field->{exclude} = 0; + last; + } } + } - # no need to process includes if nothing was excluded - if (!grep { $_->{exclude} } @$fields) { - return 0; - } - - # inclusions - # flip the bit for fields that should be included - - foreach my $field (@$fields) { - $params->{field} = $field; - foreach my $filter (grep { $_->is_include } @$filters) { - if ($filter->matches($params)) { - $field->{exclude} = 0; - last; - } - } - } - - # drop if all fields are still excluded - return !(grep { !$_->{exclude} } @$fields); + # drop if all fields are still excluded + return !(grep { !$_->{exclude} } @$fields); } # catch when fields are renamed, and update the field_name entires sub object_end_of_update { - my ($self, $args) = @_; - my $object = $args->{object}; + my ($self, $args) = @_; + my $object = $args->{object}; - return unless $object->isa('Bugzilla::Field') - || $object->isa('Bugzilla::Extension::TrackingFlags::Flag'); + return + unless $object->isa('Bugzilla::Field') + || $object->isa('Bugzilla::Extension::TrackingFlags::Flag'); - return unless exists $args->{changes}->{name}; + return unless exists $args->{changes}->{name}; - my $old_name = $args->{changes}->{name}->[0]; - my $new_name = $args->{changes}->{name}->[1]; + my $old_name = $args->{changes}->{name}->[0]; + my $new_name = $args->{changes}->{name}->[1]; - Bugzilla->dbh->do( - "UPDATE bugmail_filters SET field_name=? WHERE field_name=?", - undef, - $new_name, $old_name); + Bugzilla->dbh->do("UPDATE bugmail_filters SET field_name=? WHERE field_name=?", + undef, $new_name, $old_name); } sub reorg_move_component { - my ($self, $args) = @_; - my $new_product = $args->{new_product}; - my $component = $args->{component}; - - Bugzilla->dbh->do( - "UPDATE bugmail_filters SET product_id=? WHERE component_id=?", - undef, - $new_product->id, $component->id, - ); + my ($self, $args) = @_; + my $new_product = $args->{new_product}; + my $component = $args->{component}; + + Bugzilla->dbh->do( + "UPDATE bugmail_filters SET product_id=? WHERE component_id=?", + undef, $new_product->id, $component->id,); } # @@ -440,92 +420,61 @@ sub reorg_move_component { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{schema}->{bugmail_filters} = { + my ($self, $args) = @_; + $args->{schema}->{bugmail_filters} = { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}, + }, + field_name => { + + # due to fake fields, this can't be field_id + TYPE => 'VARCHAR(64)', + NOTNULL => 0, + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE'}, + }, + component_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE'}, + }, + changer_id => { + TYPE => 'INT3', + NOTNULL => 0, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'}, + }, + relationship => {TYPE => 'INT2', NOTNULL => 0,}, + action => {TYPE => 'INT1', NOTNULL => 1,}, + ], + INDEXES => [ + bugmail_filters_unique_idx => { FIELDS => [ - id => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE' - }, - }, - field_name => { - # due to fake fields, this can't be field_id - TYPE => 'VARCHAR(64)', - NOTNULL => 0, - }, - product_id => { - TYPE => 'INT2', - NOTNULL => 0, - REFERENCES => { - TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE' - }, - }, - component_id => { - TYPE => 'INT2', - NOTNULL => 0, - REFERENCES => { - TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE' - }, - }, - changer_id => { - TYPE => 'INT3', - NOTNULL => 0, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE' - }, - }, - relationship => { - TYPE => 'INT2', - NOTNULL => 0, - }, - action => { - TYPE => 'INT1', - NOTNULL => 1, - }, + qw( user_id field_name product_id component_id + relationship ) ], - INDEXES => [ - bugmail_filters_unique_idx => { - FIELDS => [ qw( user_id field_name product_id component_id - relationship ) ], - TYPE => 'UNIQUE', - }, - bugmail_filters_user_idx => [ - 'user_id', - ], - ], - }; + TYPE => 'UNIQUE', + }, + bugmail_filters_user_idx => ['user_id',], + ], + }; } sub install_update_db { - Bugzilla->dbh->bz_add_column( - 'bugmail_filters', - 'changer_id', - { - TYPE => 'INT3', - NOTNULL => 0, - } - ); + Bugzilla->dbh->bz_add_column('bugmail_filters', 'changer_id', + {TYPE => 'INT3', NOTNULL => 0,}); } sub db_sanitize { - my $dbh = Bugzilla->dbh; - print "Deleting bugmail filters...\n"; - $dbh->do("DELETE FROM bugmail_filters"); + my $dbh = Bugzilla->dbh; + print "Deleting bugmail filters...\n"; + $dbh->do("DELETE FROM bugmail_filters"); } __PACKAGE__->NAME; diff --git a/extensions/BugmailFilter/lib/Constants.pm b/extensions/BugmailFilter/lib/Constants.pm index a6636dda7..bed8a37d3 100644 --- a/extensions/BugmailFilter/lib/Constants.pm +++ b/extensions/BugmailFilter/lib/Constants.pm @@ -14,10 +14,10 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - FAKE_FIELD_NAMES - IGNORE_FIELDS - FIELD_DESCRIPTION_OVERRIDE - FILTER_RELATIONSHIPS + FAKE_FIELD_NAMES + IGNORE_FIELDS + FIELD_DESCRIPTION_OVERRIDE + FILTER_RELATIONSHIPS ); use Bugzilla::Constants; @@ -26,98 +26,54 @@ use Bugzilla::Constants; # header but are not real fields use constant FAKE_FIELD_NAMES => [ - { - name => 'comment.created', - description => 'Comment created', - }, - { - name => 'attachment.created', - description => 'Attachment created', - }, + {name => 'comment.created', description => 'Comment created',}, + {name => 'attachment.created', description => 'Attachment created',}, ]; # these fields don't make any sense to filter on use constant IGNORE_FIELDS => qw( - assignee_last_login - attach_data.thedata - attachments.submitter - cf_last_resolved - commenter - comment_tag - creation_ts - days_elapsed - delta_ts - everconfirmed - last_visit_ts - longdesc - longdescs.count - owner_idle_time - reporter - reporter_accessible - setters.login_name - tag - votes + assignee_last_login + attach_data.thedata + attachments.submitter + cf_last_resolved + commenter + comment_tag + creation_ts + days_elapsed + delta_ts + everconfirmed + last_visit_ts + longdesc + longdescs.count + owner_idle_time + reporter + reporter_accessible + setters.login_name + tag + votes ); # override the description of some fields -use constant FIELD_DESCRIPTION_OVERRIDE => { - bug_id => 'Bug Created', -}; +use constant FIELD_DESCRIPTION_OVERRIDE => {bug_id => 'Bug Created',}; # relationship / int mappings # _should_drop() also needs updating when this const is changed use constant FILTER_RELATIONSHIPS => [ - { - name => 'Assignee', - value => 1, - }, - { - name => 'Not Assignee', - value => 2, - }, - { - name => 'Reporter', - value => 3, - }, - { - name => 'Not Reporter', - value => 4, - }, - { - name => 'QA Contact', - value => 5, - }, - { - name => 'Not QA Contact', - value => 6, - }, - { - name => "CC'ed", - value => 7, - }, - { - name => "Not CC'ed", - value => 8, - }, - { - name => 'Watching', - value => 9, - }, - { - name => 'Not Watching', - value => 10, - }, - { - name => 'Mentoring', - value => 11, - }, - { - name => 'Not Mentoring', - value => 12, - }, + {name => 'Assignee', value => 1,}, + {name => 'Not Assignee', value => 2,}, + {name => 'Reporter', value => 3,}, + {name => 'Not Reporter', value => 4,}, + {name => 'QA Contact', value => 5,}, + {name => 'Not QA Contact', value => 6,}, + {name => "CC'ed", value => 7,}, + {name => "Not CC'ed", value => 8,}, + {name => 'Watching', value => 9,}, + {name => 'Not Watching', value => 10,}, + {name => 'Mentoring', value => 11,}, + {name => 'Not Mentoring', value => 12,}, ]; 1; diff --git a/extensions/BugmailFilter/lib/FakeField.pm b/extensions/BugmailFilter/lib/FakeField.pm index e9f8b1808..cf82aec85 100644 --- a/extensions/BugmailFilter/lib/FakeField.pm +++ b/extensions/BugmailFilter/lib/FakeField.pm @@ -16,8 +16,8 @@ use Bugzilla::Extension::BugmailFilter::Constants; # object sub new { - my ($class, $params) = @_; - return bless($params, $class); + my ($class, $params) = @_; + return bless($params, $class); } sub name { $_[0]->{name} } @@ -26,33 +26,35 @@ sub description { $_[0]->{description} } # static methods sub fake_fields { - my $cache = Bugzilla->request_cache->{bugmail_filter}; - if (!$cache->{fake_fields}) { - my @fields; - foreach my $rh (@{ FAKE_FIELD_NAMES() }) { - push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new($rh); - } - $cache->{fake_fields} = \@fields; + my $cache = Bugzilla->request_cache->{bugmail_filter}; + if (!$cache->{fake_fields}) { + my @fields; + foreach my $rh (@{FAKE_FIELD_NAMES()}) { + push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new($rh); } - return $cache->{fake_fields}; + $cache->{fake_fields} = \@fields; + } + return $cache->{fake_fields}; } sub tracking_flag_fields { - my $cache = Bugzilla->request_cache->{bugmail_filter}; - if (!$cache->{tracking_flag_fields}) { - require Bugzilla::Extension::TrackingFlags::Constants; - my @fields; - my $tracking_types = Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES(); - foreach my $tracking_type (@$tracking_types) { - push @fields, Bugzilla::Extension::BugmailFilter::FakeField->new({ - name => 'tracking.' . $tracking_type->{name}, - description => $tracking_type->{description}, - sortkey => $tracking_type->{sortkey}, - }); - } - $cache->{tracking_flag_fields} = \@fields; + my $cache = Bugzilla->request_cache->{bugmail_filter}; + if (!$cache->{tracking_flag_fields}) { + require Bugzilla::Extension::TrackingFlags::Constants; + my @fields; + my $tracking_types + = Bugzilla::Extension::TrackingFlags::Constants::FLAG_TYPES(); + foreach my $tracking_type (@$tracking_types) { + push @fields, + Bugzilla::Extension::BugmailFilter::FakeField->new({ + name => 'tracking.' . $tracking_type->{name}, + description => $tracking_type->{description}, + sortkey => $tracking_type->{sortkey}, + }); } - return $cache->{tracking_flag_fields}; + $cache->{tracking_flag_fields} = \@fields; + } + return $cache->{tracking_flag_fields}; } 1; diff --git a/extensions/BugmailFilter/lib/Filter.pm b/extensions/BugmailFilter/lib/Filter.pm index 7f2f4cb87..fa2c708cd 100644 --- a/extensions/BugmailFilter/lib/Filter.pm +++ b/extensions/BugmailFilter/lib/Filter.pm @@ -25,14 +25,14 @@ use Bugzilla::Util qw(trim); use constant DB_TABLE => 'bugmail_filters'; use constant DB_COLUMNS => qw( - id - user_id - product_id - component_id - field_name - relationship - changer_id - action + id + user_id + product_id + component_id + field_name + relationship + changer_id + action ); use constant LIST_ORDER => 'id'; @@ -40,13 +40,11 @@ use constant LIST_ORDER => 'id'; use constant UPDATE_COLUMNS => (); use constant VALIDATORS => { - user_id => \&_check_user, - field_name => \&_check_field_name, - action => \&Bugzilla::Object::check_boolean, -}; -use constant VALIDATOR_DEPENDENCIES => { - component_id => [ 'product_id' ], + user_id => \&_check_user, + field_name => \&_check_field_name, + action => \&Bugzilla::Object::check_boolean, }; +use constant VALIDATOR_DEPENDENCIES => {component_id => ['product_id'],}; use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; @@ -56,163 +54,164 @@ use constant USE_MEMCACHED => 0; # getters sub user { - my ($self) = @_; - return Bugzilla::User->new({ id => $self->{user_id}, cache => 1 }); + my ($self) = @_; + return Bugzilla::User->new({id => $self->{user_id}, cache => 1}); } sub product { - my ($self) = @_; - return $self->{product_id} - ? Bugzilla::Product->new({ id => $self->{product_id}, cache => 1 }) - : undef; + my ($self) = @_; + return $self->{product_id} + ? Bugzilla::Product->new({id => $self->{product_id}, cache => 1}) + : undef; } sub product_name { - my ($self) = @_; - return $self->{product_name} //= $self->{product_id} ? $self->product->name : ''; + my ($self) = @_; + return $self->{product_name} + //= $self->{product_id} ? $self->product->name : ''; } sub component { - my ($self) = @_; - return $self->{component_id} - ? Bugzilla::Component->new({ id => $self->{component_id}, cache => 1 }) - : undef; + my ($self) = @_; + return $self->{component_id} + ? Bugzilla::Component->new({id => $self->{component_id}, cache => 1}) + : undef; } sub component_name { - my ($self) = @_; - return $self->{component_name} //= $self->{component_id} ? $self->component->name : ''; + my ($self) = @_; + return $self->{component_name} + //= $self->{component_id} ? $self->component->name : ''; } sub field_name { - return $_[0]->{field_name} //= ''; + return $_[0]->{field_name} //= ''; } sub field_description { - my ($self, $value) = @_; - $self->{field_description} = $value if defined($value); - return $self->{field_description}; + my ($self, $value) = @_; + $self->{field_description} = $value if defined($value); + return $self->{field_description}; } sub field { - my ($self) = @_; - return unless $self->{field_name}; - if (!$self->{field}) { - if (substr($self->{field_name}, 0, 1) eq '~') { - # this should never happen - die "not implemented"; - } - foreach my $field ( - @{ Bugzilla::Extension::BugmailFilter::FakeField->fake_fields() }, - @{ Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields() }, - ) { - if ($field->{name} eq $self->{field_name}) { - return $self->{field} = $field; - } - } - $self->{field} = Bugzilla::Field->new({ name => $self->{field_name}, cache => 1 }); + my ($self) = @_; + return unless $self->{field_name}; + if (!$self->{field}) { + if (substr($self->{field_name}, 0, 1) eq '~') { + + # this should never happen + die "not implemented"; } - return $self->{field}; + foreach my $field ( + @{Bugzilla::Extension::BugmailFilter::FakeField->fake_fields()}, + @{Bugzilla::Extension::BugmailFilter::FakeField->tracking_flag_fields()}, + ) + { + if ($field->{name} eq $self->{field_name}) { + return $self->{field} = $field; + } + } + $self->{field} + = Bugzilla::Field->new({name => $self->{field_name}, cache => 1}); + } + return $self->{field}; } sub relationship { - return $_[0]->{relationship}; + return $_[0]->{relationship}; } sub changer_id { - return $_[0]->{changer_id}; + return $_[0]->{changer_id}; } sub changer { - my ($self) = @_; - return $self->{changer_id} - ? Bugzilla::User->new({ id => $self->{changer_id}, cache => 1 }) - : undef; + my ($self) = @_; + return $self->{changer_id} + ? Bugzilla::User->new({id => $self->{changer_id}, cache => 1}) + : undef; } sub relationship_name { - my ($self) = @_; - foreach my $rel (@{ FILTER_RELATIONSHIPS() }) { - return $rel->{name} - if $rel->{value} == $self->{relationship}; - } - return '?'; + my ($self) = @_; + foreach my $rel (@{FILTER_RELATIONSHIPS()}) { + return $rel->{name} if $rel->{value} == $self->{relationship}; + } + return '?'; } sub is_exclude { - return $_[0]->{action} == 1; + return $_[0]->{action} == 1; } sub is_include { - return $_[0]->{action} == 0; + return $_[0]->{action} == 0; } # validators sub _check_user { - my ($class, $user) = @_; - $user || ThrowCodeError('param_required', { param => 'user' }); + my ($class, $user) = @_; + $user || ThrowCodeError('param_required', {param => 'user'}); } sub _check_field_name { - my ($class, $field_name) = @_; - return undef unless $field_name; - if (substr($field_name, 0, 1) eq '~') { - $field_name = lc(trim($field_name)); - $field_name =~ /^~[a-z0-9_\.\-]+$/ - || ThrowUserError('bugmail_filter_invalid'); - length($field_name) <= 64 - || ThrowUserError('bugmail_filter_too_long'); - return $field_name; - } - foreach my $rh (@{ FAKE_FIELD_NAMES() }) { - return $field_name if $rh->{name} eq $field_name; - } - return $field_name - if $field_name =~ /^tracking\./; - Bugzilla::Field->check({ name => $field_name, cache => 1}); + my ($class, $field_name) = @_; + return undef unless $field_name; + if (substr($field_name, 0, 1) eq '~') { + $field_name = lc(trim($field_name)); + $field_name =~ /^~[a-z0-9_\.\-]+$/ || ThrowUserError('bugmail_filter_invalid'); + length($field_name) <= 64 || ThrowUserError('bugmail_filter_too_long'); return $field_name; + } + foreach my $rh (@{FAKE_FIELD_NAMES()}) { + return $field_name if $rh->{name} eq $field_name; + } + return $field_name if $field_name =~ /^tracking\./; + Bugzilla::Field->check({name => $field_name, cache => 1}); + return $field_name; } # methods sub matches { - my ($self, $args) = @_; - - if (my $field_name = $self->{field_name}) { - if ($args->{field}->{field_name} && substr($field_name, 0, 1) eq '~') { - my $substring = quotemeta(substr($field_name, 1)); - if ($args->{field}->{filter_field} !~ /$substring/i) { - return 0; - } - } - elsif ($field_name eq 'flagtypes.name') { - if ($args->{field}->{field_name} ne $field_name) { - return 0; - } - } - elsif ($field_name ne $args->{field}->{filter_field}) { - return 0; - } - } + my ($self, $args) = @_; - if ($self->{product_id} && $self->{product_id} != $args->{product_id}) { + if (my $field_name = $self->{field_name}) { + if ($args->{field}->{field_name} && substr($field_name, 0, 1) eq '~') { + my $substring = quotemeta(substr($field_name, 1)); + if ($args->{field}->{filter_field} !~ /$substring/i) { return 0; + } } - - if ($self->{component_id} && $self->{component_id} != $args->{component_id}) { + elsif ($field_name eq 'flagtypes.name') { + if ($args->{field}->{field_name} ne $field_name) { return 0; + } } - - if ($self->{relationship} && !$args->{rel_map}->[$self->{relationship}]) { - return 0; + elsif ($field_name ne $args->{field}->{filter_field}) { + return 0; } + } - if ($self->{changer_id} && $self->{changer_id} != $args->{changer_id}) { - return 0; - } + if ($self->{product_id} && $self->{product_id} != $args->{product_id}) { + return 0; + } + + if ($self->{component_id} && $self->{component_id} != $args->{component_id}) { + return 0; + } + + if ($self->{relationship} && !$args->{rel_map}->[$self->{relationship}]) { + return 0; + } + + if ($self->{changer_id} && $self->{changer_id} != $args->{changer_id}) { + return 0; + } - return 1; + return 1; } 1; diff --git a/extensions/BzAPI/Extension.pm b/extensions/BzAPI/Extension.pm index ac9502fcb..e7812194e 100644 --- a/extensions/BzAPI/Extension.pm +++ b/extensions/BzAPI/Extension.pm @@ -33,15 +33,13 @@ our $VERSION = '0.1'; ################ sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; + my ($self, $args) = @_; + my $files = $args->{'files'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $scriptname = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/rest.cgi"; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $scriptname = $extensionsdir . "/" . __PACKAGE__->NAME . "/bin/rest.cgi"; - $files->{$scriptname} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; + $files->{$scriptname} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; } ################## @@ -49,14 +47,14 @@ sub install_filesystem { ################## sub template_before_process { - my ($self, $args) = @_; - my $vars = $args->{'vars'}; - my $file = $args->{'file'}; - - if ($file =~ /config\.json\.tmpl$/) { - $vars->{'initial_status'} = Bugzilla::Status->can_change_to; - $vars->{'status_objects'} = [ Bugzilla::Status->get_all ]; - } + my ($self, $args) = @_; + my $vars = $args->{'vars'}; + my $file = $args->{'file'}; + + if ($file =~ /config\.json\.tmpl$/) { + $vars->{'initial_status'} = Bugzilla::Status->can_change_to; + $vars->{'status_objects'} = [Bugzilla::Status->get_all]; + } } ############## @@ -64,137 +62,136 @@ sub template_before_process { ############## sub bug_start_of_update { - my ($self, $args) = @_; - my $old_bug = $args->{old_bug}; - my $params = Bugzilla->input_params; + my ($self, $args) = @_; + my $old_bug = $args->{old_bug}; + my $params = Bugzilla->input_params; - return if !Bugzilla->request_cache->{bzapi}; + return if !Bugzilla->request_cache->{bzapi}; - # Check for a mid-air collision. Currently this only works when updating - # an individual bug and if last_changed_time is provided. Otherwise it - # allows the changes. - my $delta_ts = $params->{last_change_time} || ''; + # Check for a mid-air collision. Currently this only works when updating + # an individual bug and if last_changed_time is provided. Otherwise it + # allows the changes. + my $delta_ts = $params->{last_change_time} || ''; - if ($delta_ts && exists $params->{ids} && @{ $params->{ids} } == 1) { - _midair_check($delta_ts, $old_bug->delta_ts); - } + if ($delta_ts && exists $params->{ids} && @{$params->{ids}} == 1) { + _midair_check($delta_ts, $old_bug->delta_ts); + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{object}; - my $params = Bugzilla->input_params; + my ($self, $args) = @_; + my $object = $args->{object}; + my $params = Bugzilla->input_params; - return if !Bugzilla->request_cache->{bzapi}; - return if !$object->isa('Bugzilla::Attachment'); + return if !Bugzilla->request_cache->{bzapi}; + return if !$object->isa('Bugzilla::Attachment'); - # Check for a mid-air collision. Currently this only works when updating - # an individual attachment and if last_changed_time is provided. Otherwise it - # allows the changes. - my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; - my $delta_ts = $stash->{last_change_time}; + # Check for a mid-air collision. Currently this only works when updating + # an individual attachment and if last_changed_time is provided. Otherwise it + # allows the changes. + my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; + my $delta_ts = $stash->{last_change_time}; - _midair_check($delta_ts, $object->modification_time) if $delta_ts; + _midair_check($delta_ts, $object->modification_time) if $delta_ts; } sub _midair_check { - my ($delta_ts, $old_delta_ts) = @_; - my $delta_ts_z = datetime_from($delta_ts) - || ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts }); - my $old_delta_tz_z = datetime_from($old_delta_ts); - if ($old_delta_tz_z ne $delta_ts_z) { - ThrowUserError('bzapi_midair_collision'); - } + my ($delta_ts, $old_delta_ts) = @_; + my $delta_ts_z = datetime_from($delta_ts) + || ThrowCodeError('invalid_timestamp', {timestamp => $delta_ts}); + my $old_delta_tz_z = datetime_from($old_delta_ts); + if ($old_delta_tz_z ne $delta_ts_z) { + ThrowUserError('bzapi_midair_collision'); + } } sub webservice_error_codes { - my ($self, $args) = @_; - my $error_map = $args->{error_map}; - $error_map->{'bzapi_midair_collision'} = 400; + my ($self, $args) = @_; + my $error_map = $args->{error_map}; + $error_map->{'bzapi_midair_collision'} = 400; } sub webservice_fix_credentials { - my ($self, $args) = @_; - my $rpc = $args->{rpc}; - my $params = $args->{params}; - return if !Bugzilla->request_cache->{bzapi}; - fix_credentials($params); + my ($self, $args) = @_; + my $rpc = $args->{rpc}; + my $params = $args->{params}; + return if !Bugzilla->request_cache->{bzapi}; + fix_credentials($params); } sub webservice_rest_request { - my ($self, $args) = @_; - my $rpc = $args->{rpc}; - my $params = $args->{params}; - my $cache = Bugzilla->request_cache; + my ($self, $args) = @_; + my $rpc = $args->{rpc}; + my $params = $args->{params}; + my $cache = Bugzilla->request_cache; - return if !$cache->{bzapi}; + return if !$cache->{bzapi}; - # Stash certain values for later use - $cache->{bzapi_rpc} = $rpc; + # Stash certain values for later use + $cache->{bzapi_rpc} = $rpc; - # Internal websevice method being used - $cache->{bzapi_rpc_method} = $rpc->path_info . "." . $rpc->bz_method_name; + # Internal websevice method being used + $cache->{bzapi_rpc_method} = $rpc->path_info . "." . $rpc->bz_method_name; - # Load the appropriate request handler based on path and type - if (my $handler = _find_handler($rpc, 'request')) { - &$handler($params); - } + # Load the appropriate request handler based on path and type + if (my $handler = _find_handler($rpc, 'request')) { + &$handler($params); + } } sub webservice_rest_response { - my ($self, $args) = @_; - my $rpc = $args->{rpc}; - my $result = $args->{result}; - my $response = $args->{response}; - my $cache = Bugzilla->request_cache; - - # Stash certain values for later use - $cache->{bzapi_rpc} ||= $rpc; - - return if !Bugzilla->request_cache->{bzapi} - || ref $$result ne 'HASH'; - - if (exists $$result->{error}) { - $$result->{documentation} = BZAPI_DOC; - return; - } - - # Load the appropriate response handler based on path and type - if (my $handler = _find_handler($rpc, 'response')) { - &$handler($result, $response); - } - - # Add a Location header if a newly created resource - # such as a bug or comment. - if ($rpc->bz_success_code - && $rpc->bz_success_code == STATUS_CREATED - && $$result->{ref}) - { - $response->header("Location", $$result->{ref}); - } + my ($self, $args) = @_; + my $rpc = $args->{rpc}; + my $result = $args->{result}; + my $response = $args->{response}; + my $cache = Bugzilla->request_cache; + + # Stash certain values for later use + $cache->{bzapi_rpc} ||= $rpc; + + return if !Bugzilla->request_cache->{bzapi} || ref $$result ne 'HASH'; + + if (exists $$result->{error}) { + $$result->{documentation} = BZAPI_DOC; + return; + } + + # Load the appropriate response handler based on path and type + if (my $handler = _find_handler($rpc, 'response')) { + &$handler($result, $response); + } + + # Add a Location header if a newly created resource + # such as a bug or comment. + if ( $rpc->bz_success_code + && $rpc->bz_success_code == STATUS_CREATED + && $$result->{ref}) + { + $response->header("Location", $$result->{ref}); + } } sub webservice_rest_resources { - my ($self, $args) = @_; - my $rpc = $args->{rpc}; - my $resources = $args->{resources}; + my ($self, $args) = @_; + my $rpc = $args->{rpc}; + my $resources = $args->{resources}; - return if !Bugzilla->request_cache->{bzapi}; + return if !Bugzilla->request_cache->{bzapi}; - _add_resources($rpc, $resources); + _add_resources($rpc, $resources); } sub webservice_status_code_map { - my ($self, $args) = @_; - my $status_code_map = $args->{status_code_map}; - $status_code_map->{51} = STATUS_BAD_REQUEST; + my ($self, $args) = @_; + my $status_code_map = $args->{status_code_map}; + $status_code_map->{51} = STATUS_BAD_REQUEST; } sub psgi_builder { - my ($self, $args) = @_; - my $mount = $args->{mount}; + my ($self, $args) = @_; + my $mount = $args->{mount}; - $mount->{'bzapi'} = compile_cgi('extensions/BzAPI/bin/rest.cgi'); + $mount->{'bzapi'} = compile_cgi('extensions/BzAPI/bin/rest.cgi'); } @@ -203,93 +200,95 @@ sub psgi_builder { ##################### sub _find_handler { - my ($rpc, $type) = @_; + my ($rpc, $type) = @_; - my $path_info = $rpc->cgi->path_info; - my $request_method = $rpc->request->method; + my $path_info = $rpc->cgi->path_info; + my $request_method = $rpc->request->method; - my $module = $rpc->bz_class_name || ''; - $module =~ s/^Bugzilla::WebService:://; + my $module = $rpc->bz_class_name || ''; + $module =~ s/^Bugzilla::WebService:://; - my $cache = _preload_handlers(); + my $cache = _preload_handlers(); - return undef if !exists $cache->{$module}; + return undef if !exists $cache->{$module}; - # Make a copy of the handler array so - # as to not alter the actual cached data. - my @handlers = @{ $cache->{$module} }; + # Make a copy of the handler array so + # as to not alter the actual cached data. + my @handlers = @{$cache->{$module}}; - while (my $regex = shift @handlers) { - my $data = shift @handlers; - next if ref $data ne 'HASH'; - if ($path_info =~ $regex - && exists $data->{$request_method} - && exists $data->{$request_method}->{$type}) - { - return $data->{$request_method}->{$type}; - } + while (my $regex = shift @handlers) { + my $data = shift @handlers; + next if ref $data ne 'HASH'; + if ( $path_info =~ $regex + && exists $data->{$request_method} + && exists $data->{$request_method}->{$type}) + { + return $data->{$request_method}->{$type}; } + } - return undef; + return undef; } sub _add_resources { - my ($rpc, $native_resources) = @_; - - my $cache = _preload_handlers(); - - foreach my $module (keys %$cache) { - my $native_module = "Bugzilla::WebService::$module"; - next if !$native_resources->{$native_module}; - - # Make a copy of the handler array so - # as to not alter the actual cached data. - my @handlers = @{ $cache->{$module} }; - - my @ext_resources = (); - while (my $regex = shift @handlers) { - my $data = shift @handlers; - next if ref $data ne 'HASH'; - my $new_data = {}; - foreach my $request_method (keys %$data) { - next if !exists $data->{$request_method}->{resource}; - $new_data->{$request_method} = $data->{$request_method}->{resource}; - } - push(@ext_resources, $regex, $new_data); - } - - # Places the new resources at the beginning of the list - # so we can capture specific paths before the native resources - unshift(@{$native_resources->{$native_module}}, @ext_resources); + my ($rpc, $native_resources) = @_; + + my $cache = _preload_handlers(); + + foreach my $module (keys %$cache) { + my $native_module = "Bugzilla::WebService::$module"; + next if !$native_resources->{$native_module}; + + # Make a copy of the handler array so + # as to not alter the actual cached data. + my @handlers = @{$cache->{$module}}; + + my @ext_resources = (); + while (my $regex = shift @handlers) { + my $data = shift @handlers; + next if ref $data ne 'HASH'; + my $new_data = {}; + foreach my $request_method (keys %$data) { + next if !exists $data->{$request_method}->{resource}; + $new_data->{$request_method} = $data->{$request_method}->{resource}; + } + push(@ext_resources, $regex, $new_data); } + + # Places the new resources at the beginning of the list + # so we can capture specific paths before the native resources + unshift(@{$native_resources->{$native_module}}, @ext_resources); + } } sub _resource_modules { - my $extdir = bz_locations()->{extensionsdir}; - return map { basename($_, '.pm') } glob("$extdir/" . __PACKAGE__->NAME . "/lib/Resources/*.pm"); + my $extdir = bz_locations()->{extensionsdir}; + return + map { basename($_, '.pm') } + glob("$extdir/" . __PACKAGE__->NAME . "/lib/Resources/*.pm"); } # preload all handlers into cache # since we don't want to parse all # this multiple times sub _preload_handlers { - my $cache = Bugzilla->request_cache; - - if (!exists $cache->{rest_handlers}) { - my $all_handlers = {}; - foreach my $module (_resource_modules()) { - my $resource_class = "Bugzilla::Extension::BzAPI::Resources::$module"; - trick_taint($resource_class); - eval { require_module($resource_class) }; - next if ($@ || !$resource_class->can('rest_handlers')); - my $handlers = $resource_class->rest_handlers; - next if (ref $handlers ne 'ARRAY' || scalar @$handlers % 2 != 0); - $all_handlers->{$module} = $handlers; - } - $cache->{rest_handlers} = $all_handlers; + my $cache = Bugzilla->request_cache; + + if (!exists $cache->{rest_handlers}) { + my $all_handlers = {}; + foreach my $module (_resource_modules()) { + my $resource_class = "Bugzilla::Extension::BzAPI::Resources::$module"; + trick_taint($resource_class); + eval { require_module($resource_class) }; + next if ($@ || !$resource_class->can('rest_handlers')); + my $handlers = $resource_class->rest_handlers; + next if (ref $handlers ne 'ARRAY' || scalar @$handlers % 2 != 0); + $all_handlers->{$module} = $handlers; } + $cache->{rest_handlers} = $all_handlers; + } - return $cache->{rest_handlers}; + return $cache->{rest_handlers}; } __PACKAGE__->NAME; diff --git a/extensions/BzAPI/bin/rest.cgi b/extensions/BzAPI/bin/rest.cgi index 5642ad550..5a895bc1d 100755 --- a/extensions/BzAPI/bin/rest.cgi +++ b/extensions/BzAPI/bin/rest.cgi @@ -14,12 +14,11 @@ use Bugzilla; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::WebService::Constants; + BEGIN { - if (!Bugzilla->feature('rest') - || !Bugzilla->feature('jsonrpc')) - { - ThrowUserError('feature_disabled', { feature => 'rest' }); - } + if (!Bugzilla->feature('rest') || !Bugzilla->feature('jsonrpc')) { + ThrowUserError('feature_disabled', {feature => 'rest'}); + } } # Set request_cache bzapi value to true in order to enable the @@ -30,9 +29,10 @@ Bugzilla->request_cache->{bzapi} = 1; # otherwise native REST will complain my $path_info = Bugzilla->cgi->path_info; if ($path_info =~ s'/$'') { - # Remove first slash as cgi->path_info expects it to - # not be there when setting a new path. - Bugzilla->cgi->path_info(substr($path_info, 1)); + + # Remove first slash as cgi->path_info expects it to + # not be there when setting a new path. + Bugzilla->cgi->path_info(substr($path_info, 1)); } use Bugzilla::WebService::Server::REST; diff --git a/extensions/BzAPI/lib/Constants.pm b/extensions/BzAPI/lib/Constants.pm index fb611aae6..f31dac7a8 100644 --- a/extensions/BzAPI/lib/Constants.pm +++ b/extensions/BzAPI/lib/Constants.pm @@ -13,142 +13,144 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - USER_FIELDS - BUG_FIELD_MAP - BOOLEAN_TYPE_MAP - ATTACHMENT_FIELD_MAP - DEFAULT_BUG_FIELDS - DEFAULT_ATTACHMENT_FIELDS + USER_FIELDS + BUG_FIELD_MAP + BOOLEAN_TYPE_MAP + ATTACHMENT_FIELD_MAP + DEFAULT_BUG_FIELDS + DEFAULT_ATTACHMENT_FIELDS - BZAPI_DOC + BZAPI_DOC ); # These are fields that are normally exported as a single value such # as the user's email. BzAPI needs to convert them to user objects # where possible. -use constant USER_FIELDS => (qw( +use constant USER_FIELDS => ( + qw( assigned_to cc creator qa_contact reporter -)); + ) +); # Convert old field names from old to new use constant BUG_FIELD_MAP => { - 'opendate' => 'creation_time', # query - 'creation_ts' => 'creation_time', - 'changeddate' => 'last_change_time', # query - 'delta_ts' => 'last_change_time', - 'bug_id' => 'id', - 'rep_platform' => 'platform', - 'bug_severity' => 'severity', - 'bug_status' => 'status', - 'short_desc' => 'summary', - 'bug_file_loc' => 'url', - 'status_whiteboard' => 'whiteboard', - 'reporter' => 'creator', - 'reporter_realname' => 'creator_realname', - 'cclist_accessible' => 'is_cc_accessible', - 'reporter_accessible' => 'is_creator_accessible', - 'everconfirmed' => 'is_confirmed', - 'dependson' => 'depends_on', - 'blocked' => 'blocks', - 'attachment' => 'attachments', - 'flag' => 'flags', - 'flagtypes.name' => 'flag', - 'bug_group' => 'group', - 'group' => 'groups', - 'longdesc' => 'comment', - 'bug_file_loc_type' => 'url_type', - 'bugidtype' => 'id_mode', - 'longdesc_type' => 'comment_type', - 'short_desc_type' => 'summary_type', - 'status_whiteboard_type' => 'whiteboard_type', - 'emailassigned_to1' => 'email1_assigned_to', - 'emailassigned_to2' => 'email2_assigned_to', - 'emailcc1' => 'email1_cc', - 'emailcc2' => 'email2_cc', - 'emailqa_contact1' => 'email1_qa_contact', - 'emailqa_contact2' => 'email2_qa_contact', - 'emailreporter1' => 'email1_creator', - 'emailreporter2' => 'email2_creator', - 'emaillongdesc1' => 'email1_comment_creator', - 'emaillongdesc2' => 'email2_comment_creator', - 'emailtype1' => 'email1_type', - 'emailtype2' => 'email2_type', - 'chfieldfrom' => 'changed_after', - 'chfieldto' => 'changed_before', - 'chfield' => 'changed_field', - 'chfieldvalue' => 'changed_field_to', - 'deadlinefrom' => 'deadline_after', - 'deadlineto' => 'deadline_before', - 'attach_data.thedata' => 'attachment.data', - 'longdescs.isprivate' => 'comment.is_private', - 'commenter' => 'comment.creator', - 'requestees.login_name' => 'flag.requestee', - 'setters.login_name' => 'flag.setter', - 'days_elapsed' => 'idle', - 'owner_idle_time' => 'assignee_idle', - 'dup_id' => 'dupe_of', - 'isopened' => 'is_open', - 'flag_type' => 'flag_types', - 'attachments.submitter' => 'attachment.attacher', - 'attachments.filename' => 'attachment.file_name', - 'attachments.description' => 'attachment.description', - 'attachments.delta_ts' => 'attachment.last_change_time', - 'attachments.isobsolete' => 'attachment.is_obsolete', - 'attachments.ispatch' => 'attachment.is_patch', - 'attachments.isprivate' => 'attachment.is_private', - 'attachments.mimetype' => 'attachment.content_type', - 'attachments.date' => 'attachment.creation_time', - 'attachments.attachid' => 'attachment.id', - 'attachments.flag' => 'attachment.flags', - 'attachments.token' => 'attachment.update_token' + 'opendate' => 'creation_time', # query + 'creation_ts' => 'creation_time', + 'changeddate' => 'last_change_time', # query + 'delta_ts' => 'last_change_time', + 'bug_id' => 'id', + 'rep_platform' => 'platform', + 'bug_severity' => 'severity', + 'bug_status' => 'status', + 'short_desc' => 'summary', + 'bug_file_loc' => 'url', + 'status_whiteboard' => 'whiteboard', + 'reporter' => 'creator', + 'reporter_realname' => 'creator_realname', + 'cclist_accessible' => 'is_cc_accessible', + 'reporter_accessible' => 'is_creator_accessible', + 'everconfirmed' => 'is_confirmed', + 'dependson' => 'depends_on', + 'blocked' => 'blocks', + 'attachment' => 'attachments', + 'flag' => 'flags', + 'flagtypes.name' => 'flag', + 'bug_group' => 'group', + 'group' => 'groups', + 'longdesc' => 'comment', + 'bug_file_loc_type' => 'url_type', + 'bugidtype' => 'id_mode', + 'longdesc_type' => 'comment_type', + 'short_desc_type' => 'summary_type', + 'status_whiteboard_type' => 'whiteboard_type', + 'emailassigned_to1' => 'email1_assigned_to', + 'emailassigned_to2' => 'email2_assigned_to', + 'emailcc1' => 'email1_cc', + 'emailcc2' => 'email2_cc', + 'emailqa_contact1' => 'email1_qa_contact', + 'emailqa_contact2' => 'email2_qa_contact', + 'emailreporter1' => 'email1_creator', + 'emailreporter2' => 'email2_creator', + 'emaillongdesc1' => 'email1_comment_creator', + 'emaillongdesc2' => 'email2_comment_creator', + 'emailtype1' => 'email1_type', + 'emailtype2' => 'email2_type', + 'chfieldfrom' => 'changed_after', + 'chfieldto' => 'changed_before', + 'chfield' => 'changed_field', + 'chfieldvalue' => 'changed_field_to', + 'deadlinefrom' => 'deadline_after', + 'deadlineto' => 'deadline_before', + 'attach_data.thedata' => 'attachment.data', + 'longdescs.isprivate' => 'comment.is_private', + 'commenter' => 'comment.creator', + 'requestees.login_name' => 'flag.requestee', + 'setters.login_name' => 'flag.setter', + 'days_elapsed' => 'idle', + 'owner_idle_time' => 'assignee_idle', + 'dup_id' => 'dupe_of', + 'isopened' => 'is_open', + 'flag_type' => 'flag_types', + 'attachments.submitter' => 'attachment.attacher', + 'attachments.filename' => 'attachment.file_name', + 'attachments.description' => 'attachment.description', + 'attachments.delta_ts' => 'attachment.last_change_time', + 'attachments.isobsolete' => 'attachment.is_obsolete', + 'attachments.ispatch' => 'attachment.is_patch', + 'attachments.isprivate' => 'attachment.is_private', + 'attachments.mimetype' => 'attachment.content_type', + 'attachments.date' => 'attachment.creation_time', + 'attachments.attachid' => 'attachment.id', + 'attachments.flag' => 'attachment.flags', + 'attachments.token' => 'attachment.update_token' }; # Convert from old boolean chart type names to new names use constant BOOLEAN_TYPE_MAP => { - 'equals' => 'equals', - 'not_equals' => 'notequals', - 'equals_any' => 'anyexact', - 'contains' => 'substring', - 'not_contains' => 'notsubstring', - 'case_contains' => 'casesubstring', - 'contains_any' => 'anywordssubstr', - 'not_contains_any' => 'nowordssubstr', - 'contains_all' => 'allwordssubstr', - 'contains_any_words' => 'anywords', - 'not_contains_any_words' => 'nowords', - 'contains_all_words' => 'allwords', - 'regex' => 'regexp', - 'not_regex' => 'notregexp', - 'less_than' => 'lessthan', - 'greater_than' => 'greaterthan', - 'changed_before' => 'changedbefore', - 'changed_after' => 'changedafter', - 'changed_from' => 'changedfrom', - 'changed_to' => 'changedto', - 'changed_by' => 'changedby', - 'matches' => 'matches' + 'equals' => 'equals', + 'not_equals' => 'notequals', + 'equals_any' => 'anyexact', + 'contains' => 'substring', + 'not_contains' => 'notsubstring', + 'case_contains' => 'casesubstring', + 'contains_any' => 'anywordssubstr', + 'not_contains_any' => 'nowordssubstr', + 'contains_all' => 'allwordssubstr', + 'contains_any_words' => 'anywords', + 'not_contains_any_words' => 'nowords', + 'contains_all_words' => 'allwords', + 'regex' => 'regexp', + 'not_regex' => 'notregexp', + 'less_than' => 'lessthan', + 'greater_than' => 'greaterthan', + 'changed_before' => 'changedbefore', + 'changed_after' => 'changedafter', + 'changed_from' => 'changedfrom', + 'changed_to' => 'changedto', + 'changed_by' => 'changedby', + 'matches' => 'matches' }; # Convert old attachment field names from old to new use constant ATTACHMENT_FIELD_MAP => { - 'submitter' => 'attacher', - 'description' => 'description', - 'filename' => 'file_name', - 'delta_ts' => 'last_change_time', - 'isobsolete' => 'is_obsolete', - 'ispatch' => 'is_patch', - 'isprivate' => 'is_private', - 'mimetype' => 'content_type', - 'contenttypeentry' => 'content_type', - 'date' => 'creation_time', - 'attachid' => 'id', - 'desc' => 'description', - 'flag' => 'flags', - 'type' => 'content_type', + 'submitter' => 'attacher', + 'description' => 'description', + 'filename' => 'file_name', + 'delta_ts' => 'last_change_time', + 'isobsolete' => 'is_obsolete', + 'ispatch' => 'is_patch', + 'isprivate' => 'is_private', + 'mimetype' => 'content_type', + 'contenttypeentry' => 'content_type', + 'date' => 'creation_time', + 'attachid' => 'id', + 'desc' => 'description', + 'flag' => 'flags', + 'type' => 'content_type', }; # A base link to the current BzAPI Documentation. diff --git a/extensions/BzAPI/lib/Resources/Bug.pm b/extensions/BzAPI/lib/Resources/Bug.pm index 4fd59824c..ee76ddbcd 100644 --- a/extensions/BzAPI/lib/Resources/Bug.pm +++ b/extensions/BzAPI/lib/Resources/Bug.pm @@ -28,111 +28,86 @@ use List::Util qw(max); ################# BEGIN { - require Bugzilla::WebService::Bug; - *Bugzilla::WebService::Bug::get_bug_count = \&get_bug_count_resource; + require Bugzilla::WebService::Bug; + *Bugzilla::WebService::Bug::get_bug_count = \&get_bug_count_resource; } sub rest_handlers { - my $rest_handlers = [ - qr{^/bug$}, { - GET => { - request => \&search_bugs_request, - response => \&search_bugs_response - }, - POST => { - request => \&create_bug_request, - response => \&create_bug_response - } - }, - qr{^/bug/([^/]+)$}, { - GET => { - response => \&get_bug_response - }, - PUT => { - request => \&update_bug_request, - response => \&update_bug_response - } - }, - qr{^/bug/([^/]+)/comment$}, { - GET => { - response => \&get_comments_response - }, - POST => { - request => \&add_comment_request, - response => \&add_comment_response - } - }, - qr{^/bug/([^/]+)/history$}, { - GET => { - response => \&get_history_response - } - }, - qr{^/bug/([^/]+)/attachment$}, { - GET => { - response => \&get_attachments_response - }, - POST => { - request => \&add_attachment_request, - response => \&add_attachment_response - } - }, - qr{^/bug/attachment/([^/]+)$}, { - GET => { - response => \&get_attachment_response - }, - PUT => { - request => \&update_attachment_request, - response => \&update_attachment_response - } - }, - qr{^/attachment/([^/]+)$}, { - GET => { - response => \&get_attachment_response - }, - PUT => { - request => \&update_attachment_request, - response => \&update_attachment_response - } - }, - qr{^/bug/([^/]+)/flag$}, { - GET => { - resource => { - method => 'get', - params => sub { - return { ids => [ $_[0] ], - include_fields => ['flags'] }; - } - }, - response => \&get_bug_flags_response, - } - }, - qr{^/count$}, { - GET => { - resource => { - method => 'get_bug_count' - } - } + my $rest_handlers = [ + qr{^/bug$}, + { + GET => {request => \&search_bugs_request, response => \&search_bugs_response}, + POST => {request => \&create_bug_request, response => \&create_bug_response} + }, + qr{^/bug/([^/]+)$}, + { + GET => {response => \&get_bug_response}, + PUT => {request => \&update_bug_request, response => \&update_bug_response} + }, + qr{^/bug/([^/]+)/comment$}, + { + GET => {response => \&get_comments_response}, + POST => {request => \&add_comment_request, response => \&add_comment_response} + }, + qr{^/bug/([^/]+)/history$}, + {GET => {response => \&get_history_response}}, + qr{^/bug/([^/]+)/attachment$}, + { + GET => {response => \&get_attachments_response}, + POST => + {request => \&add_attachment_request, response => \&add_attachment_response} + }, + qr{^/bug/attachment/([^/]+)$}, + { + GET => {response => \&get_attachment_response}, + PUT => { + request => \&update_attachment_request, + response => \&update_attachment_response + } + }, + qr{^/attachment/([^/]+)$}, + { + GET => {response => \&get_attachment_response}, + PUT => { + request => \&update_attachment_request, + response => \&update_attachment_response + } + }, + qr{^/bug/([^/]+)/flag$}, + { + GET => { + resource => { + method => 'get', + params => sub { + return {ids => [$_[0]], include_fields => ['flags']}; + } }, - qr{^/attachment/([^/]+)$}, { - GET => { - resource => { - method => 'attachments', - params => sub { - return { attachment_ids => [ $_[0] ] }; - } - } - }, - PUT => { - resource => { - method => 'update_attachment', - params => sub { - return { ids => [ $_[0] ] }; - } - } - } + response => \&get_bug_flags_response, + } + }, + qr{^/count$}, + {GET => {resource => {method => 'get_bug_count'}}}, + qr{^/attachment/([^/]+)$}, + { + GET => { + resource => { + method => 'attachments', + params => sub { + return {attachment_ids => [$_[0]]}; + } } - ]; - return $rest_handlers; + }, + PUT => { + resource => { + method => 'update_attachment', + params => sub { + return {ids => [$_[0]]}; + } + } + } + } + ]; + return $rest_handlers; } ######################### @@ -144,205 +119,203 @@ sub rest_handlers { # this should be broken into it's own module so that report.cgi # and here can share the same code. sub get_bug_count_resource { - my ($self, $params) = @_; - - Bugzilla->switch_to_shadow_db(); - - my $col_field = $params->{x_axis_field} || ''; - my $row_field = $params->{y_axis_field} || ''; - my $tbl_field = $params->{z_axis_field} || ''; - - my $dimensions = $col_field ? - $row_field ? - $tbl_field ? 3 : 2 : 1 : 0; - - if ($dimensions == 0) { - $col_field = "bug_status"; - $params->{x_axis_field} = "bug_status"; - } - - # Valid bug fields that can be reported on. - my $valid_columns = Bugzilla::Search::REPORT_COLUMNS; - - # Convert external names to internal if necessary - $params = Bugzilla::Bug::map_fields($params); - $row_field = Bugzilla::Bug::FIELD_MAP->{$row_field} || $row_field; - $col_field = Bugzilla::Bug::FIELD_MAP->{$col_field} || $col_field; - $tbl_field = Bugzilla::Bug::FIELD_MAP->{$tbl_field} || $tbl_field; - - # Validate the values in the axis fields or throw an error. - !$row_field - || ($valid_columns->{$row_field} && trick_taint($row_field)) - || ThrowCodeError("report_axis_invalid", { fld => "x", val => $row_field }); - !$col_field - || ($valid_columns->{$col_field} && trick_taint($col_field)) - || ThrowCodeError("report_axis_invalid", { fld => "y", val => $col_field }); - !$tbl_field - || ($valid_columns->{$tbl_field} && trick_taint($tbl_field)) - || ThrowCodeError("report_axis_invalid", { fld => "z", val => $tbl_field }); - - my @axis_fields = grep { $_ } ($row_field, $col_field, $tbl_field); - - my $search = new Bugzilla::Search( - fields => \@axis_fields, - params => $params, - allow_unlimited => 1, - ); - - my ($results, $extra_data) = $search->data; - - # We have a hash of hashes for the data itself, and a hash to hold the - # row/col/table names. - my %data; - my %names; - - # Read the bug data and count the bugs for each possible value of row, column - # and table. - # - # We detect a numerical field, and sort appropriately, if all the values are - # numeric. - my $col_isnumeric = 1; - my $row_isnumeric = 1; - my $tbl_isnumeric = 1; - - foreach my $result (@$results) { - # handle empty dimension member names - my $row = check_value($row_field, $result); - my $col = check_value($col_field, $result); - my $tbl = check_value($tbl_field, $result); - - $data{$tbl}{$col}{$row}++; - $names{"col"}{$col}++; - $names{"row"}{$row}++; - $names{"tbl"}{$tbl}++; - - $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o); - $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o); - $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o); - } - - my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field); - my @row_names = get_names($names{"row"}, $row_isnumeric, $row_field); - my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field); - - push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1); - - my @data; - foreach my $tbl (@tbl_names) { - my @tbl_data; - foreach my $row (@row_names) { - my @col_data; - foreach my $col (@col_names) { - $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0; - push(@col_data, $data{$tbl}{$col}{$row}); - if ($tbl ne "-total-") { - # This is a bit sneaky. We spend every loop except the last - # building up the -total- data, and then last time round, - # we process it as another tbl, and push() the total values - # into the image_data array. - $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row}; - } - } - push(@tbl_data, \@col_data); - } - push(@data, \@tbl_data); - } - - my $result = {}; - if ($dimensions == 0) { - my $sum = 0; - - # If the search returns no results, we just get an 0-byte file back - # and so there is no data at all. - if (@data) { - foreach my $value (@{ $data[0][0] }) { - $sum += $value; - } - } - - $result = { - 'data' => $sum - }; - } - elsif ($dimensions == 1) { - $result = { - 'x_labels' => \@col_names, - 'data' => $data[0][0] || [] - }; - } - elsif ($dimensions == 2) { - $result = { - 'x_labels' => \@col_names, - 'y_labels' => \@row_names, - 'data' => $data[0] || [[]] - }; - } - elsif ($dimensions == 3) { - if (@data > 1 && $tbl_names[-1] eq "-total-") { - # Last table is a total, which we discard - pop(@data); - pop(@tbl_names); + my ($self, $params) = @_; + + Bugzilla->switch_to_shadow_db(); + + my $col_field = $params->{x_axis_field} || ''; + my $row_field = $params->{y_axis_field} || ''; + my $tbl_field = $params->{z_axis_field} || ''; + + my $dimensions = $col_field ? $row_field ? $tbl_field ? 3 : 2 : 1 : 0; + + if ($dimensions == 0) { + $col_field = "bug_status"; + $params->{x_axis_field} = "bug_status"; + } + + # Valid bug fields that can be reported on. + my $valid_columns = Bugzilla::Search::REPORT_COLUMNS; + + # Convert external names to internal if necessary + $params = Bugzilla::Bug::map_fields($params); + $row_field = Bugzilla::Bug::FIELD_MAP->{$row_field} || $row_field; + $col_field = Bugzilla::Bug::FIELD_MAP->{$col_field} || $col_field; + $tbl_field = Bugzilla::Bug::FIELD_MAP->{$tbl_field} || $tbl_field; + + # Validate the values in the axis fields or throw an error. + !$row_field + || ($valid_columns->{$row_field} && trick_taint($row_field)) + || ThrowCodeError("report_axis_invalid", {fld => "x", val => $row_field}); + !$col_field + || ($valid_columns->{$col_field} && trick_taint($col_field)) + || ThrowCodeError("report_axis_invalid", {fld => "y", val => $col_field}); + !$tbl_field + || ($valid_columns->{$tbl_field} && trick_taint($tbl_field)) + || ThrowCodeError("report_axis_invalid", {fld => "z", val => $tbl_field}); + + my @axis_fields = grep {$_} ($row_field, $col_field, $tbl_field); + + my $search = new Bugzilla::Search( + fields => \@axis_fields, + params => $params, + allow_unlimited => 1, + ); + + my ($results, $extra_data) = $search->data; + + # We have a hash of hashes for the data itself, and a hash to hold the + # row/col/table names. + my %data; + my %names; + + # Read the bug data and count the bugs for each possible value of row, column + # and table. + # + # We detect a numerical field, and sort appropriately, if all the values are + # numeric. + my $col_isnumeric = 1; + my $row_isnumeric = 1; + my $tbl_isnumeric = 1; + + foreach my $result (@$results) { + + # handle empty dimension member names + my $row = check_value($row_field, $result); + my $col = check_value($col_field, $result); + my $tbl = check_value($tbl_field, $result); + + $data{$tbl}{$col}{$row}++; + $names{"col"}{$col}++; + $names{"row"}{$row}++; + $names{"tbl"}{$tbl}++; + + $col_isnumeric &&= ($col =~ /^-?\d+(\.\d+)?$/o); + $row_isnumeric &&= ($row =~ /^-?\d+(\.\d+)?$/o); + $tbl_isnumeric &&= ($tbl =~ /^-?\d+(\.\d+)?$/o); + } + + my @col_names = get_names($names{"col"}, $col_isnumeric, $col_field); + my @row_names = get_names($names{"row"}, $row_isnumeric, $row_field); + my @tbl_names = get_names($names{"tbl"}, $tbl_isnumeric, $tbl_field); + + push(@tbl_names, "-total-") if (scalar(@tbl_names) > 1); + + my @data; + foreach my $tbl (@tbl_names) { + my @tbl_data; + foreach my $row (@row_names) { + my @col_data; + foreach my $col (@col_names) { + $data{$tbl}{$col}{$row} = $data{$tbl}{$col}{$row} || 0; + push(@col_data, $data{$tbl}{$col}{$row}); + if ($tbl ne "-total-") { + + # This is a bit sneaky. We spend every loop except the last + # building up the -total- data, and then last time round, + # we process it as another tbl, and push() the total values + # into the image_data array. + $data{"-total-"}{$col}{$row} += $data{$tbl}{$col}{$row}; } - - $result = { - 'x_labels' => \@col_names, - 'y_labels' => \@row_names, - 'z_labels' => \@tbl_names, - 'data' => @data ? \@data : [[[]]] - }; - } - - return $result; + } + push(@tbl_data, \@col_data); + } + push(@data, \@tbl_data); + } + + my $result = {}; + if ($dimensions == 0) { + my $sum = 0; + + # If the search returns no results, we just get an 0-byte file back + # and so there is no data at all. + if (@data) { + foreach my $value (@{$data[0][0]}) { + $sum += $value; + } + } + + $result = {'data' => $sum}; + } + elsif ($dimensions == 1) { + $result = {'x_labels' => \@col_names, 'data' => $data[0][0] || []}; + } + elsif ($dimensions == 2) { + $result = { + 'x_labels' => \@col_names, + 'y_labels' => \@row_names, + 'data' => $data[0] || [[]] + }; + } + elsif ($dimensions == 3) { + if (@data > 1 && $tbl_names[-1] eq "-total-") { + + # Last table is a total, which we discard + pop(@data); + pop(@tbl_names); + } + + $result = { + 'x_labels' => \@col_names, + 'y_labels' => \@row_names, + 'z_labels' => \@tbl_names, + 'data' => @data ? \@data : [[[]]] + }; + } + + return $result; } sub get_names { - my ($names, $isnumeric, $field_name) = @_; - my ($field, @sorted); - # XXX - This is a hack to handle the actual_time/work_time field, - # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm. - $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time'; - - # _realname fields aren't real Bugzilla::Field objects, but they are a - # valid axis, so we don't vailidate them as Bugzilla::Field objects. - $field = Bugzilla::Field->check($field_name) - if ($field_name && $field_name !~ /_realname$/); - - if ($field && $field->is_select) { - foreach my $value (@{$field->legal_values}) { - push(@sorted, $value->name) if $names->{$value->name}; - } - unshift(@sorted, '---') if $field_name eq 'resolution'; - @sorted = uniq @sorted; - } - elsif ($isnumeric) { - # It's not a field we are preserving the order of, so sort it - # numerically... - @sorted = sort { $a <=> $b } keys %$names; - } - else { - # ...or alphabetically, as appropriate. - @sorted = sort keys %$names; - } - - return @sorted; + my ($names, $isnumeric, $field_name) = @_; + my ($field, @sorted); + + # XXX - This is a hack to handle the actual_time/work_time field, + # because it's named 'actual_time' in Search.pm but 'work_time' in Field.pm. + $_[2] = $field_name = 'work_time' if $field_name eq 'actual_time'; + + # _realname fields aren't real Bugzilla::Field objects, but they are a + # valid axis, so we don't vailidate them as Bugzilla::Field objects. + $field = Bugzilla::Field->check($field_name) + if ($field_name && $field_name !~ /_realname$/); + + if ($field && $field->is_select) { + foreach my $value (@{$field->legal_values}) { + push(@sorted, $value->name) if $names->{$value->name}; + } + unshift(@sorted, '---') if $field_name eq 'resolution'; + @sorted = uniq @sorted; + } + elsif ($isnumeric) { + + # It's not a field we are preserving the order of, so sort it + # numerically... + @sorted = sort { $a <=> $b } keys %$names; + } + else { + # ...or alphabetically, as appropriate. + @sorted = sort keys %$names; + } + + return @sorted; } sub check_value { - my ($field, $result) = @_; - - my $value; - if (!defined $field) { - $value = ''; - } - elsif ($field eq '') { - $value = ' '; - } - else { - $value = shift @$result; - $value = ' ' if (!defined $value || $value eq ''); - $value = '---' if ($field eq 'resolution' && $value eq ' '); - } - return $value; + my ($field, $result) = @_; + + my $value; + if (!defined $field) { + $value = ''; + } + elsif ($field eq '') { + $value = ' '; + } + else { + $value = shift @$result; + $value = ' ' if (!defined $value || $value eq ''); + $value = '---' if ($field eq 'resolution' && $value eq ' '); + } + return $value; } ######################## @@ -350,282 +323,290 @@ sub check_value { ######################## sub search_bugs_request { - my ($params) = @_; - - if (defined $params->{changed_field} - && $params->{changed_field} eq "creation_time") - { - $params->{changed_field} = "[Bug creation]"; - } + my ($params) = @_; - my $FIELD_NEW_TO_OLD = { reverse %{ BUG_FIELD_MAP() } }; + if (defined $params->{changed_field} + && $params->{changed_field} eq "creation_time") + { + $params->{changed_field} = "[Bug creation]"; + } - # Update values of various forms. - foreach my $key (keys %$params) { - # First, search types. These are found in the value of any field ending - # _type, and the value of any field matching type\d-\d-\d. - if ($key =~ /^type(\d+)-(\d+)-(\d+)$|_type$/) { - $params->{$key} - = BOOLEAN_TYPE_MAP->{$params->{$key}} || $params->{$key}; - } + my $FIELD_NEW_TO_OLD = {reverse %{BUG_FIELD_MAP()}}; - # Field names hiding in values instead of keys: changed_field, boolean - # charts and axis names. - if ($key =~ /^(field\d+-\d+-\d+| - changed_field| - (x|y|z)_axis_field)$ - /x) { - $params->{$key} - = $FIELD_NEW_TO_OLD->{$params->{$key}} || $params->{$key}; - } - } + # Update values of various forms. + foreach my $key (keys %$params) { - # Update field names - foreach my $field (keys %$FIELD_NEW_TO_OLD) { - if (defined $params->{$field}) { - $params->{$FIELD_NEW_TO_OLD->{$field}} = delete $params->{$field}; - } - } - - if (exists $params->{bug_id_type}) { - $params->{bug_id_type} - = BOOLEAN_TYPE_MAP->{$params->{bug_id_type}} || $params->{bug_id_type}; + # First, search types. These are found in the value of any field ending + # _type, and the value of any field matching type\d-\d-\d. + if ($key =~ /^type(\d+)-(\d+)-(\d+)$|_type$/) { + $params->{$key} = BOOLEAN_TYPE_MAP->{$params->{$key}} || $params->{$key}; } - # Time field names are screwy, and got reused. We can't put this mapping - # in NEW2OLD as everything will go haywire. actual_time has to be queried - # as work_time even though work_time is the submit-only field for _adding_ - # to actual_time, which can't be arbitrarily manipulated. - if (defined $params->{work_time}) { - $params->{actual_time} = delete $params->{work_time}; - } - - # Other convenience search ariables used by BzAPI - my @field_ids = grep(/^f(\d+)$/, keys %$params); - my $last_field_id = @field_ids ? max @field_ids + 1 : 1; - foreach my $field (qw(setters.login_name requestees.login_name)) { - if (my $value = delete $params->{$field}) { - $params->{"f${last_field_id}"} = $FIELD_NEW_TO_OLD->{$field} || $field; - $params->{"o${last_field_id}"} = 'equals'; - $params->{"v${last_field_id}"} = $value; - $last_field_id++; - } - } + # Field names hiding in values instead of keys: changed_field, boolean + # charts and axis names. + if ( + $key =~ /^(field\d+-\d+-\d+| + changed_field| + (x|y|z)_axis_field)$ + /x + ) + { + $params->{$key} = $FIELD_NEW_TO_OLD->{$params->{$key}} || $params->{$key}; + } + } + + # Update field names + foreach my $field (keys %$FIELD_NEW_TO_OLD) { + if (defined $params->{$field}) { + $params->{$FIELD_NEW_TO_OLD->{$field}} = delete $params->{$field}; + } + } + + if (exists $params->{bug_id_type}) { + $params->{bug_id_type} + = BOOLEAN_TYPE_MAP->{$params->{bug_id_type}} || $params->{bug_id_type}; + } + + # Time field names are screwy, and got reused. We can't put this mapping + # in NEW2OLD as everything will go haywire. actual_time has to be queried + # as work_time even though work_time is the submit-only field for _adding_ + # to actual_time, which can't be arbitrarily manipulated. + if (defined $params->{work_time}) { + $params->{actual_time} = delete $params->{work_time}; + } + + # Other convenience search ariables used by BzAPI + my @field_ids = grep(/^f(\d+)$/, keys %$params); + my $last_field_id = @field_ids ? max @field_ids + 1 : 1; + foreach my $field (qw(setters.login_name requestees.login_name)) { + if (my $value = delete $params->{$field}) { + $params->{"f${last_field_id}"} = $FIELD_NEW_TO_OLD->{$field} || $field; + $params->{"o${last_field_id}"} = 'equals'; + $params->{"v${last_field_id}"} = $value; + $last_field_id++; + } + } } sub create_bug_request { - my ($params) = @_; - - # User roles such as assigned_to and qa_contact should be just the - # email (login) of the user you want to set to. - foreach my $field (qw(assigned_to qa_contact)) { - if (exists $params->{$field}) { - $params->{$field} = $params->{$field}->{name}; - } - } - - # CC should just be a list of bugzilla logins - if (exists $params->{cc}) { - $params->{cc} = [ map { $_->{name} } @{ $params->{cc} } ]; - } - - # Comment - if (exists $params->{comments}) { - $params->{comment_is_private} = $params->{comments}->[0]->{is_private}; - $params->{description} = $params->{comments}->[0]->{text}; - delete $params->{comments}; - } - - # Some fields are not supported by Bugzilla::Bug->create but are supported - # by Bugzilla::Bug->update :( - my $cache = Bugzilla->request_cache->{bzapi_bug_create_extra} ||= {}; - foreach my $field (qw(remaining_time)) { - next if !exists $params->{$field}; - $cache->{$field} = delete $params->{$field}; - } - - # remove username/password - delete $params->{username}; - delete $params->{password}; + my ($params) = @_; + + # User roles such as assigned_to and qa_contact should be just the + # email (login) of the user you want to set to. + foreach my $field (qw(assigned_to qa_contact)) { + if (exists $params->{$field}) { + $params->{$field} = $params->{$field}->{name}; + } + } + + # CC should just be a list of bugzilla logins + if (exists $params->{cc}) { + $params->{cc} = [map { $_->{name} } @{$params->{cc}}]; + } + + # Comment + if (exists $params->{comments}) { + $params->{comment_is_private} = $params->{comments}->[0]->{is_private}; + $params->{description} = $params->{comments}->[0]->{text}; + delete $params->{comments}; + } + + # Some fields are not supported by Bugzilla::Bug->create but are supported + # by Bugzilla::Bug->update :( + my $cache = Bugzilla->request_cache->{bzapi_bug_create_extra} ||= {}; + foreach my $field (qw(remaining_time)) { + next if !exists $params->{$field}; + $cache->{$field} = delete $params->{$field}; + } + + # remove username/password + delete $params->{username}; + delete $params->{password}; } sub update_bug_request { - my ($params) = @_; - - my $bug_id = ref $params->{ids} ? $params->{ids}->[0] : $params->{ids}; - my $bug = Bugzilla::Bug->check($bug_id); - - # Convert groups to proper add/remove lists - if (exists $params->{groups}) { - my @new_groups = map { $_->{name} } @{ $params->{groups} }; - my @old_groups = map { $_->name } @{ $bug->groups_in }; - my ($removed, $added) = diff_arrays(\@old_groups, \@new_groups); - if (@$added || @$removed) { - my $groups_data = {}; - $groups_data->{add} = $added if @$added; - $groups_data->{remove} = $removed if @$removed; - $params->{groups} = $groups_data; - } - else { - delete $params->{groups}; - } + my ($params) = @_; + + my $bug_id = ref $params->{ids} ? $params->{ids}->[0] : $params->{ids}; + my $bug = Bugzilla::Bug->check($bug_id); + + # Convert groups to proper add/remove lists + if (exists $params->{groups}) { + my @new_groups = map { $_->{name} } @{$params->{groups}}; + my @old_groups = map { $_->name } @{$bug->groups_in}; + my ($removed, $added) = diff_arrays(\@old_groups, \@new_groups); + if (@$added || @$removed) { + my $groups_data = {}; + $groups_data->{add} = $added if @$added; + $groups_data->{remove} = $removed if @$removed; + $params->{groups} = $groups_data; } - - # Other fields such as keywords, blocks depends_on - # support 'set' which will make the list exactly what - # the user passes in. - foreach my $field (qw(blocks depends_on dependson keywords)) { - if (exists $params->{$field}) { - $params->{$field} = { set => $params->{$field} }; - } + else { + delete $params->{groups}; + } + } + + # Other fields such as keywords, blocks depends_on + # support 'set' which will make the list exactly what + # the user passes in. + foreach my $field (qw(blocks depends_on dependson keywords)) { + if (exists $params->{$field}) { + $params->{$field} = {set => $params->{$field}}; + } + } + + # User roles such as assigned_to and qa_contact should be just the + # email (login) of the user you want to change to. Also if defined + # but set to NULL then we reset them to default + foreach my $field (qw(assigned_to qa_contact)) { + if (exists $params->{$field}) { + if (!$params->{$field}) { + $params->{"reset_$field"} = 1; + delete $params->{$field}; + } + else { + $params->{$field} = $params->{$field}->{name}; + } + } + } + + # CC is treated like groups in that we need 'add' and 'remove' keys + if (exists $params->{cc}) { + my $new_cc = [map { $_->{name} } @{$params->{cc}}]; + my ($removed, $added) = diff_arrays($bug->cc, $new_cc); + if (@$added || @$removed) { + my $cc_data = {}; + $cc_data->{add} = $added if @$added; + $cc_data->{remove} = $removed if @$removed; + $params->{cc} = $cc_data; } - - # User roles such as assigned_to and qa_contact should be just the - # email (login) of the user you want to change to. Also if defined - # but set to NULL then we reset them to default - foreach my $field (qw(assigned_to qa_contact)) { - if (exists $params->{$field}) { - if (!$params->{$field}) { - $params->{"reset_$field"} = 1; - delete $params->{$field}; - } - else { - $params->{$field} = $params->{$field}->{name}; - } - } + else { + delete $params->{cc}; } + } - # CC is treated like groups in that we need 'add' and 'remove' keys - if (exists $params->{cc}) { - my $new_cc = [ map { $_->{name} } @{ $params->{cc} } ]; - my ($removed, $added) = diff_arrays($bug->cc, $new_cc); - if (@$added || @$removed) { - my $cc_data = {}; - $cc_data->{add} = $added if @$added; - $cc_data->{remove} = $removed if @$removed; - $params->{cc} = $cc_data; - } - else { - delete $params->{cc}; - } + # see_also is treated like groups in that we need 'add' and 'remove' keys + if (exists $params->{see_also}) { + my $old_see_also = [map { $_->name } @{$bug->see_also}]; + my ($removed, $added) = diff_arrays($old_see_also, $params->{see_also}); + if (@$added || @$removed) { + my $data = {}; + $data->{add} = $added if @$added; + $data->{remove} = $removed if @$removed; + $params->{see_also} = $data; } - - # see_also is treated like groups in that we need 'add' and 'remove' keys - if (exists $params->{see_also}) { - my $old_see_also = [ map { $_->name } @{ $bug->see_also } ]; - my ($removed, $added) = diff_arrays($old_see_also, $params->{see_also}); - if (@$added || @$removed) { - my $data = {}; - $data->{add} = $added if @$added; - $data->{remove} = $removed if @$removed; - $params->{see_also} = $data; - } - else { - delete $params->{see_also}; - } + else { + delete $params->{see_also}; } + } - # BzAPI allows for adding comments by appending to the list of current - # comments and passing the whole list back. - # 1. If a comment id is specified, the user can update the comment privacy - # 2. If no id is specified it is considered a new comment but only the last - # one will be accepted. - my %comment_is_private; - foreach my $comment (@{ $params->{'comments'} }) { - if (my $id = $comment->{'id'}) { - # Existing comment; tweak privacy flags if necessary - $comment_is_private{$id} - = ($comment->{'is_private'} && $comment->{'is_private'} eq "true") ? 1 : 0; - } - else { - # New comment to be added - # If multiple new comments are specified, only the last one will be - # added. - $params->{comment} = { - body => $comment->{text}, - is_private => ($comment->{'is_private'} && - $comment->{'is_private'} eq "true") ? 1 : 0 - }; - } - } - $params->{comment_is_private} = \%comment_is_private if %comment_is_private; - - # Remove setter and convert requestee to just name - if (exists $params->{flags}) { - foreach my $flag (@{ $params->{flags} }) { - delete $flag->{setter}; # Always use logged in user - if (exists $flag->{requestee} && ref $flag->{requestee}) { - $flag->{requestee} = $flag->{requestee}->{name}; - } - # If no flag id provided, assume it is new - if (!exists $flag->{id}) { - $flag->{new} = 1; - } - } + # BzAPI allows for adding comments by appending to the list of current + # comments and passing the whole list back. + # 1. If a comment id is specified, the user can update the comment privacy + # 2. If no id is specified it is considered a new comment but only the last + # one will be accepted. + my %comment_is_private; + foreach my $comment (@{$params->{'comments'}}) { + if (my $id = $comment->{'id'}) { + + # Existing comment; tweak privacy flags if necessary + $comment_is_private{$id} + = ($comment->{'is_private'} && $comment->{'is_private'} eq "true") ? 1 : 0; } + else { + # New comment to be added + # If multiple new comments are specified, only the last one will be + # added. + $params->{comment} = { + body => $comment->{text}, + is_private => ($comment->{'is_private'} && $comment->{'is_private'} eq "true") + ? 1 + : 0 + }; + } + } + $params->{comment_is_private} = \%comment_is_private if %comment_is_private; + + # Remove setter and convert requestee to just name + if (exists $params->{flags}) { + foreach my $flag (@{$params->{flags}}) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + + # If no flag id provided, assume it is new + if (!exists $flag->{id}) { + $flag->{new} = 1; + } + } + } } sub add_comment_request { - my ($params) = @_; - $params->{comment} = delete $params->{text} if $params->{text}; + my ($params) = @_; + $params->{comment} = delete $params->{text} if $params->{text}; } sub add_attachment_request { - my ($params) = @_; - - # Bug.add_attachment uses 'summary' for description. - if ($params->{description}) { - $params->{summary} = $params->{description}; - delete $params->{description}; - } - - # Remove setter and convert requestee to just name - if (exists $params->{flags}) { - foreach my $flag (@{ $params->{flags} }) { - delete $flag->{setter}; # Always use logged in user - if (exists $flag->{requestee} && ref $flag->{requestee}) { - $flag->{requestee} = $flag->{requestee}->{name}; - } - } - } - - # Add comment if one is provided - if (exists $params->{comments} && scalar @{ $params->{comments} }) { - $params->{comment} = $params->{comments}->[0]->{text}; - delete $params->{comments}; - } + my ($params) = @_; + + # Bug.add_attachment uses 'summary' for description. + if ($params->{description}) { + $params->{summary} = $params->{description}; + delete $params->{description}; + } + + # Remove setter and convert requestee to just name + if (exists $params->{flags}) { + foreach my $flag (@{$params->{flags}}) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + } + } + + # Add comment if one is provided + if (exists $params->{comments} && scalar @{$params->{comments}}) { + $params->{comment} = $params->{comments}->[0]->{text}; + delete $params->{comments}; + } } sub update_attachment_request { - my ($params) = @_; - - # Stash away for midair checking later - if ($params->{last_change_time}) { - my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; - $stash->{last_change_time} = delete $params->{last_change_time}; - } - - # Immutable values - foreach my $key (qw(attacher bug_id bug_ref creation_time - encoding id ref size update_token)) { - delete $params->{$key}; - } - - # Convert setter and requestee to standard values - if (exists $params->{flags}) { - foreach my $flag (@{ $params->{flags} }) { - delete $flag->{setter}; # Always use logged in user - if (exists $flag->{requestee} && ref $flag->{requestee}) { - $flag->{requestee} = $flag->{requestee}->{name}; - } - } - } - - # Add comment if one is provided - if (exists $params->{comments} && scalar @{ $params->{comments} }) { - $params->{comment} = $params->{comments}->[0]->{text}; - delete $params->{comments}; - } + my ($params) = @_; + + # Stash away for midair checking later + if ($params->{last_change_time}) { + my $stash = Bugzilla->request_cache->{bzapi_stash} ||= {}; + $stash->{last_change_time} = delete $params->{last_change_time}; + } + + # Immutable values + foreach my $key ( + qw(attacher bug_id bug_ref creation_time + encoding id ref size update_token) + ) + { + delete $params->{$key}; + } + + # Convert setter and requestee to standard values + if (exists $params->{flags}) { + foreach my $flag (@{$params->{flags}}) { + delete $flag->{setter}; # Always use logged in user + if (exists $flag->{requestee} && ref $flag->{requestee}) { + $flag->{requestee} = $flag->{requestee}->{name}; + } + } + } + + # Add comment if one is provided + if (exists $params->{comments} && scalar @{$params->{comments}}) { + $params->{comment} = $params->{comments}->[0]->{text}; + delete $params->{comments}; + } } ######################### @@ -633,237 +614,247 @@ sub update_attachment_request { ######################### sub search_bugs_response { - my ($result, $response) = @_; - my $cache = Bugzilla->request_cache; - my $params = Bugzilla->input_params; + my ($result, $response) = @_; + my $cache = Bugzilla->request_cache; + my $params = Bugzilla->input_params; - return if !exists $$result->{bugs}; + return if !exists $$result->{bugs}; - my $bug_objs = $cache->{bzapi_search_bugs}; + my $bug_objs = $cache->{bzapi_search_bugs}; - my @fixed_bugs; - my $stash = {}; - foreach my $bug_data (@{$$result->{bugs}}) { - my $bug_obj = shift @$bug_objs; - my $fixed = fix_bug($bug_data, $bug_obj, $stash); + my @fixed_bugs; + my $stash = {}; + foreach my $bug_data (@{$$result->{bugs}}) { + my $bug_obj = shift @$bug_objs; + my $fixed = fix_bug($bug_data, $bug_obj, $stash); - # CC count and Dupe count - if (filter_wants_nocache($params, 'cc_count')) { - $fixed->{cc_count} = scalar @{ $bug_obj->cc } - if $bug_obj->cc; - } - if (filter_wants_nocache($params, 'dupe_count')) { - $fixed->{dupe_count} = scalar @{ $bug_obj->duplicate_ids } - if $bug_obj->duplicate_ids; - } - - push(@fixed_bugs, $fixed); + # CC count and Dupe count + if (filter_wants_nocache($params, 'cc_count')) { + $fixed->{cc_count} = scalar @{$bug_obj->cc} if $bug_obj->cc; + } + if (filter_wants_nocache($params, 'dupe_count')) { + $fixed->{dupe_count} = scalar @{$bug_obj->duplicate_ids} + if $bug_obj->duplicate_ids; } - $$result->{bugs} = \@fixed_bugs; + push(@fixed_bugs, $fixed); + } + + $$result->{bugs} = \@fixed_bugs; } sub create_bug_response { - my ($result, $response) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return if !exists $$result->{id}; - my $bug_id = $$result->{id}; + return if !exists $$result->{id}; + my $bug_id = $$result->{id}; - $$result->{ref} = $rpc->type('string', ref_urlbase() . "/bug/$bug_id"); - $response->code(STATUS_CREATED); + $$result->{ref} = $rpc->type('string', ref_urlbase() . "/bug/$bug_id"); + $response->code(STATUS_CREATED); } sub get_bug_response { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return if !exists $$result->{bugs}; - my $bug_data = $$result->{bugs}->[0]; + return if !exists $$result->{bugs}; + my $bug_data = $$result->{bugs}->[0]; - my $bug_id = $rpc->bz_rest_params->{ids}->[0]; - my $bug_obj = Bugzilla::Bug->check($bug_id); - my $fixed = fix_bug($bug_data, $bug_obj); + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug_obj = Bugzilla::Bug->check($bug_id); + my $fixed = fix_bug($bug_data, $bug_obj); - $$result = $fixed; + $$result = $fixed; } sub update_bug_response { - my ($result) = @_; - return if !exists $$result->{bugs} - || !scalar @{$$result->{bugs}}; - $$result = { ok => 1 }; + my ($result) = @_; + return if !exists $$result->{bugs} || !scalar @{$$result->{bugs}}; + $$result = {ok => 1}; } # Get all comments for a bug sub get_comments_response { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; - return if !exists $$result->{bugs}; + return if !exists $$result->{bugs}; - my $bug_id = $rpc->bz_rest_params->{ids}->[0]; - my $bug = Bugzilla::Bug->check($bug_id); + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug = Bugzilla::Bug->check($bug_id); - my $comment_objs = $bug->comments({ order => 'oldest_to_newest', - after => $params->{new_since} }); - my @filtered_comment_objs; - foreach my $comment (@$comment_objs) { - next if $comment->is_private && !Bugzilla->user->is_insider; - push(@filtered_comment_objs, $comment); - } + my $comment_objs + = $bug->comments({order => 'oldest_to_newest', after => $params->{new_since} + }); + my @filtered_comment_objs; + foreach my $comment (@$comment_objs) { + next if $comment->is_private && !Bugzilla->user->is_insider; + push(@filtered_comment_objs, $comment); + } - my $comments_data = $$result->{bugs}->{$bug_id}->{comments}; + my $comments_data = $$result->{bugs}->{$bug_id}->{comments}; - my @fixed_comments; - foreach my $comment_data (@$comments_data) { - my $comment_obj = shift @filtered_comment_objs; - my $fixed = fix_comment($comment_data, $comment_obj); + my @fixed_comments; + foreach my $comment_data (@$comments_data) { + my $comment_obj = shift @filtered_comment_objs; + my $fixed = fix_comment($comment_data, $comment_obj); - if (exists $fixed->{creator}) { - # /bug//comment returns full login for creator but not for /bug/?include_fields=comments :( - $fixed->{creator}->{name} = $rpc->type('string', $comment_obj->author->login); - # /bug//comment does not return real_name for creator but returns ref - $fixed->{creator}->{'ref'} = $rpc->type('string', ref_urlbase() . "/user/" . $comment_obj->author->login); - delete $fixed->{creator}->{real_name}; - } + if (exists $fixed->{creator}) { + +# /bug//comment returns full login for creator but not for /bug/?include_fields=comments :( + $fixed->{creator}->{name} = $rpc->type('string', $comment_obj->author->login); - push(@fixed_comments, filter($params, $fixed)); + # /bug//comment does not return real_name for creator but returns ref + $fixed->{creator}->{'ref'} = $rpc->type('string', + ref_urlbase() . "/user/" . $comment_obj->author->login); + delete $fixed->{creator}->{real_name}; } - $$result = { comments => \@fixed_comments }; + push(@fixed_comments, filter($params, $fixed)); + } + + $$result = {comments => \@fixed_comments}; } # Format the return response on successful comment creation sub add_comment_response { - my ($result, $response) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return if !exists $$result->{id}; - my $bug_id = $rpc->bz_rest_params->{id}; + return if !exists $$result->{id}; + my $bug_id = $rpc->bz_rest_params->{id}; - $$result = { ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id/comment") }; - $response->code(STATUS_CREATED); + $$result + = {ref => $rpc->type('string', ref_urlbase() . "/bug/$bug_id/comment")}; + $response->code(STATUS_CREATED); } # Get the history for a bug sub get_history_response { - my ($result) = @_; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $params = Bugzilla->input_params; - return if !exists $$result->{bugs}; - my $history = $$result->{bugs}->[0]->{history}; + return if !exists $$result->{bugs}; + my $history = $$result->{bugs}->[0]->{history}; - my @new_history; - foreach my $changeset (@$history) { - $changeset = fix_changeset($changeset); - push(@new_history, filter($params, $changeset)); - } + my @new_history; + foreach my $changeset (@$history) { + $changeset = fix_changeset($changeset); + push(@new_history, filter($params, $changeset)); + } - $$result = { history => \@new_history }; + $$result = {history => \@new_history}; } # Get all attachments for a bug sub get_attachments_response { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; - - return if !exists $$result->{bugs}; - my $bug_id = $rpc->bz_rest_params->{ids}->[0]; - my $bug = Bugzilla::Bug->check($bug_id); - my $attachment_objs = $bug->attachments; - - my $attachments_data = $$result->{bugs}->{$bug_id}; - - my @fixed_attachments; - foreach my $attachment (@$attachments_data) { - my $attachment_obj = shift @$attachment_objs; - my $fixed = fix_attachment($attachment, $attachment_obj); - - if ((filter_wants_nocache($params, 'data', 'extra') - || filter_wants_nocache($params, 'encoding', 'extra') - || $params->{attachmentdata})) - { - if (!$fixed->{data}) { - $fixed->{data} = $rpc->type('base64', $attachment_obj->data); - $fixed->{encoding} = $rpc->type('string', 'base64'); - } - } - else { - delete $fixed->{data}; - delete $fixed->{encoding}; - } - - push(@fixed_attachments, filter($params, $fixed)); + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{bugs}; + my $bug_id = $rpc->bz_rest_params->{ids}->[0]; + my $bug = Bugzilla::Bug->check($bug_id); + my $attachment_objs = $bug->attachments; + + my $attachments_data = $$result->{bugs}->{$bug_id}; + + my @fixed_attachments; + foreach my $attachment (@$attachments_data) { + my $attachment_obj = shift @$attachment_objs; + my $fixed = fix_attachment($attachment, $attachment_obj); + + if (( + filter_wants_nocache($params, 'data', 'extra') + || filter_wants_nocache($params, 'encoding', 'extra') + || $params->{attachmentdata} + )) + { + if (!$fixed->{data}) { + $fixed->{data} = $rpc->type('base64', $attachment_obj->data); + $fixed->{encoding} = $rpc->type('string', 'base64'); + } + } + else { + delete $fixed->{data}; + delete $fixed->{encoding}; } - $$result = { attachments => \@fixed_attachments }; + push(@fixed_attachments, filter($params, $fixed)); + } + + $$result = {attachments => \@fixed_attachments}; } # Format the return response on successful attachment creation sub add_attachment_response { - my ($result, $response) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($result, $response) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my ($attach_id) = keys %{ $$result->{attachments} }; + my ($attach_id) = keys %{$$result->{attachments}}; - $$result = { ref => $rpc->type('string', ref_urlbase() . "/attachment/$attach_id"), id => $attach_id }; - $response->code(STATUS_CREATED); + $$result = { + ref => $rpc->type('string', ref_urlbase() . "/attachment/$attach_id"), + id => $attach_id + }; + $response->code(STATUS_CREATED); } # Update an attachment's metadata sub update_attachment_response { - my ($result) = @_; - $$result = { ok => 1 }; + my ($result) = @_; + $$result = {ok => 1}; } # Get a single attachment by attachment_id sub get_attachment_response { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; - - return if !exists $$result->{attachments}; - my $attach_id = $rpc->bz_rest_params->{attachment_ids}->[0]; - my $attachment_data = $$result->{attachments}->{$attach_id}; - my $attachment_obj = Bugzilla::Attachment->new($attach_id); - my $fixed = fix_attachment($attachment_data, $attachment_obj); - - if ((filter_wants_nocache($params, 'data', 'extra') - || filter_wants_nocache($params, 'encoding', 'extra') - || $params->{attachmentdata})) - { - if (!$fixed->{data}) { - $fixed->{data} = $rpc->type('base64', $attachment_obj->data); - $fixed->{encoding} = $rpc->type('string', 'base64'); - } - } - else { - delete $fixed->{data}; - delete $fixed->{encoding}; - } - - $fixed = filter($params, $fixed); - - $$result = $fixed; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; + + return if !exists $$result->{attachments}; + my $attach_id = $rpc->bz_rest_params->{attachment_ids}->[0]; + my $attachment_data = $$result->{attachments}->{$attach_id}; + my $attachment_obj = Bugzilla::Attachment->new($attach_id); + my $fixed = fix_attachment($attachment_data, $attachment_obj); + + if (( + filter_wants_nocache($params, 'data', 'extra') + || filter_wants_nocache($params, 'encoding', 'extra') + || $params->{attachmentdata} + )) + { + if (!$fixed->{data}) { + $fixed->{data} = $rpc->type('base64', $attachment_obj->data); + $fixed->{encoding} = $rpc->type('string', 'base64'); + } + } + else { + delete $fixed->{data}; + delete $fixed->{encoding}; + } + + $fixed = filter($params, $fixed); + + $$result = $fixed; } # Get a list of flags for a bug sub get_bug_flags_response { - my ($result) = @_; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $params = Bugzilla->input_params; - return if !exists $$result->{bugs}; - my $flags = $$result->{bugs}->[0]->{flags}; + return if !exists $$result->{bugs}; + my $flags = $$result->{bugs}->[0]->{flags}; - my @new_flags; - foreach my $flag (@$flags) { - push(@new_flags, fix_flag($flag)); - } + my @new_flags; + foreach my $flag (@$flags) { + push(@new_flags, fix_flag($flag)); + } - $$result = { flags => \@new_flags }; + $$result = {flags => \@new_flags}; } 1; diff --git a/extensions/BzAPI/lib/Resources/Bugzilla.pm b/extensions/BzAPI/lib/Resources/Bugzilla.pm index 6e350d839..23d423d5a 100644 --- a/extensions/BzAPI/lib/Resources/Bugzilla.pm +++ b/extensions/BzAPI/lib/Resources/Bugzilla.pm @@ -29,124 +29,118 @@ use Digest::MD5 qw(md5_base64); ######################### BEGIN { - require Bugzilla::WebService::Bugzilla; - *Bugzilla::WebService::Bugzilla::get_configuration = \&get_configuration; - *Bugzilla::WebService::Bugzilla::get_empty = \&get_empty; + require Bugzilla::WebService::Bugzilla; + *Bugzilla::WebService::Bugzilla::get_configuration = \&get_configuration; + *Bugzilla::WebService::Bugzilla::get_empty = \&get_empty; } sub rest_handlers { - my $rest_handlers = [ - qr{^/$}, { - GET => { - resource => { - method => 'get_empty' - } - } - }, - qr{^/configuration$}, { - GET => { - resource => { - method => 'get_configuration' - } - } - } - ]; - return $rest_handlers; + my $rest_handlers = [ + qr{^/$}, {GET => {resource => {method => 'get_empty'}}}, + qr{^/configuration$}, {GET => {resource => {method => 'get_configuration'}}} + ]; + return $rest_handlers; } sub get_configuration { - my ($self) = @_; - my $user = Bugzilla->user; - my $params = Bugzilla->input_params; - - my $can_cache = !exists $params->{product} && !exists $params->{flags}; - my $cache_key = 'bzapi_get_configuration'; - - if ($can_cache) { - my $result = Bugzilla->memcached->get_config({key => $cache_key}); - return $result if defined $result; - } - - # Get data from the shadow DB as they don't change very often. - Bugzilla->switch_to_shadow_db; - - # Pass a bunch of Bugzilla configuration to the templates. - my $vars = {}; - $vars->{'priority'} = get_legal_field_values('priority'); - $vars->{'severity'} = get_legal_field_values('bug_severity'); - $vars->{'platform'} = get_legal_field_values('rep_platform'); - $vars->{'op_sys'} = get_legal_field_values('op_sys'); - $vars->{'keyword'} = [ map($_->name, Bugzilla::Keyword->get_all) ]; - $vars->{'resolution'} = get_legal_field_values('resolution'); - $vars->{'status'} = get_legal_field_values('bug_status'); - $vars->{'custom_fields'} = - [ grep {$_->is_select} Bugzilla->active_custom_fields ]; - - # Include a list of product objects. - if ($params->{'product'}) { - my @products = $params->{'product'}; - foreach my $product_name (@products) { - my $product = new Bugzilla::Product({ name => $product_name }); - if ($product && $user->can_see_product($product->name)) { - push (@{$vars->{'products'}}, $product); - } - } - } else { - $vars->{'products'} = $user->get_selectable_products; - } - - # We set the 2nd argument to 1 to also preload flag types. - Bugzilla::Product::preload($vars->{'products'}, 1, { is_active => 1 }); - - # Allow consumers to specify whether or not they want flag data. - if (defined $params->{'flags'}) { - $vars->{'show_flags'} = $params->{'flags'}; - } - else { - # We default to sending flag data. - $vars->{'show_flags'} = 1; - } - - # Create separate lists of open versus resolved statuses. This should really - # be made part of the configuration. - my @open_status; - my @closed_status; - foreach my $status (@{$vars->{'status'}}) { - is_open_state($status) ? push(@open_status, $status) - : push(@closed_status, $status); + my ($self) = @_; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; + + my $can_cache = !exists $params->{product} && !exists $params->{flags}; + my $cache_key = 'bzapi_get_configuration'; + + if ($can_cache) { + my $result = Bugzilla->memcached->get_config({key => $cache_key}); + return $result if defined $result; + } + + # Get data from the shadow DB as they don't change very often. + Bugzilla->switch_to_shadow_db; + + # Pass a bunch of Bugzilla configuration to the templates. + my $vars = {}; + $vars->{'priority'} = get_legal_field_values('priority'); + $vars->{'severity'} = get_legal_field_values('bug_severity'); + $vars->{'platform'} = get_legal_field_values('rep_platform'); + $vars->{'op_sys'} = get_legal_field_values('op_sys'); + $vars->{'keyword'} = [map($_->name, Bugzilla::Keyword->get_all)]; + $vars->{'resolution'} = get_legal_field_values('resolution'); + $vars->{'status'} = get_legal_field_values('bug_status'); + $vars->{'custom_fields'} + = [grep { $_->is_select } Bugzilla->active_custom_fields]; + + # Include a list of product objects. + if ($params->{'product'}) { + my @products = $params->{'product'}; + foreach my $product_name (@products) { + my $product = new Bugzilla::Product({name => $product_name}); + if ($product && $user->can_see_product($product->name)) { + push(@{$vars->{'products'}}, $product); + } } - $vars->{'open_status'} = \@open_status; - $vars->{'closed_status'} = \@closed_status; - - # Generate a list of fields that can be queried. - my @fields = @{Bugzilla::Field->match({obsolete => 0})}; - # Exclude fields the user cannot query. - if (!Bugzilla->user->is_timetracker) { - @fields = grep { $_->name !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ } @fields; - } - $vars->{'field'} = \@fields; - - my $json; - Bugzilla->template->process('config.json.tmpl', $vars, \$json); - if ($json) { - my $result = $self->json->decode($json); - if ($can_cache) { - Bugzilla->memcached->set_config({key => $cache_key, data => $result}); - } - return $result; - } - else { - return {}; + } + else { + $vars->{'products'} = $user->get_selectable_products; + } + + # We set the 2nd argument to 1 to also preload flag types. + Bugzilla::Product::preload($vars->{'products'}, 1, {is_active => 1}); + + # Allow consumers to specify whether or not they want flag data. + if (defined $params->{'flags'}) { + $vars->{'show_flags'} = $params->{'flags'}; + } + else { + # We default to sending flag data. + $vars->{'show_flags'} = 1; + } + + # Create separate lists of open versus resolved statuses. This should really + # be made part of the configuration. + my @open_status; + my @closed_status; + foreach my $status (@{$vars->{'status'}}) { + is_open_state($status) + ? push(@open_status, $status) + : push(@closed_status, $status); + } + $vars->{'open_status'} = \@open_status; + $vars->{'closed_status'} = \@closed_status; + + # Generate a list of fields that can be queried. + my @fields = @{Bugzilla::Field->match({obsolete => 0})}; + + # Exclude fields the user cannot query. + if (!Bugzilla->user->is_timetracker) { + @fields = grep { + $_->name + !~ /^(estimated_time|remaining_time|work_time|percentage_complete|deadline)$/ + } @fields; + } + $vars->{'field'} = \@fields; + + my $json; + Bugzilla->template->process('config.json.tmpl', $vars, \$json); + if ($json) { + my $result = $self->json->decode($json); + if ($can_cache) { + Bugzilla->memcached->set_config({key => $cache_key, data => $result}); } + return $result; + } + else { + return {}; + } } sub get_empty { - my ($self) = @_; - return { - ref => $self->type('string', Bugzilla->localconfig->{urlbase} . "bzapi/"), - documentation => $self->type('string', BZAPI_DOC), - version => $self->type('string', BUGZILLA_VERSION) - }; + my ($self) = @_; + return { + ref => $self->type('string', Bugzilla->localconfig->{urlbase} . "bzapi/"), + documentation => $self->type('string', BZAPI_DOC), + version => $self->type('string', BUGZILLA_VERSION) + }; } 1; diff --git a/extensions/BzAPI/lib/Resources/User.pm b/extensions/BzAPI/lib/Resources/User.pm index 550a61d28..7a7a183a9 100644 --- a/extensions/BzAPI/lib/Resources/User.pm +++ b/extensions/BzAPI/lib/Resources/User.pm @@ -14,67 +14,58 @@ use warnings; use Bugzilla::Extension::BzAPI::Util; sub rest_handlers { - my $rest_handlers = [ - qr{/user$}, { - GET => { - response => \&get_users, - }, - }, - qr{/user/([^/]+)$}, { - GET => { - response => \&get_user, - }, - } - ]; - return $rest_handlers; + my $rest_handlers = [ + qr{/user$}, {GET => {response => \&get_users,},}, + qr{/user/([^/]+)$}, {GET => {response => \&get_user,},} + ]; + return $rest_handlers; } sub get_users { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; - return if !exists $$result->{users}; + return if !exists $$result->{users}; - my @users; - foreach my $user (@{$$result->{users}}) { - my $object = Bugzilla::User->new( - { id => $user->{id}, cache => 1 }); + my @users; + foreach my $user (@{$$result->{users}}) { + my $object = Bugzilla::User->new({id => $user->{id}, cache => 1}); - $user = fix_user($user, $object); + $user = fix_user($user, $object); - # Use userid instead of email for 'ref' for /user calls - $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); + # Use userid instead of email for 'ref' for /user calls + $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); - # Emails are not filtered even if user is not logged in - $user->{name} = $rpc->type('string', $object->login); + # Emails are not filtered even if user is not logged in + $user->{name} = $rpc->type('string', $object->login); - push(@users, filter($params, $user)); - } + push(@users, filter($params, $user)); + } - $$result->{users} = \@users; + $$result->{users} = \@users; } sub get_user { - my ($result) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $params = Bugzilla->input_params; + my ($result) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $params = Bugzilla->input_params; - return if !exists $$result->{users}; - my $user = $$result->{users}->[0] || return; - my $object = Bugzilla::User->new({ id => $user->{id}, cache => 1 }); + return if !exists $$result->{users}; + my $user = $$result->{users}->[0] || return; + my $object = Bugzilla::User->new({id => $user->{id}, cache => 1}); - $user = fix_user($user, $object); + $user = fix_user($user, $object); - # Use userid instead of email for 'ref' for /user calls - $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); + # Use userid instead of email for 'ref' for /user calls + $user->{'ref'} = $rpc->type('string', ref_urlbase . "/user/" . $object->id); - # Emails are not filtered even if user is not logged in - $user->{name} = $rpc->type('string', $object->login); + # Emails are not filtered even if user is not logged in + $user->{name} = $rpc->type('string', $object->login); - $user = filter($params, $user); + $user = filter($params, $user); - $$result = $user; + $$result = $user; } 1; diff --git a/extensions/BzAPI/lib/Util.pm b/extensions/BzAPI/lib/Util.pm index d50679a6c..6f7e2c025 100644 --- a/extensions/BzAPI/lib/Util.pm +++ b/extensions/BzAPI/lib/Util.pm @@ -28,425 +28,437 @@ use MIME::Base64; use base qw(Exporter); our @EXPORT = qw( - ref_urlbase - fix_bug - fix_user - fix_flag - fix_comment - fix_changeset - fix_attachment - filter_wants_nocache - filter - fix_credentials - filter_email + ref_urlbase + fix_bug + fix_user + fix_flag + fix_comment + fix_changeset + fix_attachment + filter_wants_nocache + filter + fix_credentials + filter_email ); # Return an URL base appropriate for constructing a ref link # normally required by REST API calls. sub ref_urlbase { - return Bugzilla->localconfig->{urlbase} . "bzapi"; + return Bugzilla->localconfig->{urlbase} . "bzapi"; } # convert certain fields within a bug object # from a simple scalar value to their respective objects sub fix_bug { - my ($data, $bug, $stash) = @_; - my $dbh = $stash->{dbh} //= Bugzilla->dbh; - my $params = $stash->{params} //= Bugzilla->input_params; - my $rpc = $stash->{rpc} //= Bugzilla->request_cache->{bzapi_rpc}; - my $method = $stash->{method} //= Bugzilla->request_cache->{bzapi_rpc_method}; - - $bug = ref $bug ? $bug : Bugzilla::Bug->check($bug || $data->{id}); - - # Add REST API reference to the individual bug - if ($stash->{wants_ref} //= filter_wants_nocache($params, 'ref')) { - $data->{'ref'} = ref_urlbase() . "/bug/" . $bug->id; + my ($data, $bug, $stash) = @_; + my $dbh = $stash->{dbh} //= Bugzilla->dbh; + my $params = $stash->{params} //= Bugzilla->input_params; + my $rpc = $stash->{rpc} //= Bugzilla->request_cache->{bzapi_rpc}; + my $method = $stash->{method} //= Bugzilla->request_cache->{bzapi_rpc_method}; + + $bug = ref $bug ? $bug : Bugzilla::Bug->check($bug || $data->{id}); + + # Add REST API reference to the individual bug + if ($stash->{wants_ref} //= filter_wants_nocache($params, 'ref')) { + $data->{'ref'} = ref_urlbase() . "/bug/" . $bug->id; + } + + # User fields + foreach my $field (USER_FIELDS) { + next if !exists $data->{$field}; + if ($field eq 'cc') { + my @new_cc; + foreach my $cc (@{$bug->cc_users}) { + my $cc_data = {name => filter_email($cc->email)}; + push(@new_cc, fix_user($cc_data, $cc)); + } + $data->{$field} = \@new_cc; } - - # User fields - foreach my $field (USER_FIELDS) { - next if !exists $data->{$field}; - if ($field eq 'cc') { - my @new_cc; - foreach my $cc (@{ $bug->cc_users }) { - my $cc_data = { name => filter_email($cc->email) }; - push(@new_cc, fix_user($cc_data, $cc)); - } - $data->{$field} = \@new_cc; - } - else { - my $field_name = $field; - if ($field eq 'creator') { - $field_name = 'reporter'; - } - $data->{$field} - = fix_user($data->{"${field}_detail"}, $bug->$field_name); - delete $data->{$field}->{id}; - delete $data->{$field}->{email}; - $data->{$field} = filter($params, $data->{$field}, undef, $field); - } - - # Get rid of extra detail hash if exists since redundant - delete $data->{"${field}_detail"} if exists $data->{"${field}_detail"}; + else { + my $field_name = $field; + if ($field eq 'creator') { + $field_name = 'reporter'; + } + $data->{$field} = fix_user($data->{"${field}_detail"}, $bug->$field_name); + delete $data->{$field}->{id}; + delete $data->{$field}->{email}; + $data->{$field} = filter($params, $data->{$field}, undef, $field); } - # Groups - if ($stash->{wants_groups} //= filter_wants_nocache($params, 'groups')) { - my @new_groups; - foreach my $group (@{ $data->{groups} }) { - if (my $object = Bugzilla::Group->new({ name => $group, cache => 1 })) { - $group = { - id => $rpc->type('int', $object->id), - name => $rpc->type('string', $object->name), - }; - } - push(@new_groups, $group); - } - $data->{groups} = \@new_groups; + # Get rid of extra detail hash if exists since redundant + delete $data->{"${field}_detail"} if exists $data->{"${field}_detail"}; + } + + # Groups + if ($stash->{wants_groups} //= filter_wants_nocache($params, 'groups')) { + my @new_groups; + foreach my $group (@{$data->{groups}}) { + if (my $object = Bugzilla::Group->new({name => $group, cache => 1})) { + $group = { + id => $rpc->type('int', $object->id), + name => $rpc->type('string', $object->name), + }; + } + push(@new_groups, $group); } - - # Flags - if (exists $data->{flags}) { - my @new_flags; - foreach my $flag (@{ $data->{flags} }) { - push(@new_flags, fix_flag($flag)); - } - $data->{flags} = \@new_flags; + $data->{groups} = \@new_groups; + } + + # Flags + if (exists $data->{flags}) { + my @new_flags; + foreach my $flag (@{$data->{flags}}) { + push(@new_flags, fix_flag($flag)); } - - # Attachment metadata is included by default but not data - if ($stash->{wants_attachments} //= filter_wants_nocache($params, 'attachments')) { - my $attachment_params = { ids => $bug->id }; - if (!filter_wants_nocache($params, 'data', 'extra', 'attachments') - && !$params->{attachmentdata}) - { - $attachment_params->{exclude_fields} = ['data']; - } - - my $attachments = $rpc->attachments($attachment_params); - - my @fixed_attachments; - foreach my $attachment (@{ $attachments->{bugs}->{$bug->id} }) { - my $fixed = fix_attachment($attachment); - push(@fixed_attachments, filter($params, $fixed, undef, 'attachments')); - } - - $data->{attachments} = \@fixed_attachments; + $data->{flags} = \@new_flags; + } + + # Attachment metadata is included by default but not data + if ($stash->{wants_attachments} + //= filter_wants_nocache($params, 'attachments')) + { + my $attachment_params = {ids => $bug->id}; + if ( !filter_wants_nocache($params, 'data', 'extra', 'attachments') + && !$params->{attachmentdata}) + { + $attachment_params->{exclude_fields} = ['data']; } - # Comments and history are not part of _default and have to be requested + my $attachments = $rpc->attachments($attachment_params); - # Comments - if ($stash->{wants_comments} //= filter_wants_nocache($params, 'comments', 'extra', 'comments')) { - my $comments = $rpc->comments({ ids => $bug->id }); - $comments = $comments->{bugs}->{$bug->id}->{comments}; - my @new_comments; - foreach my $comment (@$comments) { - $comment = fix_comment($comment); - push(@new_comments, filter($params, $comment, 'extra', 'comments')); - } - $data->{comments} = \@new_comments; + my @fixed_attachments; + foreach my $attachment (@{$attachments->{bugs}->{$bug->id}}) { + my $fixed = fix_attachment($attachment); + push(@fixed_attachments, filter($params, $fixed, undef, 'attachments')); } - # History - if ($stash->{wants_history} //= filter_wants_nocache($params, 'history', 'extra', 'history')) { - my $history = $rpc->history({ ids => [ $bug->id ] }); - my @new_history; - foreach my $changeset (@{ $history->{bugs}->[0]->{history} }) { - push(@new_history, fix_changeset($changeset, $bug)); - } - $data->{history} = \@new_history; + $data->{attachments} = \@fixed_attachments; + } + + # Comments and history are not part of _default and have to be requested + + # Comments + if ($stash->{wants_comments} + //= filter_wants_nocache($params, 'comments', 'extra', 'comments')) + { + my $comments = $rpc->comments({ids => $bug->id}); + $comments = $comments->{bugs}->{$bug->id}->{comments}; + my @new_comments; + foreach my $comment (@$comments) { + $comment = fix_comment($comment); + push(@new_comments, filter($params, $comment, 'extra', 'comments')); + } + $data->{comments} = \@new_comments; + } + + # History + if ($stash->{wants_history} + //= filter_wants_nocache($params, 'history', 'extra', 'history')) + { + my $history = $rpc->history({ids => [$bug->id]}); + my @new_history; + foreach my $changeset (@{$history->{bugs}->[0]->{history}}) { + push(@new_history, fix_changeset($changeset, $bug)); + } + $data->{history} = \@new_history; + } + + # Add in all custom fields even if not set or visible on this bug + my $custom_fields = $stash->{custom_fields} + //= Bugzilla->fields({custom => 1, obsolete => 0, by_name => 1}); + foreach my $field (values %$custom_fields) { + my $name = $field->name; + my $type = $field->type; + if (!filter_wants_nocache($params, $name, ['default', 'custom'])) { + delete $custom_fields->{$name}; + next; + } + if ($type == FIELD_TYPE_BUG_ID) { + $data->{$name} = $rpc->type('int', $bug->$name); + } + elsif ($type == FIELD_TYPE_DATETIME || $type == FIELD_TYPE_DATE) { + $data->{$name} = $rpc->type('dateTime', $bug->$name); } + elsif ($type == FIELD_TYPE_MULTI_SELECT) { + +# Bug.search, when include_fields=_all, returns array, otherwise return as comma delimited string :( + if ($method eq 'Bug.search' + && !grep($_ eq '_all', @{$params->{include_fields}})) + { + $data->{$name} = $rpc->type('string', join(', ', @{$bug->$name})); + } + else { + my @values = map { $rpc->type('string', $_) } @{$bug->$name}; + $data->{$name} = \@values; + } + } + else { + $data->{$name} = $rpc->type('string', $bug->$name); + } + } - # Add in all custom fields even if not set or visible on this bug - my $custom_fields = $stash->{custom_fields} //= - Bugzilla->fields({ custom => 1, obsolete => 0, by_name => 1 }); - foreach my $field (values %$custom_fields) { - my $name = $field->name; - my $type = $field->type; - if (!filter_wants_nocache($params, $name, ['default','custom'])) { - delete $custom_fields->{$name}; - next; - } - if ($type == FIELD_TYPE_BUG_ID) { - $data->{$name} = $rpc->type('int', $bug->$name); - } - elsif ($type == FIELD_TYPE_DATETIME - || $type == FIELD_TYPE_DATE) - { - $data->{$name} = $rpc->type('dateTime', $bug->$name); - } - elsif ($type == FIELD_TYPE_MULTI_SELECT) { - # Bug.search, when include_fields=_all, returns array, otherwise return as comma delimited string :( - if ($method eq 'Bug.search' && !grep($_ eq '_all', @{ $params->{include_fields} })) { - $data->{$name} = $rpc->type('string', join(', ', @{ $bug->$name })); - } - else { - my @values = map { $rpc->type('string', $_) } @{ $bug->$name }; - $data->{$name} = \@values; - } - } - else { - $data->{$name} = $rpc->type('string', $bug->$name); - } + # Remove empty values in some cases + foreach my $key (keys %$data) { + + # QA Contact is null if single bug or "" if doing search + if ($key eq 'qa_contact' && !$data->{$key}->{name}) { + if ($method eq 'Bug.search') { + $data->{$key}->{name} = $rpc->type('string', ''); + } + next; } - # Remove empty values in some cases - foreach my $key (keys %$data) { - # QA Contact is null if single bug or "" if doing search - if ($key eq 'qa_contact' && !$data->{$key}->{name}) { - if ($method eq 'Bug.search') { - $data->{$key}->{name} = $rpc->type('string', ''); - } - next; - } + next if $method eq 'Bug.search' && $key eq 'url'; # Return url even if empty + next if $method eq 'Bug.search' && $key eq 'keywords'; # Return keywords even if empty + next if $method eq 'Bug.search' && $key eq 'whiteboard'; # Return whiteboard even if empty + next if $method eq 'Bug.get' && grep($_ eq $key, TIMETRACKING_FIELDS); - next if $method eq 'Bug.search' && $key eq 'url'; # Return url even if empty - next if $method eq 'Bug.search' && $key eq 'keywords'; # Return keywords even if empty - next if $method eq 'Bug.search' && $key eq 'whiteboard'; # Return whiteboard even if empty - next if $method eq 'Bug.get' && grep($_ eq $key, TIMETRACKING_FIELDS); + next + if ($method eq 'Bug.search' + && $key =~ /^(resolution|cc_count|dupe_count)$/ + && !grep($_ eq '_all', @{$params->{include_fields}})); - next if ($method eq 'Bug.search' - && $key =~ /^(resolution|cc_count|dupe_count)$/ - && !grep($_ eq '_all', @{ $params->{include_fields} })); + if (!ref $data->{$key}) { + delete $data->{$key} if !$data->{$key}; + } + else { + if (ref $data->{$key} eq 'ARRAY' && !@{$data->{$key}}) { - if (!ref $data->{$key}) { - delete $data->{$key} if !$data->{$key}; + # Return empty string if blocks or depends_on is empty + if ($method eq 'Bug.search' && ($key eq 'depends_on' || $key eq 'blocks')) { + $data->{$key} = ''; } else { - if (ref $data->{$key} eq 'ARRAY' && !@{$data->{$key}}) { - # Return empty string if blocks or depends_on is empty - if ($method eq 'Bug.search' && ($key eq 'depends_on' || $key eq 'blocks')) { - $data->{$key} = ''; - } - else { - delete $data->{$key}; - } - } - elsif (ref $data->{$key} eq 'HASH' && !%{$data->{$key}}) { - delete $data->{$key}; - } + delete $data->{$key}; } + } + elsif (ref $data->{$key} eq 'HASH' && !%{$data->{$key}}) { + delete $data->{$key}; + } } + } - return $data; + return $data; } # convert a user related field from being just login # names to user objects sub fix_user { - my ($data, $object) = @_; - my $user = Bugzilla->user; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($data, $object) = @_; + my $user = Bugzilla->user; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return { name => undef } if !$data; + return {name => undef} if !$data; - if (!ref $data) { - $data = { - name => filter_email($object->login) - }; - $data->{real_name} = $rpc->type('string', $object->name); - } - else { - $data->{name} = filter_email($data->{name}); - } + if (!ref $data) { + $data = {name => filter_email($object->login)}; + $data->{real_name} = $rpc->type('string', $object->name); + } + else { + $data->{name} = filter_email($data->{name}); + } - if ($user->id) { - $data->{ref} = $rpc->type('string', ref_urlbase . "/user/" . $object->login); - } + if ($user->id) { + $data->{ref} = $rpc->type('string', ref_urlbase . "/user/" . $object->login); + } - return $data; + return $data; } # convert certain attributes of a comment to objects # and also remove other unwanted key/values. sub fix_comment { - my ($data, $object) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $method = Bugzilla->request_cache->{bzapi_rpc_method}; + my ($data, $object) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $method = Bugzilla->request_cache->{bzapi_rpc_method}; - $object ||= Bugzilla::Comment->new({ id => $data->{id}, cache => 1 }); + $object ||= Bugzilla::Comment->new({id => $data->{id}, cache => 1}); - if (exists $data->{creator}) { - $data->{creator} = fix_user($data->{creator}, $object->author); - } + if (exists $data->{creator}) { + $data->{creator} = fix_user($data->{creator}, $object->author); + } - if ($data->{attachment_id} && $method ne 'Bug.search') { - $data->{attachment_ref} = $rpc->type('string', ref_urlbase() . - "/attachment/" . $object->extra_data); - } - else { - delete $data->{attachment_id}; - } + if ($data->{attachment_id} && $method ne 'Bug.search') { + $data->{attachment_ref} + = $rpc->type('string', ref_urlbase() . "/attachment/" . $object->extra_data); + } + else { + delete $data->{attachment_id}; + } - delete $data->{author}; - delete $data->{time}; - delete $data->{raw_text}; + delete $data->{author}; + delete $data->{time}; + delete $data->{raw_text}; - return $data; + return $data; } # convert certain attributes of a changeset object from # scalar values to related objects. Also remove other unwanted # key/values. sub fix_changeset { - my ($data, $object) = @_; - my $user = Bugzilla->user; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - - if ($data->{who}) { - $data->{changer} = { - name => $rpc->type('string', $data->{who}), - ref => $rpc->type('string', ref_urlbase() . "/user/" . $data->{who}) - }; - delete $data->{who}; - } - - if ($data->{when}) { - $data->{change_time} = $rpc->type('dateTime', $data->{when}); - delete $data->{when}; - } - - foreach my $change (@{ $data->{changes} }) { - $change->{field_name} = 'flag' if $change->{field_name} eq 'flagtypes.name'; - } - - return $data; + my ($data, $object) = @_; + my $user = Bugzilla->user; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + + if ($data->{who}) { + $data->{changer} = { + name => $rpc->type('string', $data->{who}), + ref => $rpc->type('string', ref_urlbase() . "/user/" . $data->{who}) + }; + delete $data->{who}; + } + + if ($data->{when}) { + $data->{change_time} = $rpc->type('dateTime', $data->{when}); + delete $data->{when}; + } + + foreach my $change (@{$data->{changes}}) { + $change->{field_name} = 'flag' if $change->{field_name} eq 'flagtypes.name'; + } + + return $data; } # convert certain attributes of an attachment object from # scalar values to related objects. Also add in additional # key/values. sub fix_attachment { - my ($data, $object) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - my $method = Bugzilla->request_cache->{bzapi_rpc_method}; - my $params = Bugzilla->input_params; - my $user = Bugzilla->user; - - $object ||= Bugzilla::Attachment->new({ id => $data->{id}, cache => 1 }); - - if (exists $data->{attacher}) { - $data->{attacher} = fix_user($data->{attacher}, $object->attacher); - if ($method eq 'Bug.search') { - delete $data->{attacher}->{real_name}; - } - else { - $data->{attacher}->{real_name} = $rpc->type('string', $object->attacher->name); - } + my ($data, $object) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my $method = Bugzilla->request_cache->{bzapi_rpc_method}; + my $params = Bugzilla->input_params; + my $user = Bugzilla->user; + + $object ||= Bugzilla::Attachment->new({id => $data->{id}, cache => 1}); + + if (exists $data->{attacher}) { + $data->{attacher} = fix_user($data->{attacher}, $object->attacher); + if ($method eq 'Bug.search') { + delete $data->{attacher}->{real_name}; } - - if (exists $data->{data}) { - $data->{encoding} = $rpc->type('string', 'base64'); - if ($params->{attachmentdata} - || filter_wants_nocache($params, 'attachments.data')) - { - $data->{encoding} = $rpc->type('string', 'base64'); - } - else { - delete $data->{data}; - } - } - - if (exists $data->{bug_id}) { - $data->{bug_ref} = $rpc->type('string', ref_urlbase() . "/bug/" . $object->bug_id); - } - - # Upstream API returns these as integers where bzapi returns as booleans - if (exists $data->{is_patch}) { - $data->{is_patch} = $rpc->type('boolean', $data->{is_patch}); - } - if (exists $data->{is_obsolete}) { - $data->{is_obsolete} = $rpc->type('boolean', $data->{is_obsolete}); - } - if (exists $data->{is_private}) { - $data->{is_private} = $rpc->type('boolean', $data->{is_private}); + else { + $data->{attacher}->{real_name} = $rpc->type('string', $object->attacher->name); } - - if (exists $data->{flags} && @{ $data->{flags} }) { - my @new_flags; - foreach my $flag (@{ $data->{flags} }) { - push(@new_flags, fix_flag($flag)); - } - $data->{flags} = \@new_flags; + } + + if (exists $data->{data}) { + $data->{encoding} = $rpc->type('string', 'base64'); + if ($params->{attachmentdata} + || filter_wants_nocache($params, 'attachments.data')) + { + $data->{encoding} = $rpc->type('string', 'base64'); } else { - delete $data->{flags}; + delete $data->{data}; } + } + + if (exists $data->{bug_id}) { + $data->{bug_ref} + = $rpc->type('string', ref_urlbase() . "/bug/" . $object->bug_id); + } + + # Upstream API returns these as integers where bzapi returns as booleans + if (exists $data->{is_patch}) { + $data->{is_patch} = $rpc->type('boolean', $data->{is_patch}); + } + if (exists $data->{is_obsolete}) { + $data->{is_obsolete} = $rpc->type('boolean', $data->{is_obsolete}); + } + if (exists $data->{is_private}) { + $data->{is_private} = $rpc->type('boolean', $data->{is_private}); + } + + if (exists $data->{flags} && @{$data->{flags}}) { + my @new_flags; + foreach my $flag (@{$data->{flags}}) { + push(@new_flags, fix_flag($flag)); + } + $data->{flags} = \@new_flags; + } + else { + delete $data->{flags}; + } - $data->{ref} = $rpc->type('string', ref_urlbase() . "/attachment/" . $object->id); + $data->{ref} + = $rpc->type('string', ref_urlbase() . "/attachment/" . $object->id); - # Add update token if we are getting an attachment outside of Bug.get and user is logged in - if ($user->id && ($method eq 'Bug.attachments'|| $method eq 'Bug.search')) { - $data->{update_token} = issue_hash_token([ $object->id, $object->modification_time ]); - } +# Add update token if we are getting an attachment outside of Bug.get and user is logged in + if ($user->id && ($method eq 'Bug.attachments' || $method eq 'Bug.search')) { + $data->{update_token} + = issue_hash_token([$object->id, $object->modification_time]); + } - delete $data->{creator}; - delete $data->{summary}; + delete $data->{creator}; + delete $data->{summary}; - return $data; + return $data; } # convert certain attributes of a flag object from # scalar values to related objects. Also remove other unwanted # key/values. sub fix_flag { - my ($data, $object) = @_; - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + my ($data, $object) = @_; + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - $object ||= Bugzilla::Flag->new({ id => $data->{id}, cache => 1 }); + $object ||= Bugzilla::Flag->new({id => $data->{id}, cache => 1}); - if (exists $data->{setter}) { - $data->{setter} = fix_user($data->{setter}, $object->setter); - delete $data->{setter}->{real_name}; - } + if (exists $data->{setter}) { + $data->{setter} = fix_user($data->{setter}, $object->setter); + delete $data->{setter}->{real_name}; + } - if (exists $data->{requestee}) { - $data->{requestee} = fix_user($data->{requestee}, $object->requestee); - delete $data->{requestee}->{real_name}; - } + if (exists $data->{requestee}) { + $data->{requestee} = fix_user($data->{requestee}, $object->requestee); + delete $data->{requestee}->{real_name}; + } - return $data; + return $data; } # Calls Bugzilla::WebService::Util::filter_wants but disables caching # as we make several webservice calls in a single REST call and the # caching can cause unexpected results. sub filter_wants_nocache { - my ($params, $field, $types, $prefix) = @_; - delete Bugzilla->request_cache->{filter_wants}; - return filter_wants($params, $field, $types, $prefix); + my ($params, $field, $types, $prefix) = @_; + delete Bugzilla->request_cache->{filter_wants}; + return filter_wants($params, $field, $types, $prefix); } sub filter { - my ($params, $hash, $types, $prefix) = @_; - my %newhash = %$hash; - foreach my $key (keys %$hash) { - delete $newhash{$key} if !filter_wants_nocache($params, $key, $types, $prefix); - } - return \%newhash; + my ($params, $hash, $types, $prefix) = @_; + my %newhash = %$hash; + foreach my $key (keys %$hash) { + delete $newhash{$key} if !filter_wants_nocache($params, $key, $types, $prefix); + } + return \%newhash; } sub fix_credentials { - my ($params) = @_; - # Allow user to pass in username=foo&password=bar to be compatible - $params->{'Bugzilla_login'} = $params->{'login'} = delete $params->{'username'} - if exists $params->{'username'}; - $params->{'Bugzilla_password'} = $params->{'password'} if exists $params->{'password'}; - - # Allow user to pass userid=1&cookie=3iYGuKZdyz for compatibility with BzAPI - if (exists $params->{'userid'} && exists $params->{'cookie'}) { - my $userid = delete $params->{'userid'}; - my $cookie = delete $params->{'cookie'}; - $params->{'Bugzilla_token'} = "${userid}-${cookie}"; - } + my ($params) = @_; + + # Allow user to pass in username=foo&password=bar to be compatible + $params->{'Bugzilla_login'} = $params->{'login'} = delete $params->{'username'} + if exists $params->{'username'}; + $params->{'Bugzilla_password'} = $params->{'password'} + if exists $params->{'password'}; + + # Allow user to pass userid=1&cookie=3iYGuKZdyz for compatibility with BzAPI + if (exists $params->{'userid'} && exists $params->{'cookie'}) { + my $userid = delete $params->{'userid'}; + my $cookie = delete $params->{'cookie'}; + $params->{'Bugzilla_token'} = "${userid}-${cookie}"; + } } # Filter email addresses by default ignoring the system # webservice_email_filter setting sub filter_email { - my $rpc = Bugzilla->request_cache->{bzapi_rpc}; - return $rpc->type('string', email_filter($_[0])); + my $rpc = Bugzilla->request_cache->{bzapi_rpc}; + return $rpc->type('string', email_filter($_[0])); } 1; diff --git a/extensions/ComponentWatching/Extension.pm b/extensions/ComponentWatching/Extension.pm index fdeedff98..654094191 100644 --- a/extensions/ComponentWatching/Extension.pm +++ b/extensions/ComponentWatching/Extension.pm @@ -32,92 +32,58 @@ use constant REL_COMPONENT_WATCHER => 15; # sub db_schema_abstract_schema { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - # Bugzilla 5.0+, the components.id type - # is INT3, while earlier versions used INT2 - my $component_id_type = 'INT2'; - my $len = scalar @{ $args->{schema}->{components}->{FIELDS} }; - for (my $i = 0; $i < $len - 1; $i+=2) { - next if $args->{schema}->{components}->{FIELDS}->[$i] ne 'id'; - $component_id_type = 'INT3' - if $args->{schema}->{components}->{FIELDS}->[$i+1]->{TYPE} eq 'MEDIUMSERIAL'; - last; - } - $args->{'schema'}->{'component_watch'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - component_id => { - TYPE => $component_id_type, - NOTNULL => 0, - REFERENCES => { - TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - product_id => { - TYPE => 'INT2', - NOTNULL => 0, - REFERENCES => { - TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - component_prefix => { - TYPE => 'VARCHAR(64)', - NOTNULL => 0, - }, - ], - }; + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + # Bugzilla 5.0+, the components.id type + # is INT3, while earlier versions used INT2 + my $component_id_type = 'INT2'; + my $len = scalar @{$args->{schema}->{components}->{FIELDS}}; + for (my $i = 0; $i < $len - 1; $i += 2) { + next if $args->{schema}->{components}->{FIELDS}->[$i] ne 'id'; + $component_id_type = 'INT3' + if $args->{schema}->{components}->{FIELDS}->[$i + 1]->{TYPE} eq + 'MEDIUMSERIAL'; + last; + } + $args->{'schema'}->{'component_watch'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + component_id => { + TYPE => $component_id_type, + NOTNULL => 0, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE',} + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE',} + }, + component_prefix => {TYPE => 'VARCHAR(64)', NOTNULL => 0,}, + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column( - 'components', - 'watch_user', - { - TYPE => 'INT3', - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL', - } - } - ); - $dbh->bz_add_column( - 'component_watch', - 'id', - { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - ); - $dbh->bz_add_column( - 'component_watch', - 'component_prefix', - { - TYPE => 'VARCHAR(64)', - NOTNULL => 0, - } - ); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column( + 'components', + 'watch_user', + { + TYPE => 'INT3', + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL',} + } + ); + $dbh->bz_add_column('component_watch', 'id', + {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + ); + $dbh->bz_add_column('component_watch', 'component_prefix', + {TYPE => 'VARCHAR(64)', NOTNULL => 0,}); } # @@ -125,16 +91,16 @@ sub install_update_db { # sub template_before_create { - my ($self, $args) = @_; - my $config = $args->{config}; - my $constants = $config->{VARIABLES}{constants}; - $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER; + my ($self, $args) = @_; + my $config = $args->{config}; + my $constants = $config->{VARIABLES}{constants}; + $constants->{REL_COMPONENT_WATCHER} = REL_COMPONENT_WATCHER; } sub template_before_process { - my ($self, $args) = @_; - return unless $args->{file} eq 'admin/components/create.html.tmpl'; - $args->{vars}{comp}{default_assignee}{login} = DEFAULT_ASSIGNEE; + my ($self, $args) = @_; + return unless $args->{file} eq 'admin/components/create.html.tmpl'; + $args->{vars}{comp}{default_assignee}{login} = DEFAULT_ASSIGNEE; } # @@ -142,147 +108,147 @@ sub template_before_process { # BEGIN { - *Bugzilla::Component::watch_user = \&_component_watch_user; + *Bugzilla::Component::watch_user = \&_component_watch_user; } sub _component_watch_user { - my ($self) = @_; - return unless $self->{watch_user}; - $self->{watch_user_object} ||= Bugzilla::User->new($self->{watch_user}); - return $self->{watch_user_object}; + my ($self) = @_; + return unless $self->{watch_user}; + $self->{watch_user_object} ||= Bugzilla::User->new($self->{watch_user}); + return $self->{watch_user_object}; } sub object_columns { - my ($self, $args) = @_; - my $class = $args->{class}; - my $columns = $args->{columns}; - return unless $class->isa('Bugzilla::Component'); - - if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'watch_user')) { - push @$columns, 'watch_user'; - } + my ($self, $args) = @_; + my $class = $args->{class}; + my $columns = $args->{columns}; + return unless $class->isa('Bugzilla::Component'); + + if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'watch_user')) { + push @$columns, 'watch_user'; + } } sub object_update_columns { - my ($self, $args) = @_; - my $object = $args->{object}; - my $columns = $args->{columns}; - return unless $object->isa('Bugzilla::Component'); + my ($self, $args) = @_; + my $object = $args->{object}; + my $columns = $args->{columns}; + return unless $object->isa('Bugzilla::Component'); - push(@$columns, 'watch_user'); + push(@$columns, 'watch_user'); - # add the user if not yet exists and user chooses 'automatic' - $self->_create_watch_user(); + # add the user if not yet exists and user chooses 'automatic' + $self->_create_watch_user(); - # editcomponents.cgi doesn't call set_all, so we have to do this here - my $input = Bugzilla->input_params; - $object->set('watch_user', $input->{watch_user}); + # editcomponents.cgi doesn't call set_all, so we have to do this here + my $input = Bugzilla->input_params; + $object->set('watch_user', $input->{watch_user}); } sub object_validators { - my ($self, $args) = @_; - my $class = $args->{class}; - my $validators = $args->{validators}; - return unless $class->isa('Bugzilla::Component'); + my ($self, $args) = @_; + my $class = $args->{class}; + my $validators = $args->{validators}; + return unless $class->isa('Bugzilla::Component'); - $validators->{watch_user} = \&_check_watch_user; + $validators->{watch_user} = \&_check_watch_user; } sub object_before_create { - my ($self, $args) = @_; - my $class = $args->{class}; - my $params = $args->{params}; - return unless $class->isa('Bugzilla::Component'); - - # We need to create a watch user for the default product/component - # if we are creating the database for the first time. - my $dbh = Bugzilla->dbh; - if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE - && !$dbh->selectrow_array('SELECT 1 FROM components')) - { - my $watch_user = Bugzilla::User->create({ - login_name => 'testcomponent@testproduct.bugs', - cryptpassword => '*', - disable_mail => 1 - }); - $params->{watch_user} = $watch_user->login; - } - else { - my $input = Bugzilla->input_params; - $params->{watch_user} = $input->{watch_user}; - $self->_create_watch_user(); - } + my ($self, $args) = @_; + my $class = $args->{class}; + my $params = $args->{params}; + return unless $class->isa('Bugzilla::Component'); + + # We need to create a watch user for the default product/component + # if we are creating the database for the first time. + my $dbh = Bugzilla->dbh; + if (Bugzilla->usage_mode == USAGE_MODE_CMDLINE + && !$dbh->selectrow_array('SELECT 1 FROM components')) + { + my $watch_user = Bugzilla::User->create({ + login_name => 'testcomponent@testproduct.bugs', + cryptpassword => '*', + disable_mail => 1 + }); + $params->{watch_user} = $watch_user->login; + } + else { + my $input = Bugzilla->input_params; + $params->{watch_user} = $input->{watch_user}; + $self->_create_watch_user(); + } } sub object_end_of_update { - my ($self, $args) = @_; - my $object = $args->{object}; - my $old_object = $args->{old_object}; - my $changes = $args->{changes}; - return unless $object->isa('Bugzilla::Component'); - - my $old_id = $old_object->watch_user ? $old_object->watch_user->id : 0; - my $new_id = $object->watch_user ? $object->watch_user->id : 0; - if ($old_id != $new_id) { - $changes->{watch_user} = [ $old_id ? $old_id : undef, $new_id ? $new_id : undef ]; - } - - # when a component is renamed, update the watch-user to follow - # this only happens when the user appears to have been auto-generated from the old name - if ($changes->{name} - && $old_object->watch_user - && $object->watch_user - && $old_object->watch_user->id == $object->watch_user->id - && _generate_watch_user_name($old_object) eq $object->watch_user->login - ) - { - my $old_login = $object->watch_user->login; - $object->watch_user->set_login(_generate_watch_user_name($object)); - $object->watch_user->update(); - $changes->{watch_user_login} = [ $old_login, $object->watch_user->login ]; - } + my ($self, $args) = @_; + my $object = $args->{object}; + my $old_object = $args->{old_object}; + my $changes = $args->{changes}; + return unless $object->isa('Bugzilla::Component'); + + my $old_id = $old_object->watch_user ? $old_object->watch_user->id : 0; + my $new_id = $object->watch_user ? $object->watch_user->id : 0; + if ($old_id != $new_id) { + $changes->{watch_user} = [$old_id ? $old_id : undef, $new_id ? $new_id : undef]; + } + +# when a component is renamed, update the watch-user to follow +# this only happens when the user appears to have been auto-generated from the old name + if ( $changes->{name} + && $old_object->watch_user + && $object->watch_user + && $old_object->watch_user->id == $object->watch_user->id + && _generate_watch_user_name($old_object) eq $object->watch_user->login) + { + my $old_login = $object->watch_user->login; + $object->watch_user->set_login(_generate_watch_user_name($object)); + $object->watch_user->update(); + $changes->{watch_user_login} = [$old_login, $object->watch_user->login]; + } } sub _generate_watch_user_name { - # this is mirrored in template/en/default/hook/admin/components/edit-common-rows.html.tmpl - # that javascript needs to be kept in sync with this perl - my ($component) = @_; - return _sanitise_name($component->name) - . '@' . _sanitise_name($component->product->name) . '.bugs'; + +# this is mirrored in template/en/default/hook/admin/components/edit-common-rows.html.tmpl +# that javascript needs to be kept in sync with this perl + my ($component) = @_; + return + _sanitise_name($component->name) . '@' + . _sanitise_name($component->product->name) . '.bugs'; } sub _sanitise_name { - my ($name) = @_; - $name = lc($name); - $name =~ s/[^a-z0-9_]/-/g; - $name =~ s/-+/-/g; - $name =~ s/(^-|-$)//g; - return $name; + my ($name) = @_; + $name = lc($name); + $name =~ s/[^a-z0-9_]/-/g; + $name =~ s/-+/-/g; + $name =~ s/(^-|-$)//g; + return $name; } sub _create_watch_user { - my $input = Bugzilla->input_params; - if ($input->{watch_user_auto} - && !Bugzilla::User->new({ name => $input->{watch_user} })) - { - Bugzilla::User->create({ - login_name => $input->{watch_user}, - cryptpassword => '*', - }); - } + my $input = Bugzilla->input_params; + if ($input->{watch_user_auto} + && !Bugzilla::User->new({name => $input->{watch_user}})) + { + Bugzilla::User->create({ + login_name => $input->{watch_user}, cryptpassword => '*', + }); + } } sub _check_watch_user { - my ($self, $value, $field) = @_; - $value = trim($value || ''); - return undef if !REQUIRE_WATCH_USER && $value eq ''; - if ($value eq '') { - ThrowUserError('component_watch_missing_watch_user'); - } - if ($value !~ /\.bugs$/i) { - ThrowUserError('component_watch_invalid_watch_user'); - } - return Bugzilla::User->check($value)->id; + my ($self, $value, $field) = @_; + $value = trim($value || ''); + return undef if !REQUIRE_WATCH_USER && $value eq ''; + if ($value eq '') { + ThrowUserError('component_watch_missing_watch_user'); + } + if ($value !~ /\.bugs$/i) { + ThrowUserError('component_watch_invalid_watch_user'); + } + return Bugzilla::User->check($value)->id; } # @@ -290,74 +256,79 @@ sub _check_watch_user { # sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{'current_tab'}; - return unless $tab eq 'component_watch'; - - my $save = $args->{'save_changes'}; - my $handled = $args->{'handled'}; - my $vars = $args->{'vars'}; - my $user = Bugzilla->user; - my $input = Bugzilla->input_params; + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'component_watch'; - if ($save) { - if ($input->{'add'} && $input->{'add_product'}) { - # add watch + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $input = Bugzilla->input_params; - # load product and verify access - my $productName = $input->{'add_product'}; - my $product = Bugzilla::Product->new({ name => $productName, cache => 1 }); - unless ($product && $user->can_access_product($product)) { - ThrowUserError('product_access_denied', { product => $productName }); - } + if ($save) { + if ($input->{'add'} && $input->{'add_product'}) { - # starting-with - if (my $prefix = $input->{add_starting}) { - _addPrefixWatch($user, $product, $prefix); - - } else { - my $ra_componentNames = $input->{'add_component'}; - $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); - - if (grep { $_ eq '' } @$ra_componentNames) { - # watching a product - _addProductWatch($user, $product); - - } else { - # watching specific components - foreach my $componentName (@$ra_componentNames) { - my $component = Bugzilla::Component->new({ - name => $componentName, product => $product, cache => 1 - }); - unless ($component) { - ThrowUserError('product_access_denied', { product => $productName }); - } - _addComponentWatch($user, $component); - } - } - } + # add watch + + # load product and verify access + my $productName = $input->{'add_product'}; + my $product = Bugzilla::Product->new({name => $productName, cache => 1}); + unless ($product && $user->can_access_product($product)) { + ThrowUserError('product_access_denied', {product => $productName}); + } - _addDefaultSettings($user); + # starting-with + if (my $prefix = $input->{add_starting}) { + _addPrefixWatch($user, $product, $prefix); - } else { - # remove watch(s) + } + else { + my $ra_componentNames = $input->{'add_component'}; + $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); - my $delete = ref $input->{del_watch} - ? $input->{del_watch} - : [ $input->{del_watch} ]; - foreach my $id (@$delete) { - _deleteWatch($user, $id); + if (grep { $_ eq '' } @$ra_componentNames) { + + # watching a product + _addProductWatch($user, $product); + + } + else { + # watching specific components + foreach my $componentName (@$ra_componentNames) { + my $component + = Bugzilla::Component->new({ + name => $componentName, product => $product, cache => 1 + }); + unless ($component) { + ThrowUserError('product_access_denied', {product => $productName}); } + _addComponentWatch($user, $component); + } } + } + + _addDefaultSettings($user); } + else { + # remove watch(s) - $vars->{'add_product'} = $input->{'product'}; - $vars->{'add_component'} = $input->{'component'}; - $vars->{'watches'} = _getWatches($user); - $vars->{'user_watches'} = _getUserWatches($user); + my $delete + = ref $input->{del_watch} ? $input->{del_watch} : [$input->{del_watch}]; + foreach my $id (@$delete) { + _deleteWatch($user, $id); + } + } + + } - $$handled = 1; + $vars->{'add_product'} = $input->{'product'}; + $vars->{'add_component'} = $input->{'component'}; + $vars->{'watches'} = _getWatches($user); + $vars->{'user_watches'} = _getUserWatches($user); + + $$handled = 1; } # @@ -365,44 +336,45 @@ sub user_preferences { # sub bugmail_recipients { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - my $recipients = $args->{'recipients'}; - my $diffs = $args->{'diffs'}; - - my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id); - my ($oldComponentId, $newComponentId) = ($bug->component_id, $bug->component_id); - - # notify when the product/component is switch from one being watched - if (@$diffs) { - # we need the product to process the component, so scan for that first - my $product; - foreach my $ra (@$diffs) { - next if !(exists $ra->{'old'} - && exists $ra->{'field_name'}); - if ($ra->{'field_name'} eq 'product') { - $product = Bugzilla::Product->new({ name => $ra->{'old'}, cache => 1 }); - $oldProductId = $product->id; - } - } - if (!$product) { - $product = Bugzilla::Product->new({ id => $oldProductId, cache => 1 }); - } - foreach my $ra (@$diffs) { - next if !(exists $ra->{'old'} - && exists $ra->{'field_name'}); - if ($ra->{'field_name'} eq 'component') { - my $component = Bugzilla::Component->new({ - name => $ra->{'old'}, product => $product, cache => 1 - }); - $oldComponentId = $component->id; - } - } + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $recipients = $args->{'recipients'}; + my $diffs = $args->{'diffs'}; + + my ($oldProductId, $newProductId) = ($bug->product_id, $bug->product_id); + my ($oldComponentId, $newComponentId) + = ($bug->component_id, $bug->component_id); + + # notify when the product/component is switch from one being watched + if (@$diffs) { + + # we need the product to process the component, so scan for that first + my $product; + foreach my $ra (@$diffs) { + next if !(exists $ra->{'old'} && exists $ra->{'field_name'}); + if ($ra->{'field_name'} eq 'product') { + $product = Bugzilla::Product->new({name => $ra->{'old'}, cache => 1}); + $oldProductId = $product->id; + } } + if (!$product) { + $product = Bugzilla::Product->new({id => $oldProductId, cache => 1}); + } + foreach my $ra (@$diffs) { + next if !(exists $ra->{'old'} && exists $ra->{'field_name'}); + if ($ra->{'field_name'} eq 'component') { + my $component + = Bugzilla::Component->new({ + name => $ra->{'old'}, product => $product, cache => 1 + }); + $oldComponentId = $component->id; + } + } + } - # add component watchers - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + # add component watchers + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare(" SELECT user_id FROM component_watch WHERE ((product_id = ? OR product_id = ?) AND component_id IS NULL) @@ -416,52 +388,52 @@ sub bugmail_recipients { AND components.name LIKE @{[$dbh->sql_string_concat('component_prefix', q{'%'})]} AND (components.id = ? OR components.id = ?) "); - $sth->execute( - $oldProductId, $newProductId, - $oldComponentId, $newComponentId, - $oldProductId, $newProductId, - $oldComponentId, $newComponentId, - ); - while (my ($uid) = $sth->fetchrow_array) { - if (!exists $recipients->{$uid}) { - $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING(); - } + $sth->execute( + $oldProductId, $newProductId, $oldComponentId, $newComponentId, + $oldProductId, $newProductId, $oldComponentId, $newComponentId, + ); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} + = Bugzilla::BugMail::BIT_WATCHING(); } + } - # add component watchers from watch-users - my $uidList = join(',', keys %$recipients); - $sth = $dbh->prepare(" + # add component watchers from watch-users + my $uidList = join(',', keys %$recipients); + $sth = $dbh->prepare(" SELECT component_watch.user_id FROM components INNER JOIN component_watch ON component_watch.component_id = components.id WHERE components.watch_user in ($uidList) "); - $sth->execute(); - while (my ($uid) = $sth->fetchrow_array) { - if (!exists $recipients->{$uid}) { - $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_WATCHING(); - } + $sth->execute(); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} + = Bugzilla::BugMail::BIT_WATCHING(); } + } - # add watch-users from component watchers - $sth = $dbh->prepare(" + # add watch-users from component watchers + $sth = $dbh->prepare(" SELECT watch_user FROM components WHERE (id = ? OR id = ?) AND (watch_user IS NOT NULL) "); - $sth->execute($oldComponentId, $newComponentId); - while (my ($uid) = $sth->fetchrow_array) { - if (!exists $recipients->{$uid}) { - $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_DIRECT(); - } + $sth->execute($oldComponentId, $newComponentId); + while (my ($uid) = $sth->fetchrow_array) { + if (!exists $recipients->{$uid}) { + $recipients->{$uid}->{+REL_COMPONENT_WATCHER} = Bugzilla::BugMail::BIT_DIRECT(); } + } } sub bugmail_relationships { - my ($self, $args) = @_; - my $relationships = $args->{relationships}; - $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher'; + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_COMPONENT_WATCHER} = 'Component-Watcher'; } # @@ -469,141 +441,140 @@ sub bugmail_relationships { # sub _getWatches { - my ($user, $watch_id) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $watch_id) = @_; + my $dbh = Bugzilla->dbh; - $watch_id = (defined $watch_id && $watch_id =~ /^(\d+)$/) ? $1 : undef; + $watch_id = (defined $watch_id && $watch_id =~ /^(\d+)$/) ? $1 : undef; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT id, product_id, component_id, component_prefix FROM component_watch - WHERE user_id = ?" . ($watch_id ? " AND id = ?" : "") + WHERE user_id = ?" . ($watch_id ? " AND id = ?" : "")); + $watch_id ? $sth->execute($user->id, $watch_id) : $sth->execute($user->id); + + my @watches; + while (my ($id, $productId, $componentId, $prefix) = $sth->fetchrow_array) { + my $product = Bugzilla::Product->new({id => $productId, cache => 1}); + next unless $product && $user->can_access_product($product); + + my %watch = ( + id => $id, + product => $product, + product_name => $product->name, + component_name => '', + component_prefix => $prefix, ); - $watch_id ? $sth->execute($user->id, $watch_id) : $sth->execute($user->id); - - my @watches; - while (my ($id, $productId, $componentId, $prefix) = $sth->fetchrow_array) { - my $product = Bugzilla::Product->new({ id => $productId, cache => 1 }); - next unless $product && $user->can_access_product($product); - - my %watch = ( - id => $id, - product => $product, - product_name => $product->name, - component_name => '', - component_prefix => $prefix, - ); - if ($componentId) { - my $component = Bugzilla::Component->new({ id => $componentId, cache => 1 }); - next unless $component; - $watch{'component'} = $component; - $watch{'component_name'} = $component->name; - } - - push @watches, \%watch; + if ($componentId) { + my $component = Bugzilla::Component->new({id => $componentId, cache => 1}); + next unless $component; + $watch{'component'} = $component; + $watch{'component_name'} = $component->name; } - if ($watch_id) { - return $watches[0] || {}; - } + push @watches, \%watch; + } + + if ($watch_id) { + return $watches[0] || {}; + } - @watches = sort { - $a->{'product_name'} cmp $b->{'product_name'} - || $a->{'component_name'} cmp $b->{'component_name'} - || $a->{'component_prefix'} cmp $b->{'component_prefix'} - } @watches; + @watches = sort { + $a->{'product_name'} cmp $b->{'product_name'} + || $a->{'component_name'} cmp $b->{'component_name'} + || $a->{'component_prefix'} cmp $b->{'component_prefix'} + } @watches; - return \@watches; + return \@watches; } sub _getUserWatches { - my ($user) = @_; - my $dbh = Bugzilla->dbh; + my ($user) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT components.product_id, components.id as component, profiles.login_name FROM watch INNER JOIN components ON components.watch_user = watched INNER JOIN profiles ON profiles.userid = watched WHERE watcher = ? "); - $sth->execute($user->id); - my @watches; - while (my ($productId, $componentId, $login) = $sth->fetchrow_array) { - my $product = Bugzilla::Product->new({ id => $productId, cache => 1 }); - next unless $product && $user->can_access_product($product); - - my %watch = ( - product => $product, - component => Bugzilla::Component->new({ id => $componentId, cache => 1 }), - user => Bugzilla::User->check($login), - ); - push @watches, \%watch; - } + $sth->execute($user->id); + my @watches; + while (my ($productId, $componentId, $login) = $sth->fetchrow_array) { + my $product = Bugzilla::Product->new({id => $productId, cache => 1}); + next unless $product && $user->can_access_product($product); + + my %watch = ( + product => $product, + component => Bugzilla::Component->new({id => $componentId, cache => 1}), + user => Bugzilla::User->check($login), + ); + push @watches, \%watch; + } - @watches = sort { - $a->{'product'}->name cmp $b->{'product'}->name - || $a->{'component'}->name cmp $b->{'component'}->name - } @watches; + @watches = sort { + $a->{'product'}->name cmp $b->{'product'}->name + || $a->{'component'}->name cmp $b->{'component'}->name + } @watches; - return \@watches; + return \@watches; } sub _addProductWatch { - my ($user, $product) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $product) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT 1 FROM component_watch WHERE user_id = ? AND product_id = ? AND component_id IS NULL "); - $sth->execute($user->id, $product->id); - return if $sth->fetchrow_array; + $sth->execute($user->id, $product->id); + return if $sth->fetchrow_array; - $sth = $dbh->prepare(" + $sth = $dbh->prepare(" DELETE FROM component_watch WHERE user_id = ? AND product_id = ? "); - $sth->execute($user->id, $product->id); + $sth->execute($user->id, $product->id); - $sth = $dbh->prepare(" + $sth = $dbh->prepare(" INSERT INTO component_watch(user_id, product_id) VALUES (?, ?) "); - $sth->execute($user->id, $product->id); + $sth->execute($user->id, $product->id); - return _getWatches($user, $dbh->bz_last_key()); + return _getWatches($user, $dbh->bz_last_key()); } sub _addComponentWatch { - my ($user, $component) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $component) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT 1 FROM component_watch WHERE user_id = ? AND (component_id = ? OR (product_id = ? AND component_id IS NULL)) "); - $sth->execute($user->id, $component->id, $component->product_id); - return if $sth->fetchrow_array; + $sth->execute($user->id, $component->id, $component->product_id); + return if $sth->fetchrow_array; - $sth = $dbh->prepare(" + $sth = $dbh->prepare(" INSERT INTO component_watch(user_id, product_id, component_id) VALUES (?, ?, ?) "); - $sth->execute($user->id, $component->product_id, $component->id); + $sth->execute($user->id, $component->product_id, $component->id); - return _getWatches($user, $dbh->bz_last_key()); + return _getWatches($user, $dbh->bz_last_key()); } sub _addPrefixWatch { - my ($user, $product, $prefix) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $product, $prefix) = @_; + my $dbh = Bugzilla->dbh; - trick_taint($prefix); - my $sth = $dbh->prepare(" + trick_taint($prefix); + my $sth = $dbh->prepare(" SELECT 1 FROM component_watch WHERE user_id = ? @@ -612,121 +583,102 @@ sub _addPrefixWatch { OR (product_id = ? AND component_id IS NULL) ) "); - $sth->execute( - $user->id, - $product->id, $prefix, - $product->id - ); - return if $sth->fetchrow_array; + $sth->execute($user->id, $product->id, $prefix, $product->id); + return if $sth->fetchrow_array; - $sth = $dbh->prepare(" + $sth = $dbh->prepare(" INSERT INTO component_watch(user_id, product_id, component_prefix) VALUES (?, ?, ?) "); - $sth->execute($user->id, $product->id, $prefix); + $sth->execute($user->id, $product->id, $prefix); } sub _deleteWatch { - my ($user, $id) = @_; - my $dbh = Bugzilla->dbh; + my ($user, $id) = @_; + my $dbh = Bugzilla->dbh; - detaint_natural($id) || ThrowCodeError("component_watch_invalid_id"); + detaint_natural($id) || ThrowCodeError("component_watch_invalid_id"); - return $dbh->do("DELETE FROM component_watch WHERE id=? AND user_id=?", - undef, $id, $user->id); + return $dbh->do("DELETE FROM component_watch WHERE id=? AND user_id=?", + undef, $id, $user->id); } sub _addDefaultSettings { - my ($user) = @_; - my $dbh = Bugzilla->dbh; + my ($user) = @_; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT 1 FROM email_setting WHERE user_id = ? AND relationship = ? "); - $sth->execute($user->id, REL_COMPONENT_WATCHER); - return if $sth->fetchrow_array; - - my @defaultEvents = ( - EVT_OTHER, - EVT_COMMENT, - EVT_ATTACHMENT, - EVT_ATTACHMENT_DATA, - EVT_PROJ_MANAGEMENT, - EVT_OPENED_CLOSED, - EVT_KEYWORD, - EVT_DEPEND_BLOCK, - EVT_BUG_CREATED, - ); - foreach my $event (@defaultEvents) { - $dbh->do( - "INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", - undef, - $user->id, REL_COMPONENT_WATCHER, $event - ); - } + $sth->execute($user->id, REL_COMPONENT_WATCHER); + return if $sth->fetchrow_array; + + my @defaultEvents = (EVT_OTHER, EVT_COMMENT, + EVT_ATTACHMENT, EVT_ATTACHMENT_DATA, + EVT_PROJ_MANAGEMENT, EVT_OPENED_CLOSED, + EVT_KEYWORD, EVT_DEPEND_BLOCK, + EVT_BUG_CREATED, + ); + foreach my $event (@defaultEvents) { + $dbh->do("INSERT INTO email_setting(user_id,relationship,event) VALUES (?,?,?)", + undef, $user->id, REL_COMPONENT_WATCHER, $event); + } } sub reorg_move_component { - my ($self, $args) = @_; - my $new_product = $args->{new_product}; - my $component = $args->{component}; - - Bugzilla->dbh->do( - "UPDATE component_watch SET product_id=? WHERE component_id=?", - undef, - $new_product->id, $component->id, - ); + my ($self, $args) = @_; + my $new_product = $args->{new_product}; + my $component = $args->{component}; + + Bugzilla->dbh->do( + "UPDATE component_watch SET product_id=? WHERE component_id=?", + undef, $new_product->id, $component->id,); } sub sanitycheck_check { - my ($self, $args) = @_; - my $status = $args->{status}; + my ($self, $args) = @_; + my $status = $args->{status}; - $status->('component_watching_check'); + $status->('component_watching_check'); - my ($count) = Bugzilla->dbh->selectrow_array(" + my ($count) = Bugzilla->dbh->selectrow_array(" SELECT COUNT(*) FROM component_watch INNER JOIN components ON components.id = component_watch.component_id WHERE component_watch.product_id <> components.product_id "); - if ($count) { - $status->('component_watching_alert', undef, 'alert'); - $status->('component_watching_repair'); - } + if ($count) { + $status->('component_watching_alert', undef, 'alert'); + $status->('component_watching_repair'); + } } sub sanitycheck_repair { - my ($self, $args) = @_; - return unless Bugzilla->cgi->param('component_watching_repair'); + my ($self, $args) = @_; + return unless Bugzilla->cgi->param('component_watching_repair'); - my $status = $args->{'status'}; - my $dbh = Bugzilla->dbh; - $status->('component_watching_repairing'); + my $status = $args->{'status'}; + my $dbh = Bugzilla->dbh; + $status->('component_watching_repairing'); - my $rows = $dbh->selectall_arrayref(" + my $rows = $dbh->selectall_arrayref(" SELECT DISTINCT component_watch.product_id AS bad_product_id, components.product_id AS good_product_id, component_watch.component_id FROM component_watch INNER JOIN components ON components.id = component_watch.component_id WHERE component_watch.product_id <> components.product_id - ", - { Slice => {} } - ); - foreach my $row (@$rows) { - $dbh->do(" + ", {Slice => {}}); + foreach my $row (@$rows) { + $dbh->do(" UPDATE component_watch SET product_id=? WHERE product_id=? AND component_id=? - ", undef, - $row->{good_product_id}, - $row->{bad_product_id}, - $row->{component_id}, - ); - } + ", undef, $row->{good_product_id}, $row->{bad_product_id}, + $row->{component_id},); + } } # @@ -734,8 +686,9 @@ sub sanitycheck_repair { # sub webservice { - my ($self, $args) = @_; - $args->{dispatch}->{ComponentWatching} = "Bugzilla::Extension::ComponentWatching::WebService"; + my ($self, $args) = @_; + $args->{dispatch}->{ComponentWatching} + = "Bugzilla::Extension::ComponentWatching::WebService"; } __PACKAGE__->NAME; diff --git a/extensions/ComponentWatching/lib/WebService.pm b/extensions/ComponentWatching/lib/WebService.pm index ba4cb0225..331842f7b 100644 --- a/extensions/ComponentWatching/lib/WebService.pm +++ b/extensions/ComponentWatching/lib/WebService.pm @@ -21,30 +21,25 @@ use Bugzilla::Product; use Bugzilla::User; sub rest_resources { - return [ - qr{^/component-watching$}, { - GET => { - method => 'list', - }, - POST => { - method => 'add', - }, + return [ + qr{^/component-watching$}, + {GET => {method => 'list',}, POST => {method => 'add',},}, + qr{^/component-watching/(\d+)$}, + { + GET => { + method => 'get', + params => sub { + return {id => $_[0]}; }, - qr{^/component-watching/(\d+)$}, { - GET => { - method => 'get', - params => sub { - return { id => $_[0] } - }, - }, - DELETE => { - method => 'remove', - params => sub { - return { id => $_[0] } - }, - }, + }, + DELETE => { + method => 'remove', + params => sub { + return {id => $_[0]}; }, - ]; + }, + }, + ]; } # @@ -52,62 +47,69 @@ sub rest_resources { # sub list { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); - return Bugzilla::Extension::ComponentWatching::_getWatches($user); + return Bugzilla::Extension::ComponentWatching::_getWatches($user); } sub add { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $result; - - # load product and verify access - my $productName = $params->{'product'}; - my $product = Bugzilla::Product->new({ name => $productName, cache => 1 }); - unless ($product && $user->can_access_product($product)) { - ThrowUserError('product_access_denied', { product => $productName }); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $result; + + # load product and verify access + my $productName = $params->{'product'}; + my $product = Bugzilla::Product->new({name => $productName, cache => 1}); + unless ($product && $user->can_access_product($product)) { + ThrowUserError('product_access_denied', {product => $productName}); + } + + my $ra_componentNames = $params->{'component'}; + $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); + + if (grep { $_ eq '' } @$ra_componentNames) { + + # watching a product + $result + = Bugzilla::Extension::ComponentWatching::_addProductWatch($user, $product); + + } + else { + # watching specific components + foreach my $componentName (@$ra_componentNames) { + my $component + = Bugzilla::Component->new({ + name => $componentName, product => $product, cache => 1 + }); + unless ($component) { + ThrowUserError('product_access_denied', {product => $productName}); + } + $result = Bugzilla::Extension::ComponentWatching::_addComponentWatch($user, + $component); } + } - my $ra_componentNames = $params->{'component'}; - $ra_componentNames = [$ra_componentNames || ''] unless ref($ra_componentNames); - - if (grep { $_ eq '' } @$ra_componentNames) { - # watching a product - $result = Bugzilla::Extension::ComponentWatching::_addProductWatch($user, $product); - - } else { - # watching specific components - foreach my $componentName (@$ra_componentNames) { - my $component = Bugzilla::Component->new({ - name => $componentName, product => $product, cache => 1 - }); - unless ($component) { - ThrowUserError('product_access_denied', { product => $productName }); - } - $result = Bugzilla::Extension::ComponentWatching::_addComponentWatch($user, $component); - } - } - - Bugzilla::Extension::ComponentWatching::_addDefaultSettings($user); + Bugzilla::Extension::ComponentWatching::_addDefaultSettings($user); - return $result; + return $result; } sub get { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); - return Bugzilla::Extension::ComponentWatching::_getWatches($user, $params->{'id'}); + return Bugzilla::Extension::ComponentWatching::_getWatches($user, + $params->{'id'}); } sub remove { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - my %result = (status => Bugzilla::Extension::ComponentWatching::_deleteWatch($user, $params->{'id'})); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my %result = (status => + Bugzilla::Extension::ComponentWatching::_deleteWatch($user, $params->{'id'})); - return \%result; + return \%result; } 1; diff --git a/extensions/ContributorEngagement/Config.pm b/extensions/ContributorEngagement/Config.pm index d48de3bc6..5f46efdb0 100644 --- a/extensions/ContributorEngagement/Config.pm +++ b/extensions/ContributorEngagement/Config.pm @@ -13,10 +13,8 @@ use warnings; use constant NAME => 'ContributorEngagement'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/ContributorEngagement/Extension.pm b/extensions/ContributorEngagement/Extension.pm index 35eba24ab..ae5c1b809 100644 --- a/extensions/ContributorEngagement/Extension.pm +++ b/extensions/ContributorEngagement/Extension.pm @@ -23,105 +23,110 @@ use Bugzilla::Extension::ContributorEngagement::Constants; our $VERSION = '2.0'; BEGIN { - *Bugzilla::User::first_patch_reviewed_id = \&_first_patch_reviewed_id; + *Bugzilla::User::first_patch_reviewed_id = \&_first_patch_reviewed_id; } sub _first_patch_reviewed_id { return $_[0]->{'first_patch_reviewed_id'}; } sub install_update_db { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - if ($dbh->bz_column_info('profiles', 'first_patch_approved_id')) { - $dbh->bz_drop_column('profiles', 'first_patch_approved_id'); - } - if (!$dbh->bz_column_info('profiles', 'first_patch_reviewed_id')) { - $dbh->bz_add_column('profiles', 'first_patch_reviewed_id', { TYPE => 'INT3' }); - _populate_first_reviewed_ids(); - } + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + if ($dbh->bz_column_info('profiles', 'first_patch_approved_id')) { + $dbh->bz_drop_column('profiles', 'first_patch_approved_id'); + } + if (!$dbh->bz_column_info('profiles', 'first_patch_reviewed_id')) { + $dbh->bz_add_column('profiles', 'first_patch_reviewed_id', {TYPE => 'INT3'}); + _populate_first_reviewed_ids(); + } } sub _populate_first_reviewed_ids { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare('UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?'); - my $ra = $dbh->selectall_arrayref("SELECT attachments.submitter_id, + my $sth = $dbh->prepare( + 'UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?'); + my $ra = $dbh->selectall_arrayref( + "SELECT attachments.submitter_id, attachments.attach_id FROM attachments INNER JOIN flags ON attachments.attach_id = flags.attach_id INNER JOIN flagtypes ON flags.type_id = flagtypes.id WHERE flagtypes.name LIKE 'review%' AND flags.status = '+' - ORDER BY flags.modification_date"); - my $count = 1; - my $total = scalar @$ra; - my %user_seen; - foreach my $ra_row (@$ra) { - my ($user_id, $attach_id) = @$ra_row; - indicate_progress({ current => $count++, total => $total, every => 25 }); - next if $user_seen{$user_id}; - $sth->execute($attach_id, $user_id); - $user_seen{$user_id} = 1; - } - - print "done\n"; + ORDER BY flags.modification_date" + ); + my $count = 1; + my $total = scalar @$ra; + my %user_seen; + foreach my $ra_row (@$ra) { + my ($user_id, $attach_id) = @$ra_row; + indicate_progress({current => $count++, total => $total, every => 25}); + next if $user_seen{$user_id}; + $sth->execute($attach_id, $user_id); + $user_seen{$user_id} = 1; + } + + print "done\n"; } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info($class->DB_TABLE, 'first_patch_reviewed_id')) { - push @$columns, 'first_patch_reviewed_id'; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info($class->DB_TABLE, 'first_patch_reviewed_id')) { + push @$columns, 'first_patch_reviewed_id'; } + } } sub flag_end_of_update { - my ($self, $args) = @_; - my ($object, $timestamp, $new_flags) = @$args{qw(object timestamp new_flags)}; - - if ($object->isa('Bugzilla::Attachment') - && @$new_flags - && !$object->attacher->first_patch_reviewed_id - && grep($_ eq $object->bug->product, ENABLED_PRODUCTS)) - { - my $attachment = $object; - - foreach my $orig_change (@$new_flags) { - my $change = $orig_change; - $change =~ s/^[^:]+://; # get rid of setter - $change =~ s/\([^\)]+\)$//; # get rid of requestee - my ($name, $value) = $change =~ /^(.+)(.)$/; - - # Only interested in review flags set to + - next unless $name =~ /^review/ && $value eq '+'; - - _send_mail($attachment, $timestamp); - - Bugzilla->dbh->do("UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?", - undef, $attachment->id, $attachment->attacher->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $attachment->attacher->id }); - last; - } + my ($self, $args) = @_; + my ($object, $timestamp, $new_flags) = @$args{qw(object timestamp new_flags)}; + + if ( $object->isa('Bugzilla::Attachment') + && @$new_flags + && !$object->attacher->first_patch_reviewed_id + && grep($_ eq $object->bug->product, ENABLED_PRODUCTS)) + { + my $attachment = $object; + + foreach my $orig_change (@$new_flags) { + my $change = $orig_change; + $change =~ s/^[^:]+://; # get rid of setter + $change =~ s/\([^\)]+\)$//; # get rid of requestee + my ($name, $value) = $change =~ /^(.+)(.)$/; + + # Only interested in review flags set to + + next unless $name =~ /^review/ && $value eq '+'; + + _send_mail($attachment, $timestamp); + + Bugzilla->dbh->do( + "UPDATE profiles SET first_patch_reviewed_id = ? WHERE userid = ?", + undef, $attachment->id, $attachment->attacher->id); + Bugzilla->memcached->clear( + {table => 'profiles', id => $attachment->attacher->id}); + last; } + } } sub _send_mail { - my ($attachment, $timestamp) = @_; + my ($attachment, $timestamp) = @_; - my $vars = { - date => format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'), - attachment => $attachment, - from_user => EMAIL_FROM, - }; + my $vars = { + date => format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'), + attachment => $attachment, + from_user => EMAIL_FROM, + }; - my $msg; - my $template = Bugzilla->template_inner($attachment->attacher->setting('lang')); - $template->process("contributor/email.txt.tmpl", $vars, \$msg) - || ThrowTemplateError($template->error()); + my $msg; + my $template = Bugzilla->template_inner($attachment->attacher->setting('lang')); + $template->process("contributor/email.txt.tmpl", $vars, \$msg) + || ThrowTemplateError($template->error()); - MessageToMTA($msg); + MessageToMTA($msg); } __PACKAGE__->NAME; diff --git a/extensions/ContributorEngagement/lib/Constants.pm b/extensions/ContributorEngagement/lib/Constants.pm index dd379adcd..030a463a4 100644 --- a/extensions/ContributorEngagement/lib/Constants.pm +++ b/extensions/ContributorEngagement/lib/Constants.pm @@ -14,20 +14,17 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - EMAIL_FROM - ENABLED_PRODUCTS + EMAIL_FROM + ENABLED_PRODUCTS ); use constant EMAIL_FROM => 'bugzilla-daemon@mozilla.org'; use constant ENABLED_PRODUCTS => ( - "Cloud Services", - "Core", - "Firefox for Android", - "Firefox for Metro", - "Firefox", - "Testing", - "Toolkit", + "Cloud Services", "Core", + "Firefox for Android", "Firefox for Metro", + "Firefox", "Testing", + "Toolkit", ); 1; diff --git a/extensions/EditComments/Config.pm b/extensions/EditComments/Config.pm index b95647940..e310dda84 100644 --- a/extensions/EditComments/Config.pm +++ b/extensions/EditComments/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'EditComments'; +use constant NAME => 'EditComments'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/EditComments/Extension.pm b/extensions/EditComments/Extension.pm index e2ace3f23..b7c398967 100644 --- a/extensions/EditComments/Extension.pm +++ b/extensions/EditComments/Extension.pm @@ -28,33 +28,36 @@ our $VERSION = '0.01'; ################ sub db_schema_abstract_schema { - my ($self, $args) = @_; - my $schema = $args->{schema}; - - $schema->{'longdescs_activity'} = { - FIELDS => [ - comment_id => {TYPE => 'INT', NOTNULL => 1, - REFERENCES => {TABLE => 'longdescs', - COLUMN => 'comment_id', - DELETE => 'CASCADE'}}, - who => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - change_when => {TYPE => 'DATETIME', NOTNULL => 1}, - old_comment => {TYPE => 'LONGTEXT', NOTNULL => 1}, - ], - INDEXES => [ - longdescs_activity_comment_id_idx => ['comment_id'], - longdescs_activity_change_when_idx => ['change_when'], - longdescs_activity_comment_id_change_when_idx => [qw(comment_id change_when)], - ], - }; + my ($self, $args) = @_; + my $schema = $args->{schema}; + + $schema->{'longdescs_activity'} = { + FIELDS => [ + comment_id => { + TYPE => 'INT', + NOTNULL => 1, + REFERENCES => + {TABLE => 'longdescs', COLUMN => 'comment_id', DELETE => 'CASCADE'} + }, + who => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + change_when => {TYPE => 'DATETIME', NOTNULL => 1}, + old_comment => {TYPE => 'LONGTEXT', NOTNULL => 1}, + ], + INDEXES => [ + longdescs_activity_comment_id_idx => ['comment_id'], + longdescs_activity_change_when_idx => ['change_when'], + longdescs_activity_comment_id_change_when_idx => [qw(comment_id change_when)], + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('longdescs', 'edit_count', { TYPE => 'INT3', DEFAULT => 0 }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('longdescs', 'edit_count', {TYPE => 'INT3', DEFAULT => 0}); } #################### @@ -62,36 +65,33 @@ sub install_update_db { #################### sub page_before_template { - my ($self, $args) = @_; + my ($self, $args) = @_; - return if $args->{'page_id'} ne 'editcomments.html'; + return if $args->{'page_id'} ne 'editcomments.html'; - my $vars = $args->{'vars'}; - my $user = Bugzilla->user; - my $params = Bugzilla->input_params; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; - # validate group membership - my $edit_comments_group = Bugzilla->params->{edit_comments_group}; - if (!$edit_comments_group || !$user->in_group($edit_comments_group)) { - ThrowUserError('auth_failure', { group => $edit_comments_group, - action => 'view', - object => 'editcomments' }); - } + # validate group membership + my $edit_comments_group = Bugzilla->params->{edit_comments_group}; + if (!$edit_comments_group || !$user->in_group($edit_comments_group)) { + ThrowUserError('auth_failure', + {group => $edit_comments_group, action => 'view', object => 'editcomments'}); + } - my $bug_id = $params->{bug_id}; - my $bug = Bugzilla::Bug->check($bug_id); + my $bug_id = $params->{bug_id}; + my $bug = Bugzilla::Bug->check($bug_id); - my $comment_id = $params->{comment_id}; + my $comment_id = $params->{comment_id}; - my ($comment) = grep($_->id == $comment_id, @{ $bug->comments }); - if (!$comment - || ($comment->is_private && !$user->is_insider)) - { - ThrowUserError("edit_comment_invalid_comment_id", { comment_id => $comment_id }); - } + my ($comment) = grep($_->id == $comment_id, @{$bug->comments}); + if (!$comment || ($comment->is_private && !$user->is_insider)) { + ThrowUserError("edit_comment_invalid_comment_id", {comment_id => $comment_id}); + } - $vars->{'bug'} = $bug; - $vars->{'comment'} = $comment; + $vars->{'bug'} = $bug; + $vars->{'comment'} = $comment; } ################## @@ -99,88 +99,94 @@ sub page_before_template { ################## BEGIN { - no warnings 'redefine'; - *Bugzilla::Comment::activity = \&_get_activity; - *Bugzilla::Comment::edit_count = \&_edit_count; - *Bugzilla::WebService::Bug::_super_translate_comment = \&Bugzilla::WebService::Bug::_translate_comment; - *Bugzilla::WebService::Bug::_translate_comment = \&_new_translate_comment; + no warnings 'redefine'; + *Bugzilla::Comment::activity = \&_get_activity; + *Bugzilla::Comment::edit_count = \&_edit_count; + *Bugzilla::WebService::Bug::_super_translate_comment + = \&Bugzilla::WebService::Bug::_translate_comment; + *Bugzilla::WebService::Bug::_translate_comment = \&_new_translate_comment; } sub _new_translate_comment { - my ($self, $comment, $filters) = @_; + my ($self, $comment, $filters) = @_; - my $comment_hash = $self->_super_translate_comment($comment, $filters); + my $comment_hash = $self->_super_translate_comment($comment, $filters); - if (filter_wants $filters, 'raw_text') { - $comment_hash->{raw_text} = $self->type('string', $comment->body); - } + if (filter_wants $filters, 'raw_text') { + $comment_hash->{raw_text} = $self->type('string', $comment->body); + } - return $comment_hash; + return $comment_hash; } sub _edit_count { return $_[0]->{'edit_count'}; } sub _get_activity { - my ($self, $activity_sort_order) = @_; + my ($self, $activity_sort_order) = @_; - return $self->{'activity'} if $self->{'activity'}; + return $self->{'activity'} if $self->{'activity'}; - my $dbh = Bugzilla->dbh; - my $query = 'SELECT longdescs_activity.comment_id AS id, profiles.userid, ' . - $dbh->sql_date_format('longdescs_activity.change_when', '%Y.%m.%d %H:%i:%s') . ' + my $dbh = Bugzilla->dbh; + my $query + = 'SELECT longdescs_activity.comment_id AS id, profiles.userid, ' + . $dbh->sql_date_format('longdescs_activity.change_when', '%Y.%m.%d %H:%i:%s') + . ' AS time, longdescs_activity.old_comment AS old FROM longdescs_activity INNER JOIN profiles ON profiles.userid = longdescs_activity.who WHERE longdescs_activity.comment_id = ?'; - $query .= " ORDER BY longdescs_activity.change_when DESC"; - my $sth = $dbh->prepare($query); - $sth->execute($self->id); - - # We are shifting each comment activity body 1 back. The reason this - # has to be done is that the longdescs_activity table stores the comment - # body that the comment was before the edit, not the actual new version - # of the comment. - my @activity; - my $new_comment; - my $last_old_comment; - my $count = 0; - while (my $change_ref = $sth->fetchrow_hashref()) { - my %change = %$change_ref; - $change{'author'} = Bugzilla::User->new({ id => $change{'userid'}, cache => 1 }); - if ($count == 0) { - $change{new} = $self->body; - } - else { - $change{new} = $new_comment; - } - $new_comment = $change{old}; - $last_old_comment = $change{old}; - push (@activity, \%change); - $count++; + $query .= " ORDER BY longdescs_activity.change_when DESC"; + my $sth = $dbh->prepare($query); + $sth->execute($self->id); + + # We are shifting each comment activity body 1 back. The reason this + # has to be done is that the longdescs_activity table stores the comment + # body that the comment was before the edit, not the actual new version + # of the comment. + my @activity; + my $new_comment; + my $last_old_comment; + my $count = 0; + while (my $change_ref = $sth->fetchrow_hashref()) { + my %change = %$change_ref; + $change{'author'} = Bugzilla::User->new({id => $change{'userid'}, cache => 1}); + if ($count == 0) { + $change{new} = $self->body; } + else { + $change{new} = $new_comment; + } + $new_comment = $change{old}; + $last_old_comment = $change{old}; + push(@activity, \%change); + $count++; + } + + return [] if !@activity; + + # Store the original comment as the first or last entry + # depending on sort order + push( + @activity, + { + author => $self->author, + body => $last_old_comment, + time => $self->creation_ts, + original => 1 + } + ); - return [] if !@activity; - - # Store the original comment as the first or last entry - # depending on sort order - push(@activity, { - author => $self->author, - body => $last_old_comment, - time => $self->creation_ts, - original => 1 - }); - - $activity_sort_order - ||= Bugzilla->user->settings->{'comment_sort_order'}->{'value'}; + $activity_sort_order + ||= Bugzilla->user->settings->{'comment_sort_order'}->{'value'}; - if ($activity_sort_order eq "oldest_to_newest") { - @activity = reverse @activity; - } + if ($activity_sort_order eq "oldest_to_newest") { + @activity = reverse @activity; + } - $self->{'activity'} = \@activity; + $self->{'activity'} = \@activity; - return $self->{'activity'}; + return $self->{'activity'}; } ######### @@ -188,86 +194,91 @@ sub _get_activity { ######### sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Comment')) { - if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'edit_count')) { - push @$columns, 'edit_count'; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Comment')) { + if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'edit_count')) { + push @$columns, 'edit_count'; } + } } sub bug_end_of_update { - my ($self, $args) = @_; - - # Silently return if not in the proper group - # or if editing comments is disabled - my $user = Bugzilla->user; - my $edit_comments_group = Bugzilla->params->{edit_comments_group}; - return if (!$edit_comments_group || !$user->in_group($edit_comments_group)); - - my $bug = $args->{bug}; - my $timestamp = $args->{timestamp}; - my $params = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; - - my $updated = 0; - foreach my $param (grep(/^edit_comment_textarea_/, keys %$params)) { - my ($comment_id) = $param =~ /edit_comment_textarea_(\d+)$/; - next if !detaint_natural($comment_id); - - # The comment ID must belong to this bug. - my ($comment_obj) = grep($_->id == $comment_id, @{ $bug->comments}); - next if (!$comment_obj || ($comment_obj->is_private && !$user->is_insider)); - - my $new_comment = $comment_obj->_check_thetext($params->{$param}); - - my $old_comment = $comment_obj->body; - next if $old_comment eq $new_comment; - - trick_taint($new_comment); - $dbh->do("UPDATE longdescs SET thetext = ?, edit_count = edit_count + 1 - WHERE comment_id = ?", - undef, $new_comment, $comment_id); - Bugzilla->memcached->clear({ table => 'longdescs', id => $comment_id }); - - # Log old comment to the longdescs activity table - $timestamp ||= $dbh->selectrow_array("SELECT NOW()"); - $dbh->do("INSERT INTO longdescs_activity " . - "(comment_id, who, change_when, old_comment) " . - "VALUES (?, ?, ?, ?)", - undef, ($comment_id, $user->id, $timestamp, $old_comment)); - - $comment_obj->{thetext} = $new_comment; - - $updated = 1; - } - - $bug->_sync_fulltext( update_comments => 1 ) if $updated; + my ($self, $args) = @_; + + # Silently return if not in the proper group + # or if editing comments is disabled + my $user = Bugzilla->user; + my $edit_comments_group = Bugzilla->params->{edit_comments_group}; + return if (!$edit_comments_group || !$user->in_group($edit_comments_group)); + + my $bug = $args->{bug}; + my $timestamp = $args->{timestamp}; + my $params = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + + my $updated = 0; + foreach my $param (grep(/^edit_comment_textarea_/, keys %$params)) { + my ($comment_id) = $param =~ /edit_comment_textarea_(\d+)$/; + next if !detaint_natural($comment_id); + + # The comment ID must belong to this bug. + my ($comment_obj) = grep($_->id == $comment_id, @{$bug->comments}); + next if (!$comment_obj || ($comment_obj->is_private && !$user->is_insider)); + + my $new_comment = $comment_obj->_check_thetext($params->{$param}); + + my $old_comment = $comment_obj->body; + next if $old_comment eq $new_comment; + + trick_taint($new_comment); + $dbh->do( + "UPDATE longdescs SET thetext = ?, edit_count = edit_count + 1 + WHERE comment_id = ?", undef, $new_comment, $comment_id + ); + Bugzilla->memcached->clear({table => 'longdescs', id => $comment_id}); + + # Log old comment to the longdescs activity table + $timestamp ||= $dbh->selectrow_array("SELECT NOW()"); + $dbh->do( + "INSERT INTO longdescs_activity " + . "(comment_id, who, change_when, old_comment) " + . "VALUES (?, ?, ?, ?)", + undef, + ($comment_id, $user->id, $timestamp, $old_comment) + ); + + $comment_obj->{thetext} = $new_comment; + + $updated = 1; + } + + $bug->_sync_fulltext(update_comments => 1) if $updated; } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{groupsecurity}->{params} }, { - name => 'edit_comments_group', - type => 's', - choices => \&get_all_group_names, - default => 'admin', - checker => \&check_group + my ($self, $args) = @_; + push @{$args->{panels}->{groupsecurity}->{params}}, + { + name => 'edit_comments_group', + type => 's', + choices => \&get_all_group_names, + default => 'admin', + checker => \&check_group }; } sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{EditComments} = "Bugzilla::Extension::EditComments::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{EditComments} = "Bugzilla::Extension::EditComments::WebService"; } sub db_sanitize { - my $dbh = Bugzilla->dbh; - print "Deleting edited comment histories...\n"; - $dbh->do("DELETE FROM longdescs_activity"); - $dbh->do("UPDATE longdescs SET edit_count=0"); + my $dbh = Bugzilla->dbh; + print "Deleting edited comment histories...\n"; + $dbh->do("DELETE FROM longdescs_activity"); + $dbh->do("UPDATE longdescs SET edit_count=0"); } __PACKAGE__->NAME; diff --git a/extensions/EditComments/lib/WebService.pm b/extensions/EditComments/lib/WebService.pm index 6969ca742..97e40b8b6 100644 --- a/extensions/EditComments/lib/WebService.pm +++ b/extensions/EditComments/lib/WebService.pm @@ -18,64 +18,61 @@ use Bugzilla::Util qw(trim); use Bugzilla::WebService::Util qw(validate); use constant PUBLIC_METHODS => qw( - comments + comments ); sub comments { - my ($self, $params) = validate(@_, 'comment_ids'); - my $dbh = Bugzilla->switch_to_shadow_db(); - my $user = Bugzilla->user; - - if (!defined $params->{comment_ids}) { - ThrowCodeError('param_required', - { function => 'Bug.comments', - param => 'comment_ids' }); + my ($self, $params) = validate(@_, 'comment_ids'); + my $dbh = Bugzilla->switch_to_shadow_db(); + my $user = Bugzilla->user; + + if (!defined $params->{comment_ids}) { + ThrowCodeError('param_required', + {function => 'Bug.comments', param => 'comment_ids'}); + } + + my @ids = map { trim($_) } @{$params->{comment_ids} || []}; + my $comment_data = Bugzilla::Comment->new_from_list(\@ids); + + # See if we were passed any invalid comment ids. + my %got_ids = map { $_->id => 1 } @$comment_data; + foreach my $comment_id (@ids) { + if (!$got_ids{$comment_id}) { + ThrowUserError('comment_id_invalid', {id => $comment_id}); } + } - my @ids = map { trim($_) } @{ $params->{comment_ids} || [] }; - my $comment_data = Bugzilla::Comment->new_from_list(\@ids); + # Now make sure that we can see all the associated bugs. + my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; + $user->visible_bugs([keys %got_bug_ids]); # preload cache for visibility check + Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); - # See if we were passed any invalid comment ids. - my %got_ids = map { $_->id => 1 } @$comment_data; - foreach my $comment_id (@ids) { - if (!$got_ids{$comment_id}) { - ThrowUserError('comment_id_invalid', { id => $comment_id }); - } + my %comments; + foreach my $comment (@$comment_data) { + if ($comment->is_private && !$user->is_insider) { + ThrowUserError('comment_is_private', {id => $comment->id}); } + $comments{$comment->id} = $comment->body; + } - # Now make sure that we can see all the associated bugs. - my %got_bug_ids = map { $_->bug_id => 1 } @$comment_data; - $user->visible_bugs([ keys %got_bug_ids ]); # preload cache for visibility check - Bugzilla::Bug->check($_) foreach (keys %got_bug_ids); - - my %comments; - foreach my $comment (@$comment_data) { - if ($comment->is_private && !$user->is_insider) { - ThrowUserError('comment_is_private', { id => $comment->id }); - } - $comments{$comment->id} = $comment->body; - } - - return { comments => \%comments }; + return {comments => \%comments}; } sub rest_resources { - return [ - qr{^/editcomments/comment/(\d+)$}, { - GET => { - method => 'comments', - params => sub { - return { comment_ids => $_[0] }; - }, - }, + return [ + qr{^/editcomments/comment/(\d+)$}, + { + GET => { + method => 'comments', + params => sub { + return {comment_ids => $_[0]}; }, - qr{^/editcomments/comment$}, { - GET => { - method => 'comments', - }, - }, - ]; -}; + }, + }, + qr{^/editcomments/comment$}, + {GET => {method => 'comments',},}, + ]; +} 1; diff --git a/extensions/EditTable/Config.pm b/extensions/EditTable/Config.pm index b9bd003b2..f62e5f0c0 100644 --- a/extensions/EditTable/Config.pm +++ b/extensions/EditTable/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'EditTable'; +use constant NAME => 'EditTable'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/EditTable/Extension.pm b/extensions/EditTable/Extension.pm index 1eb0d82f9..a6d01be65 100644 --- a/extensions/EditTable/Extension.pm +++ b/extensions/EditTable/Extension.pm @@ -49,137 +49,128 @@ our $VERSION = '1'; # }, sub EDITABLE_TABLES { - my $tables = {}; - Bugzilla::Hook::process("editable_tables", { tables => $tables }); - return $tables; + my $tables = {}; + Bugzilla::Hook::process("editable_tables", {tables => $tables}); + return $tables; } sub page_before_template { - my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; - return unless $page eq 'edit_table.html'; - my $input = Bugzilla->input_params; - - # we only support editing a particular set of tables - my $table_name = $input->{table}; - exists $self->EDITABLE_TABLES()->{$table_name} - || ThrowUserError('edittable_unsupported', { table => $table_name } ); - my $table = $self->EDITABLE_TABLES()->{$table_name}; - my $id_field = $table->{id_field}; - my $order_by = $table->{order_by} || $id_field; - my $group = $table->{group} || 'admin'; - trick_taint($table_name); - - Bugzilla->user->in_group($group) - || ThrowUserError('auth_failure', { group => $group, - action => 'edit', - object => 'tables' }); - - # load columns - my $dbh = Bugzilla->dbh; - my @fields = sort - grep { $_ ne $id_field && $_ ne $order_by; } - $dbh->bz_table_columns($table_name); - if ($order_by ne $id_field) { - unshift @fields, $order_by; - } + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'edit_table.html'; + my $input = Bugzilla->input_params; + + # we only support editing a particular set of tables + my $table_name = $input->{table}; + exists $self->EDITABLE_TABLES()->{$table_name} + || ThrowUserError('edittable_unsupported', {table => $table_name}); + my $table = $self->EDITABLE_TABLES()->{$table_name}; + my $id_field = $table->{id_field}; + my $order_by = $table->{order_by} || $id_field; + my $group = $table->{group} || 'admin'; + trick_taint($table_name); + + Bugzilla->user->in_group($group) + || ThrowUserError('auth_failure', + {group => $group, action => 'edit', object => 'tables'}); + + # load columns + my $dbh = Bugzilla->dbh; + my @fields = sort grep { $_ ne $id_field && $_ ne $order_by; } + $dbh->bz_table_columns($table_name); + if ($order_by ne $id_field) { + unshift @fields, $order_by; + } + + # update table + my $data = $input->{table_data}; + my $edits = []; + if ($data) { + check_hash_token($input->{token}, [$table_name]); + + $data = from_json($data)->{data}; + $edits = dclone($data); + eval { + $dbh->bz_start_transaction; + + foreach my $row (@$data) { + map { trick_taint($_) } @$row; + if ($row->[0] eq '-') { + + # add + shift @$row; + next unless grep { $_ ne '' } @$row; + my $placeholders = join(',', split(//, '?' x scalar(@fields))); + $dbh->do( + "INSERT INTO $table_name(" + . join(',', @fields) . ") " + . "VALUES ($placeholders)", + undef, @$row + ); + } + elsif ($row->[0] < 0) { - # update table - my $data = $input->{table_data}; - my $edits = []; - if ($data) { - check_hash_token($input->{token}, [$table_name]); - - $data = from_json($data)->{data}; - $edits = dclone($data); - eval { - $dbh->bz_start_transaction; - - foreach my $row (@$data) { - map { trick_taint($_) } @$row; - if ($row->[0] eq '-') { - # add - shift @$row; - next unless grep { $_ ne '' } @$row; - my $placeholders = join(',', split(//, '?' x scalar(@fields))); - $dbh->do( - "INSERT INTO $table_name(" . join(',', @fields) . ") " . - "VALUES ($placeholders)", - undef, - @$row - ); - } - elsif ($row->[0] < 0) { - # delete - $dbh->do( - "DELETE FROM $table_name WHERE $id_field=?", - undef, - -$row->[0] - ); - } - else { - # update - my $id = shift @$row; - $dbh->do( - "UPDATE $table_name " . - "SET " . join(',', map { "$_ = ?" } @fields) . " " . - "WHERE $id_field = ?", - undef, - @$row, $id - ); - } - } - - $dbh->bz_commit_transaction; - $vars->{updated} = 1; - $edits = []; - }; - if ($@) { - my $error = $@; - $error =~ s/^DBD::[^:]+::db do failed: //; - $error =~ s/^(.+) \[for Statement ".+$/$1/s; - $vars->{error} = $error; - $dbh->bz_rollback_transaction; + # delete + $dbh->do("DELETE FROM $table_name WHERE $id_field=?", undef, -$row->[0]); + } + else { + # update + my $id = shift @$row; + $dbh->do( + "UPDATE $table_name " . "SET " + . join(',', map {"$_ = ?"} @fields) . " " + . "WHERE $id_field = ?", + undef, @$row, $id + ); } + } + + $dbh->bz_commit_transaction; + $vars->{updated} = 1; + $edits = []; + }; + if ($@) { + my $error = $@; + $error =~ s/^DBD::[^:]+::db do failed: //; + $error =~ s/^(.+) \[for Statement ".+$/$1/s; + $vars->{error} = $error; + $dbh->bz_rollback_transaction; } + } - # load data from table - unshift @fields, $id_field; - $data = $dbh->selectall_arrayref( - "SELECT " . join(',', @fields) . " FROM $table_name ORDER BY $order_by" - ); + # load data from table + unshift @fields, $id_field; + $data = $dbh->selectall_arrayref( + "SELECT " . join(',', @fields) . " FROM $table_name ORDER BY $order_by"); - # we don't support nulls currently - foreach my $row (@$data) { - if (grep { !defined($_) } @$row) { - ThrowUserError('edittable_nulls', { table => $table_name } ); - } + # we don't support nulls currently + foreach my $row (@$data) { + if (grep { !defined($_) } @$row) { + ThrowUserError('edittable_nulls', {table => $table_name}); } + } - # apply failed edits - foreach my $edit (@$edits) { - if ($edit->[0] eq '-') { - push @$data, $edit; - } - else { - my $id = $edit->[0]; - foreach my $row (@$data) { - if ($row->[0] == $id) { - @$row = @$edit; - last; - } - } + # apply failed edits + foreach my $edit (@$edits) { + if ($edit->[0] eq '-') { + push @$data, $edit; + } + else { + my $id = $edit->[0]; + foreach my $row (@$data) { + if ($row->[0] == $id) { + @$row = @$edit; + last; } + } } + } - $vars->{table_name} = $table_name; - $vars->{blurb} = $table->{blurb}; - $vars->{token} = issue_hash_token([$table_name]); - $vars->{table_data} = to_json({ - fields => \@fields, - id_field => $id_field, - data => $data, - }); + $vars->{table_name} = $table_name; + $vars->{blurb} = $table->{blurb}; + $vars->{token} = issue_hash_token([$table_name]); + $vars->{table_data} + = to_json({fields => \@fields, id_field => $id_field, data => $data,}); } __PACKAGE__->NAME; diff --git a/extensions/Ember/Extension.pm b/extensions/Ember/Extension.pm index 8bb558c6f..d3e322919 100644 --- a/extensions/Ember/Extension.pm +++ b/extensions/Ember/Extension.pm @@ -16,9 +16,9 @@ use parent qw(Bugzilla::Extension); our $VERSION = '0.01'; sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{Ember} = "Bugzilla::Extension::Ember::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{Ember} = "Bugzilla::Extension::Ember::WebService"; } __PACKAGE__->NAME; diff --git a/extensions/Ember/lib/FakeBug.pm b/extensions/Ember/lib/FakeBug.pm index 46fef4ea7..32cbb5287 100644 --- a/extensions/Ember/lib/FakeBug.pm +++ b/extensions/Ember/lib/FakeBug.pm @@ -16,62 +16,64 @@ use Bugzilla::Bug; our $AUTOLOAD; sub new { - my $class = shift; - my $self = shift; - bless $self, $class; - return $self; + my $class = shift; + my $self = shift; + bless $self, $class; + return $self; } sub AUTOLOAD { - my $self = shift; - my $name = $AUTOLOAD; - $name =~ s/.*://; - return exists $self->{$name} ? $self->{$name} : undef; + my $self = shift; + my $name = $AUTOLOAD; + $name =~ s/.*://; + return exists $self->{$name} ? $self->{$name} : undef; } sub check_can_change_field { - return Bugzilla::Bug::check_can_change_field(@_); + return Bugzilla::Bug::check_can_change_field(@_); } -sub id { return undef; } +sub id { return undef; } sub product_obj { return $_[0]->{product_obj}; } -sub reporter { return Bugzilla->user; } +sub reporter { return Bugzilla->user; } 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 @statuses = @{ Bugzilla::Status->can_change_to }; - - # UNCONFIRMED is only a valid status if it is enabled in this product. - if (!$self->product_obj->allows_unconfirmed) { - @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; - } - - my %choices = ( - bug_status => \@statuses, - 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; - - $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 @statuses = @{Bugzilla::Status->can_change_to}; + + # UNCONFIRMED is only a valid status if it is enabled in this product. + if (!$self->product_obj->allows_unconfirmed) { + @statuses = grep { $_->name ne 'UNCONFIRMED' } @statuses; + } + + my %choices = ( + bug_status => \@statuses, + 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; + + $self->{'choices'} = \%choices; + return $self->{'choices'}; } 1; diff --git a/extensions/Ember/lib/WebService.pm b/extensions/Ember/lib/WebService.pm index 10c828537..6ad33cd81 100644 --- a/extensions/Ember/lib/WebService.pm +++ b/extensions/Ember/lib/WebService.pm @@ -12,8 +12,8 @@ use strict; use warnings; use parent qw(Bugzilla::WebService - Bugzilla::WebService::Bug - Bugzilla::WebService::Product); + Bugzilla::WebService::Bug + Bugzilla::WebService::Product); use Bugzilla::Bug; use Bugzilla::Component; @@ -29,69 +29,68 @@ use Scalar::Util qw(blessed); use Storable qw(dclone); use constant PUBLIC_METHODS => qw( - bug - create - get_attachments - search - show + bug + create + get_attachments + search + show ); -use constant DATE_FIELDS => { - show => ['last_updated'], -}; +use constant DATE_FIELDS => {show => ['last_updated'],}; use constant FIELD_TYPE_MAP => { - 0 => 'unknown', - 1 => 'freetext', - 2 => 'single_select', - 3 => 'multiple_select', - 4 => 'textarea', - 5 => 'datetime', - 6 => 'date', - 7 => 'bug_id', - 8 => 'bug_urls', - 9 => 'keywords', - 99 => 'extension' + 0 => 'unknown', + 1 => 'freetext', + 2 => 'single_select', + 3 => 'multiple_select', + 4 => 'textarea', + 5 => 'datetime', + 6 => 'date', + 7 => 'bug_id', + 8 => 'bug_urls', + 9 => 'keywords', + 99 => 'extension' }; use constant NON_EDIT_FIELDS => qw( - assignee_accessible - bug_group - bug_id - commenter - cclist_accessible - content - creation_ts - days_elapsed - everconfirmed - qacontact_accessible - reporter - reporter_accessible - restrict_comments - tag - votes + assignee_accessible + bug_group + bug_id + commenter + cclist_accessible + content + creation_ts + days_elapsed + everconfirmed + qacontact_accessible + reporter + reporter_accessible + restrict_comments + tag + votes ); use constant BUG_CHOICE_FIELDS => qw( - bug_status - component - product - resolution - target_milestone - version + bug_status + component + product + resolution + target_milestone + version ); use constant DEFAULT_VALUE_MAP => { - op_sys => 'defaultopsys', - rep_platform => 'defaultplatform', - priority => 'defaultpriority', - bug_severity => 'defaultseverity' + op_sys => 'defaultopsys', + rep_platform => 'defaultplatform', + priority => 'defaultpriority', + bug_severity => 'defaultseverity' }; sub API_NAMES { - # Internal field names converted to the API equivalents - my %api_names = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - return \%api_names; + + # Internal field names converted to the API equivalents + my %api_names = reverse %{Bugzilla::Bug::FIELD_MAP()}; + return \%api_names; } ############### @@ -99,227 +98,221 @@ sub API_NAMES { ############### sub create { - my ($self, $params) = @_; + my ($self, $params) = @_; - Bugzilla->login(LOGIN_REQUIRED); - Bugzilla->switch_to_shadow_db(); + Bugzilla->login(LOGIN_REQUIRED); + Bugzilla->switch_to_shadow_db(); - my $product = delete $params->{product}; - $product || ThrowCodeError('params_required', - { function => 'Ember.create', params => ['product'] }); + my $product = delete $params->{product}; + $product + || ThrowCodeError('params_required', + {function => 'Ember.create', params => ['product']}); - my $product_obj = Bugzilla::Product->check($product); + my $product_obj = Bugzilla::Product->check($product); - my $fake_bug = Bugzilla::Extension::Ember::FakeBug->new( - { product_obj => $product_obj, reporter_id => Bugzilla->user->id }); + my $fake_bug = Bugzilla::Extension::Ember::FakeBug->new( + {product_obj => $product_obj, reporter_id => Bugzilla->user->id}); - my @fields = $self->_get_fields($fake_bug); + my @fields = $self->_get_fields($fake_bug); - return { - fields => \@fields - }; + return {fields => \@fields}; } sub show { - my ($self, $params) = @_; - my (@fields, $attachments, $comments, $data); - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + my ($self, $params) = @_; + my (@fields, $attachments, $comments, $data); + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - Bugzilla->switch_to_shadow_db(); + Bugzilla->switch_to_shadow_db(); - # Throw error if token was provided and user is not logged - # in meaning token was invalid/expired. - if (exists $params->{token} && !$user->id) { - ThrowUserError('invalid_token'); - } + # Throw error if token was provided and user is not logged + # in meaning token was invalid/expired. + if (exists $params->{token} && !$user->id) { + ThrowUserError('invalid_token'); + } - my $bug_id = delete $params->{id}; - $bug_id || ThrowCodeError('params_required', - { function => 'Ember.show', params => ['id'] }); + my $bug_id = delete $params->{id}; + $bug_id + || ThrowCodeError('params_required', + {function => 'Ember.show', params => ['id']}); - my $bug = Bugzilla::Bug->check($bug_id); + my $bug = Bugzilla::Bug->check($bug_id); - my $bug_hash = $self->_bug_to_hash($bug, $params); + my $bug_hash = $self->_bug_to_hash($bug, $params); - # Only return changes since last_updated if provided - my $last_updated = delete $params->{last_updated}; - if ($last_updated) { - trick_taint($last_updated); + # Only return changes since last_updated if provided + my $last_updated = delete $params->{last_updated}; + if ($last_updated) { + trick_taint($last_updated); - my $updated_fields = - $dbh->selectcol_arrayref('SELECT fieldid FROM bugs_activity - WHERE bug_when > ? AND bug_id = ?', - undef, ($last_updated, $bug->id)); + my $updated_fields = $dbh->selectcol_arrayref( + 'SELECT fieldid FROM bugs_activity + WHERE bug_when > ? AND bug_id = ?', undef, + ($last_updated, $bug->id) + ); - if (@$updated_fields) { - # Also add in the delta_ts value which is in the bugs_activity - # entries - push(@$updated_fields, get_field_id('delta_ts')); - @fields = $self->_get_fields($bug, $updated_fields); - } - } - # Return all the things - else { - @fields = $self->_get_fields($bug); - } + if (@$updated_fields) { - # Place the fields current value along with the field definition - foreach my $field (@fields) { - if (($field->{name} eq 'depends_on' - || $field->{name} eq 'blocks') - && scalar @{ $bug_hash->{$field->{name}} }) - { - my $bug_ids = delete $bug_hash->{$field->{name}}; - $user->visible_bugs($bug_ids); - my $bug_objs = Bugzilla::Bug->new_from_list($bug_ids); - - my @new_list; - foreach my $bug (@$bug_objs) { - my $data; - if ($user->can_see_bug($bug)) { - $data = { - id => $bug->id, - status => $bug->bug_status, - summary => $bug->short_desc - }; - } - else { - $data = { id => $bug->id }; - } - push(@new_list, $data); - } - $field->{current_value} = \@new_list; + # Also add in the delta_ts value which is in the bugs_activity + # entries + push(@$updated_fields, get_field_id('delta_ts')); + @fields = $self->_get_fields($bug, $updated_fields); + } + } + + # Return all the things + else { + @fields = $self->_get_fields($bug); + } + + # Place the fields current value along with the field definition + foreach my $field (@fields) { + if (($field->{name} eq 'depends_on' || $field->{name} eq 'blocks') + && scalar @{$bug_hash->{$field->{name}}}) + { + my $bug_ids = delete $bug_hash->{$field->{name}}; + $user->visible_bugs($bug_ids); + my $bug_objs = Bugzilla::Bug->new_from_list($bug_ids); + + my @new_list; + foreach my $bug (@$bug_objs) { + my $data; + if ($user->can_see_bug($bug)) { + $data + = {id => $bug->id, status => $bug->bug_status, summary => $bug->short_desc}; } else { - $field->{current_value} = delete $bug_hash->{$field->{name}} || ''; + $data = {id => $bug->id}; } + push(@new_list, $data); + } + $field->{current_value} = \@new_list; } - - # Any left over bug values will be added to the field list - # These are extra fields that do not have a corresponding - # Field.pm object - if (!$last_updated) { - foreach my $key (keys %$bug_hash) { - my $field = { - name => $key, - current_value => $bug_hash->{$key} - }; - my $name = Bugzilla::Bug::FIELD_MAP()->{$key} || $key; - $field->{can_edit} = $self->_can_change_field($name, $bug); - push(@fields, $field); - } + else { + $field->{current_value} = delete $bug_hash->{$field->{name}} || ''; + } + } + + # Any left over bug values will be added to the field list + # These are extra fields that do not have a corresponding + # Field.pm object + if (!$last_updated) { + foreach my $key (keys %$bug_hash) { + my $field = {name => $key, current_value => $bug_hash->{$key}}; + my $name = Bugzilla::Bug::FIELD_MAP()->{$key} || $key; + $field->{can_edit} = $self->_can_change_field($name, $bug); + push(@fields, $field); } + } - # Complete the return data - my $data = { id => $bug->id, fields => \@fields }; + # Complete the return data + my $data = {id => $bug->id, fields => \@fields}; - return $data; + return $data; } sub search { - my ($self, $params) = @_; - - my $total; - if (exists $params->{offset} && exists $params->{limit}) { - my $count_params = dclone($params); - delete $count_params->{offset}; - delete $count_params->{limit}; - $count_params->{count_only} = 1; - $total = $self->SUPER::search($count_params); - } - - my $result = $self->SUPER::search($params); - $result->{total} = defined $total ? $total : scalar(@{ $result->{bugs} }); - return $result; + my ($self, $params) = @_; + + my $total; + if (exists $params->{offset} && exists $params->{limit}) { + my $count_params = dclone($params); + delete $count_params->{offset}; + delete $count_params->{limit}; + $count_params->{count_only} = 1; + $total = $self->SUPER::search($count_params); + } + + my $result = $self->SUPER::search($params); + $result->{total} = defined $total ? $total : scalar(@{$result->{bugs}}); + return $result; } sub bug { - my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - - my $bug_id = delete $params->{id}; - $bug_id || ThrowCodeError('param_required', - { function => 'Ember.bug', param => 'id' }); - - my ($comments, $attachments) = ([], []); - my $bug = $self->get({ ids => [ $bug_id ] }); - $bug = $bug->{bugs}->[0]; - - # Only return changes since last_updated if provided - my $last_updated = delete $params->{last_updated}; - if ($last_updated) { - trick_taint($last_updated); - my $updated_fields = $dbh->selectcol_arrayref('SELECT fielddefs.name + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + + my $bug_id = delete $params->{id}; + $bug_id + || ThrowCodeError('param_required', {function => 'Ember.bug', param => 'id'}); + + my ($comments, $attachments) = ([], []); + my $bug = $self->get({ids => [$bug_id]}); + $bug = $bug->{bugs}->[0]; + + # Only return changes since last_updated if provided + my $last_updated = delete $params->{last_updated}; + if ($last_updated) { + trick_taint($last_updated); + my $updated_fields = $dbh->selectcol_arrayref( + 'SELECT fielddefs.name FROM fielddefs INNER JOIN bugs_activity ON fielddefs.id = bugs_activity.fieldid WHERE bugs_activity.bug_when > ? AND bugs_activity.bug_id = ?', - undef, ($last_updated, $bug->{id})); - - my %field_map = reverse %{ Bugzilla::Bug::FIELD_MAP() }; - $field_map{'flagtypes.name'} = 'flags'; - - my $changed_bug = {}; - foreach my $field (@$updated_fields) { - my $field_name = $field_map{$field} || $field; - if ($bug->{$field_name}) { - $changed_bug->{$field_name} = $bug->{$field_name}; - } - } - $bug = $changed_bug; + undef, ($last_updated, $bug->{id}) + ); + + my %field_map = reverse %{Bugzilla::Bug::FIELD_MAP()}; + $field_map{'flagtypes.name'} = 'flags'; + + my $changed_bug = {}; + foreach my $field (@$updated_fields) { + my $field_name = $field_map{$field} || $field; + if ($bug->{$field_name}) { + $changed_bug->{$field_name} = $bug->{$field_name}; + } + } + $bug = $changed_bug; - # Find any comments created since the last_updated date - $comments = $self->comments({ ids => $bug_id, new_since => $last_updated }); + # Find any comments created since the last_updated date + $comments = $self->comments({ids => $bug_id, new_since => $last_updated}); - # Find any new attachments or modified attachments since the - # last_updated date - my $updated_attachments = - $dbh->selectcol_arrayref('SELECT attach_id FROM attachments + # Find any new attachments or modified attachments since the + # last_updated date + my $updated_attachments = $dbh->selectcol_arrayref( + 'SELECT attach_id FROM attachments WHERE (creation_ts > ? OR modification_time > ?) - AND bug_id = ?', - undef, ($last_updated, $last_updated, $bug->{id})); - if ($updated_attachments) { - $attachments = $self->_get_attachments({ attachment_ids => $updated_attachments, - exclude_fields => ['data'] }); - } + AND bug_id = ?', undef, + ($last_updated, $last_updated, $bug->{id}) + ); + if ($updated_attachments) { + $attachments + = $self->_get_attachments({ + attachment_ids => $updated_attachments, exclude_fields => ['data'] + }); } - else { - $comments = $self->comments({ ids => [ $bug_id ] }); - $attachments = $self->_get_attachments({ ids => [ $bug_id ], - exclude_fields => ['data'] }); + } + else { + $comments = $self->comments({ids => [$bug_id]}); + $attachments + = $self->_get_attachments({ids => [$bug_id], exclude_fields => ['data']}); - } + } - $comments = $comments->{bugs}->{$bug_id}->{comments}; + $comments = $comments->{bugs}->{$bug_id}->{comments}; - return { - bug => $bug, - comments => $comments, - attachments => $attachments, - }; + return {bug => $bug, comments => $comments, attachments => $attachments,}; } sub get_attachments { - my ($self, $params) = @_; - my $attachments = $self->_get_attachments($params); - my $flag_types = []; - my $bug; - if ($params->{ids}) { - $bug = Bugzilla::Bug->check($params->{ids}->[0]); - $flag_types = $self->_get_flag_types_bug($bug, 'attachment'); - } - elsif ($params->{attachment_ids} && @$attachments) { - $bug = Bugzilla::Bug->check($attachments->[0]->{bug_id}); - $flag_types = $self->_get_flag_types_all($bug, 'attachment')->{attachment}; - } - if (@$flag_types) { - @$flag_types = map { $self->_flagtype_to_hash($_, $bug) } @$flag_types; - } - return { - attachments => $attachments, - flag_types => $flag_types - }; + my ($self, $params) = @_; + my $attachments = $self->_get_attachments($params); + my $flag_types = []; + my $bug; + if ($params->{ids}) { + $bug = Bugzilla::Bug->check($params->{ids}->[0]); + $flag_types = $self->_get_flag_types_bug($bug, 'attachment'); + } + elsif ($params->{attachment_ids} && @$attachments) { + $bug = Bugzilla::Bug->check($attachments->[0]->{bug_id}); + $flag_types = $self->_get_flag_types_all($bug, 'attachment')->{attachment}; + } + if (@$flag_types) { + @$flag_types = map { $self->_flagtype_to_hash($_, $bug) } @$flag_types; + } + return {attachments => $attachments, flag_types => $flag_types}; } ################### @@ -327,449 +320,477 @@ sub get_attachments { ################### sub _get_attachments { - my ($self, $params) = @_; - my $user = Bugzilla->user; - - my $attachments = $self->attachments($params); - - if ($params->{ids}) { - $attachments = [ map { @{ $attachments->{bugs}->{$_} } } - keys %{ $attachments->{bugs} } ]; - } - elsif ($params->{attachment_ids}) { - $attachments = [ map { $attachments->{attachments}->{$_} } - keys %{ $attachments->{attachments} } ]; - } - - foreach my $attachment (@$attachments) { - $attachment->{can_edit} - = ($user->login eq $attachment->{creator} || $user->in_group('editbugs')) ? 1 : 0; - } - - return $attachments; + my ($self, $params) = @_; + my $user = Bugzilla->user; + + my $attachments = $self->attachments($params); + + if ($params->{ids}) { + $attachments + = [map { @{$attachments->{bugs}->{$_}} } keys %{$attachments->{bugs}}]; + } + elsif ($params->{attachment_ids}) { + $attachments = [map { $attachments->{attachments}->{$_} } + keys %{$attachments->{attachments}}]; + } + + foreach my $attachment (@$attachments) { + $attachment->{can_edit} + = ($user->login eq $attachment->{creator} || $user->in_group('editbugs')) + ? 1 + : 0; + } + + return $attachments; } sub _get_fields { - my ($self, $bug, $field_ids) = @_; - my $user = Bugzilla->user; - - # Load the field objects we need - my @field_objs; - if ($field_ids) { - # Load just the fields that match the ids provided - @field_objs = @{ Bugzilla::Field->match({ id => $field_ids }) }; - + my ($self, $bug, $field_ids) = @_; + my $user = Bugzilla->user; + + # Load the field objects we need + my @field_objs; + if ($field_ids) { + + # Load just the fields that match the ids provided + @field_objs = @{Bugzilla::Field->match({id => $field_ids})}; + + } + else { + # load up standard fields + @field_objs = @{Bugzilla->fields({custom => 0})}; + + # Load custom fields + my $cf_params = {product => $bug->product_obj}; + $cf_params->{component} = $bug->component_obj if $bug->can('component_obj'); + $cf_params->{bug_id} = $bug->id if $bug->id; + push(@field_objs, Bugzilla->active_custom_fields($cf_params)); + } + + my $return_groups = my $return_flags = $field_ids ? 0 : 1; + my @fields; + foreach my $field (@field_objs) { + $return_groups = 1 if $field->name eq 'bug_group'; + $return_flags = 1 if $field->name eq 'flagtypes.name'; + + # Skip any special fields containing . in the name such as + # for attachments.*, etc. + next if $field->name =~ /\./; + + # Remove time tracking fields if the user is privileged + next + if (grep($field->name eq $_, TIMETRACKING_FIELDS) + && !Bugzilla->user->is_timetracker); + + # These fields should never be set by the user + next if grep($field->name eq $_, NON_EDIT_FIELDS); + + # We already selected a product so no need to display all choices + # Might as well skip classification for new bugs as well. + next + if (!$bug->id + && ($field->name eq 'product' || $field->name eq 'classification')); + + # Skip assigned_to and qa_contact for new bugs if user not in + # editbugs group + next + if (!$bug->id + && ($field->name eq 'assigned_to' || $field->name eq 'qa_contact') + && !$user->in_group('editbugs', $bug->product_obj->id)); + +# Do not display obsolete fields or fields that should be displayed for create bug form + next + if (!$bug->id && $field->custom && ($field->obsolete || !$field->enter_bug)); + + push(@fields, $self->_field_to_hash($field, $bug)); + } + + # Add group information as separate field + if ($return_groups) { + push( + @fields, + { + description => $self->type('string', 'Groups'), + is_custom => $self->type('boolean', 0), + is_mandatory => $self->type('boolean', 0), + name => $self->type('string', 'groups'), + values => [ + map { $self->_group_to_hash($_, $bug) } @{$bug->product_obj->groups_available} + ] + } + ); + } + + # Add flag information as separate field + if ($return_flags) { + my $flag_hash; + if ($bug->id) { + foreach my $flag_type ('bug', 'attachment') { + $flag_hash->{$flag_type} = $self->_get_flag_types_bug($bug, $flag_type); + } } else { - # load up standard fields - @field_objs = @{ Bugzilla->fields({ custom => 0 }) }; - - # Load custom fields - my $cf_params = { product => $bug->product_obj }; - $cf_params->{component} = $bug->component_obj if $bug->can('component_obj'); - $cf_params->{bug_id} = $bug->id if $bug->id; - push(@field_objs, Bugzilla->active_custom_fields($cf_params)); - } - - my $return_groups = my $return_flags = $field_ids ? 0 : 1; - my @fields; - foreach my $field (@field_objs) { - $return_groups = 1 if $field->name eq 'bug_group'; - $return_flags = 1 if $field->name eq 'flagtypes.name'; - - # Skip any special fields containing . in the name such as - # for attachments.*, etc. - next if $field->name =~ /\./; - - # Remove time tracking fields if the user is privileged - next if (grep($field->name eq $_, TIMETRACKING_FIELDS) - && !Bugzilla->user->is_timetracker); - - # These fields should never be set by the user - next if grep($field->name eq $_, NON_EDIT_FIELDS); - - # We already selected a product so no need to display all choices - # Might as well skip classification for new bugs as well. - next if (!$bug->id && ($field->name eq 'product' || $field->name eq 'classification')); - - # Skip assigned_to and qa_contact for new bugs if user not in - # editbugs group - next if (!$bug->id - && ($field->name eq 'assigned_to' || $field->name eq 'qa_contact') - && !$user->in_group('editbugs', $bug->product_obj->id)); - - # Do not display obsolete fields or fields that should be displayed for create bug form - next if (!$bug->id && $field->custom - && ($field->obsolete || !$field->enter_bug)); - - push(@fields, $self->_field_to_hash($field, $bug)); + $flag_hash = $self->_get_flag_types_all($bug); } - - # Add group information as separate field - if ($return_groups) { - push(@fields, { - description => $self->type('string', 'Groups'), - is_custom => $self->type('boolean', 0), - is_mandatory => $self->type('boolean', 0), - name => $self->type('string', 'groups'), - values => [ map { $self->_group_to_hash($_, $bug) } - @{ $bug->product_obj->groups_available } ] - }); - } - - # Add flag information as separate field - if ($return_flags) { - my $flag_hash; - if ($bug->id) { - foreach my $flag_type ('bug', 'attachment') { - $flag_hash->{$flag_type} = $self->_get_flag_types_bug($bug, $flag_type); - } - } - else { - $flag_hash = $self->_get_flag_types_all($bug); - } - my @flag_values; - foreach my $flag_type ('bug', 'attachment') { - foreach my $flag (@{ $flag_hash->{$flag_type} }) { - push(@flag_values, $self->_flagtype_to_hash($flag, $bug)); - } - } - - push(@fields, { - description => $self->type('string', 'Flags'), - is_custom => $self->type('boolean', 0), - is_mandatory => $self->type('boolean', 0), - name => $self->type('string', 'flags'), - values => \@flag_values - }); + my @flag_values; + foreach my $flag_type ('bug', 'attachment') { + foreach my $flag (@{$flag_hash->{$flag_type}}) { + push(@flag_values, $self->_flagtype_to_hash($flag, $bug)); + } } - return @fields; + push( + @fields, + { + description => $self->type('string', 'Flags'), + is_custom => $self->type('boolean', 0), + is_mandatory => $self->type('boolean', 0), + name => $self->type('string', 'flags'), + values => \@flag_values + } + ); + } + + return @fields; } sub _get_flag_types_all { - my ($self, $bug, $type) = @_; - my $params = { is_active => 1 }; - $params->{target_type} = $type if $type; - return $bug->product_obj->flag_types($params); + my ($self, $bug, $type) = @_; + my $params = {is_active => 1}; + $params->{target_type} = $type if $type; + return $bug->product_obj->flag_types($params); } sub _get_flag_types_bug { - my ($self, $bug, $type) = @_; - my $params = { - target_type => $type, - product_id => $bug->product_obj->id, - component_id => $bug->component_obj->id, - bug_id => $bug->id, - active_or_has_flags => $bug->id, - }; - return Bugzilla::Flag->_flag_types($params); + my ($self, $bug, $type) = @_; + my $params = { + target_type => $type, + product_id => $bug->product_obj->id, + component_id => $bug->component_obj->id, + bug_id => $bug->id, + active_or_has_flags => $bug->id, + }; + return Bugzilla::Flag->_flag_types($params); } sub _group_to_hash { - my ($self, $group, $bug) = @_; + my ($self, $group, $bug) = @_; - my $data = { - description => $self->type('string', $group->description), - name => $self->type('string', $group->name) - }; + my $data = { + description => $self->type('string', $group->description), + name => $self->type('string', $group->name) + }; - if ($group->name eq $bug->product_obj->default_security_group) { - $data->{security_default} = $self->type('boolean', 1); - } + if ($group->name eq $bug->product_obj->default_security_group) { + $data->{security_default} = $self->type('boolean', 1); + } - return $data; + return $data; } sub _field_to_hash { - my ($self, $field, $bug) = @_; - - my $data = { - is_custom => $self->type('boolean', $field->custom), - description => $self->type('string', $field->description), - is_mandatory => $self->type('boolean', $field->is_mandatory), - }; - - if ($field->custom) { - $data->{type} = $self->type('string', FIELD_TYPE_MAP->{$field->type}); - } - - # Use the API name if one is present instead of the internal field name - my $field_name = $field->name; - $field_name = API_NAMES->{$field_name} || $field_name; - - if ($field_name eq 'longdesc') { - $field_name = $bug->id ? 'comment' : 'description'; - } - - $data->{name} = $self->type('string', $field_name); - - # Set can_edit true or false if we are editing a current bug - if ($bug->id) { - # 'delta_ts's can_edit is incorrectly set in fielddefs - $data->{can_edit} = $field->name eq 'delta_ts' - ? $self->type('boolean', 0) - : $self->_can_change_field($field, $bug); - } - - # description for creating a new bug, otherwise comment - - # FIXME 'version' and 'target_milestone' types are incorrectly set in fielddefs - if ($field->is_select || $field->name eq 'version' || $field->name eq 'target_milestone') { - $data->{values} = [ $self->_get_field_values($field, $bug) ]; - } - - # Add default values for specific fields if new bug - if (!$bug->id && DEFAULT_VALUE_MAP->{$field->name}) { - my $default_value = Bugzilla->params->{DEFAULT_VALUE_MAP->{$field->name}}; - $data->{default_value} = $default_value; - } - - return $data; + my ($self, $field, $bug) = @_; + + my $data = { + is_custom => $self->type('boolean', $field->custom), + description => $self->type('string', $field->description), + is_mandatory => $self->type('boolean', $field->is_mandatory), + }; + + if ($field->custom) { + $data->{type} = $self->type('string', FIELD_TYPE_MAP->{$field->type}); + } + + # Use the API name if one is present instead of the internal field name + my $field_name = $field->name; + $field_name = API_NAMES->{$field_name} || $field_name; + + if ($field_name eq 'longdesc') { + $field_name = $bug->id ? 'comment' : 'description'; + } + + $data->{name} = $self->type('string', $field_name); + + # Set can_edit true or false if we are editing a current bug + if ($bug->id) { + + # 'delta_ts's can_edit is incorrectly set in fielddefs + $data->{can_edit} + = $field->name eq 'delta_ts' + ? $self->type('boolean', 0) + : $self->_can_change_field($field, $bug); + } + + # description for creating a new bug, otherwise comment + + # FIXME 'version' and 'target_milestone' types are incorrectly set in fielddefs + if ( $field->is_select + || $field->name eq 'version' + || $field->name eq 'target_milestone') + { + $data->{values} = [$self->_get_field_values($field, $bug)]; + } + + # Add default values for specific fields if new bug + if (!$bug->id && DEFAULT_VALUE_MAP->{$field->name}) { + my $default_value = Bugzilla->params->{DEFAULT_VALUE_MAP->{$field->name}}; + $data->{default_value} = $default_value; + } + + return $data; } sub _value_to_hash { - my ($self, $value, $bug) = @_; + my ($self, $value, $bug) = @_; - my $data = { name=> $self->type('string', $value->name) }; + my $data = {name => $self->type('string', $value->name)}; - if ($bug->{bug_id}) { - $data->{is_active} = $self->type('boolean', $value->is_active); - } + if ($bug->{bug_id}) { + $data->{is_active} = $self->type('boolean', $value->is_active); + } - if ($value->can('sortkey')) { - $data->{sort_key} = $self->type('int', $value->sortkey || 0); - } + if ($value->can('sortkey')) { + $data->{sort_key} = $self->type('int', $value->sortkey || 0); + } - if ($value->isa('Bugzilla::Component')) { - $data->{default_assignee} = $self->_user_to_hash($value->default_assignee); - $data->{initial_cc} = [ map { $self->_user_to_hash($_) } @{ $value->initial_cc } ]; - if (Bugzilla->params->{useqacontact} && $value->default_qa_contact) { - $data->{default_qa_contact} = $self->_user_to_hash($value->default_qa_contact); - } + if ($value->isa('Bugzilla::Component')) { + $data->{default_assignee} = $self->_user_to_hash($value->default_assignee); + $data->{initial_cc} = [map { $self->_user_to_hash($_) } @{$value->initial_cc}]; + if (Bugzilla->params->{useqacontact} && $value->default_qa_contact) { + $data->{default_qa_contact} = $self->_user_to_hash($value->default_qa_contact); } + } - if ($value->can('description')) { - $data->{description} = $self->type('string', $value->description); - } + if ($value->can('description')) { + $data->{description} = $self->type('string', $value->description); + } - return $data; + return $data; } sub _user_to_hash { - my ($self, $user) = @_; + my ($self, $user) = @_; - my $data = { - real_name => $self->type('string', $user->name) - }; + my $data = {real_name => $self->type('string', $user->name)}; - if (Bugzilla->user->id) { - $data->{email} = $self->type('string', $user->email); - } + if (Bugzilla->user->id) { + $data->{email} = $self->type('string', $user->email); + } - return $data; + return $data; } sub _get_field_values { - my ($self, $field, $bug) = @_; - - # Certain fields are special and should use $bug->choices - # to determine editability and not $bug->check_can_change_field - my @values; - if (grep($field->name eq $_, BUG_CHOICE_FIELDS)) { - @values = @{ $bug->choices->{$field->name} }; + my ($self, $field, $bug) = @_; + + # Certain fields are special and should use $bug->choices + # to determine editability and not $bug->check_can_change_field + my @values; + if (grep($field->name eq $_, BUG_CHOICE_FIELDS)) { + @values = @{$bug->choices->{$field->name}}; + } + else { + # We need to get the values from the product for + # component, version, and milestones. + if ($field->name eq 'component') { + @values = @{$bug->product_obj->components}; + } + elsif ($field->name eq 'target_milestone') { + @values = @{$bug->product_obj->milestones}; + } + elsif ($field->name eq 'version') { + @values = @{$bug->product_obj->versions}; } else { - # We need to get the values from the product for - # component, version, and milestones. - if ($field->name eq 'component') { - @values = @{ $bug->product_obj->components }; - } - elsif ($field->name eq 'target_milestone') { - @values = @{ $bug->product_obj->milestones }; - } - elsif ($field->name eq 'version') { - @values = @{ $bug->product_obj->versions }; - } - else { - @values = @{ $field->legal_values }; - } + @values = @{$field->legal_values}; } + } - my @filtered_values; - foreach my $value (@values) { - next if !$bug->id && !$value->is_active; - next if $bug->id && !$self->_can_change_field($field, $bug, $value->name); - push(@filtered_values, $value); - } + my @filtered_values; + foreach my $value (@values) { + next if !$bug->id && !$value->is_active; + next if $bug->id && !$self->_can_change_field($field, $bug, $value->name); + push(@filtered_values, $value); + } - return map { $self->_value_to_hash($_, $bug) } @filtered_values; + return map { $self->_value_to_hash($_, $bug) } @filtered_values; } sub _can_change_field { - my ($self, $field, $bug, $value) = @_; - my $user = Bugzilla->user; - my $field_name = blessed $field ? $field->name : $field; - - # Cannot set resolution on bug creation - return $self->type('boolean', 0) if ($field_name eq 'resolution' && !$bug->{bug_id}); - - # Cannot edit an obsolete or inactive custom field - return $self->type('boolean', 0) if (blessed $field && $field->custom && $field->obsolete); - - # If not a multi-select or single-select, value is not provided - # and we just check if the field itself is editable by the user. - if (!defined $value) { - return $self->type('boolean', $bug->check_can_change_field($field_name, 0, 1)); - } - - return $self->type('boolean', $bug->check_can_change_field($field_name, '', $value)); + my ($self, $field, $bug, $value) = @_; + my $user = Bugzilla->user; + my $field_name = blessed $field ? $field->name : $field; + + # Cannot set resolution on bug creation + return $self->type('boolean', 0) + if ($field_name eq 'resolution' && !$bug->{bug_id}); + + # Cannot edit an obsolete or inactive custom field + return $self->type('boolean', 0) + if (blessed $field && $field->custom && $field->obsolete); + + # If not a multi-select or single-select, value is not provided + # and we just check if the field itself is editable by the user. + if (!defined $value) { + return $self->type('boolean', $bug->check_can_change_field($field_name, 0, 1)); + } + + return $self->type('boolean', + $bug->check_can_change_field($field_name, '', $value)); } sub _flag_to_hash { - my ($self, $flag) = @_; - - my $data = { - id => $self->type('int', $flag->id), - name => $self->type('string', $flag->name), - type_id => $self->type('int', $flag->type_id), - creation_date => $self->type('dateTime', $flag->creation_date), - modification_date => $self->type('dateTime', $flag->modification_date), - status => $self->type('string', $flag->status) - }; - - foreach my $field (qw(setter requestee)) { - my $field_id = $field . "_id"; - $data->{$field} = $self->_user_to_hash($flag->$field) if $flag->$field_id; - } - - $data->{type} = $flag->attach_id ? 'attachment' : 'bug'; - $data->{attach_id} = $flag->attach_id if $flag->attach_id; - - return $data; + my ($self, $flag) = @_; + + my $data = { + id => $self->type('int', $flag->id), + name => $self->type('string', $flag->name), + type_id => $self->type('int', $flag->type_id), + creation_date => $self->type('dateTime', $flag->creation_date), + modification_date => $self->type('dateTime', $flag->modification_date), + status => $self->type('string', $flag->status) + }; + + foreach my $field (qw(setter requestee)) { + my $field_id = $field . "_id"; + $data->{$field} = $self->_user_to_hash($flag->$field) if $flag->$field_id; + } + + $data->{type} = $flag->attach_id ? 'attachment' : 'bug'; + $data->{attach_id} = $flag->attach_id if $flag->attach_id; + + return $data; } sub _flagtype_to_hash { - my ($self, $flagtype, $bug) = @_; - my $user = Bugzilla->user; - - my $cansetflag = $user->can_set_flag($flagtype); - my $canrequestflag = $user->can_request_flag($flagtype); - - my $data = { - id => $self->type('int' , $flagtype->id), - name => $self->type('string' , $flagtype->name), - description => $self->type('string' , $flagtype->description), - type => $self->type('string' , $flagtype->target_type), - is_requestable => $self->type('boolean', $flagtype->is_requestable), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), - can_set_flag => $self->type('boolean', $cansetflag), - can_request_flag => $self->type('boolean', $canrequestflag) - }; - - my @values; - foreach my $value ('?','+','-') { - push(@values, $self->type('string', $value)); - } - $data->{values} = \@values; - - # if we're creating a bug, we need to return all valid flags for - # this product, as well as inclusions & exclusions so ember can - # display relevant flags once the component is selected - if (!$bug->id) { - my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, $bug->product_obj->id); - my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, $bug->product_obj->id); - # if we have both inclusions and exclusions, the exclusions are redundant - $exclusions = [] if @$inclusions && @$exclusions; - # no need to return anything if there's just "any component" - $data->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; - $data->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; - } - - return $data; + my ($self, $flagtype, $bug) = @_; + my $user = Bugzilla->user; + + my $cansetflag = $user->can_set_flag($flagtype); + my $canrequestflag = $user->can_request_flag($flagtype); + + my $data = { + id => $self->type('int', $flagtype->id), + name => $self->type('string', $flagtype->name), + description => $self->type('string', $flagtype->description), + type => $self->type('string', $flagtype->target_type), + is_requestable => $self->type('boolean', $flagtype->is_requestable), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), + can_set_flag => $self->type('boolean', $cansetflag), + can_request_flag => $self->type('boolean', $canrequestflag) + }; + + my @values; + foreach my $value ('?', '+', '-') { + push(@values, $self->type('string', $value)); + } + $data->{values} = \@values; + + # if we're creating a bug, we need to return all valid flags for + # this product, as well as inclusions & exclusions so ember can + # display relevant flags once the component is selected + if (!$bug->id) { + my $inclusions = $self->_flagtype_clusions_to_hash($flagtype->inclusions, + $bug->product_obj->id); + my $exclusions = $self->_flagtype_clusions_to_hash($flagtype->exclusions, + $bug->product_obj->id); + + # if we have both inclusions and exclusions, the exclusions are redundant + $exclusions = [] if @$inclusions && @$exclusions; + + # no need to return anything if there's just "any component" + $data->{inclusions} = $inclusions if @$inclusions && $inclusions->[0] ne ''; + $data->{exclusions} = $exclusions if @$exclusions && $exclusions->[0] ne ''; + } + + return $data; } sub _flagtype_clusions_to_hash { - my ($self, $clusions, $product_id) = @_; - my $result = []; - foreach my $key (keys %$clusions) { - my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); - if ($prod_id == 0 || $prod_id == $product_id) { - if ($comp_id) { - my $component = Bugzilla::Component->new({ id => $comp_id, cache => 1 }); - push @$result, $component->name; - } - else { - return [ '' ]; - } - } + my ($self, $clusions, $product_id) = @_; + my $result = []; + foreach my $key (keys %$clusions) { + my ($prod_id, $comp_id) = split(/:/, $clusions->{$key}, 2); + if ($prod_id == 0 || $prod_id == $product_id) { + if ($comp_id) { + my $component = Bugzilla::Component->new({id => $comp_id, cache => 1}); + push @$result, $component->name; + } + else { + return ['']; + } } - return $result; + } + return $result; } sub rest_resources { - return [ - # create page - single product name - qr{^/ember/create/(.*)$}, { - GET => { - method => 'create', - params => sub { - return { product => $_[0] }; - } - } - }, - # create page - one or more products - qr{^/ember/create$}, { - GET => { - method => 'create' - } - }, - # show bug page - single bug id - qr{^/ember/show/(\d+)$}, { - GET => { - method => 'show', - params => sub { - return { id => $_[0] }; - } - } - }, - # search - wrapper around SUPER::search which also includes the total - # number of bugs when using pagination - qr{^/ember/search$}, { - GET => { - method => 'search', - }, - }, - # get current bug attributes without field information - single bug id - qr{^/ember/bug/(\d+)$}, { - GET => { - method => 'bug', - params => sub { - return { id => $_[0] }; - } - } - }, - # attachments - wrapper around SUPER::attachments that also includes - # can_edit attribute - qr{^/ember/bug/(\d+)/attachments$}, { - GET => { - method => 'get_attachments', - params => sub { - return { ids => $_[0] }; - } - } - }, - qr{^/ember/bug/attachments/(\d+)$}, { - GET => { - method => 'get_attachments', - params => sub { - return { attachment_ids => $_[0] }; - } - } + return [ + # create page - single product name + qr{^/ember/create/(.*)$}, + { + GET => { + method => 'create', + params => sub { + return {product => $_[0]}; } - ]; -}; + } + }, + + # create page - one or more products + qr{^/ember/create$}, + {GET => {method => 'create'}}, + + # show bug page - single bug id + qr{^/ember/show/(\d+)$}, + { + GET => { + method => 'show', + params => sub { + return {id => $_[0]}; + } + } + }, + + # search - wrapper around SUPER::search which also includes the total + # number of bugs when using pagination + qr{^/ember/search$}, + {GET => {method => 'search',},}, + + # get current bug attributes without field information - single bug id + qr{^/ember/bug/(\d+)$}, + { + GET => { + method => 'bug', + params => sub { + return {id => $_[0]}; + } + } + }, + + # attachments - wrapper around SUPER::attachments that also includes + # can_edit attribute + qr{^/ember/bug/(\d+)/attachments$}, + { + GET => { + method => 'get_attachments', + params => sub { + return {ids => $_[0]}; + } + } + }, + qr{^/ember/bug/attachments/(\d+)$}, + { + GET => { + method => 'get_attachments', + params => sub { + return {attachment_ids => $_[0]}; + } + } + } + ]; +} 1; diff --git a/extensions/Example/Config.pm b/extensions/Example/Config.pm index e7782ef6c..696da2de9 100644 --- a/extensions/Example/Config.pm +++ b/extensions/Example/Config.pm @@ -12,21 +12,16 @@ use strict; use warnings; use constant NAME => 'Example'; -use constant REQUIRED_MODULES => [ - { - package => 'Data-Dumper', - module => 'Data::Dumper', - version => 0, - }, -]; +use constant REQUIRED_MODULES => + [{package => 'Data-Dumper', module => 'Data::Dumper', version => 0,},]; use constant OPTIONAL_MODULES => [ - { - package => 'Acme', - module => 'Acme', - version => 1.11, - feature => ['example_acme'], - }, + { + package => 'Acme', + module => 'Acme', + version => 1.11, + feature => ['example_acme'], + }, ]; __PACKAGE__->NAME; diff --git a/extensions/Example/Extension.pm b/extensions/Example/Extension.pm index 22c4042b5..9ecbf25a6 100644 --- a/extensions/Example/Extension.pm +++ b/extensions/Example/Extension.pm @@ -33,338 +33,355 @@ use constant REL_EXAMPLE => -127; our $VERSION = '1.0'; sub admin_editusers_action { - my ($self, $args) = @_; - my ($vars, $action, $user) = @$args{qw(vars action user)}; - my $template = Bugzilla->template; - - if ($action eq 'my_action') { - # Allow to restrict the search to any group the user is allowed to bless. - $vars->{'restrictablegroups'} = $user->bless_groups(); - $template->process('admin/users/search.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - } + my ($self, $args) = @_; + my ($vars, $action, $user) = @$args{qw(vars action user)}; + my $template = Bugzilla->template; + + if ($action eq 'my_action') { + + # Allow to restrict the search to any group the user is allowed to bless. + $vars->{'restrictablegroups'} = $user->bless_groups(); + $template->process('admin/users/search.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } } sub attachment_process_data { - my ($self, $args) = @_; - my $type = $args->{attributes}->{mimetype}; - my $filename = $args->{attributes}->{filename}; - - # Make sure images have the correct extension. - # Uncomment the two lines below to make this check effective. - if ($type =~ /^image\/(\w+)$/) { - my $format = $1; - if ($filename =~ /^(.+)(:?\.[^\.]+)$/) { - my $name = $1; - #$args->{attributes}->{filename} = "${name}.$format"; - } - else { - # The file has no extension. We append it. - #$args->{attributes}->{filename} .= ".$format"; - } + my ($self, $args) = @_; + my $type = $args->{attributes}->{mimetype}; + my $filename = $args->{attributes}->{filename}; + + # Make sure images have the correct extension. + # Uncomment the two lines below to make this check effective. + if ($type =~ /^image\/(\w+)$/) { + my $format = $1; + if ($filename =~ /^(.+)(:?\.[^\.]+)$/) { + my $name = $1; + + #$args->{attributes}->{filename} = "${name}.$format"; } + else { + # The file has no extension. We append it. + #$args->{attributes}->{filename} .= ".$format"; + } + } } sub auth_login_methods { - my ($self, $args) = @_; - my $modules = $args->{modules}; - if (exists $modules->{Example}) { - $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Login.pm'; - } + my ($self, $args) = @_; + my $modules = $args->{modules}; + if (exists $modules->{Example}) { + $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Login.pm'; + } } sub auth_verify_methods { - my ($self, $args) = @_; - my $modules = $args->{modules}; - if (exists $modules->{Example}) { - $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Verify.pm'; - } + my ($self, $args) = @_; + my $modules = $args->{modules}; + if (exists $modules->{Example}) { + $modules->{Example} = 'Bugzilla/Extension/Example/Auth/Verify.pm'; + } } sub bug_check_can_change_field { - my ($self, $args) = @_; - - my ($bug, $field, $new_value, $old_value, $priv_results) - = @$args{qw(bug field new_value old_value priv_results)}; - - my $user = Bugzilla->user; - - # Disallow a bug from being reopened if currently closed unless user - # is in 'admin' group - if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') { - if (!is_open_state($old_value) && is_open_state($new_value) - && !$user->in_group('admin')) - { - push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - return; - } - } + my ($self, $args) = @_; - # Disallow a bug's keywords from being edited unless user is the - # reporter of the bug - if ($field eq 'keywords' && $bug->product_obj->name eq 'Example' - && $user->login ne $bug->reporter->login) - { - push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER); - return; - } + my ($bug, $field, $new_value, $old_value, $priv_results) + = @$args{qw(bug field new_value old_value priv_results)}; + + my $user = Bugzilla->user; - # Allow updating of priority even if user cannot normally edit the bug - # and they are in group 'engineering' - if ($field eq 'priority' && $bug->product_obj->name eq 'Example' - && $user->in_group('engineering')) + # Disallow a bug from being reopened if currently closed unless user + # is in 'admin' group + if ($field eq 'bug_status' && $bug->product_obj->name eq 'Example') { + if (!is_open_state($old_value) + && is_open_state($new_value) + && !$user->in_group('admin')) { - push(@$priv_results, PRIVILEGES_REQUIRED_NONE); - return; + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + return; } + } + + # Disallow a bug's keywords from being edited unless user is the + # reporter of the bug + if ( $field eq 'keywords' + && $bug->product_obj->name eq 'Example' + && $user->login ne $bug->reporter->login) + { + push(@$priv_results, PRIVILEGES_REQUIRED_REPORTER); + return; + } + + # Allow updating of priority even if user cannot normally edit the bug + # and they are in group 'engineering' + if ( $field eq 'priority' + && $bug->product_obj->name eq 'Example' + && $user->in_group('engineering')) + { + push(@$priv_results, PRIVILEGES_REQUIRED_NONE); + return; + } } sub bug_columns { - my ($self, $args) = @_; - my $columns = $args->{'columns'}; - push (@$columns, "delta_ts AS example") + my ($self, $args) = @_; + my $columns = $args->{'columns'}; + push(@$columns, "delta_ts AS example"); } sub bug_end_of_create { - my ($self, $args) = @_; + my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $bug = $args->{'bug'}; - my $timestamp = $args->{'timestamp'}; + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $bug = $args->{'bug'}; + my $timestamp = $args->{'timestamp'}; - my $bug_id = $bug->id; - # Uncomment this line to see a line in your webserver's error log whenever - # you file a bug. - # warn "Bug $bug_id has been filed!"; + my $bug_id = $bug->id; + + # Uncomment this line to see a line in your webserver's error log whenever + # you file a bug. + # warn "Bug $bug_id has been filed!"; } sub bug_end_of_create_validators { - my ($self, $args) = @_; + my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $bug_params = $args->{'params'}; + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $bug_params = $args->{'params'}; - # Uncomment this line below to see a line in your webserver's error log - # containing all validated bug field values every time you file a bug. - # warn Dumper($bug_params); + # Uncomment this line below to see a line in your webserver's error log + # containing all validated bug field values every time you file a bug. + # warn Dumper($bug_params); - # This would remove all ccs from the bug, preventing ANY ccs from being - # added on bug creation. - # $bug_params->{cc} = []; + # This would remove all ccs from the bug, preventing ANY ccs from being + # added on bug creation. + # $bug_params->{cc} = []; } sub bug_start_of_update { - my ($self, $args) = @_; - - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my ($bug, $old_bug, $timestamp, $changes) = - @$args{qw(bug old_bug timestamp changes)}; - - foreach my $field (keys %$changes) { - my $used_to_be = $changes->{$field}->[0]; - my $now_it_is = $changes->{$field}->[1]; + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my ($bug, $old_bug, $timestamp, $changes) + = @$args{qw(bug old_bug timestamp changes)}; + + foreach my $field (keys %$changes) { + my $used_to_be = $changes->{$field}->[0]; + my $now_it_is = $changes->{$field}->[1]; + } + + my $old_summary = $old_bug->short_desc; + + my $status_message; + if (my $status_change = $changes->{'bug_status'}) { + my $old_status = new Bugzilla::Status({name => $status_change->[0]}); + my $new_status = new Bugzilla::Status({name => $status_change->[1]}); + if ($new_status->is_open && !$old_status->is_open) { + $status_message = "Bug re-opened!"; } - - my $old_summary = $old_bug->short_desc; - - my $status_message; - if (my $status_change = $changes->{'bug_status'}) { - my $old_status = new Bugzilla::Status({ name => $status_change->[0] }); - my $new_status = new Bugzilla::Status({ name => $status_change->[1] }); - if ($new_status->is_open && !$old_status->is_open) { - $status_message = "Bug re-opened!"; - } - if (!$new_status->is_open && $old_status->is_open) { - $status_message = "Bug closed!"; - } + if (!$new_status->is_open && $old_status->is_open) { + $status_message = "Bug closed!"; } + } + + my $bug_id = $bug->id; + my $num_changes = scalar keys %$changes; + my $result = "There were $num_changes changes to fields on bug $bug_id" + . " at $timestamp."; - my $bug_id = $bug->id; - my $num_changes = scalar keys %$changes; - my $result = "There were $num_changes changes to fields on bug $bug_id" - . " at $timestamp."; - # Uncomment this line to see $result in your webserver's error log whenever - # you update a bug. - # warn $result; + # Uncomment this line to see $result in your webserver's error log whenever + # you update a bug. + # warn $result; } sub bug_end_of_update { - my ($self, $args) = @_; - - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my ($bug, $old_bug, $timestamp, $changes) = - @$args{qw(bug old_bug timestamp changes)}; - - foreach my $field (keys %$changes) { - my $used_to_be = $changes->{$field}->[0]; - my $now_it_is = $changes->{$field}->[1]; + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my ($bug, $old_bug, $timestamp, $changes) + = @$args{qw(bug old_bug timestamp changes)}; + + foreach my $field (keys %$changes) { + my $used_to_be = $changes->{$field}->[0]; + my $now_it_is = $changes->{$field}->[1]; + } + + my $old_summary = $old_bug->short_desc; + + my $status_message; + if (my $status_change = $changes->{'bug_status'}) { + my $old_status = new Bugzilla::Status({name => $status_change->[0]}); + my $new_status = new Bugzilla::Status({name => $status_change->[1]}); + if ($new_status->is_open && !$old_status->is_open) { + $status_message = "Bug re-opened!"; } - - my $old_summary = $old_bug->short_desc; - - my $status_message; - if (my $status_change = $changes->{'bug_status'}) { - my $old_status = new Bugzilla::Status({ name => $status_change->[0] }); - my $new_status = new Bugzilla::Status({ name => $status_change->[1] }); - if ($new_status->is_open && !$old_status->is_open) { - $status_message = "Bug re-opened!"; - } - if (!$new_status->is_open && $old_status->is_open) { - $status_message = "Bug closed!"; - } + if (!$new_status->is_open && $old_status->is_open) { + $status_message = "Bug closed!"; } + } - my $bug_id = $bug->id; - my $num_changes = scalar keys %$changes; - my $result = "There were $num_changes changes to fields on bug $bug_id" - . " at $timestamp."; - # Uncomment this line to see $result in your webserver's error log whenever - # you update a bug. - # warn $result; + my $bug_id = $bug->id; + my $num_changes = scalar keys %$changes; + my $result = "There were $num_changes changes to fields on bug $bug_id" + . " at $timestamp."; + + # Uncomment this line to see $result in your webserver's error log whenever + # you update a bug. + # warn $result; } sub bug_fields { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $fields = $args->{'fields'}; - push (@$fields, "example") + my $fields = $args->{'fields'}; + push(@$fields, "example"); } sub bug_format_comment { - my ($self, $args) = @_; + my ($self, $args) = @_; - # This replaces every occurrence of the word "foo" with the word - # "bar" + # This replaces every occurrence of the word "foo" with the word + # "bar" - my $regexes = $args->{'regexes'}; - push(@$regexes, { match => qr/\bfoo\b/, replace => 'bar' }); + my $regexes = $args->{'regexes'}; + push(@$regexes, {match => qr/\bfoo\b/, replace => 'bar'}); - # And this links every occurrence of the word "bar" to example.com, - # but it won't affect "foo"s that have already been turned into "bar" - # above (because each regex is run in order, and later regexes don't modify - # earlier matches, due to some cleverness in Bugzilla's internals). - # - # For example, the phrase "foo bar" would become: - # bar bar - my $bar_match = qr/\b(bar)\b/; - push(@$regexes, { match => $bar_match, replace => \&_replace_bar }); + # And this links every occurrence of the word "bar" to example.com, + # but it won't affect "foo"s that have already been turned into "bar" + # above (because each regex is run in order, and later regexes don't modify + # earlier matches, due to some cleverness in Bugzilla's internals). + # + # For example, the phrase "foo bar" would become: + # bar bar + my $bar_match = qr/\b(bar)\b/; + push(@$regexes, {match => $bar_match, replace => \&_replace_bar}); } # Used by bug_format_comment--see its code for an explanation. sub _replace_bar { - my $args = shift; - # $match is the first parentheses match in the $bar_match regex - # in bug-format_comment.pl. We get up to 10 regex matches as - # arguments to this function. - my $match = $args->{matches}->[0]; - # Remember, you have to HTML-escape any data that you are returning! - $match = html_quote($match); - return qq{$match}; -}; + my $args = shift; + + # $match is the first parentheses match in the $bar_match regex + # in bug-format_comment.pl. We get up to 10 regex matches as + # arguments to this function. + my $match = $args->{matches}->[0]; + + # Remember, you have to HTML-escape any data that you are returning! + $match = html_quote($match); + return qq{$match}; +} sub buglist_columns { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $columns = $args->{'columns'}; - $columns->{'example'} = { 'name' => 'bugs.delta_ts' , 'title' => 'Example' }; - $columns->{'product_desc'} = { 'name' => 'prod_desc.description', - 'title' => 'Product Description' }; + my $columns = $args->{'columns'}; + $columns->{'example'} = {'name' => 'bugs.delta_ts', 'title' => 'Example'}; + $columns->{'product_desc'} + = {'name' => 'prod_desc.description', 'title' => 'Product Description'}; } sub buglist_column_joins { - my ($self, $args) = @_; - my $joins = $args->{'column_joins'}; + my ($self, $args) = @_; + my $joins = $args->{'column_joins'}; - # This column is added using the "buglist_columns" hook - $joins->{'product_desc'} = { - from => 'product_id', - to => 'id', - table => 'products', - as => 'prod_desc', - join => 'INNER', - }; + # This column is added using the "buglist_columns" hook + $joins->{'product_desc'} = { + from => 'product_id', + to => 'id', + table => 'products', + as => 'prod_desc', + join => 'INNER', + }; } sub search_operator_field_override { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $operators = $args->{'operators'}; + my $operators = $args->{'operators'}; - my $original = $operators->{component}->{_non_changed}; - $operators->{component} = { - _non_changed => sub { _component_nonchanged($original, @_) } - }; + my $original = $operators->{component}->{_non_changed}; + $operators->{component} = { + _non_changed => sub { _component_nonchanged($original, @_) } + }; } sub _component_nonchanged { - my $original = shift; - my ($invocant, $args) = @_; + my $original = shift; + my ($invocant, $args) = @_; + + $invocant->$original($args); - $invocant->$original($args); - # Actually, it does not change anything in the result, - # just an example. - $args->{term} = $args->{term} . " OR 1=2"; + # Actually, it does not change anything in the result, + # just an example. + $args->{term} = $args->{term} . " OR 1=2"; } sub bugmail_recipients { - my ($self, $args) = @_; - my $recipients = $args->{recipients}; - my $bug = $args->{bug}; - - my $user = - new Bugzilla::User({ name => Bugzilla->params->{'maintainer'} }); - - if ($bug->id == 1) { - # Uncomment the line below to add the maintainer to the recipients - # list of every bugmail from bug 1 as though that the maintainer - # were on the CC list. - #$recipients->{$user->id}->{+REL_CC} = 1; - - # And this line adds the maintainer as though he had the "REL_EXAMPLE" - # relationship from the bugmail_relationships hook below. - #$recipients->{$user->id}->{+REL_EXAMPLE} = 1; - } + my ($self, $args) = @_; + my $recipients = $args->{recipients}; + my $bug = $args->{bug}; + + my $user = new Bugzilla::User({name => Bugzilla->params->{'maintainer'}}); + + if ($bug->id == 1) { + + # Uncomment the line below to add the maintainer to the recipients + # list of every bugmail from bug 1 as though that the maintainer + # were on the CC list. + #$recipients->{$user->id}->{+REL_CC} = 1; + + # And this line adds the maintainer as though he had the "REL_EXAMPLE" + # relationship from the bugmail_relationships hook below. + #$recipients->{$user->id}->{+REL_EXAMPLE} = 1; + } } sub bugmail_relationships { - my ($self, $args) = @_; - my $relationships = $args->{relationships}; - $relationships->{+REL_EXAMPLE} = 'Example'; + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_EXAMPLE} = 'Example'; } sub config_add_panels { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{Example} = "Bugzilla::Extension::Example::Config"; + my $modules = $args->{panel_modules}; + $modules->{Example} = "Bugzilla::Extension::Example::Config"; } sub config_modify_panels { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $panels = $args->{panels}; + my $panels = $args->{panels}; - # Add the "Example" auth methods. - my $auth_params = $panels->{'auth'}->{params}; - my ($info_class) = grep($_->{name} eq 'user_info_class', @$auth_params); - my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params); + # Add the "Example" auth methods. + my $auth_params = $panels->{'auth'}->{params}; + my ($info_class) = grep($_->{name} eq 'user_info_class', @$auth_params); + my ($verify_class) = grep($_->{name} eq 'user_verify_class', @$auth_params); - push(@{ $info_class->{choices} }, 'CGI,Example'); - push(@{ $verify_class->{choices} }, 'Example'); + push(@{$info_class->{choices}}, 'CGI,Example'); + push(@{$verify_class->{choices}}, 'Example'); - push(@$auth_params, { name => 'param_example', - type => 't', - default => 0, - checker => \&check_numeric }); + push( + @$auth_params, + { + name => 'param_example', + type => 't', + default => 0, + checker => \&check_numeric + } + ); } sub db_schema_abstract_schema { - my ($self, $args) = @_; + my ($self, $args) = @_; + # $args->{'schema'}->{'example_table'} = { # FIELDS => [ # id => {TYPE => 'SMALLSERIAL', NOTNULL => 1, @@ -383,639 +400,662 @@ sub db_schema_abstract_schema { } sub email_in_before_parse { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $subject = $args->{mail}->header('Subject'); - # Correctly extract the bug ID from email subjects of the form [Bug comp/NNN]. - if ($subject =~ /\[.*(\d+)\].*/) { - $args->{fields}->{bug_id} = $1; - } + my $subject = $args->{mail}->header('Subject'); + + # Correctly extract the bug ID from email subjects of the form [Bug comp/NNN]. + if ($subject =~ /\[.*(\d+)\].*/) { + $args->{fields}->{bug_id} = $1; + } } sub email_in_after_parse { - my ($self, $args) = @_; - my $reporter = $args->{fields}->{reporter}; - my $dbh = Bugzilla->dbh; - - # No other check needed if this is a valid regular user. - return if login_to_id($reporter); - - # The reporter is not a regular user. We create an account for him, - # but he can only comment on existing bugs. - # This is useful for people who reply by email to bugmails received - # in mailing-lists. - if ($args->{fields}->{bug_id}) { - # WARNING: we return now to skip the remaining code below. - # You must understand that removing this line would make the code - # below effective! Do it only if you are OK with the behavior - # described here. - return; - - Bugzilla::User->create({ login_name => $reporter, cryptpassword => '*' }); - - # For security reasons, delete all fields unrelated to comments. - foreach my $field (keys %{$args->{fields}}) { - next if $field =~ /^(?:bug_id|comment|reporter)$/; - delete $args->{fields}->{$field}; - } - } - else { - ThrowUserError('invalid_username', { name => $reporter }); + my ($self, $args) = @_; + my $reporter = $args->{fields}->{reporter}; + my $dbh = Bugzilla->dbh; + + # No other check needed if this is a valid regular user. + return if login_to_id($reporter); + + # The reporter is not a regular user. We create an account for him, + # but he can only comment on existing bugs. + # This is useful for people who reply by email to bugmails received + # in mailing-lists. + if ($args->{fields}->{bug_id}) { + + # WARNING: we return now to skip the remaining code below. + # You must understand that removing this line would make the code + # below effective! Do it only if you are OK with the behavior + # described here. + return; + + Bugzilla::User->create({login_name => $reporter, cryptpassword => '*'}); + + # For security reasons, delete all fields unrelated to comments. + foreach my $field (keys %{$args->{fields}}) { + next if $field =~ /^(?:bug_id|comment|reporter)$/; + delete $args->{fields}->{$field}; } + } + else { + ThrowUserError('invalid_username', {name => $reporter}); + } } sub enter_bug_entrydefaultvars { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $vars = $args->{vars}; - $vars->{'example'} = 1; + my $vars = $args->{vars}; + $vars->{'example'} = 1; } sub error_catch { - my ($self, $args) = @_; - # Customize the error message displayed when someone tries to access - # page.cgi with an invalid page ID, and keep track of this attempt - # in the web server log. - return unless Bugzilla->error_mode == ERROR_MODE_WEBPAGE; - return unless $args->{error} eq 'bad_page_cgi_id'; - - my $page_id = $args->{vars}->{page_id}; - my $login = Bugzilla->user->identity || "Someone"; - warn "$login attempted to access page.cgi with id = $page_id"; - - my $page = $args->{message}; - my $new_error_msg = "Ah ah, you tried to access $page_id? Good try!"; - $new_error_msg = html_quote($new_error_msg); - # There are better tools to parse an HTML page, but it's just an example. - # Since Perl 5.16, we can no longer write "class" inside look-behind - # assertions, because "ss" is also seen as the german ß character, which - # makes Perl 5.16 complain. The right fix is to use the /aa modifier, - # but it's only understood since Perl 5.14. So the workaround is to write - # "clas[s]" instead of "class". Stupid and ugly hack, but it works with - # all Perl versions. - $$page =~ s/(?<=).*(?=<\/td>)/$new_error_msg/si; + my ($self, $args) = @_; + + # Customize the error message displayed when someone tries to access + # page.cgi with an invalid page ID, and keep track of this attempt + # in the web server log. + return unless Bugzilla->error_mode == ERROR_MODE_WEBPAGE; + return unless $args->{error} eq 'bad_page_cgi_id'; + + my $page_id = $args->{vars}->{page_id}; + my $login = Bugzilla->user->identity || "Someone"; + warn "$login attempted to access page.cgi with id = $page_id"; + + my $page = $args->{message}; + my $new_error_msg = "Ah ah, you tried to access $page_id? Good try!"; + $new_error_msg = html_quote($new_error_msg); + + # There are better tools to parse an HTML page, but it's just an example. + # Since Perl 5.16, we can no longer write "class" inside look-behind + # assertions, because "ss" is also seen as the german ß character, which + # makes Perl 5.16 complain. The right fix is to use the /aa modifier, + # but it's only understood since Perl 5.14. So the workaround is to write + # "clas[s]" instead of "class". Stupid and ugly hack, but it works with + # all Perl versions. + $$page + =~ s/(?<=).*(?=<\/td>)/$new_error_msg/si; } sub flag_end_of_update { - my ($self, $args) = @_; - - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $flag_params = $args; - my ($object, $timestamp, $old_flags, $new_flags) = - @$flag_params{qw(object timestamp old_flags new_flags)}; - my ($removed, $added) = diff_arrays($old_flags, $new_flags); - my ($granted, $denied) = (0, 0); - foreach my $new_flag (@$added) { - $granted++ if $new_flag =~ /\+$/; - $denied++ if $new_flag =~ /-$/; - } - my $bug_id = $object->isa('Bugzilla::Bug') ? $object->id - : $object->bug_id; - my $result = "$granted flags were granted and $denied flags were denied" - . " on bug $bug_id at $timestamp."; - # Uncomment this line to see $result in your webserver's error log whenever - # you update flags. - # warn $result; + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $flag_params = $args; + my ($object, $timestamp, $old_flags, $new_flags) + = @$flag_params{qw(object timestamp old_flags new_flags)}; + my ($removed, $added) = diff_arrays($old_flags, $new_flags); + my ($granted, $denied) = (0, 0); + foreach my $new_flag (@$added) { + $granted++ if $new_flag =~ /\+$/; + $denied++ if $new_flag =~ /-$/; + } + my $bug_id = $object->isa('Bugzilla::Bug') ? $object->id : $object->bug_id; + my $result = "$granted flags were granted and $denied flags were denied" + . " on bug $bug_id at $timestamp."; + + # Uncomment this line to see $result in your webserver's error log whenever + # you update flags. + # warn $result; } sub group_before_delete { - my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. - my $group = $args->{'group'}; - my $group_id = $group->id; - # Uncomment this line to see a line in your webserver's error log whenever - # you file a bug. - # warn "Group $group_id is about to be deleted!"; + my $group = $args->{'group'}; + my $group_id = $group->id; + + # Uncomment this line to see a line in your webserver's error log whenever + # you file a bug. + # warn "Group $group_id is about to be deleted!"; } sub group_end_of_create { - my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $group = $args->{'group'}; + my ($self, $args) = @_; + + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $group = $args->{'group'}; + + my $group_id = $group->id; - my $group_id = $group->id; - # Uncomment this line to see a line in your webserver's error log whenever - # you create a new group. - #warn "Group $group_id has been created!"; + # Uncomment this line to see a line in your webserver's error log whenever + # you create a new group. + #warn "Group $group_id has been created!"; } sub group_end_of_update { - my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. + my ($self, $args) = @_; - my ($group, $changes) = @$args{qw(group changes)}; + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. - foreach my $field (keys %$changes) { - my $used_to_be = $changes->{$field}->[0]; - my $now_it_is = $changes->{$field}->[1]; - } + my ($group, $changes) = @$args{qw(group changes)}; + + foreach my $field (keys %$changes) { + my $used_to_be = $changes->{$field}->[0]; + my $now_it_is = $changes->{$field}->[1]; + } + + my $group_id = $group->id; + my $num_changes = scalar keys %$changes; + my $result = "There were $num_changes changes to fields on group $group_id."; - my $group_id = $group->id; - my $num_changes = scalar keys %$changes; - my $result = - "There were $num_changes changes to fields on group $group_id."; - # Uncomment this line to see $result in your webserver's error log whenever - # you update a group. - #warn $result; + # Uncomment this line to see $result in your webserver's error log whenever + # you update a group. + #warn $result; } sub install_before_final_checks { - my ($self, $args) = @_; - print "Install-before_final_checks hook\n" unless $args->{silent}; + my ($self, $args) = @_; + print "Install-before_final_checks hook\n" unless $args->{silent}; - # Add a new user setting like this: - # - # add_setting({ - # name => 'product_chooser', # setting name - # options => ['pretty', 'full', 'small'], # options - # category => 'pretty' # default - # }); - # To add descriptions for the setting and choices, add extra values to - # the hash defined in global/setting-descs.none.tmpl. Do this in a hook: - # hook/global/setting-descs-settings.none.tmpl . + # Add a new user setting like this: + # + # add_setting({ + # name => 'product_chooser', # setting name + # options => ['pretty', 'full', 'small'], # options + # category => 'pretty' # default + # }); + # To add descriptions for the setting and choices, add extra values to + # the hash defined in global/setting-descs.none.tmpl. Do this in a hook: + # hook/global/setting-descs-settings.none.tmpl . } sub install_filesystem { - my ($self, $args) = @_; - my $create_dirs = $args->{'create_dirs'}; - my $recurse_dirs = $args->{'recurse_dirs'}; - my $htaccess = $args->{'htaccess'}; - - # Create a new directory in datadir specifically for this extension. - # The directory will need to allow files to be created by the extension - # code as well as allow the webserver to server content from it. - # my $data_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; - # $create_dirs->{$data_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE; - - # Update the permissions of any files and directories that currently reside - # in the extension's directory. - # $recurse_dirs->{$data_path} = { - # files => Bugzilla::Install::Filesystem::CGI_READ, - # dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE - # }; - - # Create a htaccess file that allows specific content to be served from the - # extension's directory. - # $htaccess->{"$data_path/.htaccess"} = { - # perms => Bugzilla::Install::Filesystem::WS_SERVE, - # contents => Bugzilla::Install::Filesystem::HT_DEFAULT_DENY - # }; + my ($self, $args) = @_; + my $create_dirs = $args->{'create_dirs'}; + my $recurse_dirs = $args->{'recurse_dirs'}; + my $htaccess = $args->{'htaccess'}; + + # Create a new directory in datadir specifically for this extension. + # The directory will need to allow files to be created by the extension + # code as well as allow the webserver to server content from it. + # my $data_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; + # $create_dirs->{$data_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE; + + # Update the permissions of any files and directories that currently reside + # in the extension's directory. + # $recurse_dirs->{$data_path} = { + # files => Bugzilla::Install::Filesystem::CGI_READ, + # dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE + # }; + + # Create a htaccess file that allows specific content to be served from the + # extension's directory. + # $htaccess->{"$data_path/.htaccess"} = { + # perms => Bugzilla::Install::Filesystem::WS_SERVE, + # contents => Bugzilla::Install::Filesystem::HT_DEFAULT_DENY + # }; } sub install_update_db { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + # $dbh->bz_add_column('example', 'new_column', # {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); # $dbh->bz_add_index('example', 'example_new_column_idx', [qw(value)]); } sub install_update_db_fielddefs { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; + # $dbh->bz_add_column('fielddefs', 'example_column', # {TYPE => 'MEDIUMTEXT', NOTNULL => 1, DEFAULT => ''}); } sub job_map { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $job_map = $args->{job_map}; + my $job_map = $args->{job_map}; - # This adds the named class (an instance of TheSchwartz::Worker) as a - # handler for when a job is added with the name "some_task". - $job_map->{'some_task'} = 'Bugzilla::Extension::Example::Job::SomeClass'; + # This adds the named class (an instance of TheSchwartz::Worker) as a + # handler for when a job is added with the name "some_task". + $job_map->{'some_task'} = 'Bugzilla::Extension::Example::Job::SomeClass'; - # Schedule a job like this: - # my $queue = Bugzilla->job_queue(); - # $queue->insert('some_task', { some_parameter => $some_variable }); + # Schedule a job like this: + # my $queue = Bugzilla->job_queue(); + # $queue->insert('some_task', { some_parameter => $some_variable }); } sub mailer_before_send { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my $email = $args->{email}; - my $email = $args->{email}; - # If you add a header to an email, it's best to start it with - # 'X-Bugzilla-' so that you don't conflict with - # other extensions. - $email->header_set('X-Bugzilla-Example-Header', 'Example'); + # If you add a header to an email, it's best to start it with + # 'X-Bugzilla-' so that you don't conflict with + # other extensions. + $email->header_set('X-Bugzilla-Example-Header', 'Example'); } sub object_before_create { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $class = $args->{'class'}; - my $object_params = $args->{'params'}; + my $class = $args->{'class'}; + my $object_params = $args->{'params'}; - # Note that this is a made-up class, for this example. - if ($class->isa('Bugzilla::ExampleObject')) { - warn "About to create an ExampleObject!"; - warn "Got the following parameters: " - . join(', ', keys(%$object_params)); - } + # Note that this is a made-up class, for this example. + if ($class->isa('Bugzilla::ExampleObject')) { + warn "About to create an ExampleObject!"; + warn "Got the following parameters: " . join(', ', keys(%$object_params)); + } } sub object_before_delete { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $object = $args->{'object'}; + my $object = $args->{'object'}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - my $id = $object->id; - warn "An object with id $id is about to be deleted!"; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + my $id = $object->id; + warn "An object with id $id is about to be deleted!"; + } } sub object_before_set { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($object, $field, $value) = @$args{qw(object field value)}; + my ($object, $field, $value) = @$args{qw(object field value)}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - warn "The field $field is changing from " . $object->{$field} - . " to $value!"; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + warn "The field $field is changing from " . $object->{$field} . " to $value!"; + } } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::ExampleObject')) { - push(@$columns, 'example'); - } + if ($class->isa('Bugzilla::ExampleObject')) { + push(@$columns, 'example'); + } } sub object_end_of_create { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $class = $args->{'class'}; - my $object = $args->{'object'}; + my $class = $args->{'class'}; + my $object = $args->{'object'}; - warn "Created a new $class object!"; + warn "Created a new $class object!"; } sub object_end_of_create_validators { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $class = $args->{'class'}; - my $object_params = $args->{'params'}; + my $class = $args->{'class'}; + my $object_params = $args->{'params'}; - # Note that this is a made-up class, for this example. - if ($class->isa('Bugzilla::ExampleObject')) { - # Always set example_field to 1, even if the validators said otherwise. - $object_params->{example_field} = 1; - } + # Note that this is a made-up class, for this example. + if ($class->isa('Bugzilla::ExampleObject')) { + + # Always set example_field to 1, even if the validators said otherwise. + $object_params->{example_field} = 1; + } } sub object_end_of_set { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($object, $field) = @$args{qw(object field)}; + my ($object, $field) = @$args{qw(object field)}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - warn "The field $field has changed to " . $object->{$field}; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + warn "The field $field has changed to " . $object->{$field}; + } } sub object_end_of_set_all { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $object = $args->{'object'}; - my $object_params = $args->{'params'}; + my $object = $args->{'object'}; + my $object_params = $args->{'params'}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - if ($object_params->{example_field} == 1) { - $object->{example_field} = 1; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + if ($object_params->{example_field} == 1) { + $object->{example_field} = 1; } + } } sub object_end_of_update { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($object, $old_object, $changes) = - @$args{qw(object old_object changes)}; + my ($object, $old_object, $changes) = @$args{qw(object old_object changes)}; - # Note that this is a made-up class, for this example. - if ($object->isa('Bugzilla::ExampleObject')) { - if (defined $changes->{'name'}) { - my ($old, $new) = @{ $changes->{'name'} }; - print "The name field changed from $old to $new!"; - } + # Note that this is a made-up class, for this example. + if ($object->isa('Bugzilla::ExampleObject')) { + if (defined $changes->{'name'}) { + my ($old, $new) = @{$changes->{'name'}}; + print "The name field changed from $old to $new!"; } + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::ExampleObject')) { - push(@$columns, 'example'); - } + if ($object->isa('Bugzilla::ExampleObject')) { + push(@$columns, 'example'); + } } sub object_validators { - my ($self, $args) = @_; - my ($class, $validators) = @$args{qw(class validators)}; - - if ($class->isa('Bugzilla::Bug')) { - # This is an example of adding a new validator. - # See the _check_example subroutine below. - $validators->{example} = \&_check_example; - - # This is an example of overriding an existing validator. - # See the check_short_desc validator below. - my $original = $validators->{short_desc}; - $validators->{short_desc} = sub { _check_short_desc($original, @_) }; - } + my ($self, $args) = @_; + my ($class, $validators) = @$args{qw(class validators)}; + + if ($class->isa('Bugzilla::Bug')) { + + # This is an example of adding a new validator. + # See the _check_example subroutine below. + $validators->{example} = \&_check_example; + + # This is an example of overriding an existing validator. + # See the check_short_desc validator below. + my $original = $validators->{short_desc}; + $validators->{short_desc} = sub { _check_short_desc($original, @_) }; + } } sub _check_example { - my ($invocant, $value, $field) = @_; - warn "I was called to validate the value of $field."; - warn "The value of $field that I was passed in is: $value"; + my ($invocant, $value, $field) = @_; + warn "I was called to validate the value of $field."; + warn "The value of $field that I was passed in is: $value"; - # Make the value always be 1. - my $fixed_value = 1; - return $fixed_value; + # Make the value always be 1. + my $fixed_value = 1; + return $fixed_value; } sub _check_short_desc { - my $original = shift; - my $invocant = shift; - my $value = $invocant->$original(@_); - if ($value !~ /example/i) { - # Use this line to make Bugzilla throw an error every time - # you try to file a bug or update a bug without the word "example" - # in the summary. - if (0) { - ThrowUserError('example_short_desc_invalid'); - } + my $original = shift; + my $invocant = shift; + my $value = $invocant->$original(@_); + if ($value !~ /example/i) { + + # Use this line to make Bugzilla throw an error every time + # you try to file a bug or update a bug without the word "example" + # in the summary. + if (0) { + ThrowUserError('example_short_desc_invalid'); } - return $value; + } + return $value; } sub page_before_template { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; + my ($vars, $page) = @$args{qw(vars page_id)}; - # You can see this hook in action by loading page.cgi?id=example.html - if ($page eq 'example.html') { - $vars->{cgi_variables} = { Bugzilla->cgi->Vars }; - } + # You can see this hook in action by loading page.cgi?id=example.html + if ($page eq 'example.html') { + $vars->{cgi_variables} = {Bugzilla->cgi->Vars}; + } } sub path_info_whitelist { - my ($self, $args) = @_; - my $whitelist = $args->{whitelist}; - push(@$whitelist, "page.cgi"); + my ($self, $args) = @_; + my $whitelist = $args->{whitelist}; + push(@$whitelist, "page.cgi"); } sub post_bug_after_creation { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $vars = $args->{vars}; - $vars->{'example'} = 1; + my $vars = $args->{vars}; + $vars->{'example'} = 1; } sub product_confirm_delete { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $vars = $args->{vars}; - $vars->{'example'} = 1; + my $vars = $args->{vars}; + $vars->{'example'} = 1; } sub product_end_of_create { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my $product = $args->{product}; - my $product = $args->{product}; + # For this example, any lines of code that actually make changes to your + # database have been commented out. - # For this example, any lines of code that actually make changes to your - # database have been commented out. + # This section will take a group that exists in your installation + # (possible called test_group) and automatically makes the new + # product hidden to only members of the group. Just remove + # the restriction if you want the new product to be public. - # This section will take a group that exists in your installation - # (possible called test_group) and automatically makes the new - # product hidden to only members of the group. Just remove - # the restriction if you want the new product to be public. + my $example_group = new Bugzilla::Group({name => 'example_group'}); - my $example_group = new Bugzilla::Group({ name => 'example_group' }); + if ($example_group) { + $product->set_group_controls( + $example_group, + { + entry => 1, + membercontrol => CONTROLMAPMANDATORY, + othercontrol => CONTROLMAPMANDATORY + } + ); - if ($example_group) { - $product->set_group_controls($example_group, - { entry => 1, - membercontrol => CONTROLMAPMANDATORY, - othercontrol => CONTROLMAPMANDATORY }); # $product->update(); - } + } - # This section will automatically add a default component - # to the new product called 'No Component'. + # This section will automatically add a default component + # to the new product called 'No Component'. - my $default_assignee = new Bugzilla::User( - { name => Bugzilla->params->{maintainer} }); + my $default_assignee + = new Bugzilla::User({name => Bugzilla->params->{maintainer}}); + + if ($default_assignee) { - if ($default_assignee) { # Bugzilla::Component->create( # { name => 'No Component', # product => $product, # description => 'Select this component if one does not ' . # 'exist in the current list of components', # initialowner => $default_assignee }); - } + } } sub quicksearch_map { - my ($self, $args) = @_; - my $map = $args->{'map'}; + my ($self, $args) = @_; + my $map = $args->{'map'}; - # This demonstrates adding a shorter alias for a long custom field name. - $map->{'impact'} = $map->{'cf_long_field_name_for_impact_field'}; + # This demonstrates adding a shorter alias for a long custom field name. + $map->{'impact'} = $map->{'cf_long_field_name_for_impact_field'}; } sub sanitycheck_check { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - my $sth; + my $dbh = Bugzilla->dbh; + my $sth; - my $status = $args->{'status'}; + my $status = $args->{'status'}; - # Check that all users are Australian - $status->('example_check_au_user'); + # Check that all users are Australian + $status->('example_check_au_user'); - $sth = $dbh->prepare("SELECT userid, login_name + $sth = $dbh->prepare( + "SELECT userid, login_name FROM profiles - WHERE login_name NOT LIKE '%.au'"); - $sth->execute; - - my $seen_nonau = 0; - while (my ($userid, $login, $numgroups) = $sth->fetchrow_array) { - $status->('example_check_au_user_alert', - { userid => $userid, login => $login }, - 'alert'); - $seen_nonau = 1; - } + WHERE login_name NOT LIKE '%.au'" + ); + $sth->execute; + + my $seen_nonau = 0; + while (my ($userid, $login, $numgroups) = $sth->fetchrow_array) { + $status->( + 'example_check_au_user_alert', {userid => $userid, login => $login}, 'alert' + ); + $seen_nonau = 1; + } - $status->('example_check_au_user_prompt') if $seen_nonau; + $status->('example_check_au_user_prompt') if $seen_nonau; } sub sanitycheck_repair { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; - my $status = $args->{'status'}; + my $status = $args->{'status'}; - if ($cgi->param('example_repair_au_user')) { - $status->('example_repair_au_user_start'); + if ($cgi->param('example_repair_au_user')) { + $status->('example_repair_au_user_start'); - #$dbh->do("UPDATE profiles - # SET login_name = CONCAT(login_name, '.au') - # WHERE login_name NOT LIKE '%.au'"); + #$dbh->do("UPDATE profiles + # SET login_name = CONCAT(login_name, '.au') + # WHERE login_name NOT LIKE '%.au'"); - $status->('example_repair_au_user_end'); - } + $status->('example_repair_au_user_end'); + } } sub template_before_create { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $config = $args->{'config'}; - # This will be accessible as "example_global_variable" in every - # template in Bugzilla. See Bugzilla/Template.pm's create() function - # for more things that you can set. - $config->{VARIABLES}->{example_global_variable} = sub { return 'value' }; + my $config = $args->{'config'}; + + # This will be accessible as "example_global_variable" in every + # template in Bugzilla. See Bugzilla/Template.pm's create() function + # for more things that you can set. + $config->{VARIABLES}->{example_global_variable} = sub { return 'value' }; } sub template_before_process { - my ($self, $args) = @_; + my ($self, $args) = @_; - my ($vars, $file, $context) = @$args{qw(vars file context)}; + my ($vars, $file, $context) = @$args{qw(vars file context)}; - if ($file eq 'bug/edit.html.tmpl') { - $vars->{'viewing_the_bug_form'} = 1; - } + if ($file eq 'bug/edit.html.tmpl') { + $vars->{'viewing_the_bug_form'} = 1; + } } sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{current_tab}; - my $save = $args->{save_changes}; - my $handled = $args->{handled}; + my ($self, $args) = @_; + my $tab = $args->{current_tab}; + my $save = $args->{save_changes}; + my $handled = $args->{handled}; - return unless $tab eq 'my_tab'; + return unless $tab eq 'my_tab'; - my $value = Bugzilla->input_params->{'example_pref'}; - if ($save) { - # Validate your data and update the DB accordingly. - $value =~ s/\s+/:/g; - } - $args->{'vars'}->{example_pref} = $value; + my $value = Bugzilla->input_params->{'example_pref'}; + if ($save) { + + # Validate your data and update the DB accordingly. + $value =~ s/\s+/:/g; + } + $args->{'vars'}->{example_pref} = $value; - # Set the 'handled' scalar reference to true so that the caller - # knows the panel name is valid and that an extension took care of it. - $$handled = 1; + # Set the 'handled' scalar reference to true so that the caller + # knows the panel name is valid and that an extension took care of it. + $$handled = 1; } sub webservice { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{Example} = "Bugzilla::Extension::Example::WebService"; + my $dispatch = $args->{dispatch}; + $dispatch->{Example} = "Bugzilla::Extension::Example::WebService"; } sub webservice_error_codes { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $error_map = $args->{error_map}; - $error_map->{'example_my_error'} = 10001; + my $error_map = $args->{error_map}; + $error_map->{'example_my_error'} = 10001; } sub webservice_status_code_map { - my ($self, $args) = @_; + my ($self, $args) = @_; + + my $status_code_map = $args->{status_code_map}; - my $status_code_map = $args->{status_code_map}; - # Uncomment this line to override the status code for the - # error 'object_does_not_exist' to STATUS_BAD_REQUEST - #$status_code_map->{51} = STATUS_BAD_REQUEST; + # Uncomment this line to override the status code for the + # error 'object_does_not_exist' to STATUS_BAD_REQUEST + #$status_code_map->{51} = STATUS_BAD_REQUEST; } sub webservice_before_call { - my ($self, $args) = @_; + my ($self, $args) = @_; - # This code doesn't actually *do* anything, it's just here to show you - # how to use this hook. - my $method = $args->{method}; - my $full_method = $args->{full_method}; + # This code doesn't actually *do* anything, it's just here to show you + # how to use this hook. + my $method = $args->{method}; + my $full_method = $args->{full_method}; - # Uncomment this line to see a line in your webserver's error log whenever - # a webservice call is made - #warn "RPC call $full_method made by ", Bugzilla->user->login, "\n"; + # Uncomment this line to see a line in your webserver's error log whenever + # a webservice call is made + #warn "RPC call $full_method made by ", Bugzilla->user->login, "\n"; } sub webservice_fix_credentials { - my ($self, $args) = @_; - my $rpc = $args->{'rpc'}; - my $params = $args->{'params'}; - # Allow user to pass in username=foo&password=bar - if (exists $params->{'username'} && exists $params->{'password'}) { - $params->{'Bugzilla_login'} = $params->{'username'}; - $params->{'Bugzilla_password'} = $params->{'password'}; - } + my ($self, $args) = @_; + my $rpc = $args->{'rpc'}; + my $params = $args->{'params'}; + + # Allow user to pass in username=foo&password=bar + if (exists $params->{'username'} && exists $params->{'password'}) { + $params->{'Bugzilla_login'} = $params->{'username'}; + $params->{'Bugzilla_password'} = $params->{'password'}; + } } sub webservice_rest_request { - my ($self, $args) = @_; - my $rpc = $args->{'rpc'}; - my $params = $args->{'params'}; - # Internally we may have a field called 'cf_test_field' but we allow users - # to use the shorter 'test_field' name. - if (exists $params->{'test_field'}) { - $params->{'test_field'} = delete $params->{'cf_test_field'}; - } + my ($self, $args) = @_; + my $rpc = $args->{'rpc'}; + my $params = $args->{'params'}; + + # Internally we may have a field called 'cf_test_field' but we allow users + # to use the shorter 'test_field' name. + if (exists $params->{'test_field'}) { + $params->{'test_field'} = delete $params->{'cf_test_field'}; + } } sub webservice_rest_resources { - my ($self, $args) = @_; - my $rpc = $args->{'rpc'}; - my $resources = $args->{'resources'}; - # Add a new resource that allows for /rest/example/hello - # to call Example.hello - $resources->{'Bugzilla::Extension::Example::WebService'} = [ - qr{^/example/hello$}, { - GET => { - method => 'hello', - } - } - ]; + my ($self, $args) = @_; + my $rpc = $args->{'rpc'}; + my $resources = $args->{'resources'}; + + # Add a new resource that allows for /rest/example/hello + # to call Example.hello + $resources->{'Bugzilla::Extension::Example::WebService'} + = [qr{^/example/hello$}, {GET => {method => 'hello',}}]; } sub webservice_rest_response { - my ($self, $args) = @_; - my $rpc = $args->{'rpc'}; - my $result = $args->{'result'}; - my $response = $args->{'response'}; - # Convert a list of bug hashes to a single bug hash if only one is - # being returned. - if (ref $$result eq 'HASH' - && exists $$result->{'bugs'} - && scalar @{ $$result->{'bugs'} } == 1) - { - $$result = $$result->{'bugs'}->[0]; - } + my ($self, $args) = @_; + my $rpc = $args->{'rpc'}; + my $result = $args->{'result'}; + my $response = $args->{'response'}; + + # Convert a list of bug hashes to a single bug hash if only one is + # being returned. + if ( ref $$result eq 'HASH' + && exists $$result->{'bugs'} + && scalar @{$$result->{'bugs'}} == 1) + { + $$result = $$result->{'bugs'}->[0]; + } } # This must be the last line of your extension. diff --git a/extensions/Example/lib/Auth/Login.pm b/extensions/Example/lib/Auth/Login.pm index 376fe21a8..10be62f85 100644 --- a/extensions/Example/lib/Auth/Login.pm +++ b/extensions/Example/lib/Auth/Login.pm @@ -15,7 +15,7 @@ use Bugzilla::Constants; # Always returns no data. sub get_login_info { - return { failure => AUTH_NODATA }; + return {failure => AUTH_NODATA}; } 1; diff --git a/extensions/Example/lib/Auth/Verify.pm b/extensions/Example/lib/Auth/Verify.pm index cac6d1019..4170b93c3 100644 --- a/extensions/Example/lib/Auth/Verify.pm +++ b/extensions/Example/lib/Auth/Verify.pm @@ -14,7 +14,7 @@ use Bugzilla::Constants; # A verifier that always fails. sub check_credentials { - return { failure => AUTH_NO_SUCH_USER }; + return {failure => AUTH_NO_SUCH_USER}; } 1; diff --git a/extensions/Example/lib/Config.pm b/extensions/Example/lib/Config.pm index fac0046af..360a57510 100644 --- a/extensions/Example/lib/Config.pm +++ b/extensions/Example/lib/Config.pm @@ -16,16 +16,11 @@ use Bugzilla::Config::Common; our $sortkey = 5000; sub get_param_list { - my ($class) = @_; + my ($class) = @_; - my @param_list = ( - { - name => 'example_string', - type => 't', - default => 'EXAMPLE', - }, - ); - return @param_list; + my @param_list + = ({name => 'example_string', type => 't', default => 'EXAMPLE',},); + return @param_list; } 1; diff --git a/extensions/Example/lib/WebService.pm b/extensions/Example/lib/WebService.pm index 7b1940462..f50c6e6cb 100644 --- a/extensions/Example/lib/WebService.pm +++ b/extensions/Example/lib/WebService.pm @@ -14,8 +14,8 @@ use base qw(Bugzilla::WebService); use Bugzilla::Error; use constant PUBLIC_METHODS => qw( - hello - throw_an_error + hello + throw_an_error ); # This can be called as Example.hello() from the WebService. diff --git a/extensions/Example/template/en/default/setup/strings.txt.pl b/extensions/Example/template/en/default/setup/strings.txt.pl index 7b19a9f4d..6ee4b5fdc 100644 --- a/extensions/Example/template/en/default/setup/strings.txt.pl +++ b/extensions/Example/template/en/default/setup/strings.txt.pl @@ -17,8 +17,6 @@ # Contributor(s): # Max Kanat-Alexander -%strings = ( - feature_example_acme => 'Example Extension: Acme Feature', -); +%strings = (feature_example_acme => 'Example Extension: Acme Feature',); 1; diff --git a/extensions/FlagDefaultRequestee/Extension.pm b/extensions/FlagDefaultRequestee/Extension.pm index b534f9904..df23d9cc4 100644 --- a/extensions/FlagDefaultRequestee/Extension.pm +++ b/extensions/FlagDefaultRequestee/Extension.pm @@ -27,14 +27,16 @@ our $VERSION = '1'; ################ sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('flagtypes', 'default_requestee', { - TYPE => 'INT3', - NOTNULL => 0, - REFERENCES => { TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'SET NULL' } - }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column( + 'flagtypes', + 'default_requestee', + { + TYPE => 'INT3', + NOTNULL => 0, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'SET NULL'} + } + ); } ############# @@ -42,51 +44,51 @@ sub install_update_db { ############# sub template_before_process { - my ($self, $args) = @_; - return unless Bugzilla->user->id; - my ($vars, $file) = @$args{qw(vars file)}; - return unless grep { $_ eq $file } FLAGTYPE_TEMPLATES; - - my $flag_types = []; - if (exists $vars->{bug} || exists $vars->{attachment}) { - my $bug; - if (exists $vars->{bug}) { - $bug = $vars->{'bug'}; - } elsif (exists $vars->{'attachment'}) { - $bug = $vars->{'attachment'}->{bug}; - } - - $flag_types = Bugzilla::FlagType::match({ - 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'), - 'product_id' => $bug->product_id, - 'component_id' => $bug->component_id, - 'bug_id' => $bug->id, - 'active_or_has_flags' => $bug->id, - }); - - $vars->{flag_currently_requested} ||= {}; - foreach my $type (@$flag_types) { - my $flags = Bugzilla::Flag->match({ - type_id => $type->id, - bug_id => $bug->id, - status => '?' - }); - map { $vars->{flag_currently_requested}->{$_->id} = 1 } @$flags; - } + my ($self, $args) = @_; + return unless Bugzilla->user->id; + my ($vars, $file) = @$args{qw(vars file)}; + return unless grep { $_ eq $file } FLAGTYPE_TEMPLATES; + + my $flag_types = []; + if (exists $vars->{bug} || exists $vars->{attachment}) { + my $bug; + if (exists $vars->{bug}) { + $bug = $vars->{'bug'}; } - elsif ($file =~ /^bug\/create/ && exists $vars->{product}) { - my $bug_flags = $vars->{product}->flag_types->{bug}; - my $attachment_flags = $vars->{product}->flag_types->{attachment}; - $flag_types = [ map { $_ } (@$bug_flags, @$attachment_flags) ]; + elsif (exists $vars->{'attachment'}) { + $bug = $vars->{'attachment'}->{bug}; } - return if !@$flag_types; + $flag_types = Bugzilla::FlagType::match({ + 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'), + 'product_id' => $bug->product_id, + 'component_id' => $bug->component_id, + 'bug_id' => $bug->id, + 'active_or_has_flags' => $bug->id, + }); - $vars->{flag_default_requestees} ||= {}; + $vars->{flag_currently_requested} ||= {}; foreach my $type (@$flag_types) { - next if !$type->default_requestee; - $vars->{flag_default_requestees}->{$type->id} = $type->default_requestee->login; + my $flags + = Bugzilla::Flag->match({ + type_id => $type->id, bug_id => $bug->id, status => '?' + }); + map { $vars->{flag_currently_requested}->{$_->id} = 1 } @$flags; } + } + elsif ($file =~ /^bug\/create/ && exists $vars->{product}) { + my $bug_flags = $vars->{product}->flag_types->{bug}; + my $attachment_flags = $vars->{product}->flag_types->{attachment}; + $flag_types = [map {$_} (@$bug_flags, @$attachment_flags)]; + } + + return if !@$flag_types; + + $vars->{flag_default_requestees} ||= {}; + foreach my $type (@$flag_types) { + next if !$type->default_requestee; + $vars->{flag_default_requestees}->{$type->id} = $type->default_requestee->login; + } } ################## @@ -94,83 +96,79 @@ sub template_before_process { ################## BEGIN { - *Bugzilla::FlagType::default_requestee = \&_default_requestee; + *Bugzilla::FlagType::default_requestee = \&_default_requestee; } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::FlagType')) { - push(@$columns, 'default_requestee'); - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::FlagType')) { + push(@$columns, 'default_requestee'); + } } sub object_update_columns { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::FlagType'); + my ($self, $args) = @_; + my $object = $args->{object}; + return unless $object->isa('Bugzilla::FlagType'); - my $columns = $args->{columns}; - push(@$columns, 'default_requestee'); + my $columns = $args->{columns}; + push(@$columns, 'default_requestee'); - # editflagtypes.cgi doesn't call set_all, so we have to do this here - my $input = Bugzilla->input_params; - $object->set('default_requestee', $input->{default_requestee}) - if exists $input->{default_requestee}; + # editflagtypes.cgi doesn't call set_all, so we have to do this here + my $input = Bugzilla->input_params; + $object->set('default_requestee', $input->{default_requestee}) + if exists $input->{default_requestee}; } sub object_validators { - my ($self, $args) = @_; - my $class = $args->{class}; - return unless $class->isa('Bugzilla::FlagType'); + my ($self, $args) = @_; + my $class = $args->{class}; + return unless $class->isa('Bugzilla::FlagType'); - my $validators = $args->{validators}; - $validators->{default_requestee} = \&_check_default_requestee; + my $validators = $args->{validators}; + $validators->{default_requestee} = \&_check_default_requestee; } sub object_before_create { - my ($self, $args) = @_; - my $class = $args->{class}; - return unless $class->isa('Bugzilla::FlagType'); - - my $params = $args->{params}; - my $input = Bugzilla->input_params; - $params->{default_requestee} = $input->{default_requestee} - if exists $params->{default_requestee}; + my ($self, $args) = @_; + my $class = $args->{class}; + return unless $class->isa('Bugzilla::FlagType'); + + my $params = $args->{params}; + my $input = Bugzilla->input_params; + $params->{default_requestee} = $input->{default_requestee} + if exists $params->{default_requestee}; } sub object_end_of_update { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::FlagType'); - - my $old_object = $args->{old_object}; - my $changes = $args->{changes}; - my $old_id = $old_object->default_requestee - ? $old_object->default_requestee->id - : 0; - my $new_id = $object->default_requestee - ? $object->default_requestee->id - : 0; - return if $old_id == $new_id; - - $changes->{default_requestee} = [ $old_id, $new_id ]; + my ($self, $args) = @_; + my $object = $args->{object}; + return unless $object->isa('Bugzilla::FlagType'); + + my $old_object = $args->{old_object}; + my $changes = $args->{changes}; + my $old_id + = $old_object->default_requestee ? $old_object->default_requestee->id : 0; + my $new_id = $object->default_requestee ? $object->default_requestee->id : 0; + return if $old_id == $new_id; + + $changes->{default_requestee} = [$old_id, $new_id]; } sub _check_default_requestee { - my ($self, $value, $field) = @_; - $value = trim($value // ''); - return undef if $value eq ''; - ThrowUserError("flag_default_requestee_review") - if $self->name eq 'review'; - return Bugzilla::User->check($value)->id; + my ($self, $value, $field) = @_; + $value = trim($value // ''); + return undef if $value eq ''; + ThrowUserError("flag_default_requestee_review") if $self->name eq 'review'; + return Bugzilla::User->check($value)->id; } sub _default_requestee { - my ($self) = @_; - return $self->{default_requestee} - ? Bugzilla::User->new({ id => $self->{default_requestee}, cache => 1 }) - : undef; + my ($self) = @_; + return $self->{default_requestee} + ? Bugzilla::User->new({id => $self->{default_requestee}, cache => 1}) + : undef; } __PACKAGE__->NAME; diff --git a/extensions/FlagDefaultRequestee/lib/Constants.pm b/extensions/FlagDefaultRequestee/lib/Constants.pm index 2c2cdf35c..26d3e0d9e 100644 --- a/extensions/FlagDefaultRequestee/lib/Constants.pm +++ b/extensions/FlagDefaultRequestee/lib/Constants.pm @@ -14,14 +14,12 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - FLAGTYPE_TEMPLATES + FLAGTYPE_TEMPLATES ); use constant FLAGTYPE_TEMPLATES => ( - "attachment/edit.html.tmpl", - "attachment/createformcontents.html.tmpl", - "bug/edit.html.tmpl", - "bug/create/create.html.tmpl" + "attachment/edit.html.tmpl", "attachment/createformcontents.html.tmpl", + "bug/edit.html.tmpl", "bug/create/create.html.tmpl" ); 1; diff --git a/extensions/FlagTypeComment/Extension.pm b/extensions/FlagTypeComment/Extension.pm index e7b34113d..e26b08b27 100644 --- a/extensions/FlagTypeComment/Extension.pm +++ b/extensions/FlagTypeComment/Extension.pm @@ -39,31 +39,19 @@ our $VERSION = '1'; ################ sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'flagtype_comments'} = { - FIELDS => [ - type_id => { - TYPE => 'SMALLINT(6)', - NOTNULL => 1, - REFERENCES => { - TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE' - } - }, - on_status => { - TYPE => 'CHAR(1)', - NOTNULL => 1 - }, - comment => { - TYPE => 'MEDIUMTEXT', - NOTNULL => 1 - }, - ], - INDEXES => [ - flagtype_comments_idx => ['type_id'], - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'flagtype_comments'} = { + FIELDS => [ + type_id => { + TYPE => 'SMALLINT(6)', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + on_status => {TYPE => 'CHAR(1)', NOTNULL => 1}, + comment => {TYPE => 'MEDIUMTEXT', NOTNULL => 1}, + ], + INDEXES => [flagtype_comments_idx => ['type_id'],], + }; } ############# @@ -71,83 +59,89 @@ sub db_schema_abstract_schema { ############# sub template_before_process { - my ($self, $args) = @_; - my ($vars, $file) = @$args{qw(vars file)}; + my ($self, $args) = @_; + my ($vars, $file) = @$args{qw(vars file)}; - return unless Bugzilla->user->id; - if (grep { $_ eq $file } FLAGTYPE_COMMENT_TEMPLATES) { - _set_ftc_states($file, $vars); - } + return unless Bugzilla->user->id; + if (grep { $_ eq $file } FLAGTYPE_COMMENT_TEMPLATES) { + _set_ftc_states($file, $vars); + } } sub _set_ftc_states { - my ($file, $vars) = @_; - my $dbh = Bugzilla->dbh; - - my $ftc_flags; - my $db_result; - if ($file =~ /^admin\//) { - # admin - my $type = $vars->{'type'} || return; - my ($target_type, $id); - if (blessed($type)) { - ($target_type, $id) = ($type->target_type, $type->id); - } else { - ($target_type, $id) = ($type->{target_type}, $type->{id}); - trick_taint($id) if $id; - } - if ($target_type eq 'bug') { - return unless FLAGTYPE_COMMENT_BUG_FLAGS; - } else { - return unless FLAGTYPE_COMMENT_ATTACHMENT_FLAGS; - } - if ($id) { - $db_result = $dbh->selectall_arrayref( - "SELECT type_id AS flagtype, on_status AS state, comment AS text + my ($file, $vars) = @_; + my $dbh = Bugzilla->dbh; + + my $ftc_flags; + my $db_result; + if ($file =~ /^admin\//) { + + # admin + my $type = $vars->{'type'} || return; + my ($target_type, $id); + if (blessed($type)) { + ($target_type, $id) = ($type->target_type, $type->id); + } + else { + ($target_type, $id) = ($type->{target_type}, $type->{id}); + trick_taint($id) if $id; + } + if ($target_type eq 'bug') { + return unless FLAGTYPE_COMMENT_BUG_FLAGS; + } + else { + return unless FLAGTYPE_COMMENT_ATTACHMENT_FLAGS; + } + if ($id) { + $db_result = $dbh->selectall_arrayref( + "SELECT type_id AS flagtype, on_status AS state, comment AS text FROM flagtype_comments - WHERE type_id = ?", - { Slice => {} }, $id); - } - } else { - # creating/editing attachment / viewing bug - my $bug; - if (exists $vars->{'bug'}) { - $bug = $vars->{'bug'}; - } elsif (exists $vars->{'attachment'}) { - $bug = $vars->{'attachment'}->{bug}; - } else { - return; - } - - my $flag_types = Bugzilla::FlagType::match({ - 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'), - 'product_id' => $bug->product_id, - 'component_id' => $bug->component_id, - 'bug_id' => $bug->id, - 'active_or_has_flags' => $bug->id, - }); - - if (@$flag_types) { - my $types = join(',', map { $_->id } @$flag_types); - my $states = "'" . join("','", FLAGTYPE_COMMENT_STATES) . "'"; - $db_result = $dbh->selectall_arrayref( - "SELECT type_id AS flagtype, on_status AS state, comment AS text - FROM flagtype_comments - WHERE type_id IN ($types) AND on_status IN ($states)", - { Slice => {} }); - } - else { - $db_result = []; - } + WHERE type_id = ?", {Slice => {}}, $id + ); + } + } + else { + # creating/editing attachment / viewing bug + my $bug; + if (exists $vars->{'bug'}) { + $bug = $vars->{'bug'}; + } + elsif (exists $vars->{'attachment'}) { + $bug = $vars->{'attachment'}->{bug}; + } + else { + return; } - foreach my $row (@$db_result) { - $ftc_flags->{$row->{'flagtype'}} ||= {}; - $ftc_flags->{$row->{'flagtype'}}{$row->{'state'}} = $row->{text}; + my $flag_types = Bugzilla::FlagType::match({ + 'target_type' => ($file =~ /^bug/ ? 'bug' : 'attachment'), + 'product_id' => $bug->product_id, + 'component_id' => $bug->component_id, + 'bug_id' => $bug->id, + 'active_or_has_flags' => $bug->id, + }); + + if (@$flag_types) { + my $types = join(',', map { $_->id } @$flag_types); + my $states = "'" . join("','", FLAGTYPE_COMMENT_STATES) . "'"; + $db_result = $dbh->selectall_arrayref( + "SELECT type_id AS flagtype, on_status AS state, comment AS text + FROM flagtype_comments + WHERE type_id IN ($types) AND on_status IN ($states)", {Slice => {}} + ); + } + else { + $db_result = []; } + } - $vars->{'ftc_states'} = [ FLAGTYPE_COMMENT_STATES ]; - $vars->{'ftc_flags'} = $ftc_flags; + foreach my $row (@$db_result) { + $ftc_flags->{$row->{'flagtype'}} ||= {}; + $ftc_flags->{$row->{'flagtype'}}{$row->{'state'}} = $row->{text}; + } + + $vars->{'ftc_states'} = [FLAGTYPE_COMMENT_STATES]; + $vars->{'ftc_flags'} = $ftc_flags; } ######### @@ -155,55 +149,55 @@ sub _set_ftc_states { ######### sub flagtype_end_of_create { - my ($self, $args) = @_; - _set_flagtypes($args->{type}); + my ($self, $args) = @_; + _set_flagtypes($args->{type}); } sub flagtype_end_of_update { - my ($self, $args) = @_; - _set_flagtypes($args->{type}); + my ($self, $args) = @_; + _set_flagtypes($args->{type}); } sub _set_flagtypes { - my $flag_type = shift; - my $flagtype_id = $flag_type->id; - my $input = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; - - foreach my $state (FLAGTYPE_COMMENT_STATES) { - next if (!defined $input->{"ftc_${flagtype_id}_$state"} - && !defined $input->{"ftc_new_$state"}); - - my $text = $input->{"ftc_${flagtype_id}_$state"} || $input->{"ftc_new_$state"} || ''; - $text =~ s/\r\n/\n/g; - trick_taint($text); - - if ($text ne '') { - if ($dbh->selectrow_array( - "SELECT 1 FROM flagtype_comments WHERE type_id=? AND on_status=?", - undef, - $flagtype_id, $state) - ) { - $dbh->do( - "UPDATE flagtype_comments SET comment=? - WHERE type_id=? AND on_status=?", - undef, - $text, $flagtype_id, $state); - } else { - $dbh->do( - "INSERT INTO flagtype_comments(type_id, on_status, comment) - VALUES (?, ?, ?)", - undef, - $flagtype_id, $state, $text); - } - - } else { - $dbh->do( - "DELETE FROM flagtype_comments WHERE type_id=? AND on_status=?", - undef, - $flagtype_id, $state); - } + my $flag_type = shift; + my $flagtype_id = $flag_type->id; + my $input = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + + foreach my $state (FLAGTYPE_COMMENT_STATES) { + next + if (!defined $input->{"ftc_${flagtype_id}_$state"} + && !defined $input->{"ftc_new_$state"}); + + my $text + = $input->{"ftc_${flagtype_id}_$state"} || $input->{"ftc_new_$state"} || ''; + $text =~ s/\r\n/\n/g; + trick_taint($text); + + if ($text ne '') { + if ($dbh->selectrow_array( + "SELECT 1 FROM flagtype_comments WHERE type_id=? AND on_status=?", undef, + $flagtype_id, $state + )) + { + $dbh->do( + "UPDATE flagtype_comments SET comment=? + WHERE type_id=? AND on_status=?", undef, $text, $flagtype_id, $state + ); + } + else { + $dbh->do( + "INSERT INTO flagtype_comments(type_id, on_status, comment) + VALUES (?, ?, ?)", undef, $flagtype_id, $state, $text + ); + } + + } + else { + $dbh->do("DELETE FROM flagtype_comments WHERE type_id=? AND on_status=?", + undef, $flagtype_id, $state); } + } } __PACKAGE__->NAME; diff --git a/extensions/FlagTypeComment/lib/Constants.pm b/extensions/FlagTypeComment/lib/Constants.pm index d6242b78b..ee3a54076 100644 --- a/extensions/FlagTypeComment/lib/Constants.pm +++ b/extensions/FlagTypeComment/lib/Constants.pm @@ -26,28 +26,26 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - FLAGTYPE_COMMENT_TEMPLATES - FLAGTYPE_COMMENT_STATES - FLAGTYPE_COMMENT_BUG_FLAGS - FLAGTYPE_COMMENT_ATTACHMENT_FLAGS + FLAGTYPE_COMMENT_TEMPLATES + FLAGTYPE_COMMENT_STATES + FLAGTYPE_COMMENT_BUG_FLAGS + FLAGTYPE_COMMENT_ATTACHMENT_FLAGS ); -use constant FLAGTYPE_COMMENT_STATES => ("?", "+", "-"); -use constant FLAGTYPE_COMMENT_BUG_FLAGS => 0; +use constant FLAGTYPE_COMMENT_STATES => ("?", "+", "-"); +use constant FLAGTYPE_COMMENT_BUG_FLAGS => 0; use constant FLAGTYPE_COMMENT_ATTACHMENT_FLAGS => 1; sub FLAGTYPE_COMMENT_TEMPLATES { - my @result = ("admin/flag-type/edit.html.tmpl"); - if (FLAGTYPE_COMMENT_BUG_FLAGS) { - push @result, ("bug/comments.html.tmpl"); - } - if (FLAGTYPE_COMMENT_ATTACHMENT_FLAGS) { - push @result, ( - "attachment/edit.html.tmpl", - "attachment/createformcontents.html.tmpl", - ); - } - return @result; + my @result = ("admin/flag-type/edit.html.tmpl"); + if (FLAGTYPE_COMMENT_BUG_FLAGS) { + push @result, ("bug/comments.html.tmpl"); + } + if (FLAGTYPE_COMMENT_ATTACHMENT_FLAGS) { + push @result, + ("attachment/edit.html.tmpl", "attachment/createformcontents.html.tmpl",); + } + return @result; } 1; diff --git a/extensions/GitHubAuth/Extension.pm b/extensions/GitHubAuth/Extension.pm index d0d9f42f1..85d81b02a 100644 --- a/extensions/GitHubAuth/Extension.pm +++ b/extensions/GitHubAuth/Extension.pm @@ -24,69 +24,73 @@ use URI::QueryParam; our $VERSION = '0.01'; BEGIN { - # Monkey-patch can() on Bugzilla::Auth::Login::CGI so that our own fail_nodata gets called. - # Our fail_nodata behaves like CGI's, so this shouldn't be a problem for CGI-based logins. +# Monkey-patch can() on Bugzilla::Auth::Login::CGI so that our own fail_nodata gets called. +# Our fail_nodata behaves like CGI's, so this shouldn't be a problem for CGI-based logins. - *Bugzilla::Auth::Login::CGI::can = sub { - my ($stack, $method) = @_; + *Bugzilla::Auth::Login::CGI::can = sub { + my ($stack, $method) = @_; - return undef if $method eq 'fail_nodata'; - return $stack->SUPER::can($method); - }; + return undef if $method eq 'fail_nodata'; + return $stack->SUPER::can($method); + }; } sub install_before_final_checks { - Bugzilla::Group->create({ - name => 'no-github-auth', - description => 'Group containing groups whose members may not use GitHubAuth to log in', - isbuggroup => 0, - }) unless Bugzilla::Group->new({ name => 'no-github-auth' }); + Bugzilla::Group->create({ + name => 'no-github-auth', + description => + 'Group containing groups whose members may not use GitHubAuth to log in', + isbuggroup => 0, + }) + unless Bugzilla::Group->new({name => 'no-github-auth'}); } sub attachment_should_redirect_login { - my ($self, $args) = @_; - my $cgi = Bugzilla->cgi; + my ($self, $args) = @_; + my $cgi = Bugzilla->cgi; - if ($cgi->param('github_state') || $cgi->param('github_email')) { - ${$args->{do_redirect}} = 1; - } + if ($cgi->param('github_state') || $cgi->param('github_email')) { + ${$args->{do_redirect}} = 1; + } } sub auth_login_methods { - my ($self, $args) = @_; - my $modules = $args->{'modules'}; - if (exists $modules->{'GitHubAuth'}) { - $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Login.pm'; - } + my ($self, $args) = @_; + my $modules = $args->{'modules'}; + if (exists $modules->{'GitHubAuth'}) { + $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Login.pm'; + } } sub auth_verify_methods { - my ($self, $args) = @_; - my $modules = $args->{'modules'}; - if (exists $modules->{'GitHubAuth'}) { - $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Verify.pm'; - } + my ($self, $args) = @_; + my $modules = $args->{'modules'}; + if (exists $modules->{'GitHubAuth'}) { + $modules->{'GitHubAuth'} = 'Bugzilla/Extension/GitHubAuth/Verify.pm'; + } } sub config_modify_panels { - my ($self, $args) = @_; - my $auth_panel_params = $args->{panels}{auth}{params}; - - my $user_info_class = first { $_->{name} eq 'user_info_class' } @$auth_panel_params; - if ($user_info_class) { - push @{ $user_info_class->{choices} }, "GitHubAuth,CGI"; - } - - my $user_verify_class = first { $_->{name} eq 'user_verify_class' } @$auth_panel_params; - if ($user_verify_class) { - unshift @{ $user_verify_class->{choices} }, "GitHubAuth"; - } + my ($self, $args) = @_; + my $auth_panel_params = $args->{panels}{auth}{params}; + + my $user_info_class + = first { $_->{name} eq 'user_info_class' } @$auth_panel_params; + if ($user_info_class) { + push @{$user_info_class->{choices}}, "GitHubAuth,CGI"; + } + + my $user_verify_class + = first { $_->{name} eq 'user_verify_class' } @$auth_panel_params; + if ($user_verify_class) { + unshift @{$user_verify_class->{choices}}, "GitHubAuth"; + } } sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{GitHubAuth} = "Bugzilla::Extension::GitHubAuth::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{GitHubAuth} = "Bugzilla::Extension::GitHubAuth::Config"; } __PACKAGE__->NAME; diff --git a/extensions/GitHubAuth/lib/Client.pm b/extensions/GitHubAuth/lib/Client.pm index 291501961..328bab48f 100644 --- a/extensions/GitHubAuth/lib/Client.pm +++ b/extensions/GitHubAuth/lib/Client.pm @@ -16,7 +16,8 @@ use URI; use URI::QueryParam; use Digest; -use Bugzilla::Extension::GitHubAuth::Client::Error qw(ThrowUserError ThrowCodeError); +use Bugzilla::Extension::GitHubAuth::Client::Error + qw(ThrowUserError ThrowCodeError); use Bugzilla::Util qw(remote_ip); use constant DIGEST_HASH => 'SHA1'; @@ -24,107 +25,108 @@ use constant DIGEST_HASH => 'SHA1'; use fields qw(user_agent); use constant { - GH_ACCESS_TOKEN_URI => 'https://github.com/login/oauth/access_token', - GH_AUTHORIZE_URI => 'https://github.com/login/oauth/authorize', - GH_USER_EMAILS_URI => 'https://api.github.com/user/emails', + GH_ACCESS_TOKEN_URI => 'https://github.com/login/oauth/access_token', + GH_AUTHORIZE_URI => 'https://github.com/login/oauth/authorize', + GH_USER_EMAILS_URI => 'https://api.github.com/user/emails', }; sub new { - my ($class, %init) = @_; - my $self = $class->fields::new(); + my ($class, %init) = @_; + my $self = $class->fields::new(); - return $self; + return $self; } sub login_uri { - my ($class, $target_uri) = @_; + my ($class, $target_uri) = @_; - my $uri = URI->new(Bugzilla->localconfig->{urlbase} . "github.cgi"); - $uri->query_form(target_uri => $target_uri); - return $uri; + my $uri = URI->new(Bugzilla->localconfig->{urlbase} . "github.cgi"); + $uri->query_form(target_uri => $target_uri); + return $uri; } sub authorize_uri { - my ($class, $state) = @_; + my ($class, $state) = @_; - my $uri = URI->new(GH_AUTHORIZE_URI); - $uri->query_form( - client_id => Bugzilla->params->{github_client_id}, - scope => 'user:email', - state => $state, - redirect_uri => Bugzilla->localconfig->{urlbase} . "github.cgi", - ); + my $uri = URI->new(GH_AUTHORIZE_URI); + $uri->query_form( + client_id => Bugzilla->params->{github_client_id}, + scope => 'user:email', + state => $state, + redirect_uri => Bugzilla->localconfig->{urlbase} . "github.cgi", + ); - return $uri; + return $uri; } sub get_email_key { - my ($class, $email) = @_; - - my $cgi = Bugzilla->cgi; - my $digest = Digest->new(DIGEST_HASH); - $digest->add($email); - $digest->add(remote_ip()); - $digest->add($cgi->cookie('Bugzilla_github_token') // Bugzilla->request_cache->{github_token} // ''); - $digest->add(Bugzilla->localconfig->{site_wide_secret}); - return $digest->hexdigest; + my ($class, $email) = @_; + + my $cgi = Bugzilla->cgi; + my $digest = Digest->new(DIGEST_HASH); + $digest->add($email); + $digest->add(remote_ip()); + $digest->add($cgi->cookie('Bugzilla_github_token') + // Bugzilla->request_cache->{github_token} // ''); + $digest->add(Bugzilla->localconfig->{site_wide_secret}); + return $digest->hexdigest; } sub _handle_response { - my ($self, $response) = @_; - my $data = eval { - decode_json($response->content); - }; - if ($@) { - ThrowCodeError("github_bad_response", { message => "Unable to parse json response" }); - } - - unless ($response->is_success) { - ThrowCodeError("github_error", { response => $response }); - } - return $data; + my ($self, $response) = @_; + my $data = eval { decode_json($response->content); }; + if ($@) { + ThrowCodeError("github_bad_response", + {message => "Unable to parse json response"}); + } + + unless ($response->is_success) { + ThrowCodeError("github_error", {response => $response}); + } + return $data; } sub get_access_token { - my ($self, $code) = @_; - - my $response = $self->user_agent->post( - GH_ACCESS_TOKEN_URI, - { client_id => Bugzilla->params->{github_client_id}, - client_secret => Bugzilla->params->{github_client_secret}, - code => $code }, - Accept => 'application/json', - ); - my $data = $self->_handle_response($response); - return $data->{access_token} if exists $data->{access_token}; + my ($self, $code) = @_; + + my $response = $self->user_agent->post(GH_ACCESS_TOKEN_URI, + { + client_id => Bugzilla->params->{github_client_id}, + client_secret => Bugzilla->params->{github_client_secret}, + code => $code + }, + Accept => 'application/json', + ); + my $data = $self->_handle_response($response); + return $data->{access_token} if exists $data->{access_token}; } sub get_user_emails { - my ($self, $access_token) = @_; - my $uri = URI->new(GH_USER_EMAILS_URI); - $uri->query_form(access_token => $access_token); + my ($self, $access_token) = @_; + my $uri = URI->new(GH_USER_EMAILS_URI); + $uri->query_form(access_token => $access_token); - my $response = $self->user_agent->get($uri, Accept => 'application/json'); + my $response = $self->user_agent->get($uri, Accept => 'application/json'); - return $self->_handle_response($response); + return $self->_handle_response($response); } sub user_agent { - my ($self) = @_; - $self->{user_agent} //= $self->_build_user_agent; + my ($self) = @_; + $self->{user_agent} //= $self->_build_user_agent; - return $self->{user_agent}; + return $self->{user_agent}; } sub _build_user_agent { - my ($self) = @_; - my $ua = LWP::UserAgent->new( timeout => 10 ); + my ($self) = @_; + my $ua = LWP::UserAgent->new(timeout => 10); - if (Bugzilla->params->{proxy_url}) { - $ua->proxy('https', Bugzilla->params->{proxy_url}); - } + if (Bugzilla->params->{proxy_url}) { + $ua->proxy('https', Bugzilla->params->{proxy_url}); + } - return $ua; + return $ua; } 1; diff --git a/extensions/GitHubAuth/lib/Client/Error.pm b/extensions/GitHubAuth/lib/Client/Error.pm index adb6ec07b..00e8415d1 100644 --- a/extensions/GitHubAuth/lib/Client/Error.pm +++ b/extensions/GitHubAuth/lib/Client/Error.pm @@ -16,39 +16,39 @@ use Bugzilla::Error (); use base qw(Exporter); use fields qw(type error vars); -our @EXPORT = qw(ThrowUserError ThrowCodeError); +our @EXPORT = qw(ThrowUserError ThrowCodeError); our $USE_EXCEPTION_OBJECTS = 0; sub _new { - my ($class, $type, $error, $vars) = @_; - my $self = $class->fields::new(); - $self->{type} = $type; - $self->{error} = $error; - $self->{vars} = $vars // {}; + my ($class, $type, $error, $vars) = @_; + my $self = $class->fields::new(); + $self->{type} = $type; + $self->{error} = $error; + $self->{vars} = $vars // {}; - return $self; + return $self; } -sub type { $_[0]->{type} } +sub type { $_[0]->{type} } sub error { $_[0]->{error} } -sub vars { $_[0]->{vars} } +sub vars { $_[0]->{vars} } sub ThrowUserError { - if ($USE_EXCEPTION_OBJECTS) { - die __PACKAGE__->_new('user', @_); - } - else { - Bugzilla::Error::ThrowUserError(@_); - } + if ($USE_EXCEPTION_OBJECTS) { + die __PACKAGE__->_new('user', @_); + } + else { + Bugzilla::Error::ThrowUserError(@_); + } } sub ThrowCodeError { - if ($USE_EXCEPTION_OBJECTS) { - die __PACKAGE__->_new('code', @_); - } - else { - Bugzilla::Error::ThrowCodeError(@_); - } + if ($USE_EXCEPTION_OBJECTS) { + die __PACKAGE__->_new('code', @_); + } + else { + Bugzilla::Error::ThrowCodeError(@_); + } } 1; diff --git a/extensions/GitHubAuth/lib/Config.pm b/extensions/GitHubAuth/lib/Config.pm index 0c8874129..b718b4be5 100644 --- a/extensions/GitHubAuth/lib/Config.pm +++ b/extensions/GitHubAuth/lib/Config.pm @@ -16,22 +16,14 @@ use Bugzilla::Config::Common; our $sortkey = 1350; sub get_param_list { - my ($class) = @_; - - my @params = ( - { - name => 'github_client_id', - type => 't', - default => '', - }, - { - name => 'github_client_secret', - type => 't', - default => '', - }, - ); - - return @params; + my ($class) = @_; + + my @params = ( + {name => 'github_client_id', type => 't', default => '',}, + {name => 'github_client_secret', type => 't', default => '',}, + ); + + return @params; } 1; diff --git a/extensions/GitHubAuth/lib/Login.pm b/extensions/GitHubAuth/lib/Login.pm index 073fbfeea..df3195bc7 100644 --- a/extensions/GitHubAuth/lib/Login.pm +++ b/extensions/GitHubAuth/lib/Login.pm @@ -24,187 +24,208 @@ use Bugzilla::Extension::GitHubAuth::Client; use Bugzilla::Extension::GitHubAuth::Client::Error (); use Bugzilla::Error; -use constant { requires_verification => 1, - is_automatic => 1, - user_can_create_account => 1 }; +use constant {requires_verification => 1, is_automatic => 1, + user_can_create_account => 1}; sub get_login_info { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $github_action = Bugzilla->request_cache->{github_action}; - - return { failure => AUTH_NODATA } unless $github_action; - - my $response; - if ($github_action eq 'login') { - $response = $self->_get_login_info_from_github(); - } - elsif ($github_action eq 'email') { - $response = $self->_get_login_info_from_email(); - } - - if (!exists $response->{failure}) { - if (exists $response->{user}) { - # existing account - my $user = $response->{user}; - return { failure => AUTH_ERROR, - user_error => 'github_auth_account_too_powerful' } if $user->in_group('no-github-auth'); - return { failure => AUTH_ERROR, - user_error => 'mfa_prevents_login', - details => { provider => 'GitHub' } } if $user->mfa; - $response = { - username => $user->login, - user_id => $user->id, - github_auth => 1, - }; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $github_action = Bugzilla->request_cache->{github_action}; + + return {failure => AUTH_NODATA} unless $github_action; + + my $response; + if ($github_action eq 'login') { + $response = $self->_get_login_info_from_github(); + } + elsif ($github_action eq 'email') { + $response = $self->_get_login_info_from_email(); + } + + if (!exists $response->{failure}) { + if (exists $response->{user}) { + + # existing account + my $user = $response->{user}; + return { + failure => AUTH_ERROR, + user_error => 'github_auth_account_too_powerful' } - else { - # new account - my $email = $response->{email}; - $response = { - username => $email, - github_auth => 1, - }; + if $user->in_group('no-github-auth'); + return { + failure => AUTH_ERROR, + user_error => 'mfa_prevents_login', + details => {provider => 'GitHub'} } + if $user->mfa; + $response = {username => $user->login, user_id => $user->id, github_auth => 1,}; } - return $response; + else { + # new account + my $email = $response->{email}; + $response = {username => $email, github_auth => 1,}; + } + } + return $response; } sub _get_login_info_from_github { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; - my $code = $cgi->param('code'); - - return { failure => AUTH_ERROR, error => 'github_missing_code' } unless $code; - - trick_taint($code); - - my $client = Bugzilla::Extension::GitHubAuth::Client->new; - - my ($access_token, $emails); - eval { - # The following variable lets us catch and return (rather than throw) errors - # from our github client code, as required by the Auth API. - local $Bugzilla::Extension::GitHubAuth::Client::Error::USE_EXCEPTION_OBJECTS = 1; - $access_token = $client->get_access_token($code); - $emails = $client->get_user_emails($access_token); - }; - my $e = $@; - if (blessed $e && $e->isa('Bugzilla::Extension::GitHubAuth::Client::Error')) { - my $key = $e->type eq 'user' ? 'user_error' : 'error'; - return { failure => AUTH_ERROR, $key => $e->error, details => $e->vars }; - } - elsif ($e) { - die $e; - } - - my @emails = map { $_->{email} } - grep { $_->{verified} && $_->{email} !~ /\@users\.noreply\.github\.com$/ } @$emails; - - my @bugzilla_users; - my @github_emails; - foreach my $email (@emails) { - my $user = Bugzilla::User->new({name => $email, cache => 1}); - if ($user) { - push @bugzilla_users, $user; - } - else { - push @github_emails, $email; - } - } - my @allowed_bugzilla_users = grep { not $_->in_group('no-github-auth') } @bugzilla_users; - - if (@allowed_bugzilla_users == 1) { - my ($user) = @allowed_bugzilla_users; - return { user => $user }; - } - elsif (@allowed_bugzilla_users > 1) { - $self->{github_failure} = { - template => 'account/auth/github-verify-account.html.tmpl', - vars => { - bugzilla_users => \@allowed_bugzilla_users, - choose_email => _mk_choose_email(\@emails), - }, - }; - return { failure => AUTH_NODATA }; - } - elsif (@allowed_bugzilla_users == 0 && @bugzilla_users > 0 && @github_emails == 0) { - return { failure => AUTH_ERROR, - user_error => 'github_auth_account_too_powerful' }; - } - elsif (@github_emails) { - $self->{github_failure} = { - template => 'account/auth/github-verify-account.html.tmpl', - vars => { - github_emails => \@github_emails, - choose_email => _mk_choose_email(\@emails), - }, - }; - return { failure => AUTH_NODATA }; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; + my $code = $cgi->param('code'); + + return {failure => AUTH_ERROR, error => 'github_missing_code'} unless $code; + + trick_taint($code); + + my $client = Bugzilla::Extension::GitHubAuth::Client->new; + + my ($access_token, $emails); + eval { + # The following variable lets us catch and return (rather than throw) errors + # from our github client code, as required by the Auth API. + local $Bugzilla::Extension::GitHubAuth::Client::Error::USE_EXCEPTION_OBJECTS + = 1; + $access_token = $client->get_access_token($code); + $emails = $client->get_user_emails($access_token); + }; + my $e = $@; + if (blessed $e && $e->isa('Bugzilla::Extension::GitHubAuth::Client::Error')) { + my $key = $e->type eq 'user' ? 'user_error' : 'error'; + return {failure => AUTH_ERROR, $key => $e->error, details => $e->vars}; + } + elsif ($e) { + die $e; + } + + my @emails + = map { $_->{email} } + grep { $_->{verified} && $_->{email} !~ /\@users\.noreply\.github\.com$/ } + @$emails; + + my @bugzilla_users; + my @github_emails; + foreach my $email (@emails) { + my $user = Bugzilla::User->new({name => $email, cache => 1}); + if ($user) { + push @bugzilla_users, $user; } else { - return { failure => AUTH_ERROR, user_error => 'github_no_emails' }; + push @github_emails, $email; } + } + my @allowed_bugzilla_users + = grep { not $_->in_group('no-github-auth') } @bugzilla_users; + + if (@allowed_bugzilla_users == 1) { + my ($user) = @allowed_bugzilla_users; + return {user => $user}; + } + elsif (@allowed_bugzilla_users > 1) { + $self->{github_failure} = { + template => 'account/auth/github-verify-account.html.tmpl', + vars => { + bugzilla_users => \@allowed_bugzilla_users, + choose_email => _mk_choose_email(\@emails), + }, + }; + return {failure => AUTH_NODATA}; + } + elsif (@allowed_bugzilla_users == 0 + && @bugzilla_users > 0 + && @github_emails == 0) + { + return { + failure => AUTH_ERROR, + user_error => 'github_auth_account_too_powerful' + }; + } + elsif (@github_emails) { + $self->{github_failure} = { + template => 'account/auth/github-verify-account.html.tmpl', + vars => { + github_emails => \@github_emails, + choose_email => _mk_choose_email(\@emails), + }, + }; + return {failure => AUTH_NODATA}; + } + else { + return {failure => AUTH_ERROR, user_error => 'github_no_emails'}; + } } sub _get_login_info_from_email { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $email = $cgi->param('email') or return { failure => AUTH_ERROR, - user_error => 'github_invalid_email', - details => { email => '' } }; - trick_taint($email); - - unless (any { $_ eq $email } @{ Bugzilla->request_cache->{github_emails} }) { - return { failure => AUTH_ERROR, - user_error => 'github_invalid_email', - details => { email => $email }}; - } + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $email = $cgi->param('email') + or return { + failure => AUTH_ERROR, + user_error => 'github_invalid_email', + details => {email => ''} + }; + trick_taint($email); - my $user = Bugzilla::User->new({name => $email, cache => 1}); - $cgi->remove_cookie('Bugzilla_github_token'); - return $user ? { user => $user } : { email => $email }; + unless (any { $_ eq $email } @{Bugzilla->request_cache->{github_emails}}) { + return { + failure => AUTH_ERROR, + user_error => 'github_invalid_email', + details => {email => $email} + }; + } + + my $user = Bugzilla::User->new({name => $email, cache => 1}); + $cgi->remove_cookie('Bugzilla_github_token'); + return $user ? {user => $user} : {email => $email}; } sub fail_nodata { - my ($self) = @_; - my $cgi = Bugzilla->cgi; - my $template = Bugzilla->template; + my ($self) = @_; + my $cgi = Bugzilla->cgi; + my $template = Bugzilla->template; - ThrowUserError('login_required') if Bugzilla->usage_mode != USAGE_MODE_BROWSER; + ThrowUserError('login_required') if Bugzilla->usage_mode != USAGE_MODE_BROWSER; - my $file = $self->{github_failure}{template} // "account/auth/login.html.tmpl"; - my $vars = $self->{github_failure}{vars} // { target => $cgi->url(-relative=>1) }; + my $file = $self->{github_failure}{template} // "account/auth/login.html.tmpl"; + my $vars = $self->{github_failure}{vars} + // {target => $cgi->url(-relative => 1)}; - print $cgi->header(); - $template->process($file, $vars) or ThrowTemplateError($template->error()); - exit; + print $cgi->header(); + $template->process($file, $vars) or ThrowTemplateError($template->error()); + exit; } sub _store_emails { - my ($emails) = @_; - my $state = issue_short_lived_session_token("github_email"); - set_token_extra_data($state, { type => 'github_email', - emails => $emails, - target_uri => Bugzilla->request_cache->{github_target_uri} }); - - Bugzilla->cgi->send_cookie(-name => 'github_state', - -value => $state, - -httponly => 1); - return $state; + my ($emails) = @_; + my $state = issue_short_lived_session_token("github_email"); + set_token_extra_data( + $state, + { + type => 'github_email', + emails => $emails, + target_uri => Bugzilla->request_cache->{github_target_uri} + } + ); + + Bugzilla->cgi->send_cookie( + -name => 'github_state', + -value => $state, + -httponly => 1 + ); + return $state; } sub _mk_choose_email { - my ($emails) = @_; - my $state = _store_emails($emails); - - return sub { - my $email = shift; - my $uri = URI->new(Bugzilla->localconfig->{urlbase} . "github.cgi"); - $uri->query_form( state => $state, email => $email ); - return $uri; - }; + my ($emails) = @_; + my $state = _store_emails($emails); + + return sub { + my $email = shift; + my $uri = URI->new(Bugzilla->localconfig->{urlbase} . "github.cgi"); + $uri->query_form(state => $state, email => $email); + return $uri; + }; } 1; diff --git a/extensions/GitHubAuth/lib/Verify.pm b/extensions/GitHubAuth/lib/Verify.pm index f399af02e..078353c80 100644 --- a/extensions/GitHubAuth/lib/Verify.pm +++ b/extensions/GitHubAuth/lib/Verify.pm @@ -16,11 +16,11 @@ use base qw(Bugzilla::Auth::Verify); use Bugzilla::Constants qw( AUTH_NO_SUCH_USER ); sub check_credentials { - my ($self, $login_data) = @_; + my ($self, $login_data) = @_; - return { failure => AUTH_NO_SUCH_USER } unless $login_data->{github_auth}; + return {failure => AUTH_NO_SUCH_USER} unless $login_data->{github_auth}; - return $login_data; + return $login_data; } 1; diff --git a/extensions/GoogleAnalytics/Extension.pm b/extensions/GoogleAnalytics/Extension.pm index e9b144da4..fb7e8adae 100644 --- a/extensions/GoogleAnalytics/Extension.pm +++ b/extensions/GoogleAnalytics/Extension.pm @@ -15,9 +15,9 @@ use parent qw(Bugzilla::Extension); our $VERSION = '0.1'; sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{GoogleAnalytics} = "Bugzilla::Extension::GoogleAnalytics::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{GoogleAnalytics} = "Bugzilla::Extension::GoogleAnalytics::Config"; } __PACKAGE__->NAME; diff --git a/extensions/GoogleAnalytics/lib/Config.pm b/extensions/GoogleAnalytics/lib/Config.pm index f9e003ce0..1c453ff74 100644 --- a/extensions/GoogleAnalytics/lib/Config.pm +++ b/extensions/GoogleAnalytics/lib/Config.pm @@ -14,28 +14,25 @@ use warnings; use Bugzilla::Config::Common; sub get_param_list { - my ($class) = @_; - - my @params = ( - { - name => 'google_analytics_tracking_id', - type => 't', - default => '', - checker => sub { - my ($tracking_id) = (@_); - - return 'must be like UA-XXXXXX-X' unless $tracking_id =~ m{^(UA-[[:xdigit:]]+-[[:xdigit:]]+)?$}; - return ''; - } - }, - { - name => 'google_analytics_debug', - type => 'b', - default => 0 - }, - ); - - return @params; + my ($class) = @_; + + my @params = ( + { + name => 'google_analytics_tracking_id', + type => 't', + default => '', + checker => sub { + my ($tracking_id) = (@_); + + return 'must be like UA-XXXXXX-X' + unless $tracking_id =~ m{^(UA-[[:xdigit:]]+-[[:xdigit:]]+)?$}; + return ''; + } + }, + {name => 'google_analytics_debug', type => 'b', default => 0}, + ); + + return @params; } 1; diff --git a/extensions/Gravatar/Config.pm b/extensions/Gravatar/Config.pm index e0c684c9b..8651966b5 100644 --- a/extensions/Gravatar/Config.pm +++ b/extensions/Gravatar/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'Gravatar'; +use constant NAME => 'Gravatar'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/Gravatar/Extension.pm b/extensions/Gravatar/Extension.pm index 97cf23b00..04d5f3090 100644 --- a/extensions/Gravatar/Extension.pm +++ b/extensions/Gravatar/Extension.pm @@ -20,37 +20,39 @@ use Digest::MD5 qw(md5_hex); use constant DEFAULT_URL => 'extensions/Gravatar/web/default.jpg'; BEGIN { - *Bugzilla::User::gravatar = \&_user_gravatar; + *Bugzilla::User::gravatar = \&_user_gravatar; } sub _user_gravatar { - my ($self, $size) = @_; - if ($self->setting('show_my_gravatar') eq 'Off') { - return DEFAULT_URL; - } - if (!$self->{gravatar}) { - my $email = $self->email; - $email = $gravatar_user_map{$self->email} if exists $gravatar_user_map{$self->email}; - $self->{gravatar} = 'https://secure.gravatar.com/avatar/' . md5_hex(lc($email)) . '?d=mm'; - } - $size ||= 64; - return $self->{gravatar} . '&size=' . $size; + my ($self, $size) = @_; + if ($self->setting('show_my_gravatar') eq 'Off') { + return DEFAULT_URL; + } + if (!$self->{gravatar}) { + my $email = $self->email; + $email = $gravatar_user_map{$self->email} + if exists $gravatar_user_map{$self->email}; + $self->{gravatar} + = 'https://secure.gravatar.com/avatar/' . md5_hex(lc($email)) . '?d=mm'; + } + $size ||= 64; + return $self->{gravatar} . '&size=' . $size; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'show_gravatars', - options => ['On', 'Off'], - default => 'Off', - category => 'Bug Editing' - }); - add_setting({ - name => 'show_my_gravatar', - options => ['On', 'Off'], - default => 'On', - category => 'Bug Editing' - }); + my ($self, $args) = @_; + add_setting({ + name => 'show_gravatars', + options => ['On', 'Off'], + default => 'Off', + category => 'Bug Editing' + }); + add_setting({ + name => 'show_my_gravatar', + options => ['On', 'Off'], + default => 'On', + category => 'Bug Editing' + }); } __PACKAGE__->NAME; diff --git a/extensions/Gravatar/lib/Data.pm b/extensions/Gravatar/lib/Data.pm index 763dba85b..13b004fa5 100644 --- a/extensions/Gravatar/lib/Data.pm +++ b/extensions/Gravatar/lib/Data.pm @@ -13,11 +13,9 @@ use warnings; use base 'Exporter'; our @EXPORT_OK = qw( - %gravatar_user_map + %gravatar_user_map ); -our %gravatar_user_map = ( - 'orangefactor@bots.tld' => 'tbplbot@gmail.com', -); +our %gravatar_user_map = ('orangefactor@bots.tld' => 'tbplbot@gmail.com',); 1; diff --git a/extensions/GuidedBugEntry/Config.pm b/extensions/GuidedBugEntry/Config.pm index 316fc6cdc..7fca2ccf0 100644 --- a/extensions/GuidedBugEntry/Config.pm +++ b/extensions/GuidedBugEntry/Config.pm @@ -13,10 +13,8 @@ use warnings; use constant NAME => 'GuidedBugEntry'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/GuidedBugEntry/Extension.pm b/extensions/GuidedBugEntry/Extension.pm index 2d58d506a..72bae0d84 100644 --- a/extensions/GuidedBugEntry/Extension.pm +++ b/extensions/GuidedBugEntry/Extension.pm @@ -23,111 +23,103 @@ use Bugzilla::Extension::BMO::Data; our $VERSION = '1'; sub enter_bug_start { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - - # hack for skipping old guided code when enabled - $vars->{'disable_guided'} = 1; - - # force guided format for new users - my $format = $cgi->param('format') || ''; - if ($cgi->param('maketemplate')) { - $format = '__default__'; + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + + # hack for skipping old guided code when enabled + $vars->{'disable_guided'} = 1; + + # force guided format for new users + my $format = $cgi->param('format') || ''; + if ($cgi->param('maketemplate')) { + $format = '__default__'; + } + + if ($format eq 'guided' || ($format eq '' && !$user->in_group('editbugs'))) { + + # skip the first step if a product is provided + if ($cgi->param('product')) { + print $cgi->redirect('enter_bug.cgi?format=guided' + . ($cgi->param('format_forced') ? '&format_forced=1' : '') + . '#h=dupes' . '|' + . url_quote($cgi->param('product')) . '|' + . url_quote($cgi->param('component') || '')); + exit; } - if ( - $format eq 'guided' || - ( - $format eq '' && - !$user->in_group('editbugs') - ) - ) { - # skip the first step if a product is provided - if ($cgi->param('product')) { - print $cgi->redirect('enter_bug.cgi?format=guided' . - ($cgi->param('format_forced') ? '&format_forced=1' : '') . - '#h=dupes' . - '|' . url_quote($cgi->param('product')) . - '|' . url_quote($cgi->param('component') || '') - ); - exit; - } - - # Do not redirect to product forms if we came from there already - $vars->{'format_forced'} = 1 if $cgi->param('format_forced'); - - $self->_init_vars($vars); - print $cgi->header(); - $template->process('guided/guided.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - } - - # we use the __default__ format to bypass the guided entry - # it isn't understood upstream, so remove it once a product - # has been selected. - if ( - ($cgi->param('format') && $cgi->param('format') eq "__default__") - && ($cgi->param('product') && $cgi->param('product') ne '') - ) { - $cgi->delete('format'); - } + # Do not redirect to product forms if we came from there already + $vars->{'format_forced'} = 1 if $cgi->param('format_forced'); + + $self->_init_vars($vars); + print $cgi->header(); + $template->process('guided/guided.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + } + + # we use the __default__ format to bypass the guided entry + # it isn't understood upstream, so remove it once a product + # has been selected. + if ( ($cgi->param('format') && $cgi->param('format') eq "__default__") + && ($cgi->param('product') && $cgi->param('product') ne '')) + { + $cgi->delete('format'); + } } sub _init_vars { - my ($self, $vars) = @_; - my $user = Bugzilla->user; - - my @enterable_products = @{$user->get_enterable_products}; - ThrowUserError('no_products') unless scalar(@enterable_products); - - my @classifications = ({object => undef, products => \@enterable_products}); - - my $class; - foreach my $product (@enterable_products) { - $class->{$product->classification_id}->{'object'} ||= - new Bugzilla::Classification($product->classification_id); - push(@{$class->{$product->classification_id}->{'products'}}, $product); - } - @classifications = - sort { - $a->{'object'}->sortkey <=> $b->{'object'}->sortkey - || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) - } (values %$class); - $vars->{'classifications'} = \@classifications; - - my @open_states = BUG_STATE_OPEN(); - $vars->{'open_states'} = \@open_states; - - $vars->{'token'} = issue_session_token('create_bug'); - - $vars->{'platform'} = detect_platform(); - $vars->{'op_sys'} = detect_op_sys(); - $vars->{'webdev'} = Bugzilla->cgi->param('webdev'); + my ($self, $vars) = @_; + my $user = Bugzilla->user; + + my @enterable_products = @{$user->get_enterable_products}; + ThrowUserError('no_products') unless scalar(@enterable_products); + + my @classifications = ({object => undef, products => \@enterable_products}); + + my $class; + foreach my $product (@enterable_products) { + $class->{$product->classification_id}->{'object'} + ||= new Bugzilla::Classification($product->classification_id); + push(@{$class->{$product->classification_id}->{'products'}}, $product); + } + @classifications = sort { + $a->{'object'}->sortkey <=> $b->{'object'}->sortkey + || lc($a->{'object'}->name) cmp lc($b->{'object'}->name) + } (values %$class); + $vars->{'classifications'} = \@classifications; + + my @open_states = BUG_STATE_OPEN(); + $vars->{'open_states'} = \@open_states; + + $vars->{'token'} = issue_session_token('create_bug'); + + $vars->{'platform'} = detect_platform(); + $vars->{'op_sys'} = detect_op_sys(); + $vars->{'webdev'} = Bugzilla->cgi->param('webdev'); } sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; - my $cgi = Bugzilla->cgi; - - return unless $page eq 'guided_products.js'; - - if (!$cgi->param('format_forced')) { - my %bug_formats; - foreach my $product (keys %create_bug_formats) { - if (my $format = Bugzilla::Extension::BMO::forced_format($product)) { - $bug_formats{$product} = $format; - } - } - $vars->{'create_bug_formats'} = \%bug_formats; + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + my $cgi = Bugzilla->cgi; + + return unless $page eq 'guided_products.js'; + + if (!$cgi->param('format_forced')) { + my %bug_formats; + foreach my $product (keys %create_bug_formats) { + if (my $format = Bugzilla::Extension::BMO::forced_format($product)) { + $bug_formats{$product} = $format; + } } + $vars->{'create_bug_formats'} = \%bug_formats; + } - $vars->{'webdev'} = $cgi->param('webdev'); + $vars->{'webdev'} = $cgi->param('webdev'); } __PACKAGE__->NAME; diff --git a/extensions/InlineHistory/Extension.pm b/extensions/InlineHistory/Extension.pm index adbfa4c74..45f1120f8 100644 --- a/extensions/InlineHistory/Extension.pm +++ b/extensions/InlineHistory/Extension.pm @@ -24,223 +24,227 @@ our $VERSION = '1.5'; use constant MAXIMUM_ACTIVITY_COUNT => 500; # don't show really long values -use constant MAXIMUM_VALUE_LENGTH => 256; +use constant MAXIMUM_VALUE_LENGTH => 256; sub template_before_create { - my ($self, $args) = @_; - $args->{config}->{FILTERS}->{ih_short_value} = sub { - my ($str) = @_; - return length($str) <= MAXIMUM_VALUE_LENGTH - ? $str - : substr($str, 0, MAXIMUM_VALUE_LENGTH - 3) . '...'; - }; + my ($self, $args) = @_; + $args->{config}->{FILTERS}->{ih_short_value} = sub { + my ($str) = @_; + return + length($str) <= MAXIMUM_VALUE_LENGTH + ? $str + : substr($str, 0, MAXIMUM_VALUE_LENGTH - 3) . '...'; + }; } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - return if $file ne 'bug/edit.html.tmpl'; - - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - return unless $user->id && $user->settings->{'inline_history'}->{'value'} eq 'on'; - - # note: bug/edit.html.tmpl doesn't support multiple bugs - my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; - my $bug_id = $bug->id; - - # build bug activity - my ($activity) = $bug->can('get_activity') - ? $bug->get_activity() - : Bugzilla::Bug::GetBugActivity($bug_id); - $activity = _add_duplicates($bug_id, $activity); - - if (scalar @$activity > MAXIMUM_ACTIVITY_COUNT) { - $activity = []; - $vars->{'ih_activity'} = 0; - $vars->{'ih_activity_max'} = 1; - return; - } - - # allow other extensions to alter history - Bugzilla::Hook::process('inline_history_activtiy', { activity => $activity }); - - my %attachment_cache; - foreach my $attachment (@{$bug->attachments}) { - $attachment_cache{$attachment->id} = $attachment; - } - - # build a list of bugs we need to check visibility of, so we can check with a single query - my %visible_bug_ids; - - # augment and tweak - foreach my $operation (@$activity) { - # make operation.who an object - $operation->{who} = - Bugzilla::User->new({ name => $operation->{who}, cache => 1 }); - - for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { - my $change = $operation->{changes}->[$i]; - - # make an attachment object - if ($change->{attachid}) { - $change->{attach} = $attachment_cache{$change->{attachid}}; - } - - # empty resolutions are displayed as --- by default - # make it explicit here to enable correct display of the change - if ($change->{fieldname} eq 'resolution') { - $change->{removed} = '---' if $change->{removed} eq ''; - $change->{added} = '---' if $change->{added} eq ''; - } - - # make boolean fields true/false instead of 1/0 - my ($table, $field) = ('bugs', $change->{fieldname}); - if ($field =~ /^([^\.]+)\.(.+)$/) { - ($table, $field) = ($1, $2); - } - my $column = $dbh->bz_column_info($table, $field); - if ($column && $column->{TYPE} eq 'BOOLEAN') { - $change->{removed} = ''; - $change->{added} = $change->{added} ? 'true' : 'false'; - } - - my $field_obj; - if ($change->{fieldname} =~ /^cf_/) { - $field_obj = Bugzilla::Field->new({ name => $change->{fieldname}, cache => 1 }); - $change->{fieldtype} = $field_obj->type; - } - - # identify buglist changes - if ($change->{fieldname} eq 'blocked' || - $change->{fieldname} eq 'dependson' || - $change->{fieldname} eq 'dupe' || - ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID) - ) { - $change->{buglist} = 1; - foreach my $what (qw(removed added)) { - my @buglist = split(/[\s,]+/, $change->{$what}); - foreach my $id (@buglist) { - if ($id && $id =~ /^\d+$/) { - $visible_bug_ids{$id} = 1; - } - } - } - } - - # split see-also - if ($change->{fieldname} eq 'see_also') { - my $url_base = Bugzilla->localconfig->{urlbase}; - foreach my $f (qw( added removed )) { - my @values; - foreach my $value (split(/, /, $change->{$f})) { - my ($bug_id) = substr($value, 0, length($url_base)) eq $url_base - ? $value =~ /id=(\d+)$/ - : undef; - push @values, { - url => $value, - bug_id => $bug_id, - }; - } - $change->{$f} = \@values; - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + return if $file ne 'bug/edit.html.tmpl'; + + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + return + unless $user->id && $user->settings->{'inline_history'}->{'value'} eq 'on'; + + # note: bug/edit.html.tmpl doesn't support multiple bugs + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + my $bug_id = $bug->id; + + # build bug activity + my ($activity) + = $bug->can('get_activity') + ? $bug->get_activity() + : Bugzilla::Bug::GetBugActivity($bug_id); + $activity = _add_duplicates($bug_id, $activity); + + if (scalar @$activity > MAXIMUM_ACTIVITY_COUNT) { + $activity = []; + $vars->{'ih_activity'} = 0; + $vars->{'ih_activity_max'} = 1; + return; + } + + # allow other extensions to alter history + Bugzilla::Hook::process('inline_history_activtiy', {activity => $activity}); + + my %attachment_cache; + foreach my $attachment (@{$bug->attachments}) { + $attachment_cache{$attachment->id} = $attachment; + } + +# build a list of bugs we need to check visibility of, so we can check with a single query + my %visible_bug_ids; + + # augment and tweak + foreach my $operation (@$activity) { + + # make operation.who an object + $operation->{who} + = Bugzilla::User->new({name => $operation->{who}, cache => 1}); + + for (my $i = 0; $i < scalar(@{$operation->{changes}}); $i++) { + my $change = $operation->{changes}->[$i]; + + # make an attachment object + if ($change->{attachid}) { + $change->{attach} = $attachment_cache{$change->{attachid}}; + } + + # empty resolutions are displayed as --- by default + # make it explicit here to enable correct display of the change + if ($change->{fieldname} eq 'resolution') { + $change->{removed} = '---' if $change->{removed} eq ''; + $change->{added} = '---' if $change->{added} eq ''; + } + + # make boolean fields true/false instead of 1/0 + my ($table, $field) = ('bugs', $change->{fieldname}); + if ($field =~ /^([^\.]+)\.(.+)$/) { + ($table, $field) = ($1, $2); + } + my $column = $dbh->bz_column_info($table, $field); + if ($column && $column->{TYPE} eq 'BOOLEAN') { + $change->{removed} = ''; + $change->{added} = $change->{added} ? 'true' : 'false'; + } + + my $field_obj; + if ($change->{fieldname} =~ /^cf_/) { + $field_obj = Bugzilla::Field->new({name => $change->{fieldname}, cache => 1}); + $change->{fieldtype} = $field_obj->type; + } + + # identify buglist changes + if ( $change->{fieldname} eq 'blocked' + || $change->{fieldname} eq 'dependson' + || $change->{fieldname} eq 'dupe' + || ($field_obj && $field_obj->type == FIELD_TYPE_BUG_ID)) + { + $change->{buglist} = 1; + foreach my $what (qw(removed added)) { + my @buglist = split(/[\s,]+/, $change->{$what}); + foreach my $id (@buglist) { + if ($id && $id =~ /^\d+$/) { + $visible_bug_ids{$id} = 1; } + } + } + } + + # split see-also + if ($change->{fieldname} eq 'see_also') { + my $url_base = Bugzilla->localconfig->{urlbase}; + foreach my $f (qw( added removed )) { + my @values; + foreach my $value (split(/, /, $change->{$f})) { + my ($bug_id) + = substr($value, 0, length($url_base)) eq $url_base + ? $value =~ /id=(\d+)$/ + : undef; + push @values, {url => $value, bug_id => $bug_id,}; + } + $change->{$f} = \@values; + } + } + + # split multiple flag changes (must be processed last) + if ($change->{fieldname} eq 'flagtypes.name') { + my @added = split(/, /, $change->{added}); + my @removed = split(/, /, $change->{removed}); + next if scalar(@added) <= 1 && scalar(@removed) <= 1; + + # remove current change + splice(@{$operation->{changes}}, $i, 1); + + # restructure into added/removed for each flag + my %flags; + foreach my $added (@added) { + my ($value, $name) = $added =~ /^((.+).)$/; + next unless defined $name; + $flags{$name}{added} = $value; + $flags{$name}{removed} |= ''; + } + foreach my $removed (@removed) { + my ($value, $name) = $removed =~ /^((.+).)$/; + next unless defined $name; + $flags{$name}{added} |= ''; + $flags{$name}{removed} = $value; + } - # split multiple flag changes (must be processed last) - if ($change->{fieldname} eq 'flagtypes.name') { - my @added = split(/, /, $change->{added}); - my @removed = split(/, /, $change->{removed}); - next if scalar(@added) <= 1 && scalar(@removed) <= 1; - # remove current change - splice(@{$operation->{changes}}, $i, 1); - # restructure into added/removed for each flag - my %flags; - foreach my $added (@added) { - my ($value, $name) = $added =~ /^((.+).)$/; - next unless defined $name; - $flags{$name}{added} = $value; - $flags{$name}{removed} |= ''; - } - foreach my $removed (@removed) { - my ($value, $name) = $removed =~ /^((.+).)$/; - next unless defined $name; - $flags{$name}{added} |= ''; - $flags{$name}{removed} = $value; - } - # clone current change, modify and insert - foreach my $flag (sort keys %flags) { - my $flag_change = {}; - foreach my $key (keys %$change) { - $flag_change->{$key} = $change->{$key}; - } - $flag_change->{removed} = $flags{$flag}{removed}; - $flag_change->{added} = $flags{$flag}{added}; - splice(@{$operation->{changes}}, $i, 0, $flag_change); - } - $i--; - } + # clone current change, modify and insert + foreach my $flag (sort keys %flags) { + my $flag_change = {}; + foreach my $key (keys %$change) { + $flag_change->{$key} = $change->{$key}; + } + $flag_change->{removed} = $flags{$flag}{removed}; + $flag_change->{added} = $flags{$flag}{added}; + splice(@{$operation->{changes}}, $i, 0, $flag_change); } + $i--; + } } + } - $user->visible_bugs([keys %visible_bug_ids]); + $user->visible_bugs([keys %visible_bug_ids]); - $vars->{'ih_activity'} = $activity; + $vars->{'ih_activity'} = $activity; } sub _add_duplicates { - # insert 'is a dupe of this bug' comment to allow js to display - # as activity - my ($bug_id, $activity) = @_; + # insert 'is a dupe of this bug' comment to allow js to display + # as activity - # we're ignoring pre-bugzilla 3.0 ".. has been marked as a duplicate .." - # comments because searching each comment's text is expensive. these - # legacy comments will not be visible at all in the bug's comment/activity - # stream. bug 928786 deals with migrating those comments to be stored as - # CMT_HAS_DUPE instead. + my ($bug_id, $activity) = @_; - my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" - SELECT profiles.login_name, " . - $dbh->sql_date_format('bug_when', '%Y.%m.%d %H:%i:%s') . ", + # we're ignoring pre-bugzilla 3.0 ".. has been marked as a duplicate .." + # comments because searching each comment's text is expensive. these + # legacy comments will not be visible at all in the bug's comment/activity + # stream. bug 928786 deals with migrating those comments to be stored as + # CMT_HAS_DUPE instead. + + my $dbh = Bugzilla->dbh; + my $sth = $dbh->prepare(" + SELECT profiles.login_name, " + . $dbh->sql_date_format('bug_when', '%Y.%m.%d %H:%i:%s') . ", extra_data FROM longdescs INNER JOIN profiles ON profiles.userid = longdescs.who WHERE bug_id = ? AND type = ? ORDER BY bug_when "); - $sth->execute($bug_id, CMT_HAS_DUPE); - - while (my($who, $when, $dupe_id) = $sth->fetchrow_array) { - my $entry = { - 'when' => $when, - 'who' => $who, - 'changes' => [ - { - 'removed' => '', - 'added' => $dupe_id, - 'attachid' => undef, - 'fieldname' => 'dupe', - 'dupe' => 1, - } - ], - }; - push @$activity, $entry; - } + $sth->execute($bug_id, CMT_HAS_DUPE); + + while (my ($who, $when, $dupe_id) = $sth->fetchrow_array) { + my $entry = { + 'when' => $when, + 'who' => $who, + 'changes' => [{ + 'removed' => '', + 'added' => $dupe_id, + 'attachid' => undef, + 'fieldname' => 'dupe', + 'dupe' => 1, + }], + }; + push @$activity, $entry; + } - return [ sort { $a->{when} cmp $b->{when} } @$activity ]; + return [sort { $a->{when} cmp $b->{when} } @$activity]; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'inline_history', - options => ['on', 'off'], - default => 'off', - category => 'Bug Editing' - }); + my ($self, $args) = @_; + add_setting({ + name => 'inline_history', + options => ['on', 'off'], + default => 'off', + category => 'Bug Editing' + }); } __PACKAGE__->NAME; diff --git a/extensions/LastResolved/Config.pm b/extensions/LastResolved/Config.pm index 8fd8f106f..c981db7f0 100644 --- a/extensions/LastResolved/Config.pm +++ b/extensions/LastResolved/Config.pm @@ -13,10 +13,8 @@ use warnings; use constant NAME => 'LastResolved'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/LastResolved/Extension.pm b/extensions/LastResolved/Extension.pm index 197bb15d9..798506ae9 100644 --- a/extensions/LastResolved/Extension.pm +++ b/extensions/LastResolved/Extension.pm @@ -22,94 +22,94 @@ use Bugzilla::Install::Util qw(indicate_progress); our $VERSION = '0.01'; sub install_update_db { - my ($self, $args) = @_; - my $last_resolved = Bugzilla::Field->new({'name' => 'cf_last_resolved'}); - if (!$last_resolved) { - Bugzilla::Field->create({ - name => 'cf_last_resolved', - description => 'Last Resolved', - type => FIELD_TYPE_DATETIME, - mailhead => 0, - enter_bug => 0, - obsolete => 0, - custom => 1, - buglist => 1, - }); - _migrate_last_resolved(); - } + my ($self, $args) = @_; + my $last_resolved = Bugzilla::Field->new({'name' => 'cf_last_resolved'}); + if (!$last_resolved) { + Bugzilla::Field->create({ + name => 'cf_last_resolved', + description => 'Last Resolved', + type => FIELD_TYPE_DATETIME, + mailhead => 0, + enter_bug => 0, + obsolete => 0, + custom => 1, + buglist => 1, + }); + _migrate_last_resolved(); + } } sub _migrate_last_resolved { - my $dbh = Bugzilla->dbh; - my $field_id = get_field_id('bug_status'); - my $resolved_activity = $dbh->selectall_arrayref( - "SELECT bugs_activity.bug_id, bugs_activity.bug_when, bugs_activity.who + my $dbh = Bugzilla->dbh; + my $field_id = get_field_id('bug_status'); + my $resolved_activity = $dbh->selectall_arrayref( + "SELECT bugs_activity.bug_id, bugs_activity.bug_when, bugs_activity.who FROM bugs_activity WHERE bugs_activity.fieldid = ? AND bugs_activity.added = 'RESOLVED' - ORDER BY bugs_activity.bug_when", - undef, $field_id); + ORDER BY bugs_activity.bug_when", undef, $field_id + ); - my $count = 1; - my $total = scalar @$resolved_activity; - my %current_last_resolved; - foreach my $activity (@$resolved_activity) { - indicate_progress({ current => $count++, total => $total, every => 25 }); - my ($id, $new, $who) = @$activity; - my $old = $current_last_resolved{$id} ? $current_last_resolved{$id} : ""; - $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", undef, $new, $id); - LogActivityEntry($id, 'cf_last_resolved', $old, $new, $who, $new); - $current_last_resolved{$id} = $new; - } + my $count = 1; + my $total = scalar @$resolved_activity; + my %current_last_resolved; + foreach my $activity (@$resolved_activity) { + indicate_progress({current => $count++, total => $total, every => 25}); + my ($id, $new, $who) = @$activity; + my $old = $current_last_resolved{$id} ? $current_last_resolved{$id} : ""; + $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", + undef, $new, $id); + LogActivityEntry($id, 'cf_last_resolved', $old, $new, $who, $new); + $current_last_resolved{$id} = $new; + } } sub bug_check_can_change_field { - my ($self, $args) = @_; - my ($field, $priv_results) = @$args{qw(field priv_results)}; - if ($field eq 'cf_last_resolved') { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + my ($self, $args) = @_; + my ($field, $priv_results) = @$args{qw(field priv_results)}; + if ($field eq 'cf_last_resolved') { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } } sub bug_end_of_update { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - my ($bug, $old_bug, $timestamp, $changes) = - @$args{qw(bug old_bug timestamp changes)}; - if ($changes->{'bug_status'}) { - # If the bug has been resolved then update the cf_last_resolved - # value to the current timestamp if cf_last_resolved exists - if ($bug->bug_status eq 'RESOLVED') { - $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", - undef, $timestamp, $bug->id); - my $old_value = $bug->cf_last_resolved || ''; - LogActivityEntry($bug->id, 'cf_last_resolved', $old_value, - $timestamp, Bugzilla->user->id, $timestamp); - } + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + my ($bug, $old_bug, $timestamp, $changes) + = @$args{qw(bug old_bug timestamp changes)}; + if ($changes->{'bug_status'}) { + + # If the bug has been resolved then update the cf_last_resolved + # value to the current timestamp if cf_last_resolved exists + if ($bug->bug_status eq 'RESOLVED') { + $dbh->do("UPDATE bugs SET cf_last_resolved = ? WHERE bug_id = ?", + undef, $timestamp, $bug->id); + my $old_value = $bug->cf_last_resolved || ''; + LogActivityEntry($bug->id, 'cf_last_resolved', $old_value, $timestamp, + Bugzilla->user->id, $timestamp); } + } } sub bug_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - push (@$fields, 'cf_last_resolved') + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + push(@$fields, 'cf_last_resolved'); } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Bug')) { - push(@$columns, 'cf_last_resolved'); - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Bug')) { + push(@$columns, 'cf_last_resolved'); + } } sub buglist_columns { - my ($self, $args) = @_; - my $columns = $args->{columns}; - $columns->{'cf_last_resolved'} = { - name => 'bugs.cf_last_resolved', - title => 'Last Resolved', - }; + my ($self, $args) = @_; + my $columns = $args->{columns}; + $columns->{'cf_last_resolved'} + = {name => 'bugs.cf_last_resolved', title => 'Last Resolved',}; } __PACKAGE__->NAME; diff --git a/extensions/LimitedEmail/Config.pm b/extensions/LimitedEmail/Config.pm index 94b9b10eb..d47fb4d91 100644 --- a/extensions/LimitedEmail/Config.pm +++ b/extensions/LimitedEmail/Config.pm @@ -11,15 +11,15 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'LimitedEmail'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'LimitedEmail'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; use constant FILTERS => [ - qr/^(?:glob|dkl|justdave|shyam)\@mozilla\.com$/i, - qr/^byron\.jones\@gmail\.com$/i, - qr/^gerv\@mozilla\.org$/i, - qr/^reed\@reedloden\.com$/i, + qr/^(?:glob|dkl|justdave|shyam)\@mozilla\.com$/i, + qr/^byron\.jones\@gmail\.com$/i, + qr/^gerv\@mozilla\.org$/i, + qr/^reed\@reedloden\.com$/i, ]; __PACKAGE__->NAME; diff --git a/extensions/LimitedEmail/Extension.pm b/extensions/LimitedEmail/Extension.pm index 9b504db91..3831ac878 100644 --- a/extensions/LimitedEmail/Extension.pm +++ b/extensions/LimitedEmail/Extension.pm @@ -21,46 +21,46 @@ use Encode qw(encode_utf8); use Bugzilla::Constants qw(bz_locations); sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{email}; - my $header = $email->{header}; - return if $header->header('to') eq ''; + my ($self, $args) = @_; + my $email = $args->{email}; + my $header = $email->{header}; + return if $header->header('to') eq ''; - my $blocked = ''; - if (!deliver_to($header->header('to'))) { - $blocked = $header->header('to'); - $header->header_set(to => ''); - } + my $blocked = ''; + if (!deliver_to($header->header('to'))) { + $blocked = $header->header('to'); + $header->header_set(to => ''); + } - my $log_filename = bz_locations->{'datadir'} . '/mail.log'; - my $fh = FileHandle->new(">>$log_filename"); - if ($fh) { - print $fh encode_utf8(sprintf( - "[%s] %s%s %s : %s\n", - time2str('%D %T', time), - ($blocked eq '' ? '' : '(blocked) '), - ($blocked eq '' ? $header->header('to') : $blocked), - $header->header('X-Bugzilla-Reason') || '-', - $header->header('subject') - )); - $fh->close(); - } + my $log_filename = bz_locations->{'datadir'} . '/mail.log'; + my $fh = FileHandle->new(">>$log_filename"); + if ($fh) { + print $fh encode_utf8(sprintf( + "[%s] %s%s %s : %s\n", + time2str('%D %T', time), + ($blocked eq '' ? '' : '(blocked) '), + ($blocked eq '' ? $header->header('to') : $blocked), + $header->header('X-Bugzilla-Reason') || '-', + $header->header('subject') + )); + $fh->close(); + } } sub deliver_to { - my $email = address_of(shift); - my $ra_filters = Bugzilla::Extension::LimitedEmail::FILTERS; - foreach my $re (@$ra_filters) { - if ($email =~ $re) { - return 1; - } + my $email = address_of(shift); + my $ra_filters = Bugzilla::Extension::LimitedEmail::FILTERS; + foreach my $re (@$ra_filters) { + if ($email =~ $re) { + return 1; } - return 0; + } + return 0; } sub address_of { - my $email = shift; - return $email =~ /<([^>]+)>/ ? $1 : $email; + my $email = shift; + return $email =~ /<([^>]+)>/ ? $1 : $email; } __PACKAGE__->NAME; diff --git a/extensions/MozProjectReview/Config.pm b/extensions/MozProjectReview/Config.pm index 1a5e14f3d..41d761a35 100644 --- a/extensions/MozProjectReview/Config.pm +++ b/extensions/MozProjectReview/Config.pm @@ -12,10 +12,8 @@ use warnings; use constant NAME => 'MozProjectReview'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/MozProjectReview/Extension.pm b/extensions/MozProjectReview/Extension.pm index 29d709ff4..c5cd27d21 100644 --- a/extensions/MozProjectReview/Extension.pm +++ b/extensions/MozProjectReview/Extension.pm @@ -22,139 +22,151 @@ use Bugzilla::Constants; use List::MoreUtils qw(any); sub post_bug_after_creation { - my ($self, $args) = @_; - my $vars = $args->{'vars'}; - my $bug = $vars->{'bug'}; - my $timestamp = $args->{'timestamp'}; - my $user = Bugzilla->user; - my $params = Bugzilla->input_params; - my $template = Bugzilla->template; - - return if !($params->{format} && $params->{format} eq 'moz-project-review'); - - # do a match if applicable - Bugzilla::User::match_field({ - 'sow_vendor_mozcontact' => { 'type' => 'single' }, - }); - - my $do_sec_review = 0; - my @sec_review_needed = ( - 'Engaging a new vendor company', - 'Adding a new SOW with a vendor', - 'Extending a SOW or renewing a contract', - 'Purchasing software', - 'Signing up for an online service', - 'Other' - ); - if ((any { $_ eq $params->{contract_type} } @sec_review_needed) - || $params->{mozilla_data} eq 'Yes') { - $do_sec_review = 1; - } - - my ($sec_review_bug, $finance_bug, $error, @dep_comment, @dep_errors, @send_mail); - - # Common parameters always passed to _file_child_bug - # bug_data and template_suffix will be different for each bug - my $child_params = { - parent_bug => $bug, - template_vars => $vars, - dep_comment => \@dep_comment, - dep_errors => \@dep_errors, - send_mail => \@send_mail, - }; - - if ($do_sec_review) { - $child_params->{'bug_data'} = { - short_desc => 'RRA: ' . $params->{contract_type} . ' with ' . $params->{other_party}, - product => 'Enterprise Information Security', - component => 'Rapid Risk Analysis', - bug_severity => 'normal', - groups => [ 'mozilla-employee-confidential' ], - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $bug->bug_id, - cc => $params->{cc}, - }; - $child_params->{'template_suffix'} = 'sec-review'; - _file_child_bug($child_params); - } - + my ($self, $args) = @_; + my $vars = $args->{'vars'}; + my $bug = $vars->{'bug'}; + my $timestamp = $args->{'timestamp'}; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; + my $template = Bugzilla->template; + + return if !($params->{format} && $params->{format} eq 'moz-project-review'); + + # do a match if applicable + Bugzilla::User::match_field({'sow_vendor_mozcontact' => {'type' => 'single'},}); + + my $do_sec_review = 0; + my @sec_review_needed = ( + 'Engaging a new vendor company', + 'Adding a new SOW with a vendor', + 'Extending a SOW or renewing a contract', + 'Purchasing software', + 'Signing up for an online service', + 'Other' + ); + if ((any { $_ eq $params->{contract_type} } @sec_review_needed) + || $params->{mozilla_data} eq 'Yes') + { + $do_sec_review = 1; + } + + my ($sec_review_bug, $finance_bug, $error, @dep_comment, @dep_errors, + @send_mail); + + # Common parameters always passed to _file_child_bug + # bug_data and template_suffix will be different for each bug + my $child_params = { + parent_bug => $bug, + template_vars => $vars, + dep_comment => \@dep_comment, + dep_errors => \@dep_errors, + send_mail => \@send_mail, + }; + + if ($do_sec_review) { $child_params->{'bug_data'} = { - short_desc => 'Finance Review: ' . $params->{contract_type} . ' with ' . $params->{other_party}, - product => 'Finance', - component => 'Purchase Request Form', - bug_severity => 'normal', - priority => '--', - groups => [ 'finance' ], - op_sys => 'All', - rep_platform => 'All', - version => 'unspecified', - blocked => $bug->bug_id, - cc => $params->{cc}, + short_desc => 'RRA: ' + . $params->{contract_type} + . ' with ' + . $params->{other_party}, + product => 'Enterprise Information Security', + component => 'Rapid Risk Analysis', + bug_severity => 'normal', + groups => ['mozilla-employee-confidential'], + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $bug->bug_id, + cc => $params->{cc}, }; - $child_params->{'template_suffix'} = 'finance'; + $child_params->{'template_suffix'} = 'sec-review'; _file_child_bug($child_params); - + } + + $child_params->{'bug_data'} = { + short_desc => 'Finance Review: ' + . $params->{contract_type} + . ' with ' + . $params->{other_party}, + product => 'Finance', + component => 'Purchase Request Form', + bug_severity => 'normal', + priority => '--', + groups => ['finance'], + op_sys => 'All', + rep_platform => 'All', + version => 'unspecified', + blocked => $bug->bug_id, + cc => $params->{cc}, + }; + $child_params->{'template_suffix'} = 'finance'; + _file_child_bug($child_params); + + if (scalar @dep_errors) { + warn "[Bug " + . $bug->id + . "] Failed to create additional moz-project-review bugs:\n" + . join("\n", @dep_errors); + $vars->{'message'} = 'moz_project_review_creation_failed'; + } + + if (scalar @dep_comment) { + my $comment = join("\n", @dep_comment); if (scalar @dep_errors) { - warn "[Bug " . $bug->id . "] Failed to create additional moz-project-review bugs:\n" . - join("\n", @dep_errors); - $vars->{'message'} = 'moz_project_review_creation_failed'; - } - - if (scalar @dep_comment) { - my $comment = join("\n", @dep_comment); - if (scalar @dep_errors) { - $comment .= "\n\nSome errors occurred creating dependent bugs and have been recorded"; - } - $bug->add_comment($comment); - $bug->update($bug->creation_ts); + $comment + .= "\n\nSome errors occurred creating dependent bugs and have been recorded"; } + $bug->add_comment($comment); + $bug->update($bug->creation_ts); + } - foreach my $bug_id (@send_mail) { - Bugzilla::BugMail::Send($bug_id, { changer => Bugzilla->user }); - } + foreach my $bug_id (@send_mail) { + Bugzilla::BugMail::Send($bug_id, {changer => Bugzilla->user}); + } } sub _file_child_bug { - my ($params) = @_; - my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, $dep_errors, $send_mail) - = @$params{qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail)}; - - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - my $new_bug; - eval { - my $comment; - my $full_template = "bug/create/comment-moz-project-review-$template_suffix.txt.tmpl"; - Bugzilla->template->process($full_template, $template_vars, \$comment) - || ThrowTemplateError(Bugzilla->template->error()); - $bug_data->{'comment'} = $comment; - if ($new_bug = Bugzilla::Bug->create($bug_data)) { - my $set_all = { - dependson => { add => [ $new_bug->bug_id ] } - }; - $parent_bug->set_all($set_all); - $parent_bug->update($parent_bug->creation_ts); - } + my ($params) = @_; + my ($parent_bug, $template_vars, $template_suffix, $bug_data, $dep_comment, + $dep_errors, $send_mail) + = @$params{ + qw(parent_bug template_vars template_suffix bug_data dep_comment dep_errors send_mail) }; - if ($@ || !($new_bug && $new_bug->{'bug_id'})) { - push(@$dep_comment, "Error creating $template_suffix review bug"); - push(@$dep_errors, "$template_suffix : $@") if $@; - # Since we performed Bugzilla::Bug::create in an eval block, we - # need to manually rollback the commit as this is not done - # in Bugzilla::Error automatically for eval'ed code. - Bugzilla->dbh->bz_rollback_transaction(); - } - else { - push(@$send_mail, $new_bug->id); - push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc); + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + my $new_bug; + eval { + my $comment; + my $full_template + = "bug/create/comment-moz-project-review-$template_suffix.txt.tmpl"; + Bugzilla->template->process($full_template, $template_vars, \$comment) + || ThrowTemplateError(Bugzilla->template->error()); + $bug_data->{'comment'} = $comment; + if ($new_bug = Bugzilla::Bug->create($bug_data)) { + my $set_all = {dependson => {add => [$new_bug->bug_id]}}; + $parent_bug->set_all($set_all); + $parent_bug->update($parent_bug->creation_ts); } - - undef $@; - Bugzilla->error_mode($old_error_mode); + }; + + if ($@ || !($new_bug && $new_bug->{'bug_id'})) { + push(@$dep_comment, "Error creating $template_suffix review bug"); + push(@$dep_errors, "$template_suffix : $@") if $@; + + # Since we performed Bugzilla::Bug::create in an eval block, we + # need to manually rollback the commit as this is not done + # in Bugzilla::Error automatically for eval'ed code. + Bugzilla->dbh->bz_rollback_transaction(); + } + else { + push(@$send_mail, $new_bug->id); + push(@$dep_comment, "Bug " . $new_bug->id . " - " . $new_bug->short_desc); + } + + undef $@; + Bugzilla->error_mode($old_error_mode); } __PACKAGE__->NAME; diff --git a/extensions/MyDashboard/Extension.pm b/extensions/MyDashboard/Extension.pm index fc3a689bf..ae7921af7 100644 --- a/extensions/MyDashboard/Extension.pm +++ b/extensions/MyDashboard/Extension.pm @@ -29,54 +29,53 @@ our $VERSION = BUGZILLA_VERSION; ################ sub db_schema_abstract_schema { - my ($self, $args) = @_; - - my $schema = $args->{schema}; - - $schema->{'mydashboard'} = { - FIELDS => [ - namedquery_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'namedqueries', - COLUMN => 'id', - DELETE => 'CASCADE'}}, - user_id => {TYPE => 'INT3', NOTNULL => 1, - REFERENCES => {TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE'}}, - ], - INDEXES => [ - mydashboard_namedquery_id_idx => {FIELDS => [qw(namedquery_id user_id)], - TYPE => 'UNIQUE'}, - mydashboard_user_id_idx => ['user_id'], - ], - }; - - $schema->{'bug_interest'} = { - FIELDS => [ - id => { TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1 }, - - bug_id => { TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' } }, - - user_id => { TYPE => 'INT3', - NOTNOLL => 1, - REFERENCES => { TABLE => 'profiles', - COLUMN => 'userid' } }, - - modification_time => { TYPE => 'DATETIME', - NOTNULL => 1 } - ], - INDEXES => [ - bug_interest_idx => { FIELDS => [qw(bug_id user_id)], - TYPE => 'UNIQUE' }, - bug_interest_user_id_idx => ['user_id'] - ], - }; + my ($self, $args) = @_; + + my $schema = $args->{schema}; + + $schema->{'mydashboard'} = { + FIELDS => [ + namedquery_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'namedqueries', COLUMN => 'id', DELETE => 'CASCADE'} + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE'} + }, + ], + INDEXES => [ + mydashboard_namedquery_id_idx => + {FIELDS => [qw(namedquery_id user_id)], TYPE => 'UNIQUE'}, + mydashboard_user_id_idx => ['user_id'], + ], + }; + + $schema->{'bug_interest'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1}, + + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + + user_id => { + TYPE => 'INT3', + NOTNOLL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid'} + }, + + modification_time => {TYPE => 'DATETIME', NOTNULL => 1} + ], + INDEXES => [ + bug_interest_idx => {FIELDS => [qw(bug_id user_id)], TYPE => 'UNIQUE'}, + bug_interest_user_id_idx => ['user_id'] + ], + }; } ########### @@ -84,34 +83,35 @@ sub db_schema_abstract_schema { ########### BEGIN { - *Bugzilla::Search::Saved::in_mydashboard = \&_in_mydashboard; - *Bugzilla::Component::watcher_ids = \&_component_watcher_ids; + *Bugzilla::Search::Saved::in_mydashboard = \&_in_mydashboard; + *Bugzilla::Component::watcher_ids = \&_component_watcher_ids; } sub _in_mydashboard { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - return $self->{'in_mydashboard'} if exists $self->{'in_mydashboard'}; - $self->{'in_mydashboard'} = $dbh->selectrow_array(" - SELECT 1 FROM mydashboard WHERE namedquery_id = ? AND user_id = ?", - undef, $self->id, Bugzilla->user->id); - return $self->{'in_mydashboard'}; + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $self->{'in_mydashboard'} if exists $self->{'in_mydashboard'}; + $self->{'in_mydashboard'} = $dbh->selectrow_array(" + SELECT 1 FROM mydashboard WHERE namedquery_id = ? AND user_id = ?", undef, + $self->id, Bugzilla->user->id); + return $self->{'in_mydashboard'}; } sub _component_watcher_ids { - my ($self) = @_; - my $dbh = Bugzilla->dbh; + my ($self) = @_; + my $dbh = Bugzilla->dbh; - my $query = "SELECT user_id FROM component_watch + my $query = "SELECT user_id FROM component_watch WHERE product_id = ? AND (component_id = ? OR component_id IS NULL OR ? LIKE @{[$dbh->sql_string_concat('component_prefix', q{'%'})]})"; - $self->{watcher_ids} ||= $dbh->selectcol_arrayref($query, undef, - $self->product_id, $self->id, $self->name); + $self->{watcher_ids} + ||= $dbh->selectcol_arrayref($query, undef, $self->product_id, $self->id, + $self->name); - return $self->{watcher_ids}; + return $self->{watcher_ids}; } ############# @@ -119,16 +119,16 @@ sub _component_watcher_ids { ############# sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; - return if $page ne 'mydashboard.html'; + return if $page ne 'mydashboard.html'; - # require user to be logged in for this page - Bugzilla->login(LOGIN_REQUIRED); + # require user to be logged in for this page + Bugzilla->login(LOGIN_REQUIRED); - $vars->{queries} = [ QUERY_DEFS ]; + $vars->{queries} = [QUERY_DEFS]; } ######### @@ -136,115 +136,122 @@ sub page_before_template { ######### sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{'current_tab'}; - return unless $tab eq 'saved-searches'; + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'saved-searches'; - my $save = $args->{'save_changes'}; - my $handled = $args->{'handled'}; - my $vars = $args->{'vars'}; + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $params = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $params = Bugzilla->input_params; - if ($save) { - my $sth_insert_fp = $dbh->prepare('INSERT INTO mydashboard + if ($save) { + my $sth_insert_fp = $dbh->prepare( + 'INSERT INTO mydashboard (namedquery_id, user_id) - VALUES (?, ?)'); - my $sth_delete_fp = $dbh->prepare('DELETE FROM mydashboard + VALUES (?, ?)' + ); + my $sth_delete_fp = $dbh->prepare( + 'DELETE FROM mydashboard WHERE namedquery_id = ? - AND user_id = ?'); - foreach my $q (@{$user->queries}) { - if (defined $params->{'in_mydashboard_' . $q->id}) { - $sth_insert_fp->execute($q->id, $user->id) if !$q->in_mydashboard; - } - else { - $sth_delete_fp->execute($q->id, $user->id) if $q->in_mydashboard; - } - } + AND user_id = ?' + ); + foreach my $q (@{$user->queries}) { + if (defined $params->{'in_mydashboard_' . $q->id}) { + $sth_insert_fp->execute($q->id, $user->id) if !$q->in_mydashboard; + } + else { + $sth_delete_fp->execute($q->id, $user->id) if $q->in_mydashboard; + } } + } } sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{MyDashboard} = "Bugzilla::Extension::MyDashboard::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{MyDashboard} = "Bugzilla::Extension::MyDashboard::WebService"; } sub bug_end_of_create { - my ($self, $args) = @_; - my ($bug, $params, $timestamp) = @$args{qw(bug params timestamp)}; - my $user = Bugzilla->user; - - # Anyone added to the CC list of a bug is now interested in that bug. - foreach my $cc_user (@{ $bug->cc_users }) { - next if $user->id == $cc_user->id; - Bugzilla::Extension::MyDashboard::BugInterest->mark($cc_user->id, $bug->id, $timestamp); - } - - # Anyone that is watching a component is interested when a bug is filed into the component. - foreach my $watcher_id (@{ $bug->component_obj->watcher_ids }) { - Bugzilla::Extension::MyDashboard::BugInterest->mark($watcher_id, $bug->id, $timestamp); - } + my ($self, $args) = @_; + my ($bug, $params, $timestamp) = @$args{qw(bug params timestamp)}; + my $user = Bugzilla->user; + + # Anyone added to the CC list of a bug is now interested in that bug. + foreach my $cc_user (@{$bug->cc_users}) { + next if $user->id == $cc_user->id; + Bugzilla::Extension::MyDashboard::BugInterest->mark($cc_user->id, $bug->id, + $timestamp); + } + +# Anyone that is watching a component is interested when a bug is filed into the component. + foreach my $watcher_id (@{$bug->component_obj->watcher_ids}) { + Bugzilla::Extension::MyDashboard::BugInterest->mark($watcher_id, $bug->id, + $timestamp); + } } sub bug_end_of_update { - my ($self, $args) = @_; - my ($bug, $old_bug, $changes, $timestamp) = @$args{qw(bug old_bug changes timestamp)}; - my $user = Bugzilla->user; - - # Anyone added to the CC list of a bug is now interested in that bug. - my %old_cc = map { $_->id => $_ } grep { defined } @{ $old_bug->cc_users }; - my @added = grep { not $old_cc{ $_->id } } grep { defined } @{ $bug->cc_users }; - foreach my $cc_user (@added) { - next if $user->id == $cc_user->id; - Bugzilla::Extension::MyDashboard::BugInterest->mark($cc_user->id, $bug->id, $timestamp); + my ($self, $args) = @_; + my ($bug, $old_bug, $changes, $timestamp) + = @$args{qw(bug old_bug changes timestamp)}; + my $user = Bugzilla->user; + + # Anyone added to the CC list of a bug is now interested in that bug. + my %old_cc = map { $_->id => $_ } grep {defined} @{$old_bug->cc_users}; + my @added = grep { not $old_cc{$_->id} } grep {defined} @{$bug->cc_users}; + foreach my $cc_user (@added) { + next if $user->id == $cc_user->id; + Bugzilla::Extension::MyDashboard::BugInterest->mark($cc_user->id, $bug->id, + $timestamp); + } + +# Anyone that is watching a component is interested when a bug is filed into the component. + if ($changes->{product} or $changes->{component}) { + + # All of the watchers would be interested in this bug update + foreach my $watcher_id (@{$bug->component_obj->watcher_ids}) { + Bugzilla::Extension::MyDashboard::BugInterest->mark($watcher_id, $bug->id, + $timestamp); } + } - # Anyone that is watching a component is interested when a bug is filed into the component. - if ($changes->{product} or $changes->{component}) { - # All of the watchers would be interested in this bug update - foreach my $watcher_id (@{ $bug->component_obj->watcher_ids }) { - Bugzilla::Extension::MyDashboard::BugInterest->mark($watcher_id, $bug->id, $timestamp); - } - } + if ($changes->{bug_status}) { + my ($old_status, $new_status) = @{$changes->{bug_status}}; + if (is_open_state($old_status) && !is_open_state($new_status)) { + my @related_bugs = (@{$bug->blocks_obj}, @{$bug->depends_on_obj}); + my %involved; - if ($changes->{bug_status}) { - my ($old_status, $new_status) = @{ $changes->{bug_status} }; - if (is_open_state($old_status) && !is_open_state($new_status)) { - my @related_bugs = (@{ $bug->blocks_obj }, @{ $bug->depends_on_obj }); - my %involved; - - foreach my $related_bug (@related_bugs) { - my @users = grep { defined } $related_bug->assigned_to, - $related_bug->reporter, - $related_bug->qa_contact, - @{ $related_bug->cc_users }; - - foreach my $involved_user (@users) { - $involved{ $involved_user->id }{ $related_bug->id } = 1; - } - } - foreach my $involved_user_id (keys %involved) { - foreach my $related_bug_id (keys %{$involved{$involved_user_id}}) { - Bugzilla::Extension::MyDashboard::BugInterest->mark($involved_user_id, - $related_bug_id, - $timestamp); - } - } + foreach my $related_bug (@related_bugs) { + my @users = grep {defined} $related_bug->assigned_to, $related_bug->reporter, + $related_bug->qa_contact, @{$related_bug->cc_users}; + + foreach my $involved_user (@users) { + $involved{$involved_user->id}{$related_bug->id} = 1; + } + } + foreach my $involved_user_id (keys %involved) { + foreach my $related_bug_id (keys %{$involved{$involved_user_id}}) { + Bugzilla::Extension::MyDashboard::BugInterest->mark($involved_user_id, + $related_bug_id, $timestamp); } + } } + } } sub merge_users_before { - my ($self, $args) = @_; - my $old_id = $args->{old_id}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my $old_id = $args->{old_id}; + my $dbh = Bugzilla->dbh; - # If the bug_interest table has both the source user - # and destination user, then we remove the old user entry. - $dbh->do("DELETE FROM bug_interest WHERE user_id = ?", undef, $old_id); + # If the bug_interest table has both the source user + # and destination user, then we remove the old user entry. + $dbh->do("DELETE FROM bug_interest WHERE user_id = ?", undef, $old_id); } __PACKAGE__->NAME; diff --git a/extensions/MyDashboard/lib/BugInterest.pm b/extensions/MyDashboard/lib/BugInterest.pm index cf33900c5..2e427d612 100644 --- a/extensions/MyDashboard/lib/BugInterest.pm +++ b/extensions/MyDashboard/lib/BugInterest.pm @@ -25,47 +25,45 @@ 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 modification_time { return $_[0]->{modification_time} } sub mark { - my ($class, $user_id, $bug_id, $timestamp) = @_; + my ($class, $user_id, $bug_id, $timestamp) = @_; - my ($interest) = @{ $class->match({ user_id => $user_id, - bug_id => $bug_id }) }; - if ($interest) { - $interest->set(modification_time => $timestamp); - $interest->update(); - return $interest; - } - else { - return $class->create({ - user_id => $user_id, - bug_id => $bug_id, - modification_time => $timestamp, - }); - } + my ($interest) = @{$class->match({user_id => $user_id, bug_id => $bug_id})}; + if ($interest) { + $interest->set(modification_time => $timestamp); + $interest->update(); + return $interest; + } + else { + return $class->create({ + user_id => $user_id, bug_id => $bug_id, modification_time => $timestamp, + }); + } } sub unmark { - my ($class, $user_id, $bug_id) = @_; + my ($class, $user_id, $bug_id) = @_; - my ($interest) = @{ $class->match({ user_id => $user_id, - bug_id => $bug_id }) }; - if ($interest) { - $interest->remove_from_db(); - } + my ($interest) = @{$class->match({user_id => $user_id, bug_id => $bug_id})}; + if ($interest) { + $interest->remove_from_db(); + } } 1; diff --git a/extensions/MyDashboard/lib/Queries.pm b/extensions/MyDashboard/lib/Queries.pm index d77be7da4..59cb4f14e 100644 --- a/extensions/MyDashboard/lib/Queries.pm +++ b/extensions/MyDashboard/lib/Queries.pm @@ -25,11 +25,11 @@ use DateTime; use base qw(Exporter); our @EXPORT = qw( - QUERY_ORDER - SELECT_COLUMNS - QUERY_DEFS - query_bugs - query_flags + QUERY_ORDER + SELECT_COLUMNS + QUERY_DEFS + query_bugs + query_flags ); # Default sort order @@ -38,294 +38,300 @@ use constant QUERY_ORDER => ("changeddate desc", "bug_id"); # List of columns that we will be selecting. In the future this should be configurable # Share with buglist.cgi? use constant SELECT_COLUMNS => qw( - bug_id - bug_status - short_desc - changeddate + bug_id + bug_status + short_desc + changeddate ); sub QUERY_DEFS { - my $user = Bugzilla->user; - - my @query_defs = ( - { - name => 'assignedbugs', - heading => 'Assigned to You', - description => 'The bug has been assigned to you, and it is not resolved or closed.', - params => { - 'bug_status' => ['__open__'], - 'emailassigned_to1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'newbugs', - heading => 'New Reported by You', - description => 'You reported the bug; it\'s unconfirmed or new. No one has assigned themselves to fix it yet.', - params => { - 'bug_status' => ['UNCONFIRMED', 'NEW'], - 'emailreporter1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'inprogressbugs', - heading => "In Progress Reported by You", - description => 'A developer accepted your bug and is working on it. (It has someone in the "Assigned to" field.)', - params => { - 'bug_status' => [ map { $_->name } grep($_->name ne 'UNCONFIRMED' && $_->name ne 'NEW', open_states()) ], - 'emailreporter1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'openccbugs', - heading => "You Are CC'd On", - description => 'You are in the CC list of the bug, so you are watching it.', - params => { - 'bug_status' => ['__open__'], - 'emailcc1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'mentorbugs', - heading => "You Are a Mentor", - description => 'You are one of the mentors for the bug.', - params => { - 'bug_status' => ['__open__'], - 'emailbug_mentor1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }, - { - name => 'lastvisitedbugs', - heading => 'Updated Since Last Visit', - description => 'Bugs updated since last visited', - mark_read => 'Mark Visited', - params => { - o1 => 'lessthan', - v1 => '%last_changed%', - f1 => 'last_visit_ts', - }, - }, - { - name => 'interestingbugs', - heading => 'Interesting Bugs', - description => 'Bugs that you may find interesting', - mark_read => 'Remove Interest', - params => { - j_top => 'OR', - f1 => 'bug_interest_ts', - o1 => 'isnotempty', - - f2 => 'last_visit_ts', - o2 => 'lessthan', - v2 => '%last_changed%', - } - }, - { - name => 'nevervisitbugs', - heading => 'Involved with and Never Visited', - description => "Bugs you've never visited, but are involved with", - mark_read => 'Mark Visited', - params => { - query_format => "advanced", - bug_status => ['__open__'],, - o1 => "isempty", - f1 => "last_visit_ts", - j2 => "OR", - f2 => "OP", - f3 => "assigned_to", - o3 => "equals", - v3 => $user->login, - o4 => "equals", - f4 => "reporter", - v4 => $user->login, - v5 => $user->login, - f5 => "qa_contact", - o5 => "equals", - o6 => "equals", - f6 => "cc", - v6 => $user->login, - f7 => "bug_mentor", - o7 => "equals", - v7 => $user->login, - f9 => "CP", - }, - }, + my $user = Bugzilla->user; + + my @query_defs = ( + { + name => 'assignedbugs', + heading => 'Assigned to You', + description => + 'The bug has been assigned to you, and it is not resolved or closed.', + params => { + 'bug_status' => ['__open__'], + 'emailassigned_to1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'newbugs', + heading => 'New Reported by You', + description => + 'You reported the bug; it\'s unconfirmed or new. No one has assigned themselves to fix it yet.', + params => { + 'bug_status' => ['UNCONFIRMED', 'NEW'], + 'emailreporter1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'inprogressbugs', + heading => "In Progress Reported by You", + description => + 'A developer accepted your bug and is working on it. (It has someone in the "Assigned to" field.)', + params => { + 'bug_status' => [ + map { $_->name } + grep($_->name ne 'UNCONFIRMED' && $_->name ne 'NEW', open_states()) + ], + 'emailreporter1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'openccbugs', + heading => "You Are CC'd On", + description => 'You are in the CC list of the bug, so you are watching it.', + params => { + 'bug_status' => ['__open__'], + 'emailcc1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'mentorbugs', + heading => "You Are a Mentor", + description => 'You are one of the mentors for the bug.', + params => { + 'bug_status' => ['__open__'], + 'emailbug_mentor1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + }, + { + name => 'lastvisitedbugs', + heading => 'Updated Since Last Visit', + description => 'Bugs updated since last visited', + mark_read => 'Mark Visited', + params => {o1 => 'lessthan', v1 => '%last_changed%', f1 => 'last_visit_ts',}, + }, + { + name => 'interestingbugs', + heading => 'Interesting Bugs', + description => 'Bugs that you may find interesting', + mark_read => 'Remove Interest', + params => { + j_top => 'OR', + f1 => 'bug_interest_ts', + o1 => 'isnotempty', + + f2 => 'last_visit_ts', + o2 => 'lessthan', + v2 => '%last_changed%', + } + }, + { + name => 'nevervisitbugs', + heading => 'Involved with and Never Visited', + description => "Bugs you've never visited, but are involved with", + mark_read => 'Mark Visited', + params => { + query_format => "advanced", + bug_status => ['__open__'], + , + o1 => "isempty", + f1 => "last_visit_ts", + j2 => "OR", + f2 => "OP", + f3 => "assigned_to", + o3 => "equals", + v3 => $user->login, + o4 => "equals", + f4 => "reporter", + v4 => $user->login, + v5 => $user->login, + f5 => "qa_contact", + o5 => "equals", + o6 => "equals", + f6 => "cc", + v6 => $user->login, + f7 => "bug_mentor", + o7 => "equals", + v7 => $user->login, + f9 => "CP", + }, + }, + ); + + if (Bugzilla->params->{'useqacontact'}) { + push( + @query_defs, + { + name => 'qacontactbugs', + heading => 'You Are QA Contact', + description => + 'You are the qa contact on this bug, and it is not resolved or closed.', + params => { + 'bug_status' => ['__open__'], + 'emailqa_contact1' => 1, + 'emailtype1' => 'exact', + 'email1' => $user->login + } + } ); - - if (Bugzilla->params->{'useqacontact'}) { - push(@query_defs, { - name => 'qacontactbugs', - heading => 'You Are QA Contact', - description => 'You are the qa contact on this bug, and it is not resolved or closed.', - params => { - 'bug_status' => ['__open__'], - 'emailqa_contact1' => 1, - 'emailtype1' => 'exact', - 'email1' => $user->login - } - }); - } - - if ($user->showmybugslink) { - my $query = Bugzilla->params->{mybugstemplate}; - my $login = $user->login; - $query =~ s/%userid%/$login/; - $query =~ s/^buglist.cgi\?//; - push(@query_defs, { - name => 'mybugs', - heading => "My Bugs", - saved => 1, - params => $query, - }); - } - - foreach my $q (@{$user->queries}) { - next if !$q->in_mydashboard; - push(@query_defs, { name => $q->name, - saved => 1, - params => $q->url }); - } - - return @query_defs; + } + + if ($user->showmybugslink) { + my $query = Bugzilla->params->{mybugstemplate}; + my $login = $user->login; + $query =~ s/%userid%/$login/; + $query =~ s/^buglist.cgi\?//; + push(@query_defs, + {name => 'mybugs', heading => "My Bugs", saved => 1, params => $query,}); + } + + foreach my $q (@{$user->queries}) { + next if !$q->in_mydashboard; + push(@query_defs, {name => $q->name, saved => 1, params => $q->url}); + } + + return @query_defs; } sub query_bugs { - my $qdef = shift; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $datetime_now = DateTime->now(time_zone => $user->timezone); - - ## HACK to remove POST - delete $ENV{REQUEST_METHOD}; - - my $params = new Bugzilla::CGI($qdef->{params}); - - my $search = new Bugzilla::Search( fields => [ SELECT_COLUMNS ], - params => scalar $params->Vars, - order => [ QUERY_ORDER ]); - my $data = $search->data; - - my @bugs; - foreach my $row (@$data) { - my $bug = {}; - foreach my $column (SELECT_COLUMNS) { - $bug->{$column} = shift @$row; - if ($column eq 'changeddate') { - my $datetime = datetime_from($bug->{$column}); - $datetime->set_time_zone($user->timezone); - $bug->{$column} = $datetime->strftime('%Y-%m-%d %T %Z'); - $bug->{'changeddate_fancy'} = time_ago($datetime, $datetime_now); - - # Provide a version for use by Bug.history and also for looking up last comment. - # We have to set to server's timezone and also subtract one second. - $datetime->set_time_zone(Bugzilla->local_timezone); - $datetime->subtract(seconds => 1); - $bug->{changeddate_api} = $datetime->strftime('%Y-%m-%d %T'); - } - } - push(@bugs, $bug); + my $qdef = shift; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $datetime_now = DateTime->now(time_zone => $user->timezone); + + ## HACK to remove POST + delete $ENV{REQUEST_METHOD}; + + my $params = new Bugzilla::CGI($qdef->{params}); + + my $search = new Bugzilla::Search( + fields => [SELECT_COLUMNS], + params => scalar $params->Vars, + order => [QUERY_ORDER] + ); + my $data = $search->data; + + my @bugs; + foreach my $row (@$data) { + my $bug = {}; + foreach my $column (SELECT_COLUMNS) { + $bug->{$column} = shift @$row; + if ($column eq 'changeddate') { + my $datetime = datetime_from($bug->{$column}); + $datetime->set_time_zone($user->timezone); + $bug->{$column} = $datetime->strftime('%Y-%m-%d %T %Z'); + $bug->{'changeddate_fancy'} = time_ago($datetime, $datetime_now); + + # Provide a version for use by Bug.history and also for looking up last comment. + # We have to set to server's timezone and also subtract one second. + $datetime->set_time_zone(Bugzilla->local_timezone); + $datetime->subtract(seconds => 1); + $bug->{changeddate_api} = $datetime->strftime('%Y-%m-%d %T'); + } } + push(@bugs, $bug); + } - return (\@bugs, $params->canonicalise_query()); + return (\@bugs, $params->canonicalise_query()); } sub query_flags { - my ($type) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - my $datetime_now = DateTime->now(time_zone => $user->timezone); - - ($type ne 'requestee' || $type ne 'requester') - || ThrowCodeError('param_required', { param => 'type' }); - - my $match_params = { status => '?' }; - - if ($type eq 'requestee') { - $match_params->{'requestee_id'} = $user->id; - } - else { - $match_params->{'setter_id'} = $user->id; - } - - my $matched = Bugzilla::Flag->match($match_params); - - return [] if !@$matched; - - my @unfiltered_flags; - my %all_bugs; # Use hash to filter out duplicates - foreach my $flag (@$matched) { - next if ($flag->attach_id && $flag->attachment->isprivate && !$user->is_insider); - - my $data = { - id => $flag->id, - type => $flag->type->name, - status => $flag->status, - attach_id => $flag->attach_id, - is_patch => $flag->attach_id ? $flag->attachment->ispatch : 0, - bug_id => $flag->bug_id, - requester => $flag->setter->login, - requestee => $flag->requestee ? $flag->requestee->login : '', - updated => $flag->modification_date, - }; - push(@unfiltered_flags, $data); - - # Record bug id for later retrieval of status/summary - $all_bugs{$flag->{'bug_id'}}++; - } - - # Filter the bug list based on permission to see the bug - my %visible_bugs = map { $_ => 1 } @{ $user->visible_bugs([ keys %all_bugs ]) }; - - return [] if !scalar keys %visible_bugs; - - # Get all bug statuses and summaries in one query instead of loading - # many separate bug objects - my $bug_rows = $dbh->selectall_arrayref("SELECT bug_id, bug_status, short_desc + my ($type) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + my $datetime_now = DateTime->now(time_zone => $user->timezone); + + ($type ne 'requestee' || $type ne 'requester') + || ThrowCodeError('param_required', {param => 'type'}); + + my $match_params = {status => '?'}; + + if ($type eq 'requestee') { + $match_params->{'requestee_id'} = $user->id; + } + else { + $match_params->{'setter_id'} = $user->id; + } + + my $matched = Bugzilla::Flag->match($match_params); + + return [] if !@$matched; + + my @unfiltered_flags; + my %all_bugs; # Use hash to filter out duplicates + foreach my $flag (@$matched) { + next + if ($flag->attach_id && $flag->attachment->isprivate && !$user->is_insider); + + my $data = { + id => $flag->id, + type => $flag->type->name, + status => $flag->status, + attach_id => $flag->attach_id, + is_patch => $flag->attach_id ? $flag->attachment->ispatch : 0, + bug_id => $flag->bug_id, + requester => $flag->setter->login, + requestee => $flag->requestee ? $flag->requestee->login : '', + updated => $flag->modification_date, + }; + push(@unfiltered_flags, $data); + + # Record bug id for later retrieval of status/summary + $all_bugs{$flag->{'bug_id'}}++; + } + + # Filter the bug list based on permission to see the bug + my %visible_bugs = map { $_ => 1 } @{$user->visible_bugs([keys %all_bugs])}; + + return [] if !scalar keys %visible_bugs; + + # Get all bug statuses and summaries in one query instead of loading + # many separate bug objects + my $bug_rows = $dbh->selectall_arrayref( + "SELECT bug_id, bug_status, short_desc FROM bugs - WHERE " . $dbh->sql_in('bug_id', [ keys %visible_bugs ]), - { Slice => {} }); - foreach my $row (@$bug_rows) { - $visible_bugs{$row->{'bug_id'}} = { - bug_status => $row->{'bug_status'}, - short_desc => $row->{'short_desc'} - }; - } - - # Now drop out any flags for bugs the user cannot see - # or if the user did not want to see closed bugs - my @filtered_flags; - foreach my $flag (@unfiltered_flags) { - # Skip this flag if the bug is not visible to the user - next if !$visible_bugs{$flag->{'bug_id'}}; - - # Include bug status and summary with each flag - $flag->{'bug_status'} = $visible_bugs{$flag->{'bug_id'}}->{'bug_status'}; - $flag->{'bug_summary'} = $visible_bugs{$flag->{'bug_id'}}->{'short_desc'}; - - # Format the updated date specific to the user's timezone - # and add the fancy human readable version - my $datetime = datetime_from($flag->{'updated'}); - $datetime->set_time_zone($user->timezone); - $flag->{'updated'} = $datetime->strftime('%Y-%m-%d %T %Z'); - $flag->{'updated_epoch'} = $datetime->epoch; - $flag->{'updated_fancy'} = time_ago($datetime, $datetime_now); - - push(@filtered_flags, $flag); - } - - return [] if !@filtered_flags; - - # Sort by most recently updated - return [ sort { $b->{'updated_epoch'} <=> $a->{'updated_epoch'} } @filtered_flags ]; + WHERE " + . $dbh->sql_in('bug_id', [keys %visible_bugs]), {Slice => {}} + ); + foreach my $row (@$bug_rows) { + $visible_bugs{$row->{'bug_id'}} + = {bug_status => $row->{'bug_status'}, short_desc => $row->{'short_desc'}}; + } + + # Now drop out any flags for bugs the user cannot see + # or if the user did not want to see closed bugs + my @filtered_flags; + foreach my $flag (@unfiltered_flags) { + + # Skip this flag if the bug is not visible to the user + next if !$visible_bugs{$flag->{'bug_id'}}; + + # Include bug status and summary with each flag + $flag->{'bug_status'} = $visible_bugs{$flag->{'bug_id'}}->{'bug_status'}; + $flag->{'bug_summary'} = $visible_bugs{$flag->{'bug_id'}}->{'short_desc'}; + + # Format the updated date specific to the user's timezone + # and add the fancy human readable version + my $datetime = datetime_from($flag->{'updated'}); + $datetime->set_time_zone($user->timezone); + $flag->{'updated'} = $datetime->strftime('%Y-%m-%d %T %Z'); + $flag->{'updated_epoch'} = $datetime->epoch; + $flag->{'updated_fancy'} = time_ago($datetime, $datetime_now); + + push(@filtered_flags, $flag); + } + + return [] if !@filtered_flags; + + # Sort by most recently updated + return [sort { $b->{'updated_epoch'} <=> $a->{'updated_epoch'} } + @filtered_flags]; } 1; diff --git a/extensions/MyDashboard/lib/Util.pm b/extensions/MyDashboard/lib/Util.pm index 77d9505cb..f2e734b63 100644 --- a/extensions/MyDashboard/lib/Util.pm +++ b/extensions/MyDashboard/lib/Util.pm @@ -17,36 +17,40 @@ use Bugzilla::Status; use base qw(Exporter); @Bugzilla::Extension::MyDashboard::Util::EXPORT = qw( - open_states - closed_states - quoted_open_states - quoted_closed_states + open_states + closed_states + quoted_open_states + quoted_closed_states ); our $_open_states; + sub open_states { - $_open_states ||= Bugzilla::Status->match({ is_open => 1, isactive => 1 }); - return wantarray ? @$_open_states : $_open_states; + $_open_states ||= Bugzilla::Status->match({is_open => 1, isactive => 1}); + return wantarray ? @$_open_states : $_open_states; } our $_quoted_open_states; + sub quoted_open_states { - my $dbh = Bugzilla->dbh; - $_quoted_open_states ||= [ map { $dbh->quote($_->name) } open_states() ]; - return wantarray ? @$_quoted_open_states : $_quoted_open_states; + my $dbh = Bugzilla->dbh; + $_quoted_open_states ||= [map { $dbh->quote($_->name) } open_states()]; + return wantarray ? @$_quoted_open_states : $_quoted_open_states; } our $_closed_states; + sub closed_states { - $_closed_states ||= Bugzilla::Status->match({ is_open => 0, isactive => 1 }); - return wantarray ? @$_closed_states : $_closed_states; + $_closed_states ||= Bugzilla::Status->match({is_open => 0, isactive => 1}); + return wantarray ? @$_closed_states : $_closed_states; } our $_quoted_closed_states; + sub quoted_closed_states { - my $dbh = Bugzilla->dbh; - $_quoted_closed_states ||= [ map { $dbh->quote($_->name) } closed_states() ]; - return wantarray ? @$_quoted_closed_states : $_quoted_closed_states; + my $dbh = Bugzilla->dbh; + $_quoted_closed_states ||= [map { $dbh->quote($_->name) } closed_states()]; + return wantarray ? @$_quoted_closed_states : $_quoted_closed_states; } 1; diff --git a/extensions/MyDashboard/lib/WebService.pm b/extensions/MyDashboard/lib/WebService.pm index 5407c1d0b..6638bacf2 100644 --- a/extensions/MyDashboard/lib/WebService.pm +++ b/extensions/MyDashboard/lib/WebService.pm @@ -17,149 +17,149 @@ use Bugzilla::Error; use Bugzilla::Util qw(detaint_natural trick_taint template_var datetime_from); use Bugzilla::WebService::Util qw(validate); -use Bugzilla::Extension::MyDashboard::Queries qw(QUERY_DEFS query_bugs query_flags); +use Bugzilla::Extension::MyDashboard::Queries + qw(QUERY_DEFS query_bugs query_flags); use Bugzilla::Extension::MyDashboard::BugInterest; use constant READ_ONLY => qw( - run_bug_query - run_flag_query + run_bug_query + run_flag_query ); use constant PUBLIC_METHODS => qw( - bug_interest_unmark - run_bug_query - run_flag_query - run_last_changes + bug_interest_unmark + run_bug_query + run_flag_query + run_last_changes ); sub run_last_changes { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); - trick_taint($params->{changeddate_api}); - trick_taint($params->{bug_id}); + trick_taint($params->{changeddate_api}); + trick_taint($params->{bug_id}); - my $last_comment_sql = " + my $last_comment_sql = " SELECT comment_id FROM longdescs WHERE bug_id = ? AND bug_when > ?"; - if (!$user->is_insider) { - $last_comment_sql .= " AND isprivate = 0"; + if (!$user->is_insider) { + $last_comment_sql .= " AND isprivate = 0"; + } + $last_comment_sql .= " LIMIT 1"; + my $last_comment_sth = $dbh->prepare($last_comment_sql); + + my $last_changes = {}; + my $activity + = $self->history({ + ids => [$params->{bug_id}], new_since => $params->{changeddate_api} + }); + if (@{$activity->{bugs}[0]{history}}) { + my $change_set = $activity->{bugs}[0]{history}[0]; + $last_changes->{activity} = $change_set->{changes}; + foreach my $change (@{$last_changes->{activity}}) { + $change->{field_desc} = template_var('field_descs')->{$change->{field_name}} + || $change->{field_name}; } - $last_comment_sql .= " LIMIT 1"; - my $last_comment_sth = $dbh->prepare($last_comment_sql); - - my $last_changes = {}; - my $activity = $self->history({ ids => [ $params->{bug_id} ], - new_since => $params->{changeddate_api} }); - if (@{$activity->{bugs}[0]{history}}) { - my $change_set = $activity->{bugs}[0]{history}[0]; - $last_changes->{activity} = $change_set->{changes}; - foreach my $change (@{ $last_changes->{activity} }) { - $change->{field_desc} - = template_var('field_descs')->{$change->{field_name}} || $change->{field_name}; - } - $last_changes->{email} = $change_set->{who}; - my $datetime = datetime_from($change_set->{when}); - $datetime->set_time_zone($user->timezone); - $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z'); - } - my $last_comment_id = $dbh->selectrow_array( - $last_comment_sth, undef, $params->{bug_id}, $params->{changeddate_api}); - if ($last_comment_id) { - my $comments = $self->comments({ comment_ids => [ $last_comment_id ] }); - my $comment = $comments->{comments}{$last_comment_id}; - $last_changes->{comment} = $comment->{text}; - $last_changes->{email} = $comment->{creator} if !$last_changes->{email}; - my $datetime = datetime_from($comment->{creation_time}); - $datetime->set_time_zone($user->timezone); - $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z'); - } - - return { results => [ {last_changes => $last_changes } ] }; + $last_changes->{email} = $change_set->{who}; + my $datetime = datetime_from($change_set->{when}); + $datetime->set_time_zone($user->timezone); + $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z'); + } + my $last_comment_id + = $dbh->selectrow_array($last_comment_sth, undef, $params->{bug_id}, + $params->{changeddate_api}); + if ($last_comment_id) { + my $comments = $self->comments({comment_ids => [$last_comment_id]}); + my $comment = $comments->{comments}{$last_comment_id}; + $last_changes->{comment} = $comment->{text}; + $last_changes->{email} = $comment->{creator} if !$last_changes->{email}; + my $datetime = datetime_from($comment->{creation_time}); + $datetime->set_time_zone($user->timezone); + $last_changes->{when} = $datetime->strftime('%Y-%m-%d %T %Z'); + } + + return {results => [{last_changes => $last_changes}]}; } sub run_bug_query { - my($self, $params) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - defined $params->{query} - || ThrowCodeError('param_required', - { function => 'MyDashboard.run_bug_query', - param => 'query' }); - - my $result; - foreach my $qdef (QUERY_DEFS) { - next if $qdef->{name} ne $params->{query}; - my ($bugs, $query_string) = query_bugs($qdef); - - # Add last changes to each bug - foreach my $b (@$bugs) { - # Set the data type properly for webservice clients - # for non-string values. - $b->{bug_id} = $self->type('int', $b->{bug_id}); - } - - $query_string =~ s/^POSTDATA=&//; - $qdef->{bugs} = $bugs; - $qdef->{buffer} = $query_string; - $result = $qdef; - last; + my ($self, $params) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + defined $params->{query} + || ThrowCodeError('param_required', + {function => 'MyDashboard.run_bug_query', param => 'query'}); + + my $result; + foreach my $qdef (QUERY_DEFS) { + next if $qdef->{name} ne $params->{query}; + my ($bugs, $query_string) = query_bugs($qdef); + + # Add last changes to each bug + foreach my $b (@$bugs) { + + # Set the data type properly for webservice clients + # for non-string values. + $b->{bug_id} = $self->type('int', $b->{bug_id}); } - return { result => $result }; + $query_string =~ s/^POSTDATA=&//; + $qdef->{bugs} = $bugs; + $qdef->{buffer} = $query_string; + $result = $qdef; + last; + } + + return {result => $result}; } sub run_flag_query { - my ($self, $params) =@_; - my $user = Bugzilla->login(LOGIN_REQUIRED); - - my $type = $params->{type}; - $type || ThrowCodeError('param_required', - { function => 'MyDashboard.run_flag_query', - param => 'type' }); - - my $results = query_flags($type); - - # Set the data type properly for webservice clients - # for non-string values. - foreach my $flag (@$results) { - $flag->{id} = $self->type('int', $flag->{id}); - $flag->{attach_id} = $self->type('int', $flag->{attach_id}); - $flag->{bug_id} = $self->type('int', $flag->{bug_id}); - $flag->{is_patch} = $self->type('boolean', $flag->{is_patch}); - } - - return { result => { $type => $results }}; + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); + + my $type = $params->{type}; + $type + || ThrowCodeError('param_required', + {function => 'MyDashboard.run_flag_query', param => 'type'}); + + my $results = query_flags($type); + + # Set the data type properly for webservice clients + # for non-string values. + foreach my $flag (@$results) { + $flag->{id} = $self->type('int', $flag->{id}); + $flag->{attach_id} = $self->type('int', $flag->{attach_id}); + $flag->{bug_id} = $self->type('int', $flag->{bug_id}); + $flag->{is_patch} = $self->type('boolean', $flag->{is_patch}); + } + + return {result => {$type => $results}}; } sub bug_interest_unmark { - my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my ($self, $params) = @_; + my $user = Bugzilla->login(LOGIN_REQUIRED); - ThrowCodeError('param_required', { function => 'MyDashboard.bug_interest_unmark', param => 'bug_ids' }) - unless $params->{bug_ids}; + ThrowCodeError('param_required', + {function => 'MyDashboard.bug_interest_unmark', param => 'bug_ids'}) + unless $params->{bug_ids}; - my @bug_ids = ref($params->{bug_ids}) ? @{$params->{bug_ids}} : ( $params->{bug_ids} ); + my @bug_ids + = ref($params->{bug_ids}) ? @{$params->{bug_ids}} : ($params->{bug_ids}); - Bugzilla->dbh->bz_start_transaction(); - foreach my $bug_id (@bug_ids) { - Bugzilla::Extension::MyDashboard::BugInterest->unmark($user->id, $bug_id); - } - Bugzilla->dbh->bz_commit_transaction(); + Bugzilla->dbh->bz_start_transaction(); + foreach my $bug_id (@bug_ids) { + Bugzilla::Extension::MyDashboard::BugInterest->unmark($user->id, $bug_id); + } + Bugzilla->dbh->bz_commit_transaction(); } sub rest_resources { - return [ - qr{^/bug_interest_unmark$}, { - PUT => { - method => 'bug_interest_unmark' - } - } - ]; + return [qr{^/bug_interest_unmark$}, {PUT => {method => 'bug_interest_unmark'}}]; } 1; diff --git a/extensions/Needinfo/Config.pm b/extensions/Needinfo/Config.pm index d523d9d78..c930134d4 100644 --- a/extensions/Needinfo/Config.pm +++ b/extensions/Needinfo/Config.pm @@ -12,10 +12,8 @@ use warnings; use constant NAME => 'Needinfo'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/Needinfo/Extension.pm b/extensions/Needinfo/Extension.pm index f3f32439e..f9702a55f 100644 --- a/extensions/Needinfo/Extension.pm +++ b/extensions/Needinfo/Extension.pm @@ -21,256 +21,265 @@ use Bugzilla::User::Setting; our $VERSION = '0.01'; BEGIN { - *Bugzilla::User::needinfo_blocked = \&_user_needinfo_blocked; + *Bugzilla::User::needinfo_blocked = \&_user_needinfo_blocked; } sub _user_needinfo_blocked { - return $_[0]->settings->{block_needinfo}->{value} eq 'on'; + return $_[0]->settings->{block_needinfo}->{value} eq 'on'; } sub install_update_db { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - - if (@{ Bugzilla::FlagType::match({ name => 'needinfo' }) }) { - return; - } - - print "Creating needinfo flag ... " . - "enable the Needinfo feature by editing the flag's properties.\n"; - - # inclusions 0:0 maps to __ANY__ : __ANY__ in the UI, - # meaning needinfo is enabled for all products and components by default - my $flagtype = Bugzilla::FlagType->create({ - name => 'needinfo', - description => "Set this flag when the bug is in need of additional information", - target_type => 'bug', - cc_list => '', - sortkey => 1, - is_active => 1, - is_requestable => 1, - is_requesteeble => 1, - is_multiplicable => 0, - request_group => '', - grant_group => '', - inclusions => ['0:0'], - exclusions => [], - }); + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + + if (@{Bugzilla::FlagType::match({name => 'needinfo'})}) { + return; + } + + print "Creating needinfo flag ... " + . "enable the Needinfo feature by editing the flag's properties.\n"; + + # inclusions 0:0 maps to __ANY__ : __ANY__ in the UI, + # meaning needinfo is enabled for all products and components by default + my $flagtype = Bugzilla::FlagType->create({ + name => 'needinfo', + description => + "Set this flag when the bug is in need of additional information", + target_type => 'bug', + cc_list => '', + sortkey => 1, + is_active => 1, + is_requestable => 1, + is_requesteeble => 1, + is_multiplicable => 0, + request_group => '', + grant_group => '', + inclusions => ['0:0'], + exclusions => [], + }); } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'block_needinfo', - options => ['on', 'off'], - default => 'off', - category => 'Reviews and Needinfo' - }); + my ($self, $args) = @_; + add_setting({ + name => 'block_needinfo', + options => ['on', 'off'], + default => 'off', + category => 'Reviews and Needinfo' + }); } # Clear the needinfo? flag if comment is being given by # requestee or someone used the override flag. sub bug_start_of_update { - my ($self, $args) = @_; - my $bug = $args->{bug}; - my $old_bug = $args->{old_bug}; - - my $user = Bugzilla->user; - my $cgi = Bugzilla->cgi; - my $params = Bugzilla->input_params; - - if ($params->{needinfo}) { - # do a match if applicable - Bugzilla::User::match_field({ - 'needinfo_from' => { 'type' => 'multi' } - }); + my ($self, $args) = @_; + my $bug = $args->{bug}; + my $old_bug = $args->{old_bug}; + + my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + my $params = Bugzilla->input_params; + + if ($params->{needinfo}) { + + # do a match if applicable + Bugzilla::User::match_field({'needinfo_from' => {'type' => 'multi'}}); + } + + # Set needinfo_done param to true so as to not loop back here + return if $params->{needinfo_done}; + $params->{needinfo_done} = 1; + Bugzilla->input_params($params); + + my $add_needinfo = delete $params->{needinfo}; + my $needinfo_type = delete $params->{needinfo_type} // ''; + my $needinfo_from = delete $params->{needinfo_from}; + my $needinfo_role = delete $params->{needinfo_role}; + my $is_redirect = $needinfo_type eq 'redirect_to' ? 1 : 0; + my $is_private = $params->{'comment_is_private'}; + + my @needinfo_overrides; + foreach my $key (grep(/^needinfo_override_/, keys %$params)) { + my ($id) = $key =~ /(\d+)$/; + + # Should always be true if key exists (checkbox) but better to be sure + push(@needinfo_overrides, $id) if $id && $params->{$key}; + } + + # Set the needinfo flag if user is requesting more information + my @new_flags; + my $needinfo_requestee; + + if ($add_needinfo) { + foreach my $type (@{$bug->flag_types}) { + next if $type->name ne 'needinfo'; + my %requestees; + + # Allow anyone to be the requestee + if (!$needinfo_role) { + $requestees{'anyone'} = 1; + } + + # Use assigned_to as requestee + elsif ($needinfo_role eq 'assigned_to') { + $requestees{$bug->assigned_to->login} = 1; + } + + # Use reporter as requestee + elsif ($needinfo_role eq 'reporter') { + $requestees{$bug->reporter->login} = 1; + } + + # Use qa_contact as requestee + elsif ($needinfo_role eq 'qa_contact') { + $requestees{$bug->qa_contact->login} = 1; + } + + # Use current user as requestee + elsif ($needinfo_role eq 'user') { + $requestees{$user->login} = 1; + } + elsif ($needinfo_role eq 'triage_owner') { + if ($bug->component_obj->triage_owner_id) { + $requestees{$bug->component_obj->triage_owner->login} = 1; + } + } + + # Use user specified requestee + elsif ($needinfo_role eq 'other' && $needinfo_from) { + my @needinfo_from_list + = ref $needinfo_from ? @$needinfo_from : ($needinfo_from); + foreach my $requestee (@needinfo_from_list) { + my $requestee_obj = Bugzilla::User->check($requestee); + $requestees{$requestee_obj->login} = 1; + } + } + + # Requestee is a mentor + elsif ($needinfo_role + && Bugzilla::User->check({name => $needinfo_role, cache => 1})) + { + $requestees{$needinfo_role} = 1; + } + + # Find out if the requestee has already been used and skip if so + my $requestee_found; + foreach my $flag (@{$type->{flags}}) { + if (!$flag->requestee && $requestees{'anyone'}) { + delete $requestees{'anyone'}; + } + if ($flag->requestee && $requestees{$flag->requestee->login}) { + delete $requestees{$flag->requestee->login}; + } + } + + foreach my $requestee (keys %requestees) { + my $needinfo_flag = {type_id => $type->id, status => '?'}; + if ($requestee ne 'anyone') { + _check_requestee($requestee); + $needinfo_flag->{requestee} = $requestee; + my $requestee_obj = Bugzilla::User->check($requestee); + if (!$requestee_obj->can_see_bug($bug->id)) { + $bug->add_cc($requestee_obj); + } + } + push(@new_flags, $needinfo_flag); + } } + } - # Set needinfo_done param to true so as to not loop back here - return if $params->{needinfo_done}; - $params->{needinfo_done} = 1; - Bugzilla->input_params($params); - - my $add_needinfo = delete $params->{needinfo}; - my $needinfo_type = delete $params->{needinfo_type} // ''; - my $needinfo_from = delete $params->{needinfo_from}; - my $needinfo_role = delete $params->{needinfo_role}; - my $is_redirect = $needinfo_type eq 'redirect_to' ? 1 : 0; - my $is_private = $params->{'comment_is_private'}; - - my @needinfo_overrides; - foreach my $key (grep(/^needinfo_override_/, keys %$params)) { - my ($id) = $key =~ /(\d+)$/; - # Should always be true if key exists (checkbox) but better to be sure - push(@needinfo_overrides, $id) if $id && $params->{$key}; - } + my @flags; + foreach my $flag (@{$bug->flags}) { + next if $flag->type->name ne 'needinfo'; - # Set the needinfo flag if user is requesting more information - my @new_flags; - my $needinfo_requestee; - - if ($add_needinfo) { - foreach my $type (@{ $bug->flag_types }) { - next if $type->name ne 'needinfo'; - my %requestees; - - # Allow anyone to be the requestee - if (!$needinfo_role) { - $requestees{'anyone'} = 1; - } - # Use assigned_to as requestee - elsif ($needinfo_role eq 'assigned_to') { - $requestees{$bug->assigned_to->login} = 1; - } - # Use reporter as requestee - elsif ($needinfo_role eq 'reporter') { - $requestees{$bug->reporter->login} = 1; - } - # Use qa_contact as requestee - elsif ($needinfo_role eq 'qa_contact') { - $requestees{$bug->qa_contact->login} = 1; - } - # Use current user as requestee - elsif ($needinfo_role eq 'user') { - $requestees{$user->login} = 1; - } - elsif ($needinfo_role eq 'triage_owner') { - if ($bug->component_obj->triage_owner_id) { - $requestees{$bug->component_obj->triage_owner->login} = 1; - } - } - # Use user specified requestee - elsif ($needinfo_role eq 'other' && $needinfo_from) { - my @needinfo_from_list = ref $needinfo_from - ? @$needinfo_from : - ($needinfo_from); - foreach my $requestee (@needinfo_from_list) { - my $requestee_obj = Bugzilla::User->check($requestee); - $requestees{$requestee_obj->login} = 1; - } - } - # Requestee is a mentor - elsif ($needinfo_role - && Bugzilla::User->check({ name => $needinfo_role, cache => 1 })) - { - $requestees{$needinfo_role} = 1; - } - - # Find out if the requestee has already been used and skip if so - my $requestee_found; - foreach my $flag (@{ $type->{flags} }) { - if (!$flag->requestee && $requestees{'anyone'}) { - delete $requestees{'anyone'}; - } - if ($flag->requestee && $requestees{$flag->requestee->login}) { - delete $requestees{$flag->requestee->login}; - } - } - - foreach my $requestee (keys %requestees) { - my $needinfo_flag = { type_id => $type->id, status => '?' }; - if ($requestee ne 'anyone') { - _check_requestee($requestee); - $needinfo_flag->{requestee} = $requestee; - my $requestee_obj = Bugzilla::User->check($requestee); - if (!$requestee_obj->can_see_bug($bug->id)) { - $bug->add_cc($requestee_obj); - } - } - push(@new_flags, $needinfo_flag); - } - } + # Clear if somehow the flag has been set to +/- + # or if the "clear needinfo" override checkbox is selected + if ($flag->status ne '?' or grep { $_ == $flag->id } @needinfo_overrides) { + push(@flags, {id => $flag->id, status => 'X'}); } + } - my @flags; - foreach my $flag (@{ $bug->flags }) { - next if $flag->type->name ne 'needinfo'; - # Clear if somehow the flag has been set to +/- - # or if the "clear needinfo" override checkbox is selected - if ($flag->status ne '?' - or grep { $_ == $flag->id } @needinfo_overrides) - { - push(@flags, { id => $flag->id, status => 'X' }); - } - } + if ($is_redirect && scalar(@new_flags) == 1) { - if ($is_redirect && scalar(@new_flags) == 1) { - # Find the current user's needinfo request - foreach my $flag (@{ $bug->flags }) { - next unless $flag->type->name eq 'needinfo' - && $flag->requestee - && $flag->requestee->id == $user->id; - # Setting the id on new_flag updates the existing flag instead of - # creating a new one. - $new_flags[0]->{id} = $flag->id; - last; - } - } + # Find the current user's needinfo request + foreach my $flag (@{$bug->flags}) { + next + unless $flag->type->name eq 'needinfo' + && $flag->requestee + && $flag->requestee->id == $user->id; - if (@flags || @new_flags) { - $bug->set_flags(\@flags, \@new_flags); + # Setting the id on new_flag updates the existing flag instead of + # creating a new one. + $new_flags[0]->{id} = $flag->id; + last; } + } + + if (@flags || @new_flags) { + $bug->set_flags(\@flags, \@new_flags); + } } sub _check_requestee { - my ($requestee) = @_; - my $user = ref($requestee) - ? $requestee - : Bugzilla::User->new({ name => $requestee, cache => 1 }); - if ($user->needinfo_blocked) { - ThrowUserError('needinfo_blocked', { requestee => $user }); - } + my ($requestee) = @_; + my $user + = ref($requestee) + ? $requestee + : Bugzilla::User->new({name => $requestee, cache => 1}); + if ($user->needinfo_blocked) { + ThrowUserError('needinfo_blocked', {requestee => $user}); + } } sub object_end_of_create { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::Flag') - && $object->type->name eq 'needinfo' - && $object->requestee; - _check_requestee($object->requestee); + my ($self, $args) = @_; + my $object = $args->{object}; + return + unless $object->isa('Bugzilla::Flag') + && $object->type->name eq 'needinfo' + && $object->requestee; + _check_requestee($object->requestee); } sub object_end_of_update { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless exists $args->{changes}->{requestee_id} - && $object->isa('Bugzilla::Flag') - && $object->type->name eq 'needinfo' - && $object->requestee; - _check_requestee($object->requestee); + my ($self, $args) = @_; + my $object = $args->{object}; + return + unless exists $args->{changes}->{requestee_id} + && $object->isa('Bugzilla::Flag') + && $object->type->name eq 'needinfo' + && $object->requestee; + _check_requestee($object->requestee); } sub object_before_delete { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless $object->isa('Bugzilla::Flag') - && $object->type->name eq 'needinfo'; - my $user = Bugzilla->user; - - # Require canconfirm to clear requests targetted at someone else - if ($object->setter_id != $user->id - && $object->requestee - && $object->requestee->id != $user->id - && !$user->in_group('canconfirm')) - { - ThrowUserError('needinfo_illegal_change'); - } + my ($self, $args) = @_; + my $object = $args->{object}; + return + unless $object->isa('Bugzilla::Flag') && $object->type->name eq 'needinfo'; + my $user = Bugzilla->user; + + # Require canconfirm to clear requests targetted at someone else + if ( $object->setter_id != $user->id + && $object->requestee + && $object->requestee->id != $user->id + && !$user->in_group('canconfirm')) + { + ThrowUserError('needinfo_illegal_change'); + } } sub user_preferences { - my ($self, $args) = @_; - return unless - $args->{current_tab} eq 'account' - && $args->{save_changes}; - - my $input = Bugzilla->input_params; - my $settings = Bugzilla->user->settings; - - my $value = $input->{block_needinfo} ? 'on' : 'off'; - $settings->{block_needinfo}->validate_value($value); - $settings->{block_needinfo}->set($value); - clear_settings_cache(Bugzilla->user->id); + my ($self, $args) = @_; + return unless $args->{current_tab} eq 'account' && $args->{save_changes}; + + my $input = Bugzilla->input_params; + my $settings = Bugzilla->user->settings; + + my $value = $input->{block_needinfo} ? 'on' : 'off'; + $settings->{block_needinfo}->validate_value($value); + $settings->{block_needinfo}->set($value); + clear_settings_cache(Bugzilla->user->id); } __PACKAGE__->NAME; diff --git a/extensions/OldBugMove/Extension.pm b/extensions/OldBugMove/Extension.pm index 1fa96c1ef..aa3d1aab0 100644 --- a/extensions/OldBugMove/Extension.pm +++ b/extensions/OldBugMove/Extension.pm @@ -39,172 +39,172 @@ use constant VERSION => BUGZILLA_VERSION; use constant CMT_MOVED_TO => 4; sub install_update_db { - my $reso_type = Bugzilla::Field::Choice->type('resolution'); - my $moved_reso = $reso_type->new({ name => 'MOVED' }); - # We make the MOVED resolution inactive, so that it doesn't show up - # as a valid drop-down option. - if ($moved_reso) { - $moved_reso->set_is_active(0); - $moved_reso->update(); - } - else { - print "Creating the MOVED resolution...\n"; - $reso_type->create( - { value => 'MOVED', sortkey => '30000', isactive => 0 }); - } + my $reso_type = Bugzilla::Field::Choice->type('resolution'); + my $moved_reso = $reso_type->new({name => 'MOVED'}); + + # We make the MOVED resolution inactive, so that it doesn't show up + # as a valid drop-down option. + if ($moved_reso) { + $moved_reso->set_is_active(0); + $moved_reso->update(); + } + else { + print "Creating the MOVED resolution...\n"; + $reso_type->create({value => 'MOVED', sortkey => '30000', isactive => 0}); + } } sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{'panel_modules'}; - $modules->{'OldBugMove'} = 'Bugzilla::Extension::OldBugMove::Params'; + my ($self, $args) = @_; + my $modules = $args->{'panel_modules'}; + $modules->{'OldBugMove'} = 'Bugzilla::Extension::OldBugMove::Params'; } sub template_before_create { - my ($self, $args) = @_; - my $config = $args->{config}; + my ($self, $args) = @_; + my $config = $args->{config}; - my $constants = $config->{VARIABLES}{constants}; - $constants->{CMT_MOVED_TO} = CMT_MOVED_TO; + my $constants = $config->{VARIABLES}{constants}; + $constants->{CMT_MOVED_TO} = CMT_MOVED_TO; - my $vars = $config->{VARIABLES}; - $vars->{oldbugmove_user_is_mover} = \&_user_is_mover; + my $vars = $config->{VARIABLES}; + $vars->{oldbugmove_user_is_mover} = \&_user_is_mover; } sub object_before_delete { - my ($self, $args) = @_; - my $object = $args->{'object'}; - if ($object->isa('Bugzilla::Field::Choice::resolution')) { - if ($object->name eq 'MOVED') { - ThrowUserError('oldbugmove_no_delete_moved'); - } + my ($self, $args) = @_; + my $object = $args->{'object'}; + if ($object->isa('Bugzilla::Field::Choice::resolution')) { + if ($object->name eq 'MOVED') { + ThrowUserError('oldbugmove_no_delete_moved'); } + } } sub object_before_set { - my ($self, $args) = @_; - my ($object, $field) = @$args{qw(object field)}; - if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) { - # Store the old value so that end_of_set can check it. - $object->{'_oldbugmove_old_resolution'} = $object->resolution; - } + my ($self, $args) = @_; + my ($object, $field) = @$args{qw(object field)}; + if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) { + + # Store the old value so that end_of_set can check it. + $object->{'_oldbugmove_old_resolution'} = $object->resolution; + } } sub object_end_of_set { - my ($self, $args) = @_; - my ($object, $field) = @$args{qw(object field)}; - if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) { - my $old_value = delete $object->{'_oldbugmove_old_resolution'}; - return if $old_value eq $object->resolution; - if ($object->resolution eq 'MOVED') { - $object->add_comment('', { type => CMT_MOVED_TO, - extra_data => Bugzilla->user->login }); - } + my ($self, $args) = @_; + my ($object, $field) = @$args{qw(object field)}; + if ($field eq 'resolution' and $object->isa('Bugzilla::Bug')) { + my $old_value = delete $object->{'_oldbugmove_old_resolution'}; + return if $old_value eq $object->resolution; + if ($object->resolution eq 'MOVED') { + $object->add_comment('', + {type => CMT_MOVED_TO, extra_data => Bugzilla->user->login}); } + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{'object'}; + my ($self, $args) = @_; + my $object = $args->{'object'}; - if ($object->isa('Bugzilla::Bug') and Bugzilla->input_params->{'oldbugmove'}) { - my $new_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; - $object->set_bug_status($new_status, { resolution => 'MOVED' }); - } + if ($object->isa('Bugzilla::Bug') and Bugzilla->input_params->{'oldbugmove'}) { + my $new_status = Bugzilla->params->{'duplicate_or_move_bug_status'}; + $object->set_bug_status($new_status, {resolution => 'MOVED'}); + } } sub object_validators { - my ($self, $args) = @_; - my ($class, $validators) = @$args{qw(class validators)}; - if ($class->isa('Bugzilla::Comment')) { - my $extra_data_validator = $validators->{extra_data}; - $validators->{extra_data} = - sub { _check_comment_extra_data($extra_data_validator, @_) }; - } - elsif ($class->isa('Bugzilla::Bug')) { - my $reso_validator = $validators->{resolution}; - $validators->{resolution} = - sub { _check_bug_resolution($reso_validator, @_) }; - } + my ($self, $args) = @_; + my ($class, $validators) = @$args{qw(class validators)}; + if ($class->isa('Bugzilla::Comment')) { + my $extra_data_validator = $validators->{extra_data}; + $validators->{extra_data} + = sub { _check_comment_extra_data($extra_data_validator, @_) }; + } + elsif ($class->isa('Bugzilla::Bug')) { + my $reso_validator = $validators->{resolution}; + $validators->{resolution} = sub { _check_bug_resolution($reso_validator, @_) }; + } } sub _check_bug_resolution { - my $original_validator = shift; - my ($invocant, $resolution) = @_; - - if ($resolution eq 'MOVED' && $invocant->resolution ne 'MOVED' - && !Bugzilla->input_params->{'oldbugmove'}) - { - # MOVED has a special meaning and can only be used when - # really moving bugs to another installation. - ThrowUserError('oldbugmove_no_manual_move'); - } - - return $original_validator->(@_); + my $original_validator = shift; + my ($invocant, $resolution) = @_; + + if ( $resolution eq 'MOVED' + && $invocant->resolution ne 'MOVED' + && !Bugzilla->input_params->{'oldbugmove'}) + { + # MOVED has a special meaning and can only be used when + # really moving bugs to another installation. + ThrowUserError('oldbugmove_no_manual_move'); + } + + return $original_validator->(@_); } sub _check_comment_extra_data { - my $original_validator = shift; - my ($invocant, $extra_data, undef, $params) = @_; - my $type = blessed($invocant) ? $invocant->type : $params->{type}; - - if ($type == CMT_MOVED_TO) { - return Bugzilla::User->check($extra_data)->login; - } - return $original_validator->(@_); + my $original_validator = shift; + my ($invocant, $extra_data, undef, $params) = @_; + my $type = blessed($invocant) ? $invocant->type : $params->{type}; + + if ($type == CMT_MOVED_TO) { + return Bugzilla::User->check($extra_data)->login; + } + return $original_validator->(@_); } sub bug_end_of_update { - my ($self, $args) = @_; - my ($bug, $old_bug, $changes) = @$args{qw(bug old_bug changes)}; - if (defined $changes->{'resolution'} - and $changes->{'resolution'}->[1] eq 'MOVED') - { - $self->_move_bug($bug, $old_bug); - } + my ($self, $args) = @_; + my ($bug, $old_bug, $changes) = @$args{qw(bug old_bug changes)}; + if (defined $changes->{'resolution'} + and $changes->{'resolution'}->[1] eq 'MOVED') + { + $self->_move_bug($bug, $old_bug); + } } sub _move_bug { - my ($self, $bug, $old_bug) = @_; - - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - - _user_is_mover(Bugzilla->user) - or ThrowUserError("auth_failure", { action => 'move', - object => 'bugs' }); - - # Don't export the new status and resolution. We want the current - # ones. - local $Storable::forgive_me = 1; - my $export_me = dclone($bug); - $export_me->{bug_status} = $old_bug->bug_status; - delete $export_me->{status}; - $export_me->{resolution} = $old_bug->resolution; - - # Prepare and send all data about these bugs to the new database - my $to = Bugzilla->params->{'move-to-address'}; - $to =~ s/@/\@/; - my $from = Bugzilla->params->{'mailfrom'}; - $from =~ s/@/\@/; - my $msg = "To: $to\n"; - $msg .= "From: Bugzilla <" . $from . ">\n"; - $msg .= "Subject: Moving bug " . $bug->id . "\n\n"; - my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc', - 'attachment', 'attachmentdata'); - my %displayfields = map { $_ => 1 } @fieldlist; - my $vars = { bugs => [$export_me], displayfields => \%displayfields }; - $template->process("bug/show.xml.tmpl", $vars, \$msg) - || ThrowTemplateError($template->error()); - $msg .= "\n"; - MessageToMTA($msg); + my ($self, $bug, $old_bug) = @_; + + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + + _user_is_mover(Bugzilla->user) + or ThrowUserError("auth_failure", {action => 'move', object => 'bugs'}); + + # Don't export the new status and resolution. We want the current + # ones. + local $Storable::forgive_me = 1; + my $export_me = dclone($bug); + $export_me->{bug_status} = $old_bug->bug_status; + delete $export_me->{status}; + $export_me->{resolution} = $old_bug->resolution; + + # Prepare and send all data about these bugs to the new database + my $to = Bugzilla->params->{'move-to-address'}; + $to =~ s/@/\@/; + my $from = Bugzilla->params->{'mailfrom'}; + $from =~ s/@/\@/; + my $msg = "To: $to\n"; + $msg .= "From: Bugzilla <" . $from . ">\n"; + $msg .= "Subject: Moving bug " . $bug->id . "\n\n"; + my @fieldlist = (Bugzilla::Bug->fields, 'group', 'long_desc', 'attachment', + 'attachmentdata'); + my %displayfields = map { $_ => 1 } @fieldlist; + my $vars = {bugs => [$export_me], displayfields => \%displayfields}; + $template->process("bug/show.xml.tmpl", $vars, \$msg) + || ThrowTemplateError($template->error()); + $msg .= "\n"; + MessageToMTA($msg); } sub _user_is_mover { - my $user = shift; + my $user = shift; - my @movers = map { trim($_) } split(',', Bugzilla->params->{'movers'}); - return ($user->id and grep($_ eq $user->login, @movers)) ? 1 : 0; + my @movers = map { trim($_) } split(',', Bugzilla->params->{'movers'}); + return ($user->id and grep($_ eq $user->login, @movers)) ? 1 : 0; } __PACKAGE__->NAME; diff --git a/extensions/OldBugMove/lib/Params.pm b/extensions/OldBugMove/lib/Params.pm index a8617e347..cea3fecf6 100644 --- a/extensions/OldBugMove/lib/Params.pm +++ b/extensions/OldBugMove/lib/Params.pm @@ -38,23 +38,11 @@ use Bugzilla::Config::Common; our $sortkey = 700; use constant get_param_list => ( - { - name => 'move-to-url', - type => 't', - default => '' - }, - - { - name => 'move-to-address', - type => 't', - default => 'bugzilla-import' - }, - - { - name => 'movers', - type => 't', - default => '' - }, + {name => 'move-to-url', type => 't', default => ''}, + + {name => 'move-to-address', type => 't', default => 'bugzilla-import'}, + + {name => 'movers', type => 't', default => ''}, ); 1; diff --git a/extensions/OpenGraph/Config.pm b/extensions/OpenGraph/Config.pm index 92d0a89bc..419d5d9d3 100644 --- a/extensions/OpenGraph/Config.pm +++ b/extensions/OpenGraph/Config.pm @@ -11,8 +11,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'OpenGraph'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'OpenGraph'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/OrangeFactor/Extension.pm b/extensions/OrangeFactor/Extension.pm index 56dd5dc6e..14d9da6df 100644 --- a/extensions/OrangeFactor/Extension.pm +++ b/extensions/OrangeFactor/Extension.pm @@ -22,37 +22,40 @@ use DateTime; our $VERSION = '1.0'; sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - my $user = Bugzilla->user; - - return unless ($file eq 'bug/show-header.html.tmpl' - || $file eq 'bug/edit.html.tmpl' - || $file eq 'bug_modal/header.html.tmpl' - || $file eq 'bug_modal/edit.html.tmpl'); - return unless ($user->id - && $user->settings->{'orange_factor'}->{'value'} eq 'on'); - - # in the header we just need to set the var, - # to ensure the css and javascript get included - my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; - if ($bug && grep($_->name eq 'intermittent-failure', @{ $bug->keyword_objects })) { - $vars->{'orange_factor'} = 1; - $vars->{'date_start'} = ( DateTime->now() - DateTime::Duration->new( days => 7 ) )->ymd(); - $vars->{'date_end'} = DateTime->now->ymd(); - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + my $user = Bugzilla->user; + + return + unless ($file eq 'bug/show-header.html.tmpl' + || $file eq 'bug/edit.html.tmpl' + || $file eq 'bug_modal/header.html.tmpl' + || $file eq 'bug_modal/edit.html.tmpl'); + return + unless ($user->id && $user->settings->{'orange_factor'}->{'value'} eq 'on'); + + # in the header we just need to set the var, + # to ensure the css and javascript get included + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + if ($bug && grep($_->name eq 'intermittent-failure', @{$bug->keyword_objects})) + { + $vars->{'orange_factor'} = 1; + $vars->{'date_start'} + = (DateTime->now() - DateTime::Duration->new(days => 7))->ymd(); + $vars->{'date_end'} = DateTime->now->ymd(); + } } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'orange_factor', - options => ['on', 'off'], - default => 'off', - category => 'User Interface' - }); + my ($self, $args) = @_; + add_setting({ + name => 'orange_factor', + options => ['on', 'off'], + default => 'off', + category => 'User Interface' + }); } __PACKAGE__->NAME; diff --git a/extensions/PhabBugz/Extension.pm b/extensions/PhabBugz/Extension.pm index c857c60ab..5622f5050 100644 --- a/extensions/PhabBugz/Extension.pm +++ b/extensions/PhabBugz/Extension.pm @@ -19,29 +19,31 @@ use Bugzilla::Extension::PhabBugz::Feed; our $VERSION = '0.01'; sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{PhabBugz} = "Bugzilla::Extension::PhabBugz::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{PhabBugz} = "Bugzilla::Extension::PhabBugz::Config"; } sub auth_delegation_confirm { - my ($self, $args) = @_; - my $phab_enabled = Bugzilla->params->{phabricator_enabled}; - my $phab_callback_url = Bugzilla->params->{phabricator_auth_callback_url}; - my $phab_app_id = Bugzilla->params->{phabricator_app_id}; - - return unless $phab_enabled; - return unless $phab_callback_url; - return unless $phab_app_id; - - if (index($args->{callback}, $phab_callback_url) == 0 && $args->{app_id} eq $phab_app_id) { - ${$args->{skip_confirmation}} = 1; - } + my ($self, $args) = @_; + my $phab_enabled = Bugzilla->params->{phabricator_enabled}; + my $phab_callback_url = Bugzilla->params->{phabricator_auth_callback_url}; + my $phab_app_id = Bugzilla->params->{phabricator_app_id}; + + return unless $phab_enabled; + return unless $phab_callback_url; + return unless $phab_app_id; + + if (index($args->{callback}, $phab_callback_url) == 0 + && $args->{app_id} eq $phab_app_id) + { + ${$args->{skip_confirmation}} = 1; + } } sub webservice { - my ($self, $args) = @_; - $args->{dispatch}->{PhabBugz} = "Bugzilla::Extension::PhabBugz::WebService"; + my ($self, $args) = @_; + $args->{dispatch}->{PhabBugz} = "Bugzilla::Extension::PhabBugz::WebService"; } # @@ -49,42 +51,25 @@ sub webservice { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'phabbugz'} = { - FIELDS => [ - id => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - name => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - value => { - TYPE => 'MEDIUMTEXT', - NOTNULL => 1 - } - ], - INDEXES => [ - phabbugz_idx => { - FIELDS => ['name'], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'phabbugz'} = { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + name => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + value => {TYPE => 'MEDIUMTEXT', NOTNULL => 1} + ], + INDEXES => [phabbugz_idx => {FIELDS => ['name'], TYPE => 'UNIQUE',},], + }; } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; + my ($self, $args) = @_; + my $files = $args->{'files'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $scriptname = $extensionsdir . "/PhabBugz/bin/phabbugz_feed.pl"; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $scriptname = $extensionsdir . "/PhabBugz/bin/phabbugz_feed.pl"; - $files->{$scriptname} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; + $files->{$scriptname} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; } __PACKAGE__->NAME; diff --git a/extensions/PhabBugz/bin/phabbugz_feed.pl b/extensions/PhabBugz/bin/phabbugz_feed.pl index 9db491bd0..2720c9104 100755 --- a/extensions/PhabBugz/bin/phabbugz_feed.pl +++ b/extensions/PhabBugz/bin/phabbugz_feed.pl @@ -14,8 +14,8 @@ use warnings; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Extension::PhabBugz::Daemon; diff --git a/extensions/PhabBugz/lib/Config.pm b/extensions/PhabBugz/lib/Config.pm index d4b71430b..d808b607a 100644 --- a/extensions/PhabBugz/lib/Config.pm +++ b/extensions/PhabBugz/lib/Config.pm @@ -16,48 +16,40 @@ use Bugzilla::Config::Common; our $sortkey = 1300; sub get_param_list { - my ($class) = @_; - - my @params = ( - { - name => 'phabricator_enabled', - type => 'b', - default => 0 - }, - { - name => 'phabricator_base_uri', - type => 't', - default => '', - checker => \&check_urlbase - }, - { - name => 'phabricator_api_key', - type => 't', - default => '', - }, - { - name => 'phabricator_auth_callback_url', - type => 't', - default => '', - checker => sub { - my ($url) = (@_); - return 'must be an HTTP/HTTPS absolute URL' unless $url =~ m{^https?://}; - return ''; - } - }, - { - name => 'phabricator_app_id', - type => 't', - default => '', - checker => sub { - my ($app_id) = (@_); - return 'must be a hex number' unless $app_id =~ /^[[:xdigit:]]+$/; - return ''; - } - } - ); - - return @params; + my ($class) = @_; + + my @params = ( + {name => 'phabricator_enabled', type => 'b', default => 0}, + { + name => 'phabricator_base_uri', + type => 't', + default => '', + checker => \&check_urlbase + }, + {name => 'phabricator_api_key', type => 't', default => '',}, + { + name => 'phabricator_auth_callback_url', + type => 't', + default => '', + checker => sub { + my ($url) = (@_); + return 'must be an HTTP/HTTPS absolute URL' unless $url =~ m{^https?://}; + return ''; + } + }, + { + name => 'phabricator_app_id', + type => 't', + default => '', + checker => sub { + my ($app_id) = (@_); + return 'must be a hex number' unless $app_id =~ /^[[:xdigit:]]+$/; + return ''; + } + } + ); + + return @params; } 1; diff --git a/extensions/PhabBugz/lib/Constants.pm b/extensions/PhabBugz/lib/Constants.pm index 19987de25..642c1962b 100644 --- a/extensions/PhabBugz/lib/Constants.pm +++ b/extensions/PhabBugz/lib/Constants.pm @@ -13,13 +13,13 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - PHAB_AUTOMATION_USER - PHAB_ATTACHMENT_PATTERN - PHAB_CONTENT_TYPE - PHAB_FEED_POLL_SECONDS - PHAB_USER_POLL_SECONDS - PHAB_GROUP_POLL_SECONDS - PHAB_TIMEOUT + PHAB_AUTOMATION_USER + PHAB_ATTACHMENT_PATTERN + PHAB_CONTENT_TYPE + PHAB_FEED_POLL_SECONDS + PHAB_USER_POLL_SECONDS + PHAB_GROUP_POLL_SECONDS + PHAB_TIMEOUT ); use constant PHAB_ATTACHMENT_PATTERN => qr/^phabricator-D(\d+)/; diff --git a/extensions/PhabBugz/lib/Daemon.pm b/extensions/PhabBugz/lib/Daemon.pm index ef4a00534..9f995553f 100644 --- a/extensions/PhabBugz/lib/Daemon.pm +++ b/extensions/PhabBugz/lib/Daemon.pm @@ -21,7 +21,7 @@ use File::Spec; use Pod::Usage; sub start { - newdaemon(); + newdaemon(); } # @@ -29,69 +29,72 @@ sub start { # sub gd_preconfig { - my $self = shift; - my $pidfile = $self->{gd_args}{pidfile}; - if (!$pidfile) { - $pidfile = File::Spec->catfile(bz_locations()->{datadir}, $self->{gd_progname} . ".pid"); - } - return (pidfile => $pidfile); + my $self = shift; + my $pidfile = $self->{gd_args}{pidfile}; + if (!$pidfile) { + $pidfile = File::Spec->catfile(bz_locations()->{datadir}, + $self->{gd_progname} . ".pid"); + } + return (pidfile => $pidfile); } sub gd_getopt { - my $self = shift; - $self->SUPER::gd_getopt(); - if ($self->{gd_args}{progname}) { - $self->{gd_progname} = $self->{gd_args}{progname}; - } else { - $self->{gd_progname} = basename($0); - } - $self->{_original_zero} = $0; - $0 = $self->{gd_progname}; + my $self = shift; + $self->SUPER::gd_getopt(); + if ($self->{gd_args}{progname}) { + $self->{gd_progname} = $self->{gd_args}{progname}; + } + else { + $self->{gd_progname} = basename($0); + } + $self->{_original_zero} = $0; + $0 = $self->{gd_progname}; } sub gd_postconfig { - my $self = shift; - $0 = delete $self->{_original_zero}; + my $self = shift; + $0 = delete $self->{_original_zero}; } sub gd_more_opt { - my $self = shift; - return ( - 'pidfile=s' => \$self->{gd_args}{pidfile}, - 'n=s' => \$self->{gd_args}{progname}, - ); + my $self = shift; + return ( + 'pidfile=s' => \$self->{gd_args}{pidfile}, + 'n=s' => \$self->{gd_args}{progname}, + ); } sub gd_usage { - pod2usage({ -verbose => 0, -exitval => 'NOEXIT' }); - return 0; -}; + pod2usage({-verbose => 0, -exitval => 'NOEXIT'}); + return 0; +} sub gd_redirect_output { - my $self = shift; - - my $filename = File::Spec->catfile(bz_locations()->{datadir}, $self->{gd_progname} . ".log"); + my $self = shift; + + my $filename = File::Spec->catfile(bz_locations()->{datadir}, + $self->{gd_progname} . ".log"); + open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); + close(STDOUT); + open(STDOUT, ">&", STDERR) or die "redirect STDOUT -> STDERR: $!"; + $SIG{HUP} = sub { + close(STDERR); open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); - close(STDOUT); - open(STDOUT, ">&", STDERR) or die "redirect STDOUT -> STDERR: $!"; - $SIG{HUP} = sub { - close(STDERR); - open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); - }; + }; } sub gd_setup_signals { - my $self = shift; - $self->SUPER::gd_setup_signals(); - $SIG{TERM} = sub { $self->gd_quit_event(); } + my $self = shift; + $self->SUPER::gd_setup_signals(); + $SIG{TERM} = sub { $self->gd_quit_event(); } } sub gd_run { - my $self = shift; - $SIG{__DIE__} = \&Carp::confess if $self->{debug}; - my $phabbugz = Bugzilla::Extension::PhabBugz::Feed->new(); - $phabbugz->is_daemon(1); - $phabbugz->start(); + my $self = shift; + $SIG{__DIE__} = \&Carp::confess if $self->{debug}; + my $phabbugz = Bugzilla::Extension::PhabBugz::Feed->new(); + $phabbugz->is_daemon(1); + $phabbugz->start(); } 1; diff --git a/extensions/PhabBugz/lib/Feed.pm b/extensions/PhabBugz/lib/Feed.pm index 71e6aa827..f199a96aa 100644 --- a/extensions/PhabBugz/lib/Feed.pm +++ b/extensions/PhabBugz/lib/Feed.pm @@ -26,7 +26,8 @@ use Bugzilla::Field; use Bugzilla::Logging; use Bugzilla::Mailer; use Bugzilla::Search; -use Bugzilla::Util qw(diff_arrays format_time with_writable_database with_readonly_database); +use Bugzilla::Util + qw(diff_arrays format_time with_writable_database with_readonly_database); use Bugzilla::Types qw(:types); use Bugzilla::Extension::PhabBugz::Types qw(:types); use Bugzilla::Extension::PhabBugz::Constants; @@ -34,663 +35,661 @@ use Bugzilla::Extension::PhabBugz::Policy; use Bugzilla::Extension::PhabBugz::Revision; use Bugzilla::Extension::PhabBugz::User; use Bugzilla::Extension::PhabBugz::Util qw( - create_revision_attachment - get_bug_role_phids - is_attachment_phab_revision - request - set_phab_user + create_revision_attachment + get_bug_role_phids + is_attachment_phab_revision + request + set_phab_user ); -has 'is_daemon' => ( is => 'rw', default => 0 ); +has 'is_daemon' => (is => 'rw', default => 0); -my $Invocant = class_type { class => __PACKAGE__ }; +my $Invocant = class_type {class => __PACKAGE__}; sub start { - my ($self) = @_; - - my $sig_alarm = IO::Async::Signal->new( - name => 'ALRM', - on_receipt => sub { - FATAL("Timeout reached"); - exit; - }, - ); - # Query for new revisions or changes - my $feed_timer = IO::Async::Timer::Periodic->new( - first_interval => 0, - interval => PHAB_FEED_POLL_SECONDS, - reschedule => 'drift', - on_tick => sub { - try { - with_writable_database { - alarm(PHAB_TIMEOUT); - $self->feed_query(); - }; - } - catch { - FATAL($_); - } - finally { - alarm(0); - Bugzilla->_cleanup(); - }; - }, - ); - - # Query for new users - my $user_timer = IO::Async::Timer::Periodic->new( - first_interval => 0, - interval => PHAB_USER_POLL_SECONDS, - reschedule => 'drift', - on_tick => sub { - try { - with_writable_database { - alarm(PHAB_TIMEOUT); - $self->user_query(); - }; - } - catch { - FATAL($_); - } - finally { - alarm(0); - Bugzilla->_cleanup(); - }; - }, - ); - - # Update project membership in Phabricator based on Bugzilla groups - my $group_timer = IO::Async::Timer::Periodic->new( - first_interval => 0, - interval => PHAB_GROUP_POLL_SECONDS, - reschedule => 'drift', - on_tick => sub { - try { - with_writable_database { - alarm(PHAB_TIMEOUT); - $self->group_query(); - }; - } - catch { - FATAL($_); - } - finally { - alarm(0); - Bugzilla->_cleanup(); - }; - - }, - ); - - my $loop = IO::Async::Loop->new; - $loop->add($feed_timer); - $loop->add($user_timer); - $loop->add($group_timer); - $loop->add($sig_alarm); - $feed_timer->start; - $user_timer->start; - $group_timer->start; - $loop->run; + my ($self) = @_; + + my $sig_alarm = IO::Async::Signal->new( + name => 'ALRM', + on_receipt => sub { + FATAL("Timeout reached"); + exit; + }, + ); + + # Query for new revisions or changes + my $feed_timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => PHAB_FEED_POLL_SECONDS, + reschedule => 'drift', + on_tick => sub { + try { + with_writable_database { + alarm(PHAB_TIMEOUT); + $self->feed_query(); + }; + } + catch { + FATAL($_); + } + finally { + alarm(0); + Bugzilla->_cleanup(); + }; + }, + ); + + # Query for new users + my $user_timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => PHAB_USER_POLL_SECONDS, + reschedule => 'drift', + on_tick => sub { + try { + with_writable_database { + alarm(PHAB_TIMEOUT); + $self->user_query(); + }; + } + catch { + FATAL($_); + } + finally { + alarm(0); + Bugzilla->_cleanup(); + }; + }, + ); + + # Update project membership in Phabricator based on Bugzilla groups + my $group_timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => PHAB_GROUP_POLL_SECONDS, + reschedule => 'drift', + on_tick => sub { + try { + with_writable_database { + alarm(PHAB_TIMEOUT); + $self->group_query(); + }; + } + catch { + FATAL($_); + } + finally { + alarm(0); + Bugzilla->_cleanup(); + }; + + }, + ); + + my $loop = IO::Async::Loop->new; + $loop->add($feed_timer); + $loop->add($user_timer); + $loop->add($group_timer); + $loop->add($sig_alarm); + $feed_timer->start; + $user_timer->start; + $group_timer->start; + $loop->run; } sub feed_query { - my ($self) = @_; + my ($self) = @_; + + local Bugzilla::Logging->fields->{type} = 'FEED'; + + # Ensure Phabricator syncing is enabled + if (!Bugzilla->params->{phabricator_enabled}) { + WARN("PHABRICATOR SYNC DISABLED"); + return; + } + + # PROCESS NEW FEED TRANSACTIONS + + INFO("Fetching new stories"); + + my $story_last_id = $self->get_last_id('feed'); + + # Check for new transctions (stories) + my $new_stories = $self->new_stories($story_last_id); + INFO("No new stories") unless @$new_stories; + + # Process each story + foreach my $story_data (@$new_stories) { + my $story_id = $story_data->{id}; + my $story_phid = $story_data->{phid}; + my $author_phid = $story_data->{authorPHID}; + my $object_phid = $story_data->{objectPHID}; + my $story_text = $story_data->{text}; - local Bugzilla::Logging->fields->{type} = 'FEED'; + TRACE("STORY ID: $story_id"); + TRACE("STORY PHID: $story_phid"); + TRACE("AUTHOR PHID: $author_phid"); + TRACE("OBJECT PHID: $object_phid"); + INFO("STORY TEXT: $story_text"); - # Ensure Phabricator syncing is enabled - if (!Bugzilla->params->{phabricator_enabled}) { - WARN("PHABRICATOR SYNC DISABLED"); - return; + # Only interested in changes to revisions for now. + if ($object_phid !~ /^PHID-DREV/) { + INFO("SKIPPING: Not a revision change"); + $self->save_last_id($story_id, 'feed'); + next; } - # PROCESS NEW FEED TRANSACTIONS - - INFO("Fetching new stories"); - - my $story_last_id = $self->get_last_id('feed'); - - # Check for new transctions (stories) - my $new_stories = $self->new_stories($story_last_id); - INFO("No new stories") unless @$new_stories; - - # Process each story - foreach my $story_data (@$new_stories) { - my $story_id = $story_data->{id}; - my $story_phid = $story_data->{phid}; - my $author_phid = $story_data->{authorPHID}; - my $object_phid = $story_data->{objectPHID}; - my $story_text = $story_data->{text}; - - TRACE("STORY ID: $story_id"); - TRACE("STORY PHID: $story_phid"); - TRACE("AUTHOR PHID: $author_phid"); - TRACE("OBJECT PHID: $object_phid"); - INFO("STORY TEXT: $story_text"); - - # Only interested in changes to revisions for now. - if ($object_phid !~ /^PHID-DREV/) { - INFO("SKIPPING: Not a revision change"); - $self->save_last_id($story_id, 'feed'); - next; - } - - # Skip changes done by phab-bot user - # If changer does not exist in bugzilla database - # we use the phab-bot account as the changer - my $author = Bugzilla::Extension::PhabBugz::User->new_from_query( - { phids => [ $author_phid ] } - ); - - if ($author && $author->bugzilla_id) { - if ($author->bugzilla_user->login eq PHAB_AUTOMATION_USER) { - INFO("SKIPPING: Change made by phabricator user"); - $self->save_last_id($story_id, 'feed'); - next; - } - } - else { - my $phab_user = Bugzilla::User->new( { name => PHAB_AUTOMATION_USER } ); - $author = Bugzilla::Extension::PhabBugz::User->new_from_query( - { - ids => [ $phab_user->id ] - } - ); - } - # Load the revision from Phabricator - my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ phids => [ $object_phid ] }); - $self->process_revision_change($revision, $author, $story_text); + # Skip changes done by phab-bot user + # If changer does not exist in bugzilla database + # we use the phab-bot account as the changer + my $author = Bugzilla::Extension::PhabBugz::User->new_from_query( + {phids => [$author_phid]}); + + if ($author && $author->bugzilla_id) { + if ($author->bugzilla_user->login eq PHAB_AUTOMATION_USER) { + INFO("SKIPPING: Change made by phabricator user"); $self->save_last_id($story_id, 'feed'); + next; + } + } + else { + my $phab_user = Bugzilla::User->new({name => PHAB_AUTOMATION_USER}); + $author + = Bugzilla::Extension::PhabBugz::User->new_from_query({ids => [$phab_user->id] + }); } - # Process any build targets as well. - my $dbh = Bugzilla->dbh; + # Load the revision from Phabricator + my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query( + {phids => [$object_phid]}); + $self->process_revision_change($revision, $author, $story_text); + $self->save_last_id($story_id, 'feed'); + } - INFO("Checking for revisions in draft mode"); - my $build_targets = $dbh->selectall_arrayref( - "SELECT name, value FROM phabbugz WHERE name LIKE 'build_target_%'", - { Slice => {} } - ); + # Process any build targets as well. + my $dbh = Bugzilla->dbh; - my $delete_build_target = $dbh->prepare( - "DELETE FROM phabbugz WHERE name = ? AND VALUE = ?" - ); + INFO("Checking for revisions in draft mode"); + my $build_targets + = $dbh->selectall_arrayref( + "SELECT name, value FROM phabbugz WHERE name LIKE 'build_target_%'", + {Slice => {}}); - foreach my $target (@$build_targets) { - my ($revision_id) = ($target->{name} =~ /^build_target_(\d+)$/); - my $build_target = $target->{value}; + my $delete_build_target + = $dbh->prepare("DELETE FROM phabbugz WHERE name = ? AND VALUE = ?"); - next unless $revision_id && $build_target; + foreach my $target (@$build_targets) { + my ($revision_id) = ($target->{name} =~ /^build_target_(\d+)$/); + my $build_target = $target->{value}; - INFO("Processing revision $revision_id with build target $build_target"); + next unless $revision_id && $build_target; - my $revision = - Bugzilla::Extension::PhabBugz::Revision->new_from_query( - { - ids => [ int($revision_id) ] - } - ); + INFO("Processing revision $revision_id with build target $build_target"); - $self->process_revision_change( $revision, $revision->author, " created D" . $revision->id ); + my $revision + = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ + ids => [int($revision_id)] + }); - # Set the build target to a passing status to - # allow the revision to exit draft state - request( 'harbormaster.sendmessage', { - buildTargetPHID => $build_target, - type => 'pass' - } ); + $self->process_revision_change($revision, $revision->author, + " created D" . $revision->id); - $delete_build_target->execute($target->{name}, $target->{value}); - } + # Set the build target to a passing status to + # allow the revision to exit draft state + request('harbormaster.sendmessage', + {buildTargetPHID => $build_target, type => 'pass'}); - if (Bugzilla->datadog) { - my $dd = Bugzilla->datadog(); - $dd->increment('bugzilla.phabbugz.feed_query_count'); - } + $delete_build_target->execute($target->{name}, $target->{value}); + } + + if (Bugzilla->datadog) { + my $dd = Bugzilla->datadog(); + $dd->increment('bugzilla.phabbugz.feed_query_count'); + } } sub user_query { - my ( $self ) = @_; + my ($self) = @_; - local Bugzilla::Logging->fields->{type} = 'USERS'; + local Bugzilla::Logging->fields->{type} = 'USERS'; - # Ensure Phabricator syncing is enabled - if (!Bugzilla->params->{phabricator_enabled}) { - WARN("PHABRICATOR SYNC DISABLED"); - return; - } + # Ensure Phabricator syncing is enabled + if (!Bugzilla->params->{phabricator_enabled}) { + WARN("PHABRICATOR SYNC DISABLED"); + return; + } - # PROCESS NEW USERS + # PROCESS NEW USERS - INFO("Fetching new users"); + INFO("Fetching new users"); - my $user_last_id = $self->get_last_id('user'); + my $user_last_id = $self->get_last_id('user'); - # Check for new users - my $new_users = $self->new_users($user_last_id); - INFO("No new users") unless @$new_users; + # Check for new users + my $new_users = $self->new_users($user_last_id); + INFO("No new users") unless @$new_users; - # Process each new user - foreach my $user_data (@$new_users) { - my $user_id = $user_data->{id}; - my $user_login = $user_data->{fields}{username}; - my $user_realname = $user_data->{fields}{realName}; - my $object_phid = $user_data->{phid}; + # Process each new user + foreach my $user_data (@$new_users) { + my $user_id = $user_data->{id}; + my $user_login = $user_data->{fields}{username}; + my $user_realname = $user_data->{fields}{realName}; + my $object_phid = $user_data->{phid}; - TRACE("ID: $user_id"); - TRACE("LOGIN: $user_login"); - TRACE("REALNAME: $user_realname"); - TRACE("OBJECT PHID: $object_phid"); + TRACE("ID: $user_id"); + TRACE("LOGIN: $user_login"); + TRACE("REALNAME: $user_realname"); + TRACE("OBJECT PHID: $object_phid"); - with_readonly_database { - $self->process_new_user($user_data); - }; - $self->save_last_id($user_id, 'user'); - } + with_readonly_database { + $self->process_new_user($user_data); + }; + $self->save_last_id($user_id, 'user'); + } - if (Bugzilla->datadog) { - my $dd = Bugzilla->datadog(); - $dd->increment('bugzilla.phabbugz.user_query_count'); - } + if (Bugzilla->datadog) { + my $dd = Bugzilla->datadog(); + $dd->increment('bugzilla.phabbugz.user_query_count'); + } } sub group_query { - my ($self) = @_; + my ($self) = @_; - local Bugzilla::Logging->fields->{type} = 'GROUPS'; + local Bugzilla::Logging->fields->{type} = 'GROUPS'; - # Ensure Phabricator syncing is enabled - if ( !Bugzilla->params->{phabricator_enabled} ) { - WARN("PHABRICATOR SYNC DISABLED"); - return; - } + # Ensure Phabricator syncing is enabled + if (!Bugzilla->params->{phabricator_enabled}) { + WARN("PHABRICATOR SYNC DISABLED"); + return; + } - # PROCESS SECURITY GROUPS - - INFO("Updating group memberships"); - - # Loop through each group and perform the following: - # - # 1. Load flattened list of group members - # 2. Check to see if Phab project exists for 'bmo-' - # 3. Create if does not exist with locked down policy. - # 4. Set project members to exact list including phab-bot user - # 5. Profit - - my $sync_groups = Bugzilla::Group->match( { isactive => 1, isbuggroup => 1 } ); - - # Load phab-bot Phabricator user to add as a member of each project group later - my $phab_bmo_user = Bugzilla::User->new( { name => PHAB_AUTOMATION_USER, cache => 1 } ); - my $phab_user = - Bugzilla::Extension::PhabBugz::User->new_from_query( - { - ids => [ $phab_bmo_user->id ] - } - ); - - # secure-revision project that will be used for bmo group projects - my $secure_revision = - Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - name => 'secure-revision' - } - ); - - foreach my $group (@$sync_groups) { - # Create group project if one does not yet exist - my $phab_project_name = 'bmo-' . $group->name; - my $project = - Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - name => $phab_project_name - } - ); - - if ( !$project ) { - INFO("Project $phab_project_name not found. Creating."); - $project = Bugzilla::Extension::PhabBugz::Project->create( - { - name => $phab_project_name, - description => 'BMO Security Group for ' . $group->name, - view_policy => $secure_revision->phid, - edit_policy => $secure_revision->phid, - join_policy => $secure_revision->phid - } - ); - } - else { - # Make sure that the group project permissions are set properly - INFO("Updating permissions on $phab_project_name"); - $project->set_policy( 'view', $secure_revision->phid ); - $project->set_policy( 'edit', $secure_revision->phid ); - $project->set_policy( 'join', $secure_revision->phid ); - } - - # Make sure phab-bot also a member of the new project group so that it can - # make policy changes to the private revisions - INFO( "Checking project members for " . $project->name ); - my $set_members = $self->get_group_members($group); - my @set_member_phids = uniq map { $_->phid } ( @$set_members, $phab_user ); - my @current_member_phids = uniq map { $_->phid } @{ $project->members }; - my ( $removed, $added ) = diff_arrays( \@current_member_phids, \@set_member_phids ); - - if (@$added) { - INFO( 'Adding project members: ' . join( ',', @$added ) ); - $project->add_member($_) foreach @$added; - } - - if (@$removed) { - INFO( 'Removing project members: ' . join( ',', @$removed ) ); - $project->remove_member($_) foreach @$removed; - } - - if (@$added || @$removed) { - my $result = $project->update(); - local Bugzilla::Logging->fields->{api_result} = $result; - INFO( "Project " . $project->name . " updated" ); - } - } + # PROCESS SECURITY GROUPS + + INFO("Updating group memberships"); + + # Loop through each group and perform the following: + # + # 1. Load flattened list of group members + # 2. Check to see if Phab project exists for 'bmo-' + # 3. Create if does not exist with locked down policy. + # 4. Set project members to exact list including phab-bot user + # 5. Profit + + my $sync_groups = Bugzilla::Group->match({isactive => 1, isbuggroup => 1}); - if (Bugzilla->datadog) { - my $dd = Bugzilla->datadog(); - $dd->increment('bugzilla.phabbugz.group_query_count'); + # Load phab-bot Phabricator user to add as a member of each project group later + my $phab_bmo_user + = Bugzilla::User->new({name => PHAB_AUTOMATION_USER, cache => 1}); + my $phab_user + = Bugzilla::Extension::PhabBugz::User->new_from_query({ + ids => [$phab_bmo_user->id] + }); + + # secure-revision project that will be used for bmo group projects + my $secure_revision + = Bugzilla::Extension::PhabBugz::Project->new_from_query({ + name => 'secure-revision' + }); + + foreach my $group (@$sync_groups) { + + # Create group project if one does not yet exist + my $phab_project_name = 'bmo-' . $group->name; + my $project + = Bugzilla::Extension::PhabBugz::Project->new_from_query({ + name => $phab_project_name + }); + + if (!$project) { + INFO("Project $phab_project_name not found. Creating."); + $project = Bugzilla::Extension::PhabBugz::Project->create({ + name => $phab_project_name, + description => 'BMO Security Group for ' . $group->name, + view_policy => $secure_revision->phid, + edit_policy => $secure_revision->phid, + join_policy => $secure_revision->phid + }); + } + else { + # Make sure that the group project permissions are set properly + INFO("Updating permissions on $phab_project_name"); + $project->set_policy('view', $secure_revision->phid); + $project->set_policy('edit', $secure_revision->phid); + $project->set_policy('join', $secure_revision->phid); } -} -sub process_revision_change { - state $check = compile($Invocant, Revision, LinkedPhabUser, Str); - my ($self, $revision, $changer, $story_text) = $check->(@_); - - # NO BUG ID - if (!$revision->bug_id) { - if ($story_text =~ /\s+created\s+D\d+/) { - # If new revision and bug id was omitted, make revision public - INFO("No bug associated with new revision. Marking public."); - $revision->make_public(); - $revision->update(); - INFO("SUCCESS"); - return; - } - else { - INFO("SKIPPING: No bug associated with revision change"); - return; - } + # Make sure phab-bot also a member of the new project group so that it can + # make policy changes to the private revisions + INFO("Checking project members for " . $project->name); + my $set_members = $self->get_group_members($group); + my @set_member_phids = uniq map { $_->phid } (@$set_members, $phab_user); + my @current_member_phids = uniq map { $_->phid } @{$project->members}; + my ($removed, $added) = diff_arrays(\@current_member_phids, \@set_member_phids); + + if (@$added) { + INFO('Adding project members: ' . join(',', @$added)); + $project->add_member($_) foreach @$added; } + if (@$removed) { + INFO('Removing project members: ' . join(',', @$removed)); + $project->remove_member($_) foreach @$removed; + } - my $log_message = sprintf( - "REVISION CHANGE FOUND: D%d: %s | bug: %d | %s | %s", - $revision->id, - $revision->title, - $revision->bug_id, - $changer->name, - $story_text); - INFO($log_message); - - # change to the phabricator user, which returns a guard that restores the previous user. - my $restore_prev_user = set_phab_user(); - my $bug = $revision->bug; - - # Check to make sure bug id is valid and author can see it - if ($bug->{error} - ||!$revision->author->bugzilla_user->can_see_bug($revision->bug_id)) - { - if ($story_text =~ /\s+created\s+D\d+/) { - INFO('Invalid bug ID or author does not have access to the bug. ' . - 'Waiting til next revision update to notify author.'); - return; - } - - INFO('Invalid bug ID or author does not have access to the bug'); - my $phab_error_message = ""; - Bugzilla->template->process('revision/comments.html.tmpl', - { message => 'invalid_bug_id' }, - \$phab_error_message); - $revision->add_comment($phab_error_message); - $revision->update(); - return; + if (@$added || @$removed) { + my $result = $project->update(); + local Bugzilla::Logging->fields->{api_result} = $result; + INFO("Project " . $project->name . " updated"); } + } - # REVISION SECURITY POLICY + if (Bugzilla->datadog) { + my $dd = Bugzilla->datadog(); + $dd->increment('bugzilla.phabbugz.group_query_count'); + } +} - # If bug is public then remove privacy policy - if (!@{ $bug->groups_in }) { - INFO('Bug is public so setting view/edit public'); - $revision->make_public(); +sub process_revision_change { + state $check = compile($Invocant, Revision, LinkedPhabUser, Str); + my ($self, $revision, $changer, $story_text) = $check->(@_); + + # NO BUG ID + if (!$revision->bug_id) { + if ($story_text =~ /\s+created\s+D\d+/) { + + # If new revision and bug id was omitted, make revision public + INFO("No bug associated with new revision. Marking public."); + $revision->make_public(); + $revision->update(); + INFO("SUCCESS"); + return; } - # else bug is private. else { - # Here we create a new custom policy containing the project - # groups that are mapped to bugzilla groups. - my $set_project_names = [ map { "bmo-" . $_->name } @{ $bug->groups_in } ]; - - # If current policy projects matches what we want to set, then - # we leave the current policy alone. - my $current_policy; - if ($revision->view_policy =~ /^PHID-PLCY/) { - INFO("Loading current policy: " . $revision->view_policy); - $current_policy - = Bugzilla::Extension::PhabBugz::Policy->new_from_query({ phids => [ $revision->view_policy ]}); - my $current_project_names = [ map { $_->name } @{ $current_policy->rule_projects } ]; - INFO("Current policy projects: " . join(", ", @$current_project_names)); - my ($added, $removed) = diff_arrays($current_project_names, $set_project_names); - if (@$added || @$removed) { - INFO('Project groups do not match. Need new custom policy'); - $current_policy = undef; - } - else { - INFO('Project groups match. Leaving current policy as-is'); - } - } - - if (!$current_policy) { - INFO("Creating new custom policy: " . join(", ", @$set_project_names)); - $revision->make_private($set_project_names); - } - - # Subscriber list of the private revision should always match - # the bug roles such as assignee, qa contact, and cc members. - my $subscribers = get_bug_role_phids($bug); - $revision->set_subscribers($subscribers); + INFO("SKIPPING: No bug associated with revision change"); + return; + } + } + + + my $log_message = sprintf( + "REVISION CHANGE FOUND: D%d: %s | bug: %d | %s | %s", + $revision->id, $revision->title, $revision->bug_id, + $changer->name, $story_text + ); + INFO($log_message); + +# change to the phabricator user, which returns a guard that restores the previous user. + my $restore_prev_user = set_phab_user(); + my $bug = $revision->bug; + + # Check to make sure bug id is valid and author can see it + if ($bug->{error} + || !$revision->author->bugzilla_user->can_see_bug($revision->bug_id)) + { + if ($story_text =~ /\s+created\s+D\d+/) { + INFO( 'Invalid bug ID or author does not have access to the bug. ' + . 'Waiting til next revision update to notify author.'); + return; } - my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - - INFO('Checking for revision attachment'); - my $rev_attachment = create_revision_attachment($bug, $revision, $timestamp, $revision->author->bugzilla_user); - INFO('Attachment ' . $rev_attachment->id . ' created or already exists.'); - - # ATTACHMENT OBSOLETES + INFO('Invalid bug ID or author does not have access to the bug'); + my $phab_error_message = ""; + Bugzilla->template->process('revision/comments.html.tmpl', + {message => 'invalid_bug_id'}, + \$phab_error_message); + $revision->add_comment($phab_error_message); + $revision->update(); + return; + } + + # REVISION SECURITY POLICY + + # If bug is public then remove privacy policy + if (!@{$bug->groups_in}) { + INFO('Bug is public so setting view/edit public'); + $revision->make_public(); + } + + # else bug is private. + else { + # Here we create a new custom policy containing the project + # groups that are mapped to bugzilla groups. + my $set_project_names = [map { "bmo-" . $_->name } @{$bug->groups_in}]; + + # If current policy projects matches what we want to set, then + # we leave the current policy alone. + my $current_policy; + if ($revision->view_policy =~ /^PHID-PLCY/) { + INFO("Loading current policy: " . $revision->view_policy); + $current_policy = Bugzilla::Extension::PhabBugz::Policy->new_from_query( + {phids => [$revision->view_policy]}); + my $current_project_names + = [map { $_->name } @{$current_policy->rule_projects}]; + INFO("Current policy projects: " . join(", ", @$current_project_names)); + my ($added, $removed) = diff_arrays($current_project_names, $set_project_names); + if (@$added || @$removed) { + INFO('Project groups do not match. Need new custom policy'); + $current_policy = undef; + } + else { + INFO('Project groups match. Leaving current policy as-is'); + } + } - # fixup attachments on current bug - my @attachments = - grep { is_attachment_phab_revision($_) } @{ $bug->attachments() }; + if (!$current_policy) { + INFO("Creating new custom policy: " . join(", ", @$set_project_names)); + $revision->make_private($set_project_names); + } - foreach my $attachment (@attachments) { - my ($attach_revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); - next if $attach_revision_id != $revision->id; + # Subscriber list of the private revision should always match + # the bug roles such as assignee, qa contact, and cc members. + my $subscribers = get_bug_role_phids($bug); + $revision->set_subscribers($subscribers); + } - my $make_obsolete = $revision->status eq 'abandoned' ? 1 : 0; - INFO('Updating obsolete status on attachmment ' . $attachment->id); - $attachment->set_is_obsolete($make_obsolete); + my ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - if ($revision->title ne $attachment->description) { - INFO('Updating description on attachment ' . $attachment->id); - $attachment->set_description($revision->title); - } + INFO('Checking for revision attachment'); + my $rev_attachment = create_revision_attachment($bug, $revision, $timestamp, + $revision->author->bugzilla_user); + INFO('Attachment ' . $rev_attachment->id . ' created or already exists.'); - $attachment->update($timestamp); - } + # ATTACHMENT OBSOLETES - # fixup attachments with same revision id but on different bugs - my %other_bugs; - my $other_attachments = Bugzilla::Attachment->match({ - mimetype => PHAB_CONTENT_TYPE, - filename => 'phabricator-D' . $revision->id . '-url.txt', - WHERE => { 'bug_id != ? AND NOT isobsolete' => $bug->id } - }); - foreach my $attachment (@$other_attachments) { - $other_bugs{$attachment->bug_id}++; - INFO('Updating obsolete status on attachment ' . - $attachment->id . " for bug " . $attachment->bug_id); - $attachment->set_is_obsolete(1); - $attachment->update($timestamp); - } + # fixup attachments on current bug + my @attachments + = grep { is_attachment_phab_revision($_) } @{$bug->attachments()}; - # FINISH UP + foreach my $attachment (@attachments) { + my ($attach_revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); + next if $attach_revision_id != $revision->id; - $bug->update($timestamp); - $revision->update(); + my $make_obsolete = $revision->status eq 'abandoned' ? 1 : 0; + INFO('Updating obsolete status on attachmment ' . $attachment->id); + $attachment->set_is_obsolete($make_obsolete); - # Email changes for this revisions bug and also for any other - # bugs that previously had these revision attachments - foreach my $bug_id ($revision->bug_id, keys %other_bugs) { - Bugzilla::BugMail::Send($bug_id, { changer => $changer->bugzilla_user }); + if ($revision->title ne $attachment->description) { + INFO('Updating description on attachment ' . $attachment->id); + $attachment->set_description($revision->title); } - INFO('SUCCESS: Revision D' . $revision->id . ' processed'); + $attachment->update($timestamp); + } + + # fixup attachments with same revision id but on different bugs + my %other_bugs; + my $other_attachments = Bugzilla::Attachment->match({ + mimetype => PHAB_CONTENT_TYPE, + filename => 'phabricator-D' . $revision->id . '-url.txt', + WHERE => {'bug_id != ? AND NOT isobsolete' => $bug->id} + }); + foreach my $attachment (@$other_attachments) { + $other_bugs{$attachment->bug_id}++; + INFO( 'Updating obsolete status on attachment ' + . $attachment->id + . " for bug " + . $attachment->bug_id); + $attachment->set_is_obsolete(1); + $attachment->update($timestamp); + } + + # FINISH UP + + $bug->update($timestamp); + $revision->update(); + + # Email changes for this revisions bug and also for any other + # bugs that previously had these revision attachments + foreach my $bug_id ($revision->bug_id, keys %other_bugs) { + Bugzilla::BugMail::Send($bug_id, {changer => $changer->bugzilla_user}); + } + + INFO('SUCCESS: Revision D' . $revision->id . ' processed'); } sub process_new_user { - state $check = compile($Invocant, HashRef); - my ( $self, $user_data ) = $check->(@_); + state $check = compile($Invocant, HashRef); + my ($self, $user_data) = $check->(@_); - # Load the user data into a proper object - my $phab_user = Bugzilla::Extension::PhabBugz::User->new($user_data); + # Load the user data into a proper object + my $phab_user = Bugzilla::Extension::PhabBugz::User->new($user_data); - if (!$phab_user->bugzilla_id) { - WARN("SKIPPING: No bugzilla id associated with user"); - return; - } + if (!$phab_user->bugzilla_id) { + WARN("SKIPPING: No bugzilla id associated with user"); + return; + } - my $bug_user = $phab_user->bugzilla_user; + my $bug_user = $phab_user->bugzilla_user; - # Pre setup before querying DB - my $restore_prev_user = set_phab_user(); + # Pre setup before querying DB + my $restore_prev_user = set_phab_user(); - # CHECK AND WARN FOR POSSIBLE USERNAME SQUATTING - INFO("Checking for username squatters"); - my $dbh = Bugzilla->dbh; - my $regexp = $dbh->quote( ":?:" . quotemeta($phab_user->name) . "[[:>:]]" ); - my $results = $dbh->selectall_arrayref( " + # CHECK AND WARN FOR POSSIBLE USERNAME SQUATTING + INFO("Checking for username squatters"); + my $dbh = Bugzilla->dbh; + my $regexp = $dbh->quote(":?:" . quotemeta($phab_user->name) . "[[:>:]]"); + my $results = $dbh->selectall_arrayref(" SELECT userid, login_name, realname FROM profiles - WHERE userid != ? AND " . $dbh->sql_regexp( 'realname', $regexp ), - { Slice => {} }, - $bug_user->id ); - if (@$results) { - # The email client will display the Date: header in the desired timezone, - # so we can always use UTC here. - my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); - - foreach my $row (@$results) { - WARN( - 'Possible username squatter: ', - 'phab user login: ' . $phab_user->name, - ' phab user realname: ' . $phab_user->realname, - ' bugzilla user id: ' . $row->{userid}, - ' bugzilla login: ' . $row->{login_name}, - ' bugzilla realname: ' . $row->{realname} - ); - - my $vars = { - date => $timestamp, - phab_user_login => $phab_user->name, - phab_user_realname => $phab_user->realname, - bugzilla_userid => $bug_user->id, - bugzilla_login => $bug_user->login, - bugzilla_realname => $bug_user->name, - squat_userid => $row->{userid}, - squat_login => $row->{login_name}, - squat_realname => $row->{realname} - }; - - my $message; - my $template = Bugzilla->template; - $template->process("admin/email/squatter-alert.txt.tmpl", $vars, \$message) - || ThrowTemplateError($template->error()); - - MessageToMTA($message); - } + WHERE userid != ? AND " . $dbh->sql_regexp('realname', $regexp), + {Slice => {}}, $bug_user->id); + if (@$results) { + + # The email client will display the Date: header in the desired timezone, + # so we can always use UTC here. + my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $timestamp = format_time($timestamp, '%a, %d %b %Y %T %z', 'UTC'); + + foreach my $row (@$results) { + WARN( + 'Possible username squatter: ', + 'phab user login: ' . $phab_user->name, + ' phab user realname: ' . $phab_user->realname, + ' bugzilla user id: ' . $row->{userid}, + ' bugzilla login: ' . $row->{login_name}, + ' bugzilla realname: ' . $row->{realname} + ); + + my $vars = { + date => $timestamp, + phab_user_login => $phab_user->name, + phab_user_realname => $phab_user->realname, + bugzilla_userid => $bug_user->id, + bugzilla_login => $bug_user->login, + bugzilla_realname => $bug_user->name, + squat_userid => $row->{userid}, + squat_login => $row->{login_name}, + squat_realname => $row->{realname} + }; + + my $message; + my $template = Bugzilla->template; + $template->process("admin/email/squatter-alert.txt.tmpl", $vars, \$message) + || ThrowTemplateError($template->error()); + + MessageToMTA($message); } + } - # ADD SUBSCRIBERS TO REVSISIONS FOR CURRENT PRIVATE BUGS + # ADD SUBSCRIBERS TO REVSISIONS FOR CURRENT PRIVATE BUGS - my $params = { - f3 => 'OP', - j3 => 'OR', + my $params = { + f3 => 'OP', + j3 => 'OR', - # User must be either reporter, assignee, qa_contact - # or on the cc list of the bug - f4 => 'cc', - o4 => 'equals', - v4 => $bug_user->login, + # User must be either reporter, assignee, qa_contact + # or on the cc list of the bug + f4 => 'cc', + o4 => 'equals', + v4 => $bug_user->login, - f5 => 'assigned_to', - o5 => 'equals', - v5 => $bug_user->login, + f5 => 'assigned_to', + o5 => 'equals', + v5 => $bug_user->login, - f6 => 'qa_contact', - o6 => 'equals', - v6 => $bug_user->login, + f6 => 'qa_contact', + o6 => 'equals', + v6 => $bug_user->login, - f7 => 'reporter', - o7 => 'equals', - v7 => $bug_user->login, + f7 => 'reporter', + o7 => 'equals', + v7 => $bug_user->login, - f9 => 'CP', + f9 => 'CP', - # The bug needs to be private - f10 => 'bug_group', - o10 => 'isnotempty', + # The bug needs to be private + f10 => 'bug_group', + o10 => 'isnotempty', - # And the bug must have one or more attachments - # that are connected to revisions - f11 => 'attachments.filename', - o11 => 'regexp', - v11 => '^phabricator-D[[:digit:]]+-url.txt$', - }; + # And the bug must have one or more attachments + # that are connected to revisions + f11 => 'attachments.filename', + o11 => 'regexp', + v11 => '^phabricator-D[[:digit:]]+-url.txt$', + }; - my $search = Bugzilla::Search->new( fields => [ 'bug_id' ], - params => $params, - order => [ 'bug_id' ] ); - my $data = $search->data; + my $search = Bugzilla::Search->new( + fields => ['bug_id'], + params => $params, + order => ['bug_id'] + ); + my $data = $search->data; - # the first value of each row should be the bug id - my @bug_ids = map { shift @$_ } @$data; + # the first value of each row should be the bug id + my @bug_ids = map { shift @$_ } @$data; - INFO("Updating subscriber values for old private bugs"); + INFO("Updating subscriber values for old private bugs"); - foreach my $bug_id (@bug_ids) { - INFO("Processing bug $bug_id"); + foreach my $bug_id (@bug_ids) { + INFO("Processing bug $bug_id"); - my $bug = Bugzilla::Bug->new({ id => $bug_id, cache => 1 }); + my $bug = Bugzilla::Bug->new({id => $bug_id, cache => 1}); - my @attachments = - grep { is_attachment_phab_revision($_) } @{ $bug->attachments() }; + my @attachments + = grep { is_attachment_phab_revision($_) } @{$bug->attachments()}; - foreach my $attachment (@attachments) { - my ($revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); + foreach my $attachment (@attachments) { + my ($revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); - if (!$revision_id) { - WARN("Skipping " . $attachment->filename . " on bug $bug_id. Filename should be fixed."); - next; - } + if (!$revision_id) { + WARN( "Skipping " + . $attachment->filename + . " on bug $bug_id. Filename should be fixed."); + next; + } - INFO("Processing revision D$revision_id"); + INFO("Processing revision D$revision_id"); - my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query( - { ids => [ int($revision_id) ] }); + my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query( + {ids => [int($revision_id)]}); - $revision->add_subscriber($phab_user->phid); - $revision->update(); + $revision->add_subscriber($phab_user->phid); + $revision->update(); - INFO("Revision $revision_id updated"); - } + INFO("Revision $revision_id updated"); } + } - INFO('SUCCESS: User ' . $phab_user->id . ' processed'); + INFO('SUCCESS: User ' . $phab_user->id . ' processed'); } ################## @@ -698,87 +697,74 @@ sub process_new_user { ################## sub new_stories { - my ( $self, $after ) = @_; - my $data = { view => 'text' }; - $data->{after} = $after if $after; + my ($self, $after) = @_; + my $data = {view => 'text'}; + $data->{after} = $after if $after; - my $result = request( 'feed.query_id', $data ); + my $result = request('feed.query_id', $data); - unless ( ref $result->{result}{data} eq 'ARRAY' - && @{ $result->{result}{data} } ) - { - return []; - } + unless (ref $result->{result}{data} eq 'ARRAY' && @{$result->{result}{data}}) { + return []; + } - # Guarantee that the data is in ascending ID order - return [ sort { $a->{id} <=> $b->{id} } @{ $result->{result}{data} } ]; + # Guarantee that the data is in ascending ID order + return [sort { $a->{id} <=> $b->{id} } @{$result->{result}{data}}]; } sub new_users { - my ( $self, $after ) = @_; - my $data = { - order => [ "id" ], - attachments => { - 'external-accounts' => 1 - } - }; - $data->{before} = $after if $after; + my ($self, $after) = @_; + my $data = {order => ["id"], attachments => {'external-accounts' => 1}}; + $data->{before} = $after if $after; - my $result = request( 'user.search', $data ); + my $result = request('user.search', $data); - unless ( ref $result->{result}{data} eq 'ARRAY' - && @{ $result->{result}{data} } ) - { - return []; - } + unless (ref $result->{result}{data} eq 'ARRAY' && @{$result->{result}{data}}) { + return []; + } - # Guarantee that the data is in ascending ID order - return [ sort { $a->{id} <=> $b->{id} } @{ $result->{result}{data} } ]; + # Guarantee that the data is in ascending ID order + return [sort { $a->{id} <=> $b->{id} } @{$result->{result}{data}}]; } sub get_last_id { - my ( $self, $type ) = @_; - my $type_full = $type . "_last_id"; - my $last_id = Bugzilla->dbh->selectrow_array( " - SELECT value FROM phabbugz WHERE name = ?", undef, $type_full ); - $last_id ||= 0; - TRACE(uc($type_full) . ": $last_id" ); - return $last_id; + my ($self, $type) = @_; + my $type_full = $type . "_last_id"; + my $last_id = Bugzilla->dbh->selectrow_array(" + SELECT value FROM phabbugz WHERE name = ?", undef, $type_full); + $last_id ||= 0; + TRACE(uc($type_full) . ": $last_id"); + return $last_id; } sub save_last_id { - my ( $self, $last_id, $type ) = @_; + my ($self, $last_id, $type) = @_; - # Store the largest last key so we can start from there in the next session - my $type_full = $type . "_last_id"; - TRACE("UPDATING " . uc($type_full) . ": $last_id" ); - Bugzilla->dbh->do( "REPLACE INTO phabbugz (name, value) VALUES (?, ?)", - undef, $type_full, $last_id ); + # Store the largest last key so we can start from there in the next session + my $type_full = $type . "_last_id"; + TRACE("UPDATING " . uc($type_full) . ": $last_id"); + Bugzilla->dbh->do("REPLACE INTO phabbugz (name, value) VALUES (?, ?)", + undef, $type_full, $last_id); } sub get_group_members { - state $check = compile( $Invocant, Group | Str ); - my ( $self, $group ) = $check->(@_); - my $group_obj = - ref $group ? $group : Bugzilla::Group->check( { name => $group, cache => 1 } ); + state $check = compile($Invocant, Group | Str); + my ($self, $group) = $check->(@_); + my $group_obj + = ref $group ? $group : Bugzilla::Group->check({name => $group, cache => 1}); - my $flat_list = join(',', - @{ Bugzilla::Group->flatten_group_membership( $group_obj->id ) } ); + my $flat_list + = join(',', @{Bugzilla::Group->flatten_group_membership($group_obj->id)}); - my $user_query = " + my $user_query = " SELECT DISTINCT profiles.userid FROM profiles, user_group_map AS ugm WHERE ugm.user_id = profiles.userid AND ugm.isbless = 0 AND ugm.group_id IN($flat_list)"; - my $user_ids = Bugzilla->dbh->selectcol_arrayref($user_query); + my $user_ids = Bugzilla->dbh->selectcol_arrayref($user_query); - # Return matching users in Phabricator - return Bugzilla::Extension::PhabBugz::User->match( - { - ids => $user_ids - } - ); + # Return matching users in Phabricator + return Bugzilla::Extension::PhabBugz::User->match({ids => $user_ids}); } 1; diff --git a/extensions/PhabBugz/lib/Policy.pm b/extensions/PhabBugz/lib/Policy.pm index 415ea20fb..658d0ddde 100644 --- a/extensions/PhabBugz/lib/Policy.pm +++ b/extensions/PhabBugz/lib/Policy.pm @@ -21,30 +21,22 @@ use Types::Standard -all; use Type::Utils; use Type::Params qw( compile ); -has 'phid' => ( is => 'ro', isa => Str ); -has 'type' => ( is => 'ro', isa => Str ); -has 'name' => ( is => 'ro', isa => Str ); -has 'shortName' => ( is => 'ro', isa => Str ); -has 'fullName' => ( is => 'ro', isa => Str ); -has 'href' => ( is => 'ro', isa => Maybe[Str] ); -has 'workflow' => ( is => 'ro', isa => Maybe[Str] ); -has 'icon' => ( is => 'ro', isa => Str ); -has 'default' => ( is => 'ro', isa => Str ); +has 'phid' => (is => 'ro', isa => Str); +has 'type' => (is => 'ro', isa => Str); +has 'name' => (is => 'ro', isa => Str); +has 'shortName' => (is => 'ro', isa => Str); +has 'fullName' => (is => 'ro', isa => Str); +has 'href' => (is => 'ro', isa => Maybe [Str]); +has 'workflow' => (is => 'ro', isa => Maybe [Str]); +has 'icon' => (is => 'ro', isa => Str); +has 'default' => (is => 'ro', isa => Str); has 'rules' => ( - is => 'ro', - isa => ArrayRef[ - Dict[ - action => Str, - rule => Str, - value => Maybe[ArrayRef[Str]] - ] - ] + is => 'ro', + isa => + ArrayRef [Dict [action => Str, rule => Str, value => Maybe [ArrayRef [Str]]]] ); -has 'rule_projects' => ( - is => 'lazy', - isa => ArrayRef[Project], -); +has 'rule_projects' => (is => 'lazy', isa => ArrayRef [Project],); # { # "data": [ @@ -81,64 +73,61 @@ has 'rule_projects' => ( # } # } -my $Invocant = class_type { class => __PACKAGE__ }; +my $Invocant = class_type {class => __PACKAGE__}; sub new_from_query { - state $check = compile($Invocant | ClassName, Dict[phids => ArrayRef[Str]]); - my ($class, $params) = $check->(@_); - my $result = request('policy.query', $params); - if (exists $result->{result}{data} && @{ $result->{result}{data} }) { - return $class->new($result->{result}->{data}->[0]); - } + state $check = compile($Invocant | ClassName, Dict [phids => ArrayRef [Str]]); + my ($class, $params) = $check->(@_); + my $result = request('policy.query', $params); + if (exists $result->{result}{data} && @{$result->{result}{data}}) { + return $class->new($result->{result}->{data}->[0]); + } } sub create { - state $check = compile($Invocant | ClassName, ArrayRef[Project]); - my ($class, $projects) = $check->(@_); - - my $data = { - objectType => 'DREV', - default => 'deny', - policy => [ - { - action => 'allow', - rule => 'PhabricatorSubscriptionsSubscribersPolicyRule', - }, - { - action => 'allow', - rule => 'PhabricatorDifferentialReviewersPolicyRule' - } - ] - }; - - if (@$projects) { - push @{ $data->{policy} }, { - action => 'allow', - rule => 'PhabricatorProjectsAllPolicyRule', - value => [ map { $_->phid } @$projects ], - }; - } - else { - my $secure_revision = Bugzilla::Extension::PhabBugz::Project->new_from_query({ - name => 'secure-revision' - }); - push @{ $data->{policy} }, { action => 'allow', value => $secure_revision->phid }; - } - - my $result = request('policy.create', $data); - return $class->new_from_query({ phids => [ $result->{result}{phid} ] }); + state $check = compile($Invocant | ClassName, ArrayRef [Project]); + my ($class, $projects) = $check->(@_); + + my $data = { + objectType => 'DREV', + default => 'deny', + policy => [ + {action => 'allow', rule => 'PhabricatorSubscriptionsSubscribersPolicyRule',}, + {action => 'allow', rule => 'PhabricatorDifferentialReviewersPolicyRule'} + ] + }; + + if (@$projects) { + push @{$data->{policy}}, + { + action => 'allow', + rule => 'PhabricatorProjectsAllPolicyRule', + value => [map { $_->phid } @$projects], + }; + } + else { + my $secure_revision + = Bugzilla::Extension::PhabBugz::Project->new_from_query({ + name => 'secure-revision' + }); + push @{$data->{policy}}, {action => 'allow', value => $secure_revision->phid}; + } + + my $result = request('policy.create', $data); + return $class->new_from_query({phids => [$result->{result}{phid}]}); } sub _build_rule_projects { - my ($self) = @_; - - return [] unless $self->rules; - my $rule = first { $_->{rule} =~ /PhabricatorProjects(?:All)?PolicyRule/ } @{ $self->rules }; - return [] unless $rule; - return [ - map { Bugzilla::Extension::PhabBugz::Project->new_from_query( { phids => [$_] } ) } - @{ $rule->{value} } - ]; + my ($self) = @_; + + return [] unless $self->rules; + my $rule = first { $_->{rule} =~ /PhabricatorProjects(?:All)?PolicyRule/ } + @{$self->rules}; + return [] unless $rule; + return [ + map { Bugzilla::Extension::PhabBugz::Project->new_from_query({phids => [$_]}) } + @{$rule->{value}} + ]; } -1; \ No newline at end of file +1; diff --git a/extensions/PhabBugz/lib/Project.pm b/extensions/PhabBugz/lib/Project.pm index c18708887..8af01f74e 100644 --- a/extensions/PhabBugz/lib/Project.pm +++ b/extensions/PhabBugz/lib/Project.pm @@ -24,63 +24,61 @@ use Bugzilla::Extension::PhabBugz::Util qw(request); # Initialization # ######################### -has id => ( is => 'ro', isa => Int ); -has phid => ( is => 'ro', isa => Str ); -has type => ( is => 'ro', isa => Str ); -has name => ( is => 'ro', isa => Str ); -has description => ( is => 'ro', isa => Maybe[Str] ); -has creation_ts => ( is => 'ro', isa => Str ); -has modification_ts => ( is => 'ro', isa => Str ); -has view_policy => ( is => 'ro', isa => Str ); -has edit_policy => ( is => 'ro', isa => Str ); -has join_policy => ( is => 'ro', isa => Str ); -has members_raw => ( is => 'ro', isa => ArrayRef [ Dict [ phid => Str ] ] ); -has members => ( is => 'lazy', isa => ArrayRef[PhabUser] ); - -my $Invocant = class_type { class => __PACKAGE__ }; +has id => (is => 'ro', isa => Int); +has phid => (is => 'ro', isa => Str); +has type => (is => 'ro', isa => Str); +has name => (is => 'ro', isa => Str); +has description => (is => 'ro', isa => Maybe [Str]); +has creation_ts => (is => 'ro', isa => Str); +has modification_ts => (is => 'ro', isa => Str); +has view_policy => (is => 'ro', isa => Str); +has edit_policy => (is => 'ro', isa => Str); +has join_policy => (is => 'ro', isa => Str); +has members_raw => (is => 'ro', isa => ArrayRef [Dict [phid => Str]]); +has members => (is => 'lazy', isa => ArrayRef [PhabUser]); + +my $Invocant = class_type {class => __PACKAGE__}; sub new_from_query { - my ( $class, $params ) = @_; - - my $data = { - queryKey => 'all', - attachments => { members => 1 }, - constraints => $params - }; - - my $result = request( 'project.search', $data ); - if ( exists $result->{result}{data} && @{ $result->{result}{data} } ) { - # If name is used as a query param, we need to loop through and look - # for exact match as Conduit will tokenize the name instead of doing - # exact string match :( If name is not used, then return first one. - if ( exists $params->{name} ) { - foreach my $item ( @{ $result->{result}{data} } ) { - next if $item->{fields}{name} ne $params->{name}; - return $class->new($item); - } - } - else { - return $class->new( $result->{result}{data}[0] ); - } + my ($class, $params) = @_; + + my $data + = {queryKey => 'all', attachments => {members => 1}, constraints => $params}; + + my $result = request('project.search', $data); + if (exists $result->{result}{data} && @{$result->{result}{data}}) { + + # If name is used as a query param, we need to loop through and look + # for exact match as Conduit will tokenize the name instead of doing + # exact string match :( If name is not used, then return first one. + if (exists $params->{name}) { + foreach my $item (@{$result->{result}{data}}) { + next if $item->{fields}{name} ne $params->{name}; + return $class->new($item); + } + } + else { + return $class->new($result->{result}{data}[0]); } + } } sub BUILDARGS { - my ( $class, $params ) = @_; + my ($class, $params) = @_; - $params->{name} = $params->{fields}->{name}; - $params->{description} = $params->{fields}->{description}; - $params->{creation_ts} = $params->{fields}->{dateCreated}; - $params->{modification_ts} = $params->{fields}->{dateModified}; - $params->{view_policy} = $params->{fields}->{policy}->{view}; - $params->{edit_policy} = $params->{fields}->{policy}->{edit}; - $params->{join_policy} = $params->{fields}->{policy}->{join}; - $params->{members_raw} = $params->{attachments}->{members}->{members}; + $params->{name} = $params->{fields}->{name}; + $params->{description} = $params->{fields}->{description}; + $params->{creation_ts} = $params->{fields}->{dateCreated}; + $params->{modification_ts} = $params->{fields}->{dateModified}; + $params->{view_policy} = $params->{fields}->{policy}->{view}; + $params->{edit_policy} = $params->{fields}->{policy}->{edit}; + $params->{join_policy} = $params->{fields}->{policy}->{join}; + $params->{members_raw} = $params->{attachments}->{members}->{members}; - delete $params->{fields}; - delete $params->{attachments}; + delete $params->{fields}; + delete $params->{attachments}; - return $params; + return $params; } # { @@ -146,131 +144,106 @@ sub BUILDARGS { ######################### sub create { - state $check = compile( - $Invocant | ClassName, - Dict[ - name => Str, - description => Str, - view_policy => Str, - edit_policy => Str, - join_policy => Str, - ] - ); - my ( $class, $params ) = $check->(@_); - - my $name = trim($params->{name}); - my $description = $params->{description}; - my $view_policy = $params->{view_policy}; - my $edit_policy = $params->{edit_policy}; - my $join_policy = $params->{join_policy}; - - my $data = { - transactions => [ - { type => 'name', value => $name }, - { type => 'description', value => $description }, - { type => 'edit', value => $edit_policy }, - { type => 'join', value => $join_policy }, - { type => 'view', value => $view_policy }, - { type => 'icon', value => 'group' }, - { type => 'color', value => 'red' } - ] - }; - - my $result = request( 'project.edit', $data ); - - return $class->new_from_query( - { phids => [ $result->{result}{object}{phid} ] } ); + state $check = compile( + $Invocant | ClassName, + Dict [ + name => Str, + description => Str, + view_policy => Str, + edit_policy => Str, + join_policy => Str, + ] + ); + my ($class, $params) = $check->(@_); + + my $name = trim($params->{name}); + my $description = $params->{description}; + my $view_policy = $params->{view_policy}; + my $edit_policy = $params->{edit_policy}; + my $join_policy = $params->{join_policy}; + + my $data = { + transactions => [ + {type => 'name', value => $name}, + {type => 'description', value => $description}, + {type => 'edit', value => $edit_policy}, + {type => 'join', value => $join_policy}, + {type => 'view', value => $view_policy}, + {type => 'icon', value => 'group'}, + {type => 'color', value => 'red'} + ] + }; + + my $result = request('project.edit', $data); + + return $class->new_from_query({phids => [$result->{result}{object}{phid}]}); } sub update { - my ($self) = @_; - - my $data = { - objectIdentifier => $self->phid, - transactions => [] - }; - - if ( $self->{set_name} ) { - push( - @{ $data->{transactions} }, - { - type => 'name', - value => $self->{set_name} - } - ); - } + my ($self) = @_; - if ( $self->{set_description} ) { - push( - @{ $data->{transactions} }, - { - type => 'description', - value => $self->{set_description} - } - ); - } + my $data = {objectIdentifier => $self->phid, transactions => []}; - if ( $self->{set_members} ) { - push( - @{ $data->{transactions} }, - { - type => 'members.set', - value => $self->{set_members} - } - ); - } - else { - if ( $self->{add_members} ) { - push( - @{ $data->{transactions} }, - { - type => 'members.add', - value => $self->{add_members} - } - ); - } - - if ( $self->{remove_members} ) { - push( - @{ $data->{transactions} }, - { - type => 'members.remove', - value => $self->{remove_members} - } - ); - } - } + if ($self->{set_name}) { + push(@{$data->{transactions}}, {type => 'name', value => $self->{set_name}}); + } - if ( $self->{set_policy} ) { - foreach my $name ( "view", "edit" ) { - next unless $self->{set_policy}->{$name}; - push( - @{ $data->{transactions} }, - { - type => $name, - value => $self->{set_policy}->{$name} - } - ); - } - } + if ($self->{set_description}) { + push( + @{$data->{transactions}}, + {type => 'description', value => $self->{set_description}} + ); + } - if ($self->{add_projects}) { - push(@{ $data->{transactions} }, { - type => 'projects.add', - value => $self->{add_projects} - }); + if ($self->{set_members}) { + push( + @{$data->{transactions}}, + {type => 'members.set', value => $self->{set_members}} + ); + } + else { + if ($self->{add_members}) { + push( + @{$data->{transactions}}, + {type => 'members.add', value => $self->{add_members}} + ); } - if ($self->{remove_projects}) { - push(@{ $data->{transactions} }, { - type => 'projects.remove', - value => $self->{remove_projects} - }); + if ($self->{remove_members}) { + push( + @{$data->{transactions}}, + {type => 'members.remove', value => $self->{remove_members}} + ); } + } + + if ($self->{set_policy}) { + foreach my $name ("view", "edit") { + next unless $self->{set_policy}->{$name}; + push( + @{$data->{transactions}}, + {type => $name, value => $self->{set_policy}->{$name}} + ); + } + } - my $result = request( 'project.edit', $data ); + if ($self->{add_projects}) { + push( + @{$data->{transactions}}, + {type => 'projects.add', value => $self->{add_projects}} + ); + } - return $result; + if ($self->{remove_projects}) { + push( + @{$data->{transactions}}, + {type => 'projects.remove', value => $self->{remove_projects}} + ); + } + + my $result = request('project.edit', $data); + + return $result; } ######################### @@ -278,40 +251,40 @@ sub update { ######################### sub set_name { - my ( $self, $name ) = @_; - $name = trim($name); - $self->{set_name} = $name; + my ($self, $name) = @_; + $name = trim($name); + $self->{set_name} = $name; } sub set_description { - my ( $self, $description ) = @_; - $description = trim($description); - $self->{set_description} = $description; + my ($self, $description) = @_; + $description = trim($description); + $self->{set_description} = $description; } sub add_member { - my ( $self, $member ) = @_; - $self->{add_members} ||= []; - my $member_phid = blessed $member ? $member->phid : $member; - push( @{ $self->{add_members} }, $member_phid ); + my ($self, $member) = @_; + $self->{add_members} ||= []; + my $member_phid = blessed $member ? $member->phid : $member; + push(@{$self->{add_members}}, $member_phid); } sub remove_member { - my ( $self, $member ) = @_; - $self->{remove_members} ||= []; - my $member_phid = blessed $member ? $member->phid : $member; - push( @{ $self->{remove_members} }, $member_phid ); + my ($self, $member) = @_; + $self->{remove_members} ||= []; + my $member_phid = blessed $member ? $member->phid : $member; + push(@{$self->{remove_members}}, $member_phid); } sub set_members { - my ( $self, $members ) = @_; - $self->{set_members} = [ map { blessed $_ ? $_->phid : $_ } @$members ]; + my ($self, $members) = @_; + $self->{set_members} = [map { blessed $_ ? $_->phid : $_ } @$members]; } sub set_policy { - my ( $self, $name, $policy ) = @_; - $self->{set_policy} ||= {}; - $self->{set_policy}->{$name} = $policy; + my ($self, $name, $policy) = @_; + $self->{set_policy} ||= {}; + $self->{set_policy}->{$name} = $policy; } ############ @@ -319,21 +292,17 @@ sub set_policy { ############ sub _build_members { - my ( $self ) = @_; - return [] unless $self->members_raw; + my ($self) = @_; + return [] unless $self->members_raw; - my @phids; - foreach my $member ( @{ $self->members_raw } ) { - push( @phids, $member->{phid} ); - } + my @phids; + foreach my $member (@{$self->members_raw}) { + push(@phids, $member->{phid}); + } - return [] if !@phids; + return [] if !@phids; - return Bugzilla::Extension::PhabBugz::User->match( - { - phids => \@phids - } - ); + return Bugzilla::Extension::PhabBugz::User->match({phids => \@phids}); } -1; \ No newline at end of file +1; diff --git a/extensions/PhabBugz/lib/Revision.pm b/extensions/PhabBugz/lib/Revision.pm index 6ad906829..d529c581d 100644 --- a/extensions/PhabBugz/lib/Revision.pm +++ b/extensions/PhabBugz/lib/Revision.pm @@ -27,100 +27,93 @@ use Bugzilla::Extension::PhabBugz::Util qw(request); # Initialization # ######################### -has id => ( is => 'ro', isa => Int ); -has phid => ( is => 'ro', isa => Str ); -has title => ( is => 'ro', isa => Str ); -has summary => ( is => 'ro', isa => Str ); -has status => ( is => 'ro', isa => Str ); -has creation_ts => ( is => 'ro', isa => Str ); -has modification_ts => ( is => 'ro', isa => Str ); -has author_phid => ( is => 'ro', isa => Str ); -has bug_id => ( is => 'ro', isa => Str ); -has view_policy => ( is => 'ro', isa => Str ); -has edit_policy => ( is => 'ro', isa => Str ); -has subscriber_count => ( is => 'ro', isa => Int ); -has bug => ( is => 'lazy', isa => Object ); -has author => ( is => 'lazy', isa => Object ); -has reviews => ( is => 'lazy', isa => ArrayRef [ Dict [ user => PhabUser, status => Str ] ] ); -has subscribers => ( is => 'lazy', isa => ArrayRef [PhabUser] ); -has projects => ( is => 'lazy', isa => ArrayRef [Project] ); +has id => (is => 'ro', isa => Int); +has phid => (is => 'ro', isa => Str); +has title => (is => 'ro', isa => Str); +has summary => (is => 'ro', isa => Str); +has status => (is => 'ro', isa => Str); +has creation_ts => (is => 'ro', isa => Str); +has modification_ts => (is => 'ro', isa => Str); +has author_phid => (is => 'ro', isa => Str); +has bug_id => (is => 'ro', isa => Str); +has view_policy => (is => 'ro', isa => Str); +has edit_policy => (is => 'ro', isa => Str); +has subscriber_count => (is => 'ro', isa => Int); +has bug => (is => 'lazy', isa => Object); +has author => (is => 'lazy', isa => Object); +has reviews => + (is => 'lazy', isa => ArrayRef [Dict [user => PhabUser, status => Str]]); +has subscribers => (is => 'lazy', isa => ArrayRef [PhabUser]); +has projects => (is => 'lazy', isa => ArrayRef [Project]); has reviewers_raw => ( - is => 'ro', - isa => ArrayRef [ - Dict [ - reviewerPHID => Str, - status => Str, - isBlocking => Bool | JSONBool, - actorPHID => Maybe [Str], - ], - ] + is => 'ro', + isa => ArrayRef [ + Dict [ + reviewerPHID => Str, + status => Str, + isBlocking => Bool | JSONBool, + actorPHID => Maybe [Str], + ], + ] ); has subscribers_raw => ( - is => 'ro', - isa => Dict [ - subscriberPHIDs => ArrayRef [Str], - subscriberCount => Int, - viewerIsSubscribed => Bool | JSONBool, - ] -); -has projects_raw => ( - is => 'ro', - isa => Dict [ - projectPHIDs => ArrayRef [Str] - ] + is => 'ro', + isa => Dict [ + subscriberPHIDs => ArrayRef [Str], + subscriberCount => Int, + viewerIsSubscribed => Bool | JSONBool, + ] ); +has projects_raw => (is => 'ro', isa => Dict [projectPHIDs => ArrayRef [Str]]); sub new_from_query { - my ( $class, $params ) = @_; - - my $data = { - queryKey => 'all', - attachments => { - projects => 1, - reviewers => 1, - subscribers => 1 - }, - constraints => $params - }; - - my $result = request( 'differential.revision.search', $data ); - if ( exists $result->{result}{data} && @{ $result->{result}{data} } ) { - $result = $result->{result}{data}[0]; - - # Some values in Phabricator for bug ids may have been saved - # white whitespace so we remove any here just in case. - $result->{fields}->{'bugzilla.bug-id'} = - $result->{fields}->{'bugzilla.bug-id'} - ? trim( $result->{fields}->{'bugzilla.bug-id'} ) - : ""; - return $class->new($result); - } - - return undef; + my ($class, $params) = @_; + + my $data = { + queryKey => 'all', + attachments => {projects => 1, reviewers => 1, subscribers => 1}, + constraints => $params + }; + + my $result = request('differential.revision.search', $data); + if (exists $result->{result}{data} && @{$result->{result}{data}}) { + $result = $result->{result}{data}[0]; + + # Some values in Phabricator for bug ids may have been saved + # white whitespace so we remove any here just in case. + $result->{fields}->{'bugzilla.bug-id'} + = $result->{fields}->{'bugzilla.bug-id'} + ? trim($result->{fields}->{'bugzilla.bug-id'}) + : ""; + return $class->new($result); + } + + return undef; } sub BUILDARGS { - my ( $class, $params ) = @_; - - $params->{title} = $params->{fields}->{title}; - $params->{summary} = $params->{fields}->{summary}; - $params->{status} = $params->{fields}->{status}->{value}; - $params->{creation_ts} = $params->{fields}->{dateCreated}; - $params->{modification_ts} = $params->{fields}->{dateModified}; - $params->{author_phid} = $params->{fields}->{authorPHID}; - $params->{bug_id} = $params->{fields}->{'bugzilla.bug-id'}; - $params->{view_policy} = $params->{fields}->{policy}->{view}; - $params->{edit_policy} = $params->{fields}->{policy}->{edit}; - $params->{reviewers_raw} = $params->{attachments}->{reviewers}->{reviewers} // []; - $params->{subscribers_raw} = $params->{attachments}->{subscribers}; - $params->{projects_raw} = $params->{attachments}->{projects}; - $params->{subscriber_count} = - $params->{attachments}->{subscribers}->{subscriberCount}; - - delete $params->{fields}; - delete $params->{attachments}; - - return $params; + my ($class, $params) = @_; + + $params->{title} = $params->{fields}->{title}; + $params->{summary} = $params->{fields}->{summary}; + $params->{status} = $params->{fields}->{status}->{value}; + $params->{creation_ts} = $params->{fields}->{dateCreated}; + $params->{modification_ts} = $params->{fields}->{dateModified}; + $params->{author_phid} = $params->{fields}->{authorPHID}; + $params->{bug_id} = $params->{fields}->{'bugzilla.bug-id'}; + $params->{view_policy} = $params->{fields}->{policy}->{view}; + $params->{edit_policy} = $params->{fields}->{policy}->{edit}; + $params->{reviewers_raw} = $params->{attachments}->{reviewers}->{reviewers} + // []; + $params->{subscribers_raw} = $params->{attachments}->{subscribers}; + $params->{projects_raw} = $params->{attachments}->{projects}; + $params->{subscriber_count} + = $params->{attachments}->{subscribers}->{subscriberCount}; + + delete $params->{fields}; + delete $params->{attachments}; + + return $params; } # { @@ -185,99 +178,71 @@ sub BUILDARGS { ######################### sub update { - my ($self) = @_; - - my $data = { - objectIdentifier => $self->phid, - transactions => [] - }; - - if ( $self->{added_comments} ) { - foreach my $comment ( @{ $self->{added_comments} } ) { - push @{ $data->{transactions} }, - { - type => 'comment', - value => $comment - }; - } - } - - if ( $self->{set_subscribers} ) { - push @{ $data->{transactions} }, - { - type => 'subscribers.set', - value => $self->{set_subscribers} - }; - } - - if ( $self->{add_subscribers} ) { - push @{ $data->{transactions} }, - { - type => 'subscribers.add', - value => $self->{add_subscribers} - }; - } + my ($self) = @_; - if ( $self->{remove_subscribers} ) { - push @{ $data->{transactions} }, - { - type => 'subscribers.remove', - value => $self->{remove_subscribers} - }; - } - - if ( $self->{set_reviewers} ) { - push @{ $data->{transactions} }, - { - type => 'reviewers.set', - value => $self->{set_reviewers} - }; - } - - if ( $self->{add_reviewers} ) { - push @{ $data->{transactions} }, - { - type => 'reviewers.add', - value => $self->{add_reviewers} - }; - } + my $data = {objectIdentifier => $self->phid, transactions => []}; - if ( $self->{remove_reviewers} ) { - push @{ $data->{transactions} }, - { - type => 'reviewers.remove', - value => $self->{remove_reviewers} - }; + if ($self->{added_comments}) { + foreach my $comment (@{$self->{added_comments}}) { + push @{$data->{transactions}}, {type => 'comment', value => $comment}; } - - if ( $self->{set_policy} ) { - foreach my $name ( "view", "edit" ) { - next unless $self->{set_policy}->{$name}; - push @{ $data->{transactions} }, - { - type => $name, - value => $self->{set_policy}->{$name} - }; - } + } + + if ($self->{set_subscribers}) { + push @{$data->{transactions}}, + {type => 'subscribers.set', value => $self->{set_subscribers}}; + } + + if ($self->{add_subscribers}) { + push @{$data->{transactions}}, + {type => 'subscribers.add', value => $self->{add_subscribers}}; + } + + if ($self->{remove_subscribers}) { + push @{$data->{transactions}}, + {type => 'subscribers.remove', value => $self->{remove_subscribers}}; + } + + if ($self->{set_reviewers}) { + push @{$data->{transactions}}, + {type => 'reviewers.set', value => $self->{set_reviewers}}; + } + + if ($self->{add_reviewers}) { + push @{$data->{transactions}}, + {type => 'reviewers.add', value => $self->{add_reviewers}}; + } + + if ($self->{remove_reviewers}) { + push @{$data->{transactions}}, + {type => 'reviewers.remove', value => $self->{remove_reviewers}}; + } + + if ($self->{set_policy}) { + foreach my $name ("view", "edit") { + next unless $self->{set_policy}->{$name}; + push @{$data->{transactions}}, + {type => $name, value => $self->{set_policy}->{$name}}; } + } - if ($self->{add_projects}) { - push(@{ $data->{transactions} }, { - type => 'projects.add', - value => $self->{add_projects} - }); - } + if ($self->{add_projects}) { + push( + @{$data->{transactions}}, + {type => 'projects.add', value => $self->{add_projects}} + ); + } - if ($self->{remove_projects}) { - push(@{ $data->{transactions} }, { - type => 'projects.remove', - value => $self->{remove_projects} - }); - } + if ($self->{remove_projects}) { + push( + @{$data->{transactions}}, + {type => 'projects.remove', value => $self->{remove_projects}} + ); + } - my $result = request( 'differential.revision.edit', $data ); + my $result = request('differential.revision.edit', $data); - return $result; + return $result; } ######################### @@ -285,80 +250,61 @@ sub update { ######################### sub _build_bug { - my ($self) = @_; - return $self->{bug} ||= - Bugzilla::Bug->new( { id => $self->bug_id, cache => 1 } ); + my ($self) = @_; + return $self->{bug} ||= Bugzilla::Bug->new({id => $self->bug_id, cache => 1}); } sub _build_author { - my ($self) = @_; - return $self->{author} if $self->{author}; - my $phab_user = Bugzilla::Extension::PhabBugz::User->new_from_query( - { - phids => [ $self->author_phid ] - } - ); - if ($phab_user) { - return $self->{author} = $phab_user; - } + my ($self) = @_; + return $self->{author} if $self->{author}; + my $phab_user + = Bugzilla::Extension::PhabBugz::User->new_from_query({ + phids => [$self->author_phid] + }); + if ($phab_user) { + return $self->{author} = $phab_user; + } } sub _build_reviews { - my ($self) = @_; + my ($self) = @_; - my %by_phid = map { $_->{reviewerPHID} => $_ } @{ $self->reviewers_raw }; - my $users = Bugzilla::Extension::PhabBugz::User->match( - { - phids => [keys %by_phid] - } - ); + my %by_phid = map { $_->{reviewerPHID} => $_ } @{$self->reviewers_raw}; + my $users + = Bugzilla::Extension::PhabBugz::User->match({phids => [keys %by_phid]}); - return [ - map { - { - user => $_, - status => $by_phid{ $_->phid }{status}, - } - } @$users - ]; + return [map { {user => $_, status => $by_phid{$_->phid}{status},} } @$users]; } sub _build_subscribers { - my ($self) = @_; + my ($self) = @_; - return $self->{subscribers} if $self->{subscribers}; - return [] unless $self->subscribers_raw->{subscriberPHIDs}; + return $self->{subscribers} if $self->{subscribers}; + return [] unless $self->subscribers_raw->{subscriberPHIDs}; - my @phids; - foreach my $phid ( @{ $self->subscribers_raw->{subscriberPHIDs} } ) { - push @phids, $phid; - } + my @phids; + foreach my $phid (@{$self->subscribers_raw->{subscriberPHIDs}}) { + push @phids, $phid; + } - my $users = Bugzilla::Extension::PhabBugz::User->match( - { - phids => \@phids - } - ); + my $users = Bugzilla::Extension::PhabBugz::User->match({phids => \@phids}); - return $self->{subscribers} = $users; + return $self->{subscribers} = $users; } sub _build_projects { - my ($self) = @_; - - return $self->{projects} if $self->{projects}; - return [] unless $self->projects_raw->{projectPHIDs}; - - my @projects; - foreach my $phid ( @{ $self->projects_raw->{projectPHIDs} } ) { - push @projects, Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - phids => [ $phid ] - } - ); - } + my ($self) = @_; - return $self->{projects} = \@projects; + return $self->{projects} if $self->{projects}; + return [] unless $self->projects_raw->{projectPHIDs}; + + my @projects; + foreach my $phid (@{$self->projects_raw->{projectPHIDs}}) { + push @projects, + Bugzilla::Extension::PhabBugz::Project->new_from_query({phids => [$phid]}); + } + + return $self->{projects} = \@projects; } ######################### @@ -366,124 +312,116 @@ sub _build_projects { ######################### sub add_comment { - my ( $self, $comment ) = @_; - $comment = trim($comment); - $self->{added_comments} ||= []; - push @{ $self->{added_comments} }, $comment; + my ($self, $comment) = @_; + $comment = trim($comment); + $self->{added_comments} ||= []; + push @{$self->{added_comments}}, $comment; } sub add_reviewer { - my ( $self, $reviewer ) = @_; - $self->{add_reviewers} ||= []; - my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer; - push @{ $self->{add_reviewers} }, $reviewer_phid; + my ($self, $reviewer) = @_; + $self->{add_reviewers} ||= []; + my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer; + push @{$self->{add_reviewers}}, $reviewer_phid; } sub remove_reviewer { - my ( $self, $reviewer ) = @_; - $self->{remove_reviewers} ||= []; - my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer; - push @{ $self->{remove_reviewers} }, $reviewer_phid; + my ($self, $reviewer) = @_; + $self->{remove_reviewers} ||= []; + my $reviewer_phid = blessed $reviewer ? $reviewer->phid : $reviewer; + push @{$self->{remove_reviewers}}, $reviewer_phid; } sub set_reviewers { - my ( $self, $reviewers ) = @_; - $self->{set_reviewers} = [ map { $_->phid } @$reviewers ]; + my ($self, $reviewers) = @_; + $self->{set_reviewers} = [map { $_->phid } @$reviewers]; } sub add_subscriber { - my ( $self, $subscriber ) = @_; - $self->{add_subscribers} ||= []; - my $subscriber_phid = - blessed $subscriber ? $subscriber->phid : $subscriber; - push @{ $self->{add_subscribers} }, $subscriber_phid; + my ($self, $subscriber) = @_; + $self->{add_subscribers} ||= []; + my $subscriber_phid = blessed $subscriber ? $subscriber->phid : $subscriber; + push @{$self->{add_subscribers}}, $subscriber_phid; } sub remove_subscriber { - my ( $self, $subscriber ) = @_; - $self->{remove_subscribers} ||= []; - my $subscriber_phid = - blessed $subscriber ? $subscriber->phid : $subscriber; - push @{ $self->{remove_subscribers} }, $subscriber_phid; + my ($self, $subscriber) = @_; + $self->{remove_subscribers} ||= []; + my $subscriber_phid = blessed $subscriber ? $subscriber->phid : $subscriber; + push @{$self->{remove_subscribers}}, $subscriber_phid; } sub set_subscribers { - my ( $self, $subscribers ) = @_; - $self->{set_subscribers} = $subscribers; + my ($self, $subscribers) = @_; + $self->{set_subscribers} = $subscribers; } sub set_policy { - my ( $self, $name, $policy ) = @_; - $self->{set_policy} ||= {}; - $self->{set_policy}->{$name} = $policy; + my ($self, $name, $policy) = @_; + $self->{set_policy} ||= {}; + $self->{set_policy}->{$name} = $policy; } sub add_project { - my ( $self, $project ) = @_; - $self->{add_projects} ||= []; - my $project_phid = blessed $project ? $project->phid : $project; - return undef unless $project_phid; - push @{ $self->{add_projects} }, $project_phid; + my ($self, $project) = @_; + $self->{add_projects} ||= []; + my $project_phid = blessed $project ? $project->phid : $project; + return undef unless $project_phid; + push @{$self->{add_projects}}, $project_phid; } sub remove_project { - my ( $self, $project ) = @_; - $self->{remove_projects} ||= []; - my $project_phid = blessed $project ? $project->phid : $project; - return undef unless $project_phid; - push @{ $self->{remove_projects} }, $project_phid; + my ($self, $project) = @_; + $self->{remove_projects} ||= []; + my $project_phid = blessed $project ? $project->phid : $project; + return undef unless $project_phid; + push @{$self->{remove_projects}}, $project_phid; } sub make_private { - my ( $self, $project_names ) = @_; - - my $secure_revision_project = - Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - name => 'secure-revision' - } - ); - - my @set_projects; - foreach my $name (@$project_names) { - my $set_project = - Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - name => $name - } - ); - push @set_projects, $set_project; - } + my ($self, $project_names) = @_; - my $new_policy = Bugzilla::Extension::PhabBugz::Policy->create(\@set_projects); - $self->set_policy('view', $new_policy->phid); - $self->set_policy('edit', $new_policy->phid); + my $secure_revision_project + = Bugzilla::Extension::PhabBugz::Project->new_from_query({ + name => 'secure-revision' + }); - foreach my $project ($secure_revision_project, @set_projects) { - $self->add_project($project->phid); - } + my @set_projects; + foreach my $name (@$project_names) { + my $set_project + = Bugzilla::Extension::PhabBugz::Project->new_from_query({name => $name}); + push @set_projects, $set_project; + } - return $self; + my $new_policy = Bugzilla::Extension::PhabBugz::Policy->create(\@set_projects); + $self->set_policy('view', $new_policy->phid); + $self->set_policy('edit', $new_policy->phid); + + foreach my $project ($secure_revision_project, @set_projects) { + $self->add_project($project->phid); + } + + return $self; } sub make_public { - my ( $self ) = @_; + my ($self) = @_; - my $editbugs = Bugzilla::Extension::PhabBugz::Project->new_from_query( - { - name => 'bmo-editbugs-team' - } - ); + my $editbugs + = Bugzilla::Extension::PhabBugz::Project->new_from_query({ + name => 'bmo-editbugs-team' + }); - $self->set_policy( 'view', 'public' ); - $self->set_policy( 'edit', ( $editbugs ? $editbugs->phid : 'users' ) ); + $self->set_policy('view', 'public'); + $self->set_policy('edit', ($editbugs ? $editbugs->phid : 'users')); - my @current_group_projects = grep { $_->name =~ /^(bmo-.*|secure-revision)$/ } @{ $self->projects }; - foreach my $project (@current_group_projects) { - $self->remove_project($project->phid); - } + my @current_group_projects + = grep { $_->name =~ /^(bmo-.*|secure-revision)$/ } @{$self->projects}; + foreach my $project (@current_group_projects) { + $self->remove_project($project->phid); + } - return $self; + return $self; } 1; diff --git a/extensions/PhabBugz/lib/Types.pm b/extensions/PhabBugz/lib/Types.pm index 493e97fbc..267b8c26a 100644 --- a/extensions/PhabBugz/lib/Types.pm +++ b/extensions/PhabBugz/lib/Types.pm @@ -11,18 +11,15 @@ use 5.10.1; use strict; use warnings; -use Type::Library - -base, - -declare => qw( Revision LinkedPhabUser PhabUser Policy Project ); +use Type::Library -base, + -declare => qw( Revision LinkedPhabUser PhabUser Policy Project ); use Type::Utils -all; use Types::Standard -all; -class_type Revision, { class => 'Bugzilla::Extension::PhabBugz::Revision' }; -class_type Policy, { class => 'Bugzilla::Extension::PhabBugz::Policy' }; -class_type Project, { class => 'Bugzilla::Extension::PhabBugz::Project' }; -class_type PhabUser, { class => 'Bugzilla::Extension::PhabBugz::User' }; -declare LinkedPhabUser, - as PhabUser, - where { is_Int($_->bugzilla_id) }; +class_type Revision, {class => 'Bugzilla::Extension::PhabBugz::Revision'}; +class_type Policy, {class => 'Bugzilla::Extension::PhabBugz::Policy'}; +class_type Project, {class => 'Bugzilla::Extension::PhabBugz::Project'}; +class_type PhabUser, {class => 'Bugzilla::Extension::PhabBugz::User'}; +declare LinkedPhabUser, as PhabUser, where { is_Int($_->bugzilla_id) }; 1; diff --git a/extensions/PhabBugz/lib/User.pm b/extensions/PhabBugz/lib/User.pm index 209425bdf..2bc2ed7dc 100644 --- a/extensions/PhabBugz/lib/User.pm +++ b/extensions/PhabBugz/lib/User.pm @@ -23,44 +23,44 @@ use Type::Params qw(compile); # Initialization # ######################### -has 'id' => ( is => 'ro', isa => Int ); -has 'type' => ( is => 'ro', isa => Str ); -has 'phid' => ( is => 'ro', isa => Str ); -has 'name' => ( is => 'ro', isa => Str ); -has 'realname' => ( is => 'ro', isa => Str ); -has 'creation_ts' => ( is => 'ro', isa => Int ); -has 'modification_ts' => ( is => 'ro', isa => Int ); -has 'roles' => ( is => 'ro', isa => ArrayRef [Str] ); -has 'view_policy' => ( is => 'ro', isa => Str ); -has 'edit_policy' => ( is => 'ro', isa => Str ); -has 'bugzilla_id' => ( is => 'ro', isa => Maybe [Int] ); -has 'bugzilla_user' => ( is => 'lazy', isa => Maybe [User] ); - -my $Invocant = class_type { class => __PACKAGE__ }; +has 'id' => (is => 'ro', isa => Int); +has 'type' => (is => 'ro', isa => Str); +has 'phid' => (is => 'ro', isa => Str); +has 'name' => (is => 'ro', isa => Str); +has 'realname' => (is => 'ro', isa => Str); +has 'creation_ts' => (is => 'ro', isa => Int); +has 'modification_ts' => (is => 'ro', isa => Int); +has 'roles' => (is => 'ro', isa => ArrayRef [Str]); +has 'view_policy' => (is => 'ro', isa => Str); +has 'edit_policy' => (is => 'ro', isa => Str); +has 'bugzilla_id' => (is => 'ro', isa => Maybe [Int]); +has 'bugzilla_user' => (is => 'lazy', isa => Maybe [User]); + +my $Invocant = class_type {class => __PACKAGE__}; sub BUILDARGS { - my ( $class, $params ) = @_; - - $params->{name} = $params->{fields}->{username}; - $params->{realname} = $params->{fields}->{realName}; - $params->{creation_ts} = $params->{fields}->{dateCreated}; - $params->{modification_ts} = $params->{fields}->{dateModified}; - $params->{roles} = $params->{fields}->{roles}; - $params->{view_policy} = $params->{fields}->{policy}->{view}; - $params->{edit_policy} = $params->{fields}->{policy}->{edit}; - - delete $params->{fields}; - - my $external_accounts = - $params->{attachments}{'external-accounts'}{'external-accounts'}; - if ($external_accounts) { - my $bug_user = first { $_->{type} eq 'bmo' } @$external_accounts; - $params->{bugzilla_id} = $bug_user->{id}; - } + my ($class, $params) = @_; + + $params->{name} = $params->{fields}->{username}; + $params->{realname} = $params->{fields}->{realName}; + $params->{creation_ts} = $params->{fields}->{dateCreated}; + $params->{modification_ts} = $params->{fields}->{dateModified}; + $params->{roles} = $params->{fields}->{roles}; + $params->{view_policy} = $params->{fields}->{policy}->{view}; + $params->{edit_policy} = $params->{fields}->{policy}->{edit}; + + delete $params->{fields}; - delete $params->{attachments}; + my $external_accounts + = $params->{attachments}{'external-accounts'}{'external-accounts'}; + if ($external_accounts) { + my $bug_user = first { $_->{type} eq 'bmo' } @$external_accounts; + $params->{bugzilla_id} = $bug_user->{id}; + } - return $params; + delete $params->{attachments}; + + return $params; } # { @@ -110,45 +110,45 @@ sub BUILDARGS { # } sub new_from_query { - my ( $class, $params ) = @_; - my $matches = $class->match($params); - return $matches->[0]; + my ($class, $params) = @_; + my $matches = $class->match($params); + return $matches->[0]; } sub match { - state $check = compile( $Invocant | ClassName, Dict[ ids => ArrayRef[Int] ] | Dict[ phids => ArrayRef[Str] ] ); - my ( $class, $params ) = $check->(@_); - - # BMO id search takes precedence if bugzilla_ids is used. - my $bugzilla_ids = delete $params->{ids}; - if ($bugzilla_ids) { - my $bugzilla_data = - $class->get_phab_bugzilla_ids( { ids => $bugzilla_ids } ); - $params->{phids} = [ map { $_->{phid} } @$bugzilla_data ]; + state $check = compile($Invocant | ClassName, + Dict [ids => ArrayRef [Int]] | Dict [phids => ArrayRef [Str]]); + my ($class, $params) = $check->(@_); + + # BMO id search takes precedence if bugzilla_ids is used. + my $bugzilla_ids = delete $params->{ids}; + if ($bugzilla_ids) { + my $bugzilla_data = $class->get_phab_bugzilla_ids({ids => $bugzilla_ids}); + $params->{phids} = [map { $_->{phid} } @$bugzilla_data]; + } + + return [] if !@{$params->{phids}}; + + # Look for BMO external user id in external-accounts attachment + my $data = { + constraints => {phids => $params->{phids}}, + attachments => {'external-accounts' => 1} + }; + + # We can only fetch 100 users at a time so we need to do this in lumps + my $phab_users = []; + my $result; + do { + $result = request('user.search', $data)->{result}; + if (exists $result->{data} && @{$result->{data}}) { + foreach my $user (@{$result->{data}}) { + push @$phab_users, $class->new($user); + } } + $data->{after} = $result->{cursor}->{after}; + } while ($result->{cursor}->{after}); - return [] if !@{ $params->{phids} }; - - # Look for BMO external user id in external-accounts attachment - my $data = { - constraints => { phids => $params->{phids} }, - attachments => { 'external-accounts' => 1 } - }; - - # We can only fetch 100 users at a time so we need to do this in lumps - my $phab_users = []; - my $result; - do { - $result = request( 'user.search', $data )->{result}; - if ( exists $result->{data} && @{ $result->{data} } ) { - foreach my $user ( @{ $result->{data} } ) { - push @$phab_users, $class->new($user); - } - } - $data->{after} = $result->{cursor}->{after}; - } while ($result->{cursor}->{after}); - - return $phab_users; + return $phab_users; } ################# @@ -156,48 +156,44 @@ sub match { ################# sub _build_bugzilla_user { - my ($self) = @_; - return undef unless $self->bugzilla_id; - return Bugzilla::User->new( { id => $self->bugzilla_id, cache => 1 } ); + my ($self) = @_; + return undef unless $self->bugzilla_id; + return Bugzilla::User->new({id => $self->bugzilla_id, cache => 1}); } sub get_phab_bugzilla_ids { - state $check = compile($Invocant | ClassName, Dict[ids => ArrayRef[Int]]); - my ( $class, $params ) = $check->(@_); - - my $memcache = Bugzilla->memcached; - - # Try to find the values in memcache first - my @results; - my %bugzilla_ids = map { $_ => 1 } @{ $params->{ids} }; - foreach my $bugzilla_id ( keys %bugzilla_ids ) { - my $phid = - $memcache->get( { key => "phab_user_bugzilla_id_" . $bugzilla_id } ); - if ($phid) { - push @results, { id => $bugzilla_id, phid => $phid }; - delete $bugzilla_ids{$bugzilla_id}; - } + state $check = compile($Invocant | ClassName, Dict [ids => ArrayRef [Int]]); + my ($class, $params) = $check->(@_); + + my $memcache = Bugzilla->memcached; + + # Try to find the values in memcache first + my @results; + my %bugzilla_ids = map { $_ => 1 } @{$params->{ids}}; + foreach my $bugzilla_id (keys %bugzilla_ids) { + my $phid = $memcache->get({key => "phab_user_bugzilla_id_" . $bugzilla_id}); + if ($phid) { + push @results, {id => $bugzilla_id, phid => $phid}; + delete $bugzilla_ids{$bugzilla_id}; } + } + + if (%bugzilla_ids) { + $params->{ids} = [keys %bugzilla_ids]; + + my $result = request('bugzilla.account.search', $params); - if (%bugzilla_ids) { - $params->{ids} = [ keys %bugzilla_ids ]; - - my $result = request( 'bugzilla.account.search', $params ); - - # Store new values in memcache for later retrieval - foreach my $user ( @{ $result->{result} } ) { - next if !$user->{phid}; - $memcache->set( - { - key => "phab_user_bugzilla_id_" . $user->{id}, - value => $user->{phid} - } - ); - push @results, $user; - } + # Store new values in memcache for later retrieval + foreach my $user (@{$result->{result}}) { + next if !$user->{phid}; + $memcache->set({ + key => "phab_user_bugzilla_id_" . $user->{id}, value => $user->{phid} + }); + push @results, $user; } + } - return \@results; + return \@results; } 1; diff --git a/extensions/PhabBugz/lib/Util.pm b/extensions/PhabBugz/lib/Util.pm index 32f860413..613fd3466 100644 --- a/extensions/PhabBugz/lib/Util.pm +++ b/extensions/PhabBugz/lib/Util.pm @@ -32,167 +32,166 @@ use Mojo::JSON qw(encode_json); use base qw(Exporter); our @EXPORT = qw( - create_revision_attachment - get_attachment_revisions - get_bug_role_phids - intersect - is_attachment_phab_revision - request - set_phab_user + create_revision_attachment + get_attachment_revisions + get_bug_role_phids + intersect + is_attachment_phab_revision + request + set_phab_user ); sub create_revision_attachment { - state $check = compile(Bug, Revision, Str, User); - my ( $bug, $revision, $timestamp, $submitter ) = $check->(@_); - - my $phab_base_uri = Bugzilla->params->{phabricator_base_uri}; - ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri; - - my $revision_uri = $phab_base_uri . "D" . $revision->id; - - # Check for previous attachment with same revision id. - # If one matches then return it instead. This is fine as - # BMO does not contain actual diff content. - my @review_attachments = grep { is_attachment_phab_revision($_) } @{ $bug->attachments }; - my $review_attachment = first { trim($_->data) eq $revision_uri } @review_attachments; - return $review_attachment if defined $review_attachment; - - # No attachment is present, so we can now create new one - - if (!$timestamp) { - ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - } - - # If submitter, then switch to that user when creating attachment - local $submitter->{groups} = [ Bugzilla::Group->get_all ]; # We need to always be able to add attachment - my $restore_prev_user = Bugzilla->set_user($submitter, scope_guard => 1); - - my $attachment = Bugzilla::Attachment->create( - { - bug => $bug, - creation_ts => $timestamp, - data => $revision_uri, - description => $revision->title, - filename => 'phabricator-D' . $revision->id . '-url.txt', - ispatch => 0, - isprivate => 0, - mimetype => PHAB_CONTENT_TYPE, - } - ); - - # Insert a comment about the new attachment into the database. - $bug->add_comment($revision->summary, { type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - - delete $bug->{attachments}; - - return $attachment; + state $check = compile(Bug, Revision, Str, User); + my ($bug, $revision, $timestamp, $submitter) = $check->(@_); + + my $phab_base_uri = Bugzilla->params->{phabricator_base_uri}; + ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri; + + my $revision_uri = $phab_base_uri . "D" . $revision->id; + + # Check for previous attachment with same revision id. + # If one matches then return it instead. This is fine as + # BMO does not contain actual diff content. + my @review_attachments + = grep { is_attachment_phab_revision($_) } @{$bug->attachments}; + my $review_attachment + = first { trim($_->data) eq $revision_uri } @review_attachments; + return $review_attachment if defined $review_attachment; + + # No attachment is present, so we can now create new one + + if (!$timestamp) { + ($timestamp) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); + } + + # If submitter, then switch to that user when creating attachment + local $submitter->{groups} = [Bugzilla::Group->get_all]; # We need to always be able to add attachment + my $restore_prev_user = Bugzilla->set_user($submitter, scope_guard => 1); + + my $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $revision_uri, + description => $revision->title, + filename => 'phabricator-D' . $revision->id . '-url.txt', + ispatch => 0, + isprivate => 0, + mimetype => PHAB_CONTENT_TYPE, + }); + + # Insert a comment about the new attachment into the database. + $bug->add_comment($revision->summary, + {type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id}); + + delete $bug->{attachments}; + + return $attachment; } sub intersect { - my ($list1, $list2) = @_; - my %e = map { $_ => undef } @{$list1}; - return grep { exists( $e{$_} ) } @{$list2}; + my ($list1, $list2) = @_; + my %e = map { $_ => undef } @{$list1}; + return grep { exists($e{$_}) } @{$list2}; } sub get_bug_role_phids { - state $check = compile(Bug); - my ($bug) = $check->(@_); - - my @bug_users = ( $bug->reporter ); - push(@bug_users, $bug->assigned_to) - if $bug->assigned_to->email != Bugzilla->params->{'nobody_user'}; - push(@bug_users, $bug->qa_contact) if $bug->qa_contact; - push(@bug_users, @{ $bug->cc_users }) if @{ $bug->cc_users }; - - my $phab_users = - Bugzilla::Extension::PhabBugz::User->match( - { - ids => [ map { $_->id } @bug_users ] - } - ); - - return [ map { $_->phid } @{ $phab_users } ]; + state $check = compile(Bug); + my ($bug) = $check->(@_); + + my @bug_users = ($bug->reporter); + push(@bug_users, $bug->assigned_to) + if $bug->assigned_to->email != Bugzilla->params->{'nobody_user'}; + push(@bug_users, $bug->qa_contact) if $bug->qa_contact; + push(@bug_users, @{$bug->cc_users}) if @{$bug->cc_users}; + + my $phab_users + = Bugzilla::Extension::PhabBugz::User->match({ + ids => [map { $_->id } @bug_users] + }); + + return [map { $_->phid } @{$phab_users}]; } sub is_attachment_phab_revision { - state $check = compile(Attachment); - my ($attachment) = $check->(@_); - return $attachment->contenttype eq PHAB_CONTENT_TYPE; + state $check = compile(Attachment); + my ($attachment) = $check->(@_); + return $attachment->contenttype eq PHAB_CONTENT_TYPE; } sub get_attachment_revisions { - state $check = compile(Bug); - my ($bug) = $check->(@_); + state $check = compile(Bug); + my ($bug) = $check->(@_); - my @attachments = - grep { is_attachment_phab_revision($_) } @{ $bug->attachments() }; + my @attachments + = grep { is_attachment_phab_revision($_) } @{$bug->attachments()}; - return unless @attachments; + return unless @attachments; - my @revision_ids; - foreach my $attachment (@attachments) { - my ($revision_id) = - ( $attachment->filename =~ PHAB_ATTACHMENT_PATTERN ); - next if !$revision_id; - push( @revision_ids, int($revision_id) ); - } + my @revision_ids; + foreach my $attachment (@attachments) { + my ($revision_id) = ($attachment->filename =~ PHAB_ATTACHMENT_PATTERN); + next if !$revision_id; + push(@revision_ids, int($revision_id)); + } - return unless @revision_ids; + return unless @revision_ids; - my @revisions; - foreach my $revision_id (@revision_ids) { - my $revision = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ - ids => [ $revision_id ] - }); - push @revisions, $revision if $revision; - } + my @revisions; + foreach my $revision_id (@revision_ids) { + my $revision + = Bugzilla::Extension::PhabBugz::Revision->new_from_query({ + ids => [$revision_id] + }); + push @revisions, $revision if $revision; + } - return \@revisions; + return \@revisions; } sub request { - state $check = compile(Str, HashRef); - my ($method, $data) = $check->(@_); - my $request_cache = Bugzilla->request_cache; - my $params = Bugzilla->params; - - my $ua = $request_cache->{phabricator_ua}; - unless ($ua) { - $ua = $request_cache->{phabricator_ua} = Mojo::UserAgent->new; - if ($params->{proxy_url}) { - $ua->proxy($params->{proxy_url}); - } + state $check = compile(Str, HashRef); + my ($method, $data) = $check->(@_); + my $request_cache = Bugzilla->request_cache; + my $params = Bugzilla->params; + + my $ua = $request_cache->{phabricator_ua}; + unless ($ua) { + $ua = $request_cache->{phabricator_ua} = Mojo::UserAgent->new; + if ($params->{proxy_url}) { + $ua->proxy($params->{proxy_url}); } + } - my $phab_api_key = $params->{phabricator_api_key}; - ThrowUserError('invalid_phabricator_api_key') unless $phab_api_key; - my $phab_base_uri = $params->{phabricator_base_uri}; - ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri; + my $phab_api_key = $params->{phabricator_api_key}; + ThrowUserError('invalid_phabricator_api_key') unless $phab_api_key; + my $phab_base_uri = $params->{phabricator_base_uri}; + ThrowUserError('invalid_phabricator_uri') unless $phab_base_uri; - my $full_uri = $phab_base_uri . '/api/' . $method; + my $full_uri = $phab_base_uri . '/api/' . $method; - $data->{__conduit__} = { token => $phab_api_key }; + $data->{__conduit__} = {token => $phab_api_key}; - my $response = $ua->post($full_uri => form => { params => encode_json($data) })->result; - ThrowCodeError('phabricator_api_error', { reason => $response->message }) - if $response->is_error; + my $response + = $ua->post($full_uri => form => {params => encode_json($data)})->result; + ThrowCodeError('phabricator_api_error', {reason => $response->message}) + if $response->is_error; - my $result = $response->json; - ThrowCodeError('phabricator_api_error', - { reason => 'JSON decode failure' }) if !defined($result); - ThrowCodeError('phabricator_api_error', - { code => $result->{error_code}, - reason => $result->{error_info} }) if $result->{error_code}; + my $result = $response->json; + ThrowCodeError('phabricator_api_error', {reason => 'JSON decode failure'}) + if !defined($result); + ThrowCodeError('phabricator_api_error', + {code => $result->{error_code}, reason => $result->{error_info}}) + if $result->{error_code}; - return $result; + return $result; } sub set_phab_user { - my $user = Bugzilla::User->new( { name => PHAB_AUTOMATION_USER } ); - $user->{groups} = [ Bugzilla::Group->get_all ]; + my $user = Bugzilla::User->new({name => PHAB_AUTOMATION_USER}); + $user->{groups} = [Bugzilla::Group->get_all]; - return Bugzilla->set_user($user, scope_guard => 1); + return Bugzilla->set_user($user, scope_guard => 1); } 1; diff --git a/extensions/PhabBugz/lib/WebService.pm b/extensions/PhabBugz/lib/WebService.pm index 19a758a70..a9115263a 100644 --- a/extensions/PhabBugz/lib/WebService.pm +++ b/extensions/PhabBugz/lib/WebService.pm @@ -26,143 +26,140 @@ use List::MoreUtils qw(any); use MIME::Base64 qw(decode_base64); use constant READ_ONLY => qw( - check_user_enter_bug_permission - check_user_permission_for_bug + check_user_enter_bug_permission + check_user_permission_for_bug ); use constant PUBLIC_METHODS => qw( - check_user_enter_bug_permission - check_user_permission_for_bug - set_build_target + check_user_enter_bug_permission + check_user_permission_for_bug + set_build_target ); sub _check_phabricator { - # Ensure PhabBugz is on - ThrowUserError('phabricator_not_enabled') - unless Bugzilla->params->{phabricator_enabled}; + + # Ensure PhabBugz is on + ThrowUserError('phabricator_not_enabled') + unless Bugzilla->params->{phabricator_enabled}; } sub _validate_phab_user { - my ($self, $user) = @_; + my ($self, $user) = @_; - $self->_check_phabricator(); + $self->_check_phabricator(); - # Validate that the requesting user's email matches phab-bot - ThrowUserError('phabricator_unauthorized_user') - unless $user->login eq PHAB_AUTOMATION_USER; + # Validate that the requesting user's email matches phab-bot + ThrowUserError('phabricator_unauthorized_user') + unless $user->login eq PHAB_AUTOMATION_USER; } sub check_user_permission_for_bug { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $self->_validate_phab_user($user); + $self->_validate_phab_user($user); - # Validate that a bug id and user id are provided - ThrowUserError('phabricator_invalid_request_params') - unless ($params->{bug_id} && $params->{user_id}); + # Validate that a bug id and user id are provided + ThrowUserError('phabricator_invalid_request_params') + unless ($params->{bug_id} && $params->{user_id}); - # Validate that the user exists - my $target_user = Bugzilla::User->check({ id => $params->{user_id}, cache => 1 }); + # Validate that the user exists + my $target_user = Bugzilla::User->check({id => $params->{user_id}, cache => 1}); - # Send back an object which says { "result": 1|0 } - return { - result => $target_user->can_see_bug($params->{bug_id}) - }; + # Send back an object which says { "result": 1|0 } + return {result => $target_user->can_see_bug($params->{bug_id})}; } sub check_user_enter_bug_permission { - my ($self, $params) = @_; + my ($self, $params) = @_; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $self->_validate_phab_user($user); + $self->_validate_phab_user($user); - # Validate that a product name and user id are provided - ThrowUserError('phabricator_invalid_request_params') - unless ($params->{product} && $params->{user_id}); + # Validate that a product name and user id are provided + ThrowUserError('phabricator_invalid_request_params') + unless ($params->{product} && $params->{user_id}); - # Validate that the user exists - my $target_user = Bugzilla::User->check({ id => $params->{user_id}, cache => 1 }); + # Validate that the user exists + my $target_user = Bugzilla::User->check({id => $params->{user_id}, cache => 1}); - # Send back an object with the attribute "result" set to 1 if the user - # can enter bugs into the given product, or 0 if not. - return { - result => $target_user->can_enter_product($params->{product}) ? 1 : 0 - }; + # Send back an object with the attribute "result" set to 1 if the user + # can enter bugs into the given product, or 0 if not. + return {result => $target_user->can_enter_product($params->{product}) ? 1 : 0}; } sub set_build_target { - my ( $self, $params ) = @_; + my ($self, $params) = @_; - # Phabricator only supports sending credentials via HTTP Basic Auth - # so we exploit that function to pass in an API key as the password - # of basic auth. BMO does not support basic auth but does support - # use of API keys. - my $http_auth = Bugzilla->cgi->http('Authorization'); - $http_auth =~ s/^Basic\s+//; - $http_auth = decode_base64($http_auth); - my ($login, $api_key) = split(':', $http_auth); - $params->{'Bugzilla_login'} = $login; - $params->{'Bugzilla_api_key'} = $api_key; + # Phabricator only supports sending credentials via HTTP Basic Auth + # so we exploit that function to pass in an API key as the password + # of basic auth. BMO does not support basic auth but does support + # use of API keys. + my $http_auth = Bugzilla->cgi->http('Authorization'); + $http_auth =~ s/^Basic\s+//; + $http_auth = decode_base64($http_auth); + my ($login, $api_key) = split(':', $http_auth); + $params->{'Bugzilla_login'} = $login; + $params->{'Bugzilla_api_key'} = $api_key; - my $user = Bugzilla->login(LOGIN_REQUIRED); + my $user = Bugzilla->login(LOGIN_REQUIRED); - $self->_validate_phab_user($user); + $self->_validate_phab_user($user); - my $revision_id = $params->{revision_id}; - my $build_target = $params->{build_target}; + my $revision_id = $params->{revision_id}; + my $build_target = $params->{build_target}; - ThrowUserError('invalid_phabricator_revision_id') - unless detaint_natural($revision_id); + ThrowUserError('invalid_phabricator_revision_id') + unless detaint_natural($revision_id); - ThrowUserError('invalid_phabricator_build_target') - unless $build_target =~ /^PHID-HMBT-[a-zA-Z0-9]+$/; - trick_taint($build_target); + ThrowUserError('invalid_phabricator_build_target') + unless $build_target =~ /^PHID-HMBT-[a-zA-Z0-9]+$/; + trick_taint($build_target); - Bugzilla->dbh->do( - "INSERT INTO phabbugz (name, value) VALUES (?, ?)", - undef, - 'build_target_' . $revision_id, - $build_target - ); + Bugzilla->dbh->do( + "INSERT INTO phabbugz (name, value) VALUES (?, ?)", + undef, 'build_target_' . $revision_id, + $build_target + ); - return { result => 1 }; + return {result => 1}; } sub rest_resources { - return [ - # Set build target in Phabricator - qr{^/phabbugz/build_target/(\d+)/(PHID-HMBT-.*)$}, { - POST => { - method => 'set_build_target', - params => sub { - return { - revision_id => $_[0], - build_target => $_[1] - }; - } - } - }, - # Bug permission checks - qr{^/phabbugz/check_bug/(\d+)/(\d+)$}, { - GET => { - method => 'check_user_permission_for_bug', - params => sub { - return { bug_id => $_[0], user_id => $_[1] }; - } - } - }, - qr{^/phabbugz/check_enter_bug/([^/]+)/(\d+)$}, { - GET => { - method => 'check_user_enter_bug_permission', - params => sub { - return { product => $_[0], user_id => $_[1] }; - }, - }, + return [ + # Set build target in Phabricator + qr{^/phabbugz/build_target/(\d+)/(PHID-HMBT-.*)$}, + { + POST => { + method => 'set_build_target', + params => sub { + return {revision_id => $_[0], build_target => $_[1]}; + } + } + }, + + # Bug permission checks + qr{^/phabbugz/check_bug/(\d+)/(\d+)$}, + { + GET => { + method => 'check_user_permission_for_bug', + params => sub { + return {bug_id => $_[0], user_id => $_[1]}; + } + } + }, + qr{^/phabbugz/check_enter_bug/([^/]+)/(\d+)$}, + { + GET => { + method => 'check_user_enter_bug_permission', + params => sub { + return {product => $_[0], user_id => $_[1]}; }, - ]; + }, + }, + ]; } 1; diff --git a/extensions/PhabBugz/t/basic.t b/extensions/PhabBugz/t/basic.t index af92dc64f..d0083c275 100644 --- a/extensions/PhabBugz/t/basic.t +++ b/extensions/PhabBugz/t/basic.t @@ -11,7 +11,7 @@ use 5.10.1; use lib qw( . lib local/lib/perl5 ); use Bugzilla; -BEGIN { Bugzilla->extensions }; +BEGIN { Bugzilla->extensions } use Test::More; use Test2::Tools::Mock; @@ -30,68 +30,65 @@ our @project_members; my $User = mock 'Bugzilla::Extension::PhabBugz::User' => ( - add_constructor => [ - 'fake_new' => 'hash', - ], - override => [ - 'match' => sub { [ mock() ] }, - ], + add_constructor => ['fake_new' => 'hash',], + override => ['match' => sub { [mock()] },], ); my $Feed = mock 'Bugzilla::Extension::PhabBugz::Feed' => ( - override => [ - get_group_members => sub { - return [ map { Bugzilla::Extension::PhabBugz::User->fake_new(%$_) } @group_members ]; - } - ] + override => [ + get_group_members => sub { + return [map { Bugzilla::Extension::PhabBugz::User->fake_new(%$_) } + @group_members]; + } + ] ); my $Project = mock 'Bugzilla::Extension::PhabBugz::Project' => ( - override_constructor => [ - new_from_query => 'ref_copy', - ], - override => [ - 'members' => sub { - return [ map { Bugzilla::Extension::PhabBugz::User->fake_new(%$_) } @project_members ]; - } - ] + override_constructor => [new_from_query => 'ref_copy',], + override => [ + 'members' => sub { + return [map { Bugzilla::Extension::PhabBugz::User->fake_new(%$_) } + @project_members]; + } + ] ); -local Bugzilla->params->{phabricator_enabled} = 1; -local Bugzilla->params->{phabricator_api_key} = 'FAKE-API-KEY'; +local Bugzilla->params->{phabricator_enabled} = 1; +local Bugzilla->params->{phabricator_api_key} = 'FAKE-API-KEY'; local Bugzilla->params->{phabricator_base_uri} = 'http://fake.fabricator.tld'; my $Bugzilla = mock 'Bugzilla' => ( - override => [ - 'dbh' => sub { mock() }, - 'user' => sub { Bugzilla::User->new({ name => 'phab-bot@bmo.tld' }) }, - ], + override => [ + 'dbh' => sub { mock() }, + 'user' => sub { Bugzilla::User->new({name => 'phab-bot@bmo.tld'}) }, + ], ); my $BugzillaGroup = mock 'Bugzilla::Group' => ( - add_constructor => [ - 'fake_new' => 'hash', - ], - override => [ - 'match' => sub { [ Bugzilla::Group->fake_new(id => 1, name => 'firefox-security' ) ] }, - ], + add_constructor => ['fake_new' => 'hash',], + override => [ + 'match' => + sub { [Bugzilla::Group->fake_new(id => 1, name => 'firefox-security')] }, + ], ); my $BugzillaUser = mock 'Bugzilla::User' => ( - add_constructor => [ - 'fake_new' => 'hash', - ], - override => [ - 'new' => sub { - my ($class, $hash) = @_; - if ($hash->{name} eq 'phab-bot@bmo.tld') { - return $class->fake_new( id => 8_675_309, login_name => 'phab-bot@bmo.tld', realname => 'Fake PhabBot' ); - } - else { - } - }, - 'match' => sub { [ mock() ] }, - ], + add_constructor => ['fake_new' => 'hash',], + override => [ + 'new' => sub { + my ($class, $hash) = @_; + if ($hash->{name} eq 'phab-bot@bmo.tld') { + return $class->fake_new( + id => 8_675_309, + login_name => 'phab-bot@bmo.tld', + realname => 'Fake PhabBot' + ); + } + else { + } + }, + 'match' => sub { [mock()] }, + ], ); @@ -99,78 +96,66 @@ my $feed = Bugzilla::Extension::PhabBugz::Feed->new; # Same members in both do { - my $UserAgent = mock 'Mojo::UserAgent' => ( - override => [ - 'post' => sub { - my ($self, $url, undef, $params) = @_; - my $data = decode_json($params->{params}); - is_deeply($data->{transactions}, [], 'no-op'); - return mock_useragent_tx('{}'); - }, - ], - ); - local @group_members = ( - { phid => 'foo' }, - ); - local @project_members = ( - { phid => 'foo' }, - ); - $feed->group_query; + my $UserAgent = mock 'Mojo::UserAgent' => ( + override => [ + 'post' => sub { + my ($self, $url, undef, $params) = @_; + my $data = decode_json($params->{params}); + is_deeply($data->{transactions}, [], 'no-op'); + return mock_useragent_tx('{}'); + }, + ], + ); + local @group_members = ({phid => 'foo'},); + local @project_members = ({phid => 'foo'},); + $feed->group_query; }; # Project has members not in group do { - my $UserAgent = mock 'Mojo::UserAgent' => ( - override => [ - 'post' => sub { - my ($self, $url, undef, $params) = @_; - my $data = decode_json($params->{params}); - my $expected = [ { type => 'members.remove', value => ['foo'] } ]; - is_deeply($data->{transactions}, $expected, 'remove foo'); - return mock_useragent_tx('{}'); - }, - ] - ); - local @group_members = (); - local @project_members = ( - { phid => 'foo' }, - ); - $feed->group_query; + my $UserAgent = mock 'Mojo::UserAgent' => ( + override => [ + 'post' => sub { + my ($self, $url, undef, $params) = @_; + my $data = decode_json($params->{params}); + my $expected = [{type => 'members.remove', value => ['foo']}]; + is_deeply($data->{transactions}, $expected, 'remove foo'); + return mock_useragent_tx('{}'); + }, + ] + ); + local @group_members = (); + local @project_members = ({phid => 'foo'},); + $feed->group_query; }; # Group has members not in project do { - my $UserAgent = mock 'Mojo::UserAgent' => ( - override => [ - 'post' => sub { - my ($self, $url, undef, $params) = @_; - my $data = decode_json($params->{params}); - my $expected = [ { type => 'members.add', value => ['foo'] } ]; - is_deeply($data->{transactions}, $expected, 'add foo'); - return mock_useragent_tx('{}'); - }, - ] - ); - local @group_members = ( - { phid => 'foo' }, - ); - local @project_members = ( - ); - $feed->group_query; + my $UserAgent = mock 'Mojo::UserAgent' => ( + override => [ + 'post' => sub { + my ($self, $url, undef, $params) = @_; + my $data = decode_json($params->{params}); + my $expected = [{type => 'members.add', value => ['foo']}]; + is_deeply($data->{transactions}, $expected, 'add foo'); + return mock_useragent_tx('{}'); + }, + ] + ); + local @group_members = ({phid => 'foo'},); + local @project_members = (); + $feed->group_query; }; do { - my $Revision = mock 'Bugzilla::Extension::PhabBugz::Revision' => ( - override => [ - 'update' => sub { 1 }, - ], - ); - my $UserAgent = mock 'Mojo::UserAgent' => ( - override => [ - 'post' => sub { - my ($self, $url, undef, $params) = @_; - if ($url =~ /differential\.revision\.search/) { - my $content = < + (override => ['update' => sub {1},],); + my $UserAgent = mock 'Mojo::UserAgent' => ( + override => [ + 'post' => sub { + my ($self, $url, undef, $params) = @_; + if ($url =~ /differential\.revision\.search/) { + my $content = < ( - add_constructor => [ fake_new => 'hash' ], - ); - my $Bug = mock 'Bugzilla::Bug' => ( - add_constructor => [ fake_new => 'hash' ], - ); - my $bug = Bugzilla::Bug->fake_new( - bug_id => 23, - attachments => [ - Bugzilla::Attachment->fake_new( - mimetype => 'text/x-phabricator-request', - filename => 'phabricator-D9999-url.txt', - ), - ] - ); + return mock_useragent_tx($content); + } + else { + return mock_useragent_tx("bad request"); + } + }, + ], + ); + my $Attachment + = mock 'Bugzilla::Attachment' => (add_constructor => [fake_new => 'hash'],); + my $Bug = mock 'Bugzilla::Bug' => (add_constructor => [fake_new => 'hash'],); + my $bug = Bugzilla::Bug->fake_new( + bug_id => 23, + attachments => [ + Bugzilla::Attachment->fake_new( + mimetype => 'text/x-phabricator-request', + filename => 'phabricator-D9999-url.txt', + ), + ] + ); - my $revisions = get_attachment_revisions($bug); - is(ref($revisions), 'ARRAY', 'it is an array ref'); - isa_ok($revisions->[0], 'Bugzilla::Extension::PhabBugz::Revision'); - is($revisions->[0]->bug_id, 23, 'Bugzila ID is 23'); - ok( try { $revisions->[0]->update }, 'update revision'); + my $revisions = get_attachment_revisions($bug); + is(ref($revisions), 'ARRAY', 'it is an array ref'); + isa_ok($revisions->[0], 'Bugzilla::Extension::PhabBugz::Revision'); + is($revisions->[0]->bug_id, 23, 'Bugzila ID is 23'); + ok(try { $revisions->[0]->update }, 'update revision'); }; diff --git a/extensions/PhabBugz/t/feed-daemon-guts.t b/extensions/PhabBugz/t/feed-daemon-guts.t index 0c508be98..44d65eab4 100644 --- a/extensions/PhabBugz/t/feed-daemon-guts.t +++ b/extensions/PhabBugz/t/feed-daemon-guts.t @@ -24,8 +24,11 @@ use Digest::SHA qw(sha1_hex); use ok 'Bugzilla::Extension::PhabBugz::Feed'; use ok 'Bugzilla::Extension::PhabBugz::Constants', 'PHAB_AUTOMATION_USER'; -use ok 'Bugzilla::Config', 'SetParam'; -can_ok('Bugzilla::Extension::PhabBugz::Feed', qw( group_query feed_query user_query )); +use ok 'Bugzilla::Config', 'SetParam'; +can_ok( + 'Bugzilla::Extension::PhabBugz::Feed', + qw( group_query feed_query user_query ) +); Bugzilla->error_mode(ERROR_MODE_TEST); @@ -34,127 +37,117 @@ my $phab_bot = create_user(PHAB_AUTOMATION_USER, '*'); my $UserAgent = mock 'Mojo::UserAgent' => (); { - SetParam('phabricator_enabled', 0); - my $feed = Bugzilla::Extension::PhabBugz::Feed->new; - my $Feed = mock 'Bugzilla::Extension::PhabBugz::Feed' => ( - override => [ - get_last_id => sub { die "get_last_id" }, - ], - ); - - foreach my $method (qw( feed_query user_query group_query )) { - try { - $feed->$method; - pass "disabling the phabricator sync: $method"; - } - catch { - fail "disabling the phabricator sync: $method"; - } + SetParam('phabricator_enabled', 0); + my $feed = Bugzilla::Extension::PhabBugz::Feed->new; + my $Feed = mock 'Bugzilla::Extension::PhabBugz::Feed' => + (override => [get_last_id => sub { die "get_last_id" },],); + + foreach my $method (qw( feed_query user_query group_query )) { + try { + $feed->$method; + pass "disabling the phabricator sync: $method"; + } + catch { + fail "disabling the phabricator sync: $method"; } + } } my @bad_response = ( - ['http error', mock_useragent_tx("doesn't matter", sub { $_->code(500) }) ], - ['invalid json', mock_useragent_tx('foo') ], - ['json containing error code', mock_useragent_tx(encode_json({error_code => 1234 }))], + ['http error', mock_useragent_tx("doesn't matter", sub { $_->code(500) })], + ['invalid json', mock_useragent_tx('foo')], + [ + 'json containing error code', + mock_useragent_tx(encode_json({error_code => 1234})) + ], ); -SetParam(phabricator_enabled => 1); -SetParam(phabricator_api_key => 'FAKE-API-KEY'); +SetParam(phabricator_enabled => 1); +SetParam(phabricator_api_key => 'FAKE-API-KEY'); SetParam(phabricator_base_uri => 'http://fake.fabricator.tld/'); foreach my $bad_response (@bad_response) { - my $feed = Bugzilla::Extension::PhabBugz::Feed->new; - $UserAgent->override( - post => sub { - my ( $self, $url, undef, $params ) = @_; - return $bad_response->[1]; - } - ); - - foreach my $method (qw( feed_query user_query group_query )) { - try { - # This is a hack to get reasonable exception objects. - local $Bugzilla::Template::is_processing = 1; - $feed->$method; - fail "$method - $bad_response->[0]"; - } - catch { - is( $_->type, 'bugzilla.code.phabricator_api_error', "$method - $bad_response->[0]" ); - }; + my $feed = Bugzilla::Extension::PhabBugz::Feed->new; + $UserAgent->override( + post => sub { + my ($self, $url, undef, $params) = @_; + return $bad_response->[1]; } - $UserAgent->reset('post'); + ); + + foreach my $method (qw( feed_query user_query group_query )) { + try { + # This is a hack to get reasonable exception objects. + local $Bugzilla::Template::is_processing = 1; + $feed->$method; + fail "$method - $bad_response->[0]"; + } + catch { + is( + $_->type, + 'bugzilla.code.phabricator_api_error', + "$method - $bad_response->[0]" + ); + }; + } + $UserAgent->reset('post'); } -my $feed = Bugzilla::Extension::PhabBugz::Feed->new; -my $json = JSON::MaybeXS->new( canonical => 1, pretty => 1 ); -my $dylan = create_user( 'dylan@mozilla.com', '*', realname => 'Dylan Hardison :dylan' ); -my $evildylan = create_user( 'dylan@gmail.com', '*', realname => 'Evil Dylan :dylan' ); -my $myk = create_user( 'myk@mozilla.com', '*', realname => 'Myk Melez :myk' ); +my $feed = Bugzilla::Extension::PhabBugz::Feed->new; +my $json = JSON::MaybeXS->new(canonical => 1, pretty => 1); +my $dylan + = create_user('dylan@mozilla.com', '*', realname => 'Dylan Hardison :dylan'); +my $evildylan + = create_user('dylan@gmail.com', '*', realname => 'Evil Dylan :dylan'); +my $myk = create_user('myk@mozilla.com', '*', realname => 'Myk Melez :myk'); my $phab_bot_phid = next_phid('PHID-USER'); done_testing; sub user_search { - my (%conf) = @_; - - return { - error_info => undef, - error_code => undef, - result => { - cursor => { - after => $conf{after}, - order => undef, - limit => 100, - before => undef + my (%conf) = @_; + + return { + error_info => undef, + error_code => undef, + result => { + cursor => + {after => $conf{after}, order => undef, limit => 100, before => undef}, + query => {queryKey => undef}, + maps => {}, + data => [ + map { + +{ + attachments => { + $_->{bmo_id} + ? ("external-accounts" => + {"external-accounts" => [{type => 'bmo', id => $_->{bmo_id},}]}) + : (), }, - query => { - queryKey => undef + fields => { + roles => ["verified", "approved", "activated"], + realName => $_->{realname}, + dateModified => time, + policy => {view => "public", edit => "no-one"}, + dateCreated => time, + username => $_->{username}, }, - maps => {}, - data => [ - map { - +{ - attachments => { - $_->{bmo_id} - ? ( "external-accounts" => { - "external-accounts" => [ - { - type => 'bmo', - id => $_->{bmo_id}, - } - ] - } - ) - : (), - }, - fields => { - roles => [ "verified", "approved", "activated" ], - realName => $_->{realname}, - dateModified => time, - policy => { - view => "public", - edit => "no-one" - }, - dateCreated => time, - username => $_->{username}, - }, - phid => next_phid("PHID-USER"), - type => "USER", - id => $_->{phab_id}, - }, - } @{ $conf{users} }, - ] - } - }; + phid => next_phid("PHID-USER"), + type => "USER", + id => $_->{phab_id}, + }, + } @{$conf{users}}, + ] + } + }; } sub next_phid { - my ($prefix) = @_; - state $number = 'a' x 20; - return $prefix . '-' . ($number++); + my ($prefix) = @_; + state $number = 'a' x 20; + return $prefix . '-' . ($number++); } diff --git a/extensions/PhabBugz/t/review-flags.t b/extensions/PhabBugz/t/review-flags.t index 610c46dca..b23a55eec 100644 --- a/extensions/PhabBugz/t/review-flags.t +++ b/extensions/PhabBugz/t/review-flags.t @@ -15,11 +15,11 @@ use Test2::V0; our @EMAILS; BEGIN { - require Bugzilla::Mailer; - no warnings 'redefine'; - *Bugzilla::Mailer::MessageToMTA = sub { - push @EMAILS, [@_]; - }; + require Bugzilla::Mailer; + no warnings 'redefine'; + *Bugzilla::Mailer::MessageToMTA = sub { + push @EMAILS, [@_]; + }; } use Bugzilla::Test::MockDB; use Bugzilla::Test::MockParams; @@ -35,138 +35,121 @@ use Data::Dumper; use ok 'Bugzilla::Extension::PhabBugz::Feed'; use ok 'Bugzilla::Extension::PhabBugz::Constants', 'PHAB_AUTOMATION_USER'; -use ok 'Bugzilla::Config', 'SetParam'; -can_ok('Bugzilla::Extension::PhabBugz::Feed', qw( group_query feed_query user_query )); +use ok 'Bugzilla::Config', 'SetParam'; +can_ok( + 'Bugzilla::Extension::PhabBugz::Feed', + qw( group_query feed_query user_query ) +); SetParam(phabricator_base_uri => 'http://fake.phabricator.tld/'); -SetParam(mailfrom => 'bugzilla-daemon'); +SetParam(mailfrom => 'bugzilla-daemon'); Bugzilla->error_mode(ERROR_MODE_TEST); -my $nobody = create_user('nobody@mozilla.org', '*'); +my $nobody = create_user('nobody@mozilla.org', '*'); my $phab_bot = create_user(PHAB_AUTOMATION_USER, '*'); # Steve Rogers is the revision author -my $steve = create_user('steverogers@avengers.org', '*', realname => 'Steve Rogers :steve'); +my $steve = create_user('steverogers@avengers.org', '*', + realname => 'Steve Rogers :steve'); # Bucky Barns is the reviewer -my $bucky = create_user('bucky@avengers.org', '*', realname => 'Bucky Barns :bucky'); +my $bucky + = create_user('bucky@avengers.org', '*', realname => 'Bucky Barns :bucky'); my $firefox = Bugzilla::Product->create( - { - name => 'Firefox', - description => 'Fake firefox product', - version => 'Unspecified', - }, + { + name => 'Firefox', + description => 'Fake firefox product', + version => 'Unspecified', + }, ); -my $general = Bugzilla::Component->create( - { - product =>$firefox, - name => 'General', - description => 'The most general description', - initialowner => { id => $nobody->id }, - } -); +my $general = Bugzilla::Component->create({ + product => $firefox, + name => 'General', + description => 'The most general description', + initialowner => {id => $nobody->id}, +}); Bugzilla->set_user($steve); -my $bug = Bugzilla::Bug->create( - { - short_desc => 'test bug', - product => $firefox, - component => $general->name, - bug_severity => 'normal', - op_sys => 'Unspecified', - rep_platform => 'Unspecified', - version => 'Unspecified', - comment => 'first post', - priority => 'P1', - } -); - -my $recipients = { changer => $steve }; +my $bug = Bugzilla::Bug->create({ + short_desc => 'test bug', + product => $firefox, + component => $general->name, + bug_severity => 'normal', + op_sys => 'Unspecified', + rep_platform => 'Unspecified', + version => 'Unspecified', + comment => 'first post', + priority => 'P1', +}); + +my $recipients = {changer => $steve}; Bugzilla::BugMail::Send($bug->bug_id, $recipients); @EMAILS = (); -my $revision = Bugzilla::Extension::PhabBugz::Revision->new( - { - id => 1, - phid => 'PHID-DREV-uozm3ggfp7e7uoqegmc3', - type => 'DREV', - fields => { - title => "title", - summary => "the summary of the revision", - status => { value => "not sure" }, - dateCreated => time() - (60 * 60), - dateModified => time() - (60 * 5), - authorPHID => 'authorPHID', - policy => { - view => 'policy.view', - edit => 'policy.edit', - }, - 'bugzilla.bug-id' => $bug->id, - }, - attachments => { - projects => { projectPHIDs => [] }, - reviewers => { - reviewers => [ ], - }, - subscribers => { - subscriberPHIDs => [], - subscriberCount => 1, - viewerIsSubscribed => 1, - } - }, - reviews => [ - { - user => new_phab_user($bucky), - status => 'accepted', - } - ] - } -); -my $PhabRevisionMock = mock 'Bugzilla::Extension::PhabBugz::Revision' => ( - override => [ - make_public => sub { }, - update => sub { }, - ] -); +my $revision = Bugzilla::Extension::PhabBugz::Revision->new({ + id => 1, + phid => 'PHID-DREV-uozm3ggfp7e7uoqegmc3', + type => 'DREV', + fields => { + title => "title", + summary => "the summary of the revision", + status => {value => "not sure"}, + dateCreated => time() - (60 * 60), + dateModified => time() - (60 * 5), + authorPHID => 'authorPHID', + policy => {view => 'policy.view', edit => 'policy.edit',}, + 'bugzilla.bug-id' => $bug->id, + }, + attachments => { + projects => {projectPHIDs => []}, + reviewers => {reviewers => [],}, + subscribers => + {subscriberPHIDs => [], subscriberCount => 1, viewerIsSubscribed => 1,} + }, + reviews => [{user => new_phab_user($bucky), status => 'accepted',}] +}); +my $PhabRevisionMock = mock 'Bugzilla::Extension::PhabBugz::Revision' => + (override => [make_public => sub { }, update => sub { },]); my $PhabUserMock = mock 'Bugzilla::Extension::PhabBugz::User' => ( - override => [ - match => sub { - my ($class, $query) = @_; - if ($query && $query->{phids} && $query->{phids}[0]) { - my $phid = $query->{phids}[0]; - if ($phid eq 'authorPHID') { - return [ new_phab_user($steve, $phid) ]; - } - } - }, - ] + override => [ + match => sub { + my ($class, $query) = @_; + if ($query && $query->{phids} && $query->{phids}[0]) { + my $phid = $query->{phids}[0]; + if ($phid eq 'authorPHID') { + return [new_phab_user($steve, $phid)]; + } + } + }, + ] ); my $feed = Bugzilla::Extension::PhabBugz::Feed->new; my $changer = new_phab_user($bucky); @EMAILS = (); -$feed->process_revision_change( - $revision, $changer, "story text" -); +$feed->process_revision_change($revision, $changer, "story text"); # The first comment, and the comment made when the attachment is attached # are made by Steve. # The review comment is made by Bucky. -my $sth = Bugzilla->dbh->prepare("select profiles.login_name, thetext from longdescs join profiles on who = userid"); +my $sth + = Bugzilla->dbh->prepare( + "select profiles.login_name, thetext from longdescs join profiles on who = userid" + ); $sth->execute; while (my $row = $sth->fetchrow_hashref) { - if ($row->{thetext} =~ /first post/i) { - is($row->{login_name}, $steve->login, 'first post author'); - } - elsif ($row->{thetext} =~ /the summary of the revision/i) { - is($row->{login_name}, $steve->login, 'the first attachment comment'); - } - elsif ($row->{thetext} =~ /has approved the revision/i) { - is($row->{login_name}, $bucky->login); - } + if ($row->{thetext} =~ /first post/i) { + is($row->{login_name}, $steve->login, 'first post author'); + } + elsif ($row->{thetext} =~ /the summary of the revision/i) { + is($row->{login_name}, $steve->login, 'the first attachment comment'); + } + elsif ($row->{thetext} =~ /has approved the revision/i) { + is($row->{login_name}, $bucky->login); + } } diag Dumper(\@EMAILS); @@ -174,36 +157,25 @@ diag Dumper(\@EMAILS); done_testing; sub new_phab_user { - my ($bug_user, $phid) = @_; - - return Bugzilla::Extension::PhabBugz::User->new( - { - id => $bug_user->id * 1000, - type => "USER", - phid => $phid // "PHID-USER-" . ( $bug_user->id * 1000 ), - fields => { - username => $bug_user->nick, - realName => $bug_user->name, - dateCreated => time() - 60 * 60 * 24, - dateModified => time(), - roles => [], - policy => { - view => 'view', - edit => 'edit', - }, - }, - attachments => { - 'external-accounts' => { - 'external-accounts' => [ - { - type => 'bmo', - id => $bug_user->id, - } - ] - } - } - } - ); + my ($bug_user, $phid) = @_; + + return Bugzilla::Extension::PhabBugz::User->new({ + id => $bug_user->id * 1000, + type => "USER", + phid => $phid // "PHID-USER-" . ($bug_user->id * 1000), + fields => { + username => $bug_user->nick, + realName => $bug_user->name, + dateCreated => time() - 60 * 60 * 24, + dateModified => time(), + roles => [], + policy => {view => 'view', edit => 'edit',}, + }, + attachments => { + 'external-accounts' => + {'external-accounts' => [{type => 'bmo', id => $bug_user->id,}]} + } + }); -} \ No newline at end of file +} diff --git a/extensions/ProdCompSearch/Config.pm b/extensions/ProdCompSearch/Config.pm index 9631de570..240530f00 100644 --- a/extensions/ProdCompSearch/Config.pm +++ b/extensions/ProdCompSearch/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'ProdCompSearch'; +use constant NAME => 'ProdCompSearch'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/ProdCompSearch/Extension.pm b/extensions/ProdCompSearch/Extension.pm index ae507a7d6..6647eb08d 100644 --- a/extensions/ProdCompSearch/Extension.pm +++ b/extensions/ProdCompSearch/Extension.pm @@ -16,9 +16,9 @@ use base qw(Bugzilla::Extension); our $VERSION = '1'; sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{PCS} = "Bugzilla::Extension::ProdCompSearch::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{PCS} = "Bugzilla::Extension::ProdCompSearch::WebService"; } diff --git a/extensions/ProdCompSearch/lib/WebService.pm b/extensions/ProdCompSearch/lib/WebService.pm index b173137dd..b47b4a402 100644 --- a/extensions/ProdCompSearch/lib/WebService.pm +++ b/extensions/ProdCompSearch/lib/WebService.pm @@ -21,20 +21,21 @@ use Bugzilla::Util qw(detaint_natural trick_taint trim); ############# use constant PUBLIC_METHODS => qw( - prod_comp_search + prod_comp_search ); sub rest_resources { - return [ - qr{^/prod_comp_search/(.*)$}, { - GET => { - method => 'prod_comp_search', - params => sub { - return { search => $_[0] } - } - } + return [ + qr{^/prod_comp_search/(.*)$}, + { + GET => { + method => 'prod_comp_search', + params => sub { + return {search => $_[0]}; } - ] + } + } + ]; } ################## @@ -42,79 +43,91 @@ sub rest_resources { ################## sub prod_comp_search { - my ($self, $params) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->switch_to_shadow_db(); - - my $search = trim($params->{'search'} || ''); - $search || ThrowCodeError('param_required', - { function => 'PCS.prod_comp_search', param => 'search' }); - - my $limit = detaint_natural($params->{'limit'}) - ? $dbh->sql_limit($params->{'limit'}) - : ''; - - # We do this in the DB directly as we want it to be fast and - # not have the overhead of loading full product objects - - # All products which the user has "Entry" access to. - my $enterable_ids = $dbh->selectcol_arrayref( - 'SELECT products.id FROM products + my ($self, $params) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->switch_to_shadow_db(); + + my $search = trim($params->{'search'} || ''); + $search + || ThrowCodeError('param_required', + {function => 'PCS.prod_comp_search', param => 'search'}); + + my $limit + = detaint_natural($params->{'limit'}) + ? $dbh->sql_limit($params->{'limit'}) + : ''; + + # We do this in the DB directly as we want it to be fast and + # not have the overhead of loading full product objects + + # All products which the user has "Entry" access to. + my $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT products.id FROM products LEFT JOIN group_control_map ON group_control_map.product_id = products.id AND group_control_map.entry != 0 AND group_id NOT IN (' . $user->groups_as_string . ') WHERE group_id IS NULL - AND products.isactive = 1'); - - if (scalar @$enterable_ids) { - # And all of these products must have at least one component - # and one version. - $enterable_ids = $dbh->selectcol_arrayref( - 'SELECT DISTINCT products.id FROM products - WHERE ' . $dbh->sql_in('products.id', $enterable_ids) . - ' AND products.id IN (SELECT DISTINCT components.product_id + AND products.isactive = 1' + ); + + if (scalar @$enterable_ids) { + + # And all of these products must have at least one component + # and one version. + $enterable_ids = $dbh->selectcol_arrayref( + 'SELECT DISTINCT products.id FROM products + WHERE ' + . $dbh->sql_in('products.id', $enterable_ids) + . ' AND products.id IN (SELECT DISTINCT components.product_id FROM components WHERE components.isactive = 1) AND products.id IN (SELECT DISTINCT versions.product_id FROM versions - WHERE versions.isactive = 1)'); - } - - return { products => [] } if !scalar @$enterable_ids; - - trick_taint($search); - my @terms; - my @order; - - if ($search =~ /^(.*?)::(.*)$/) { - my ($product, $component) = (trim($1), trim($2)); - push @terms, _build_terms($product, 1, 0); - push @terms, _build_terms($component, 0, 1); - push @order, "products.name != " . $dbh->quote($product) if $product ne ''; - push @order, "components.name != " . $dbh->quote($component) if $component ne ''; - push @order, _build_like_order($product . ' ' . $component); - push @order, "products.name"; - push @order, "components.name"; - } else { - push @terms, _build_terms($search, 1, 1); - push @order, "products.name != " . $dbh->quote($search); - push @order, "components.name != " . $dbh->quote($search); - push @order, _build_like_order($search); - push @order, "products.name"; - push @order, "components.name"; - } - return { products => [] } if !scalar @terms; - - # To help mozilla staff file bmo administration bugs into the right - # component, sort bmo first when searching for 'bugzilla' - if ($search =~ /bugzilla/i && $search !~ /^bugzilla\s*::/i - && ($user->in_group('mozilla-corporation') || $user->in_group('mozilla-foundation'))) - { - unshift @order, "products.name != 'bugzilla.mozilla.org'"; - } - - my $components = $dbh->selectall_arrayref(" + WHERE versions.isactive = 1)' + ); + } + + return {products => []} if !scalar @$enterable_ids; + + trick_taint($search); + my @terms; + my @order; + + if ($search =~ /^(.*?)::(.*)$/) { + my ($product, $component) = (trim($1), trim($2)); + push @terms, _build_terms($product, 1, 0); + push @terms, _build_terms($component, 0, 1); + push @order, "products.name != " . $dbh->quote($product) if $product ne ''; + push @order, "components.name != " . $dbh->quote($component) + if $component ne ''; + push @order, _build_like_order($product . ' ' . $component); + push @order, "products.name"; + push @order, "components.name"; + } + else { + push @terms, _build_terms($search, 1, 1); + push @order, "products.name != " . $dbh->quote($search); + push @order, "components.name != " . $dbh->quote($search); + push @order, _build_like_order($search); + push @order, "products.name"; + push @order, "components.name"; + } + return {products => []} if !scalar @terms; + + # To help mozilla staff file bmo administration bugs into the right + # component, sort bmo first when searching for 'bugzilla' + if ( + $search =~ /bugzilla/i + && $search !~ /^bugzilla\s*::/i + && ( $user->in_group('mozilla-corporation') + || $user->in_group('mozilla-foundation')) + ) + { + unshift @order, "products.name != 'bugzilla.mozilla.org'"; + } + + my $components = $dbh->selectall_arrayref(" SELECT products.name AS product, components.name AS component FROM products @@ -122,19 +135,18 @@ sub prod_comp_search { WHERE (" . join(" AND ", @terms) . ") AND products.id IN (" . join(",", @$enterable_ids) . ") AND components.isactive = 1 - ORDER BY " . join(", ", @order) . " $limit", - { Slice => {} }); - - my $products = []; - my $current_product; - foreach my $component (@$components) { - if (!$current_product || $component->{product} ne $current_product) { - $current_product = $component->{product}; - push @$products, { product => $current_product }; - } - push @$products, $component; + ORDER BY " . join(", ", @order) . " $limit", {Slice => {}}); + + my $products = []; + my $current_product; + foreach my $component (@$components) { + if (!$current_product || $component->{product} ne $current_product) { + $current_product = $component->{product}; + push @$products, {product => $current_product}; } - return { products => $products }; + push @$products, $component; + } + return {products => $products}; } ################### @@ -142,34 +154,37 @@ sub prod_comp_search { ################### sub _build_terms { - my ($query, $product, $component) = @_; - my $dbh = Bugzilla->dbh(); - - my @fields; - push @fields, 'products.name', 'products.description' if $product; - push @fields, 'components.name', 'components.description' if $component; - # note: CONCAT_WS is MySQL specific - my $field = "CONCAT_WS(' ', ". join(',', @fields) . ")"; - - my @terms; - foreach my $word (split(/[\s,]+/, $query)) { - push(@terms, $dbh->sql_iposition($dbh->quote($word), $field) . " > 0") - if $word ne ''; - } - return @terms; + my ($query, $product, $component) = @_; + my $dbh = Bugzilla->dbh(); + + my @fields; + push @fields, 'products.name', 'products.description' if $product; + push @fields, 'components.name', 'components.description' if $component; + + # note: CONCAT_WS is MySQL specific + my $field = "CONCAT_WS(' ', " . join(',', @fields) . ")"; + + my @terms; + foreach my $word (split(/[\s,]+/, $query)) { + push(@terms, $dbh->sql_iposition($dbh->quote($word), $field) . " > 0") + if $word ne ''; + } + return @terms; } sub _build_like_order { - my ($query) = @_; - my $dbh = Bugzilla->dbh; - - my @terms; - foreach my $word (split(/[\s,]+/, $query)) { - push @terms, "CONCAT(products.name, components.name) LIKE " . $dbh->quote('%' . $word . '%') - if $word ne ''; - } - - return 'NOT(' . join(' AND ', @terms) . ')'; + my ($query) = @_; + my $dbh = Bugzilla->dbh; + + my @terms; + foreach my $word (split(/[\s,]+/, $query)) { + push @terms, + "CONCAT(products.name, components.name) LIKE " + . $dbh->quote('%' . $word . '%') + if $word ne ''; + } + + return 'NOT(' . join(' AND ', @terms) . ')'; } 1; diff --git a/extensions/Profanivore/Config.pm b/extensions/Profanivore/Config.pm index 311400d16..4acaf4fa2 100644 --- a/extensions/Profanivore/Config.pm +++ b/extensions/Profanivore/Config.pm @@ -28,16 +28,8 @@ use warnings; use constant NAME => 'Profanivore'; use constant REQUIRED_MODULES => [ - { - package => 'Regexp-Common', - module => 'Regexp::Common', - version => 0 - }, - { - package => 'HTML-Tree', - module => 'HTML::Tree', - version => 0, - } + {package => 'Regexp-Common', module => 'Regexp::Common', version => 0}, + {package => 'HTML-Tree', module => 'HTML::Tree', version => 0,} ]; __PACKAGE__->NAME; diff --git a/extensions/Profanivore/Extension.pm b/extensions/Profanivore/Extension.pm index 013f92fee..4682b3d1d 100644 --- a/extensions/Profanivore/Extension.pm +++ b/extensions/Profanivore/Extension.pm @@ -35,153 +35,150 @@ use Bugzilla::Util qw(is_7bit_clean); our $VERSION = '0.01'; sub bug_format_comment { - my ($self, $args) = @_; - my $regexes = $args->{'regexes'}; - my $comment = $args->{'comment'}; - - # Censor profanities if the comment author is not reasonably trusted. - # However, allow people to see their own profanities, which might stop - # them immediately noticing and trying to go around the filter. (I.e. - # it tries to stop an arms race starting.) - if ($comment && - !$comment->author->in_group('editbugs') && - $comment->author->id != Bugzilla->user->id) - { - push (@$regexes, { - match => RE_profanity('-i'), - replace => \&_replace_profanity - }); - } + my ($self, $args) = @_; + my $regexes = $args->{'regexes'}; + my $comment = $args->{'comment'}; + + # Censor profanities if the comment author is not reasonably trusted. + # However, allow people to see their own profanities, which might stop + # them immediately noticing and trying to go around the filter. (I.e. + # it tries to stop an arms race starting.) + if ( $comment + && !$comment->author->in_group('editbugs') + && $comment->author->id != Bugzilla->user->id) + { + push(@$regexes, {match => RE_profanity('-i'), replace => \&_replace_profanity}); + } } sub _replace_profanity { - # We don't have access to the actual profanity. - return "****"; + + # We don't have access to the actual profanity. + return "****"; } sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{'email'}; - - my $author = $email->header("X-Bugzilla-Who"); - my $recipient = $email->header("To"); - - if ($author && $recipient && lc($author) ne lc($recipient)) { - my $email_suffix = Bugzilla->params->{'emailsuffix'}; - if ($email_suffix ne '') { - $recipient =~ s/\Q$email_suffix\E$//; - $author =~ s/\Q$email_suffix\E$//; - } - - $author = new Bugzilla::User({ name => $author }); - - if ($author && - $author->id && - !$author->in_group('editbugs')) - { - # Multipart emails - if (scalar $email->parts > 1) { - $email->walk_parts(sub { - my ($part) = @_; - return if $part->parts > 1; # Top-level - # do not filter attachments such as patches, etc. - if ($part->header('Content-Disposition') - && $part->header('Content-Disposition') =~ /attachment/) - { - return; - } - _fix_encoding($part); - my $body = $part->body_str; - my $new_body; - if ($part->content_type =~ /^text\/html/) { - $new_body = _filter_html($body); - if ($new_body ne $body) { - # HTML::Tree removes unnecessary whitespace, - # resulting in very long lines. We need to use - # quoted-printable encoding to avoid exceeding - # email's maximum line length. - $part->encoding_set('quoted-printable'); - } - } - elsif ($part->content_type =~ /^text\/plain/) { - $new_body = _filter_text($body); - } - if ($new_body && $new_body ne $body) { - $part->body_str_set($new_body); - } - }); - } - # Single part email - else { - _fix_encoding($email); - $email->body_str_set(_filter_text($email->body_str)); + my ($self, $args) = @_; + my $email = $args->{'email'}; + + my $author = $email->header("X-Bugzilla-Who"); + my $recipient = $email->header("To"); + + if ($author && $recipient && lc($author) ne lc($recipient)) { + my $email_suffix = Bugzilla->params->{'emailsuffix'}; + if ($email_suffix ne '') { + $recipient =~ s/\Q$email_suffix\E$//; + $author =~ s/\Q$email_suffix\E$//; + } + + $author = new Bugzilla::User({name => $author}); + + if ($author && $author->id && !$author->in_group('editbugs')) { + + # Multipart emails + if (scalar $email->parts > 1) { + $email->walk_parts(sub { + my ($part) = @_; + return if $part->parts > 1; # Top-level + # do not filter attachments such as patches, etc. + if ( $part->header('Content-Disposition') + && $part->header('Content-Disposition') =~ /attachment/) + { + return; + } + _fix_encoding($part); + my $body = $part->body_str; + my $new_body; + if ($part->content_type =~ /^text\/html/) { + $new_body = _filter_html($body); + if ($new_body ne $body) { + + # HTML::Tree removes unnecessary whitespace, + # resulting in very long lines. We need to use + # quoted-printable encoding to avoid exceeding + # email's maximum line length. + $part->encoding_set('quoted-printable'); } - } + } + elsif ($part->content_type =~ /^text\/plain/) { + $new_body = _filter_text($body); + } + if ($new_body && $new_body ne $body) { + $part->body_str_set($new_body); + } + }); + } + + # Single part email + else { + _fix_encoding($email); + $email->body_str_set(_filter_text($email->body_str)); + } } + } } sub _fix_encoding { - my $part = shift; - - # don't touch the top-level part of multi-part mail - return if $part->parts > 1; - - # nothing to do if the part already has a charset - my $ct = parse_content_type($part->content_type); - my $charset = $ct->{attributes}{charset} - ? $ct->{attributes}{charset} - : ''; - return unless !$charset || $charset eq 'us-ascii'; - - if (Bugzilla->params->{utf8}) { - $part->charset_set('UTF-8'); - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } + my $part = shift; + + # don't touch the top-level part of multi-part mail + return if $part->parts > 1; + + # nothing to do if the part already has a charset + my $ct = parse_content_type($part->content_type); + my $charset = $ct->{attributes}{charset} ? $ct->{attributes}{charset} : ''; + return unless !$charset || $charset eq 'us-ascii'; + + if (Bugzilla->params->{utf8}) { + $part->charset_set('UTF-8'); + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); } - $part->encoding_set('quoted-printable'); + } + $part->encoding_set('quoted-printable'); } sub _filter_text { - my $text = shift; - my $offensive = RE_profanity('-i'); - $text =~ s/$offensive/****/g; - return $text; + my $text = shift; + my $offensive = RE_profanity('-i'); + $text =~ s/$offensive/****/g; + return $text; } sub _filter_html { - my $html = shift; - my $tree = HTML::Tree->new->parse_content($html); - my $comments_div = $tree->look_down( _tag => 'div', id => 'comments' ); - return $html if !$comments_div; - my @comments = $comments_div->look_down( _tag => 'pre' ); - my $dirty = 0; - foreach my $comment (@comments) { - _filter_html_node($comment, \$dirty); - } - if ($dirty) { - $html = $tree->as_HTML; - $tree->delete; - } - return $html; + my $html = shift; + my $tree = HTML::Tree->new->parse_content($html); + my $comments_div = $tree->look_down(_tag => 'div', id => 'comments'); + return $html if !$comments_div; + my @comments = $comments_div->look_down(_tag => 'pre'); + my $dirty = 0; + foreach my $comment (@comments) { + _filter_html_node($comment, \$dirty); + } + if ($dirty) { + $html = $tree->as_HTML; + $tree->delete; + } + return $html; } sub _filter_html_node { - my ($node, $dirty) = @_; - my $content = [ $node->content_list ]; - foreach my $item_r ($node->content_refs_list) { - if (ref $$item_r) { - _filter_html_node($$item_r); - } else { - my $new_text = _filter_text($$item_r); - if ($new_text ne $$item_r) { - $$item_r = $new_text; - $$dirty = 1; - } - } + my ($node, $dirty) = @_; + my $content = [$node->content_list]; + foreach my $item_r ($node->content_refs_list) { + if (ref $$item_r) { + _filter_html_node($$item_r); + } + else { + my $new_text = _filter_text($$item_r); + if ($new_text ne $$item_r) { + $$item_r = $new_text; + $$dirty = 1; + } } + } } __PACKAGE__->NAME; diff --git a/extensions/Push/Config.pm b/extensions/Push/Config.pm index 59b78d5a2..9ca73815a 100644 --- a/extensions/Push/Config.pm +++ b/extensions/Push/Config.pm @@ -14,39 +14,14 @@ use warnings; use constant NAME => 'Push'; use constant REQUIRED_MODULES => [ - { - package => 'Daemon-Generic', - module => 'Daemon::Generic', - version => '0' - }, - { - package => 'JSON-XS', - module => 'JSON::XS', - version => '2.0' - }, - { - package => 'Crypt-CBC', - module => 'Crypt::CBC', - version => '0' - }, - { - package => 'Crypt-DES', - module => 'Crypt::DES', - version => '0' - }, - { - package => 'Crypt-DES_EDE3', - module => 'Crypt::DES_EDE3', - version => '0' - }, + {package => 'Daemon-Generic', module => 'Daemon::Generic', version => '0'}, + {package => 'JSON-XS', module => 'JSON::XS', version => '2.0'}, + {package => 'Crypt-CBC', module => 'Crypt::CBC', version => '0'}, + {package => 'Crypt-DES', module => 'Crypt::DES', version => '0'}, + {package => 'Crypt-DES_EDE3', module => 'Crypt::DES_EDE3', version => '0'}, ]; -use constant OPTIONAL_MODULES => [ - { - package => 'XML-Simple', - module => 'XML::Simple', - version => '0' - }, -]; +use constant OPTIONAL_MODULES => + [{package => 'XML-Simple', module => 'XML::Simple', version => '0'},]; __PACKAGE__->NAME; diff --git a/extensions/Push/Extension.pm b/extensions/Push/Extension.pm index f682dea35..4b60dcb73 100644 --- a/extensions/Push/Extension.pm +++ b/extensions/Push/Extension.pm @@ -38,18 +38,18 @@ $Carp::CarpInternal{'CGI::Carp'} = 1; # BEGIN { - *Bugzilla::push_ext = \&_get_instance; + *Bugzilla::push_ext = \&_get_instance; } sub _get_instance { - my $cache = Bugzilla->request_cache; - if (!$cache->{'push.instance'}) { - my $instance = Bugzilla::Extension::Push::Push->new(); - $cache->{'push.instance'} = $instance; - $instance->logger(Bugzilla::Extension::Push::Logger->new()); - $instance->connectors(Bugzilla::Extension::Push::Connectors->new()); - } - return $cache->{'push.instance'}; + my $cache = Bugzilla->request_cache; + if (!$cache->{'push.instance'}) { + my $instance = Bugzilla::Extension::Push::Push->new(); + $cache->{'push.instance'} = $instance; + $instance->logger(Bugzilla::Extension::Push::Logger->new()); + $instance->connectors(Bugzilla::Extension::Push::Connectors->new()); + } + return $cache->{'push.instance'}; } # @@ -57,22 +57,23 @@ sub _get_instance { # sub _enabled { - my ($self) = @_; - if (!exists $self->{'enabled'}) { - my $push = Bugzilla->push_ext; - $self->{'enabled'} = $push->config->{enabled} eq 'Enabled'; - if ($self->{'enabled'}) { - # if no connectors are enabled, no need to push anything - $self->{'enabled'} = 0; - foreach my $connector (Bugzilla->push_ext->connectors->list) { - if ($connector->enabled) { - $self->{'enabled'} = 1; - last; - } - } + my ($self) = @_; + if (!exists $self->{'enabled'}) { + my $push = Bugzilla->push_ext; + $self->{'enabled'} = $push->config->{enabled} eq 'Enabled'; + if ($self->{'enabled'}) { + + # if no connectors are enabled, no need to push anything + $self->{'enabled'} = 0; + foreach my $connector (Bugzilla->push_ext->connectors->list) { + if ($connector->enabled) { + $self->{'enabled'} = 1; + last; } + } } - return $self->{'enabled'}; + } + return $self->{'enabled'}; } # @@ -80,191 +81,186 @@ sub _enabled { # sub _object_created { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $object = _get_object_from_args($args); - return unless $object; - return unless _should_push($object); + my $object = _get_object_from_args($args); + return unless $object; + return unless _should_push($object); - $self->_push_object('create', $object, change_set_id(), { timestamp => $args->{'timestamp'} }); + $self->_push_object('create', $object, change_set_id(), + {timestamp => $args->{'timestamp'}}); } sub _object_modified { - my ($self, $args) = @_; - - my $object = _get_object_from_args($args); - return unless $object; - return unless _should_push($object); - - my $changes = $args->{'changes'} || {}; - return unless scalar keys %$changes; - - my $change_set = change_set_id(); - - # detect when a bug changes from public to private (or back), so connectors - # can remove now-private bugs if required. - if ($object->isa('Bugzilla::Bug')) { - # we can't use user->can_see_bug(old_bug) as that works on IDs, and the - # bug has already been updated, so for now assume that a bug without - # groups is public. - my $old_bug = $args->{'old_bug'}; - my $is_public = is_public($object); - my $was_public = $old_bug ? !@{$old_bug->groups_in} : $is_public; - - if (!$is_public && $was_public) { - # bug is changing from public to private - # push a fake update with the just is_private change - my $private_changes = { - timestamp => $args->{'timestamp'}, - changes => [ - { - field => 'is_private', - removed => '0', - added => '1', - }, - ], - }; - # note we're sending the old bug object so we don't leak any - # security sensitive information. - $self->_push_object('modify', $old_bug, $change_set, $private_changes); - } elsif ($is_public && !$was_public) { - # bug is changing from private to public - # push a fake update with the just is_private change - my $private_changes = { - timestamp => $args->{'timestamp'}, - changes => [ - { - field => 'is_private', - removed => '1', - added => '0', - }, - ], - }; - # it's ok to send the new bug state here - $self->_push_object('modify', $object, $change_set, $private_changes); - } - } + my ($self, $args) = @_; - # make flagtypes changes easier to process - if (exists $changes->{'flagtypes.name'}) { - _split_flagtypes($changes); - } + my $object = _get_object_from_args($args); + return unless $object; + return unless _should_push($object); + + my $changes = $args->{'changes'} || {}; + return unless scalar keys %$changes; - # TODO split group changes? + my $change_set = change_set_id(); - # restructure the changes hash - my $changes_data = { + # detect when a bug changes from public to private (or back), so connectors + # can remove now-private bugs if required. + if ($object->isa('Bugzilla::Bug')) { + + # we can't use user->can_see_bug(old_bug) as that works on IDs, and the + # bug has already been updated, so for now assume that a bug without + # groups is public. + my $old_bug = $args->{'old_bug'}; + my $is_public = is_public($object); + my $was_public = $old_bug ? !@{$old_bug->groups_in} : $is_public; + + if (!$is_public && $was_public) { + + # bug is changing from public to private + # push a fake update with the just is_private change + my $private_changes = { timestamp => $args->{'timestamp'}, - changes => [], - }; - foreach my $field_name (sort keys %$changes) { - my $new_field_name = $field_name; - $new_field_name =~ s/isprivate/is_private/; - - push @{$changes_data->{'changes'}}, { - field => $new_field_name, - removed => $changes->{$field_name}[0], - added => $changes->{$field_name}[1], - }; + changes => [{field => 'is_private', removed => '0', added => '1',},], + }; + + # note we're sending the old bug object so we don't leak any + # security sensitive information. + $self->_push_object('modify', $old_bug, $change_set, $private_changes); } + elsif ($is_public && !$was_public) { + + # bug is changing from private to public + # push a fake update with the just is_private change + my $private_changes = { + timestamp => $args->{'timestamp'}, + changes => [{field => 'is_private', removed => '1', added => '0',},], + }; - $self->_push_object('modify', $object, $change_set, $changes_data); + # it's ok to send the new bug state here + $self->_push_object('modify', $object, $change_set, $private_changes); + } + } + + # make flagtypes changes easier to process + if (exists $changes->{'flagtypes.name'}) { + _split_flagtypes($changes); + } + + # TODO split group changes? + + # restructure the changes hash + my $changes_data = {timestamp => $args->{'timestamp'}, changes => [],}; + foreach my $field_name (sort keys %$changes) { + my $new_field_name = $field_name; + $new_field_name =~ s/isprivate/is_private/; + + push @{$changes_data->{'changes'}}, + { + field => $new_field_name, + removed => $changes->{$field_name}[0], + added => $changes->{$field_name}[1], + }; + } + + $self->_push_object('modify', $object, $change_set, $changes_data); } sub _get_object_from_args { - my ($args) = @_; - return get_first_value($args, qw(object bug flag group)); + my ($args) = @_; + return get_first_value($args, qw(object bug flag group)); } sub _should_push { - my ($object_or_class) = @_; - my $class = blessed($object_or_class) || $object_or_class; - return grep { $_ eq $class } qw(Bugzilla::Bug Bugzilla::Attachment Bugzilla::Comment); + my ($object_or_class) = @_; + my $class = blessed($object_or_class) || $object_or_class; + return + grep { $_ eq $class } + qw(Bugzilla::Bug Bugzilla::Attachment Bugzilla::Comment); } # changes to bug flags are presented in a single field 'flagtypes.name' split # into individual fields sub _split_flagtypes { - my ($changes) = @_; - - my @removed = _split_flagtype($changes->{'flagtypes.name'}->[0]); - my @added = _split_flagtype($changes->{'flagtypes.name'}->[1]); - delete $changes->{'flagtypes.name'}; - - foreach my $ra (@removed, @added) { - $changes->{$ra->[0]} = ['', '']; - } - foreach my $ra (@removed) { - my ($name, $value) = @$ra; - $changes->{$name}->[0] = $value; - } - foreach my $ra (@added) { - my ($name, $value) = @$ra; - $changes->{$name}->[1] = $value; - } + my ($changes) = @_; + + my @removed = _split_flagtype($changes->{'flagtypes.name'}->[0]); + my @added = _split_flagtype($changes->{'flagtypes.name'}->[1]); + delete $changes->{'flagtypes.name'}; + + foreach my $ra (@removed, @added) { + $changes->{$ra->[0]} = ['', '']; + } + foreach my $ra (@removed) { + my ($name, $value) = @$ra; + $changes->{$name}->[0] = $value; + } + foreach my $ra (@added) { + my ($name, $value) = @$ra; + $changes->{$name}->[1] = $value; + } } sub _split_flagtype { - my ($value) = @_; - my @result; - foreach my $change (split(/, /, $value)) { - my $requestee = ''; - if ($change =~ s/\(([^\)]+)\)$//) { - $requestee = $1; - } - my ($name, $value) = $change =~ /^(.+)(.)$/; - $value .= " ($requestee)" if $requestee; - push @result, [ "flag.$name", $value ]; + my ($value) = @_; + my @result; + foreach my $change (split(/, /, $value)) { + my $requestee = ''; + if ($change =~ s/\(([^\)]+)\)$//) { + $requestee = $1; } - return @result; + my ($name, $value) = $change =~ /^(.+)(.)$/; + $value .= " ($requestee)" if $requestee; + push @result, ["flag.$name", $value]; + } + return @result; } # changes to attachment flags come in via flag_end_of_update which has a # completely different structure for reporting changes than # object_end_of_update. this morphs flag to object updates. sub _morph_flag_updates { - my ($args) = @_; - - my @removed = _morph_flag_update($args->{'old_flags'}); - my @added = _morph_flag_update($args->{'new_flags'}); - - my $changes = {}; - foreach my $ra (@removed, @added) { - $changes->{$ra->[0]} = ['', '']; - } - foreach my $ra (@removed) { - my ($name, $value) = @$ra; - $changes->{$name}->[0] = $value; - } - foreach my $ra (@added) { - my ($name, $value) = @$ra; - $changes->{$name}->[1] = $value; - } - - foreach my $flag (keys %$changes) { - if ($changes->{$flag}->[0] eq $changes->{$flag}->[1]) { - delete $changes->{$flag}; - } + my ($args) = @_; + + my @removed = _morph_flag_update($args->{'old_flags'}); + my @added = _morph_flag_update($args->{'new_flags'}); + + my $changes = {}; + foreach my $ra (@removed, @added) { + $changes->{$ra->[0]} = ['', '']; + } + foreach my $ra (@removed) { + my ($name, $value) = @$ra; + $changes->{$name}->[0] = $value; + } + foreach my $ra (@added) { + my ($name, $value) = @$ra; + $changes->{$name}->[1] = $value; + } + + foreach my $flag (keys %$changes) { + if ($changes->{$flag}->[0] eq $changes->{$flag}->[1]) { + delete $changes->{$flag}; } + } - $args->{'changes'} = $changes; + $args->{'changes'} = $changes; } sub _morph_flag_update { - my ($values) = @_; - my @result; - foreach my $orig_change (@$values) { - my $change = $orig_change; # work on a copy - $change =~ s/^[^:]+://; - my $requestee = ''; - if ($change =~ s/\(([^\)]+)\)$//) { - $requestee = $1; - } - my ($name, $value) = $change =~ /^(.+)(.)$/; - $value .= " ($requestee)" if $requestee; - push @result, [ "flag.$name", $value ]; + my ($values) = @_; + my @result; + foreach my $orig_change (@$values) { + my $change = $orig_change; # work on a copy + $change =~ s/^[^:]+://; + my $requestee = ''; + if ($change =~ s/\(([^\)]+)\)$//) { + $requestee = $1; } - return @result; + my ($name, $value) = $change =~ /^(.+)(.)$/; + $value .= " ($requestee)" if $requestee; + push @result, ["flag.$name", $value]; + } + return @result; } # @@ -272,49 +268,52 @@ sub _morph_flag_update { # sub _push_object { - my ($self, $message_type, $object, $change_set, $changes) = @_; - my $rh; - - # serialise the object - my ($rh_object, $name) = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($object); - - if (!$rh_object) { - warn "empty hash from serialiser ($message_type $object)\n"; - return; - } - $rh->{$name} = $rh_object; - - # add in the events hash - my $rh_event = Bugzilla::Extension::Push::Serialise->instance->changes_to_event($changes); - return unless $rh_event; - $rh_event->{'action'} = $message_type; - $rh_event->{'target'} = $name; - $rh_event->{'change_set'} = $change_set; - $rh_event->{'routing_key'} = "$name.$message_type"; - if (exists $rh_event->{'changes'}) { - $rh_event->{'routing_key'} .= ':' . join(',', map { $_->{'field'} } @{$rh_event->{'changes'}}); - } - $rh->{'event'} = $rh_event; - - # create message object - my $message = Bugzilla::Extension::Push::Message->new_transient({ - payload => to_json($rh), - change_set => $change_set, - routing_key => $rh_event->{'routing_key'}, - }); - - # don't hit the database unless there are interested connectors - my $should_push = 0; - foreach my $connector (Bugzilla->push_ext->connectors->list) { - next unless $connector->enabled; - next unless $connector->should_send($message); - $should_push = 1; - last; - } - return unless $should_push; - - # insert into push table - $message->create_from_transient(); + my ($self, $message_type, $object, $change_set, $changes) = @_; + my $rh; + + # serialise the object + my ($rh_object, $name) + = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($object); + + if (!$rh_object) { + warn "empty hash from serialiser ($message_type $object)\n"; + return; + } + $rh->{$name} = $rh_object; + + # add in the events hash + my $rh_event + = Bugzilla::Extension::Push::Serialise->instance->changes_to_event($changes); + return unless $rh_event; + $rh_event->{'action'} = $message_type; + $rh_event->{'target'} = $name; + $rh_event->{'change_set'} = $change_set; + $rh_event->{'routing_key'} = "$name.$message_type"; + if (exists $rh_event->{'changes'}) { + $rh_event->{'routing_key'} + .= ':' . join(',', map { $_->{'field'} } @{$rh_event->{'changes'}}); + } + $rh->{'event'} = $rh_event; + + # create message object + my $message = Bugzilla::Extension::Push::Message->new_transient({ + payload => to_json($rh), + change_set => $change_set, + routing_key => $rh_event->{'routing_key'}, + }); + + # don't hit the database unless there are interested connectors + my $should_push = 0; + foreach my $connector (Bugzilla->push_ext->connectors->list) { + next unless $connector->enabled; + next unless $connector->should_send($message); + $should_push = 1; + last; + } + return unless $should_push; + + # insert into push table + $message->create_from_transient(); } # @@ -322,104 +321,113 @@ sub _push_object { # sub object_end_of_create { - my ($self, $args) = @_; - return unless $self->_enabled; - - # it's better to process objects from a non-generic end_of_create where - # possible; don't process them here to avoid duplicate messages - my $object = _get_object_from_args($args); - return if !$object || - $object->isa('Bugzilla::Bug') || - blessed($object) =~ /^Bugzilla::Extension/; - - $self->_object_created($args); + my ($self, $args) = @_; + return unless $self->_enabled; + + # it's better to process objects from a non-generic end_of_create where + # possible; don't process them here to avoid duplicate messages + my $object = _get_object_from_args($args); + return + if !$object + || $object->isa('Bugzilla::Bug') + || blessed($object) =~ /^Bugzilla::Extension/; + + $self->_object_created($args); } sub object_end_of_update { - my ($self, $args) = @_; + my ($self, $args) = @_; - # User objects are updated with every page load (to touch the session - # token). Because we ignore user objects, there's no need to create an - # instance of Push to check if we're enabled. - my $object = _get_object_from_args($args); - return if !$object || $object->isa('Bugzilla::User'); + # User objects are updated with every page load (to touch the session + # token). Because we ignore user objects, there's no need to create an + # instance of Push to check if we're enabled. + my $object = _get_object_from_args($args); + return if !$object || $object->isa('Bugzilla::User'); - return unless $self->_enabled; + return unless $self->_enabled; - # it's better to process objects from a non-generic end_of_update where - # possible; don't process them here to avoid duplicate messages - return if $object->isa('Bugzilla::Bug') || - $object->isa('Bugzilla::Flag') || - blessed($object) =~ /^Bugzilla::Extension/; + # it's better to process objects from a non-generic end_of_update where + # possible; don't process them here to avoid duplicate messages + return + if $object->isa('Bugzilla::Bug') + || $object->isa('Bugzilla::Flag') + || blessed($object) =~ /^Bugzilla::Extension/; - $self->_object_modified($args); + $self->_object_modified($args); } # process bugs once they are fully formed # object_end_of_update is triggered while a bug is being created sub bug_end_of_create { - my ($self, $args) = @_; - return unless $self->_enabled; - $self->_object_created($args); + my ($self, $args) = @_; + return unless $self->_enabled; + $self->_object_created($args); } sub bug_end_of_update { - my ($self, $args) = @_; - return unless $self->_enabled; - $self->_object_modified($args); + my ($self, $args) = @_; + return unless $self->_enabled; + $self->_object_modified($args); } sub flag_end_of_update { - my ($self, $args) = @_; - return unless $self->_enabled; - _morph_flag_updates($args); - $self->_object_modified($args); - delete $args->{changes}; + my ($self, $args) = @_; + return unless $self->_enabled; + _morph_flag_updates($args); + $self->_object_modified($args); + delete $args->{changes}; } # comments in bugzilla 4.0 doesn't aren't included in the bug_end_of_* hooks, # this code uses custom hooks to trigger sub bug_comment_create { - my ($self, $args) = @_; - return unless $self->_enabled; + my ($self, $args) = @_; + return unless $self->_enabled; - return unless _should_push('Bugzilla::Comment'); - my $bug = $args->{'bug'} or return; - my $timestamp = $args->{'timestamp'} or return; + return unless _should_push('Bugzilla::Comment'); + my $bug = $args->{'bug'} or return; + my $timestamp = $args->{'timestamp'} or return; - my $comments = Bugzilla::Comment->match({ bug_id => $bug->id, bug_when => $timestamp }); + my $comments + = Bugzilla::Comment->match({bug_id => $bug->id, bug_when => $timestamp}); - foreach my $comment (@$comments) { - if ($comment->body ne '') { - $self->_push_object('create', $comment, change_set_id(), { timestamp => $timestamp }); - } + foreach my $comment (@$comments) { + if ($comment->body ne '') { + $self->_push_object('create', $comment, change_set_id(), + {timestamp => $timestamp}); } + } } sub bug_comment_update { - my ($self, $args) = @_; - return unless $self->_enabled; - - return unless _should_push('Bugzilla::Comment'); - my $bug = $args->{'bug'} or return; - my $timestamp = $args->{'timestamp'} or return; - - my $comment_id = $args->{'comment_id'}; - if ($comment_id) { - # XXX this should set changes. only is_private changes will trigger this event - my $comment = Bugzilla::Comment->new($comment_id); - $self->_push_object('update', $comment, change_set_id(), { timestamp => $timestamp }); - - } else { - # when a bug is created, an update is also triggered; we don't want to sent - # update messages for the initial comment, or for empty comments - my $comments = Bugzilla::Comment->match({ bug_id => $bug->id, bug_when => $timestamp }); - foreach my $comment (@$comments) { - if ($comment->body ne '' && $comment->count) { - $self->_push_object('create', $comment, change_set_id(), { timestamp => $timestamp }); - } - } + my ($self, $args) = @_; + return unless $self->_enabled; + + return unless _should_push('Bugzilla::Comment'); + my $bug = $args->{'bug'} or return; + my $timestamp = $args->{'timestamp'} or return; + + my $comment_id = $args->{'comment_id'}; + if ($comment_id) { + + # XXX this should set changes. only is_private changes will trigger this event + my $comment = Bugzilla::Comment->new($comment_id); + $self->_push_object('update', $comment, change_set_id(), + {timestamp => $timestamp}); + + } + else { + # when a bug is created, an update is also triggered; we don't want to sent + # update messages for the initial comment, or for empty comments + my $comments + = Bugzilla::Comment->match({bug_id => $bug->id, bug_when => $timestamp}); + foreach my $comment (@$comments) { + if ($comment->body ne '' && $comment->count) { + $self->_push_object('create', $comment, change_set_id(), + {timestamp => $timestamp}); + } } + } } # @@ -427,36 +435,30 @@ sub bug_comment_update { # sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; - - if ($page eq 'push_config.html') { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_config($vars); - - } elsif ($page eq 'push_queues.html' - || $page eq 'push_queues_view.html' - ) { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_queues($vars, $page); - - } elsif ($page eq 'push_log.html') { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_log($vars); - } + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'push_config.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_config($vars); + + } + elsif ($page eq 'push_queues.html' || $page eq 'push_queues_view.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_queues($vars, $page); + + } + elsif ($page eq 'push_log.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_log($vars); + } } # @@ -464,196 +466,86 @@ sub page_before_template { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'push'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - push_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - payload => { - TYPE => 'LONGTEXT', - NOTNULL => 1, - }, - change_set => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - routing_key => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - ], - }; - $args->{'schema'}->{'push_backlog'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - message_id => { - TYPE => 'INT3', - NOTNULL => 1, - }, - push_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - payload => { - TYPE => 'LONGTEXT', - NOTNULL => 1, - }, - change_set => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - routing_key => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - connector => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - attempt_ts => { - TYPE => 'DATETIME', - }, - attempts => { - TYPE => 'INT2', - NOTNULL => 1, - }, - last_error => { - TYPE => 'MEDIUMTEXT', - }, - ], - INDEXES => [ - push_backlog_idx => { - FIELDS => ['message_id', 'connector'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'push_backoff'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - connector => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - next_attempt_ts => { - TYPE => 'DATETIME', - }, - attempts => { - TYPE => 'INT2', - NOTNULL => 1, - }, - ], - INDEXES => [ - push_backoff_idx => { - FIELDS => ['connector'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'push_options'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - connector => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - option_name => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - option_value => { - TYPE => 'VARCHAR(255)', - NOTNULL => 1, - }, - ], - INDEXES => [ - push_options_idx => { - FIELDS => ['connector', 'option_name'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'push_log'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - message_id => { - TYPE => 'INT3', - NOTNULL => 1, - }, - change_set => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - routing_key => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - connector => { - TYPE => 'VARCHAR(32)', - NOTNULL => 1, - }, - push_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - processed_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - result => { - TYPE => 'INT1', - NOTNULL => 1, - }, - data => { - TYPE => 'MEDIUMTEXT', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'push'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + push_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + payload => {TYPE => 'LONGTEXT', NOTNULL => 1,}, + change_set => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + routing_key => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + ], + }; + $args->{'schema'}->{'push_backlog'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + message_id => {TYPE => 'INT3', NOTNULL => 1,}, + push_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + payload => {TYPE => 'LONGTEXT', NOTNULL => 1,}, + change_set => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + routing_key => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + connector => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + attempt_ts => {TYPE => 'DATETIME',}, + attempts => {TYPE => 'INT2', NOTNULL => 1,}, + last_error => {TYPE => 'MEDIUMTEXT',}, + ], + INDEXES => [ + push_backlog_idx => {FIELDS => ['message_id', 'connector'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'push_backoff'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + connector => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + next_attempt_ts => {TYPE => 'DATETIME',}, + attempts => {TYPE => 'INT2', NOTNULL => 1,}, + ], + INDEXES => [push_backoff_idx => {FIELDS => ['connector'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'push_options'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + connector => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + option_name => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + option_value => {TYPE => 'VARCHAR(255)', NOTNULL => 1,}, + ], + INDEXES => [ + push_options_idx => {FIELDS => ['connector', 'option_name'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'push_log'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + message_id => {TYPE => 'INT3', NOTNULL => 1,}, + change_set => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + routing_key => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + connector => {TYPE => 'VARCHAR(32)', NOTNULL => 1,}, + push_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + processed_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + result => {TYPE => 'INT1', NOTNULL => 1,}, + data => {TYPE => 'MEDIUMTEXT',}, + ], + }; } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; + my ($self, $args) = @_; + my $files = $args->{'files'}; - my $extensionsdir = bz_locations()->{'extensionsdir'}; - my $scriptname = $extensionsdir . "/Push/bin/bugzilla-pushd.pl"; + my $extensionsdir = bz_locations()->{'extensionsdir'}; + my $scriptname = $extensionsdir . "/Push/bin/bugzilla-pushd.pl"; - $files->{$scriptname} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; + $files->{$scriptname} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; } sub db_sanitize { - my $dbh = Bugzilla->dbh; - print "Deleting push extension logs and messages...\n"; - $dbh->do("DELETE FROM push"); - $dbh->do("DELETE FROM push_backlog"); - $dbh->do("DELETE FROM push_backoff"); - $dbh->do("DELETE FROM push_log"); - $dbh->do("DELETE FROM push_options"); + my $dbh = Bugzilla->dbh; + print "Deleting push extension logs and messages...\n"; + $dbh->do("DELETE FROM push"); + $dbh->do("DELETE FROM push_backlog"); + $dbh->do("DELETE FROM push_backoff"); + $dbh->do("DELETE FROM push_log"); + $dbh->do("DELETE FROM push_options"); } __PACKAGE__->NAME; diff --git a/extensions/Push/bin/bugzilla-pushd.pl b/extensions/Push/bin/bugzilla-pushd.pl index 47c905558..cc509aa45 100755 --- a/extensions/Push/bin/bugzilla-pushd.pl +++ b/extensions/Push/bin/bugzilla-pushd.pl @@ -14,8 +14,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Extension::Push::Daemon; diff --git a/extensions/Push/bin/nagios_push_checker.pl b/extensions/Push/bin/nagios_push_checker.pl index b578c33d2..4e6e94167 100755 --- a/extensions/Push/bin/nagios_push_checker.pl +++ b/extensions/Push/bin/nagios_push_checker.pl @@ -20,15 +20,14 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE); # Number of jobs required in the queue before we alert -use constant WARN_COUNT => 500; -use constant ALARM_COUNT => 750; +use constant WARN_COUNT => 500; +use constant ALARM_COUNT => 750; -use constant NAGIOS_OK => 0; -use constant NAGIOS_WARNING => 1; -use constant NAGIOS_CRITICAL => 2; +use constant NAGIOS_OK => 0; +use constant NAGIOS_WARNING => 1; +use constant NAGIOS_CRITICAL => 2; -my $connector = shift - || die "Syntax: $0 connector\neg. $0 TCL\n"; +my $connector = shift || die "Syntax: $0 connector\neg. $0 TCL\n"; $connector = uc($connector); my $sql = <switch_to_shadow_db; -my ($count) = @{ $dbh->selectcol_arrayref($sql, undef, $connector) }; +my ($count) = @{$dbh->selectcol_arrayref($sql, undef, $connector)}; if ($count < WARN_COUNT) { - print "push $connector OK: $count messages found.\n"; - exit NAGIOS_OK; -} elsif ($count < ALARM_COUNT) { - print "push $connector WARNING: $count messages found.\n"; - exit NAGIOS_WARNING; -} else { - print "push $connector CRITICAL: $count messages found.\n"; - exit NAGIOS_CRITICAL; + print "push $connector OK: $count messages found.\n"; + exit NAGIOS_OK; +} +elsif ($count < ALARM_COUNT) { + print "push $connector WARNING: $count messages found.\n"; + exit NAGIOS_WARNING; +} +else { + print "push $connector CRITICAL: $count messages found.\n"; + exit NAGIOS_CRITICAL; } diff --git a/extensions/Push/lib/Admin.pm b/extensions/Push/lib/Admin.pm index 9df2bddcb..d86d30a62 100644 --- a/extensions/Push/lib/Admin.pm +++ b/extensions/Push/lib/Admin.pm @@ -19,110 +19,109 @@ use Bugzilla::Util qw(trim detaint_natural trick_taint); use base qw(Exporter); our @EXPORT = qw( - admin_config - admin_queues - admin_log + admin_config + admin_queues + admin_log ); sub admin_config { - my ($vars) = @_; - my $push = Bugzilla->push_ext; - my $input = Bugzilla->input_params; - - if ($input->{save}) { - my $token = $input->{token}; - check_hash_token($token, ['push_config']); - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - _update_config_from_form('global', $push->config); - foreach my $connector ($push->connectors->list) { - _update_config_from_form($connector->name, $connector->config); - } - $push->set_config_last_modified(); - $dbh->bz_commit_transaction(); - $vars->{message} = 'push_config_updated'; + my ($vars) = @_; + my $push = Bugzilla->push_ext; + my $input = Bugzilla->input_params; + + if ($input->{save}) { + my $token = $input->{token}; + check_hash_token($token, ['push_config']); + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + _update_config_from_form('global', $push->config); + foreach my $connector ($push->connectors->list) { + _update_config_from_form($connector->name, $connector->config); } + $push->set_config_last_modified(); + $dbh->bz_commit_transaction(); + $vars->{message} = 'push_config_updated'; + } - $vars->{push} = $push; - $vars->{connectors} = $push->connectors; + $vars->{push} = $push; + $vars->{connectors} = $push->connectors; } sub _update_config_from_form { - my ($name, $config) = @_; - my $input = Bugzilla->input_params; - - # read values from form - my $values = {}; - foreach my $option ($config->options) { - my $option_name = $option->{name}; - $values->{$option_name} = trim($input->{$name . ".$option_name"}); + my ($name, $config) = @_; + my $input = Bugzilla->input_params; + + # read values from form + my $values = {}; + foreach my $option ($config->options) { + my $option_name = $option->{name}; + $values->{$option_name} = trim($input->{$name . ".$option_name"}); + } + + # validate + if ($values->{enabled} eq 'Enabled') { + eval { $config->validate($values); }; + if ($@) { + ThrowUserError('push_error', {error_message => clean_error($@)}); } + } + + # update + foreach my $option ($config->options) { + my $option_name = $option->{name}; + trick_taint($values->{$option_name}); + $config->{$option_name} = $values->{$option_name}; + } + $config->update(); +} - # validate - if ($values->{enabled} eq 'Enabled') { - eval { - $config->validate($values); - }; - if ($@) { - ThrowUserError('push_error', { error_message => clean_error($@) }); - } - } +sub admin_queues { + my ($vars, $page) = @_; + my $push = Bugzilla->push_ext; + my $input = Bugzilla->input_params; - # update - foreach my $option ($config->options) { - my $option_name = $option->{name}; - trick_taint($values->{$option_name}); - $config->{$option_name} = $values->{$option_name}; + if ($page eq 'push_queues.html') { + $vars->{push} = $push; + + } + elsif ($page eq 'push_queues_view.html') { + my $queue; + if ($input->{connector}) { + my $connector = $push->connectors->by_name($input->{connector}) + || ThrowUserError('push_error', {error_message => 'Invalid connector'}); + $queue = $connector->backlog; } - $config->update(); -} + else { + $queue = $push->queue; + } + $vars->{queue} = $queue; -sub admin_queues { - my ($vars, $page) = @_; - my $push = Bugzilla->push_ext; - my $input = Bugzilla->input_params; - - if ($page eq 'push_queues.html') { - $vars->{push} = $push; - - } elsif ($page eq 'push_queues_view.html') { - my $queue; - if ($input->{connector}) { - my $connector = $push->connectors->by_name($input->{connector}) - || ThrowUserError('push_error', { error_message => 'Invalid connector' }); - $queue = $connector->backlog; - } else { - $queue = $push->queue; - } - $vars->{queue} = $queue; - - my $id = $input->{message} || 0; - detaint_natural($id) - || ThrowUserError('push_error', { error_message => 'Invalid message ID' }); - my $message = $queue->by_id($id) - || ThrowUserError('push_error', { error_message => 'Invalid message ID' }); - - if ($input->{delete}) { - my $token = $input->{token}; - check_hash_token($token, ['deleteMessage']); - $message->remove_from_db(); - $vars->{message} = 'push_message_deleted'; - - } else { - $vars->{message_obj} = $message; - eval { - $vars->{json} = to_json($message->payload_decoded, 1); - }; - } + my $id = $input->{message} || 0; + detaint_natural($id) + || ThrowUserError('push_error', {error_message => 'Invalid message ID'}); + my $message = $queue->by_id($id) + || ThrowUserError('push_error', {error_message => 'Invalid message ID'}); + + if ($input->{delete}) { + my $token = $input->{token}; + check_hash_token($token, ['deleteMessage']); + $message->remove_from_db(); + $vars->{message} = 'push_message_deleted'; + + } + else { + $vars->{message_obj} = $message; + eval { $vars->{json} = to_json($message->payload_decoded, 1); }; } + } } sub admin_log { - my ($vars) = @_; - my $push = Bugzilla->push_ext; - my $input = Bugzilla->input_params; + my ($vars) = @_; + my $push = Bugzilla->push_ext; + my $input = Bugzilla->input_params; - $vars->{push} = $push; + $vars->{push} = $push; } 1; diff --git a/extensions/Push/lib/BacklogMessage.pm b/extensions/Push/lib/BacklogMessage.pm index 28b17bae3..942eb77eb 100644 --- a/extensions/Push/lib/BacklogMessage.pm +++ b/extensions/Push/lib/BacklogMessage.pm @@ -28,31 +28,31 @@ use Encode; # initialisation # -use constant DB_TABLE => 'push_backlog'; +use constant DB_TABLE => 'push_backlog'; use constant DB_COLUMNS => qw( - id - message_id - push_ts - payload - change_set - routing_key - connector - attempt_ts - attempts - last_error + id + message_id + push_ts + payload + change_set + routing_key + connector + attempt_ts + attempts + last_error ); use constant UPDATE_COLUMNS => qw( - attempt_ts - attempts - last_error + attempt_ts + attempts + last_error ); use constant LIST_ORDER => 'push_ts'; use constant VALIDATORS => { - payload => \&_check_payload, - change_set => \&_check_change_set, - routing_key => \&_check_routing_key, - connector => \&_check_connector, - attempts => \&_check_attempts, + payload => \&_check_payload, + change_set => \&_check_change_set, + routing_key => \&_check_routing_key, + connector => \&_check_connector, + attempts => \&_check_attempts, }; # @@ -60,46 +60,46 @@ use constant VALIDATORS => { # sub create_from_message { - my ($class, $message, $connector) = @_; - my $self = $class->create({ - message_id => $message->id, - push_ts => $message->push_ts, - payload => $message->payload, - change_set => $message->change_set, - routing_key => $message->routing_key, - connector => $connector->name, - attempt_ts => undef, - attempts => 0, - last_error => undef, - }); - return $self; + my ($class, $message, $connector) = @_; + my $self = $class->create({ + message_id => $message->id, + push_ts => $message->push_ts, + payload => $message->payload, + change_set => $message->change_set, + routing_key => $message->routing_key, + connector => $connector->name, + attempt_ts => undef, + attempts => 0, + last_error => undef, + }); + return $self; } # # accessors # -sub message_id { return $_[0]->{'message_id'} } -sub push_ts { return $_[0]->{'push_ts'}; } -sub payload { return $_[0]->{'payload'}; } -sub change_set { return $_[0]->{'change_set'}; } +sub message_id { return $_[0]->{'message_id'} } +sub push_ts { return $_[0]->{'push_ts'}; } +sub payload { return $_[0]->{'payload'}; } +sub change_set { return $_[0]->{'change_set'}; } sub routing_key { return $_[0]->{'routing_key'}; } -sub connector { return $_[0]->{'connector'}; } -sub attempt_ts { return $_[0]->{'attempt_ts'}; } -sub attempts { return $_[0]->{'attempts'}; } -sub last_error { return $_[0]->{'last_error'}; } +sub connector { return $_[0]->{'connector'}; } +sub attempt_ts { return $_[0]->{'attempt_ts'}; } +sub attempts { return $_[0]->{'attempts'}; } +sub last_error { return $_[0]->{'last_error'}; } sub payload_decoded { - my ($self) = @_; - return from_json($self->{'payload'}); + my ($self) = @_; + return from_json($self->{'payload'}); } sub attempt_time { - my ($self) = @_; - if (!exists $self->{'attempt_time'}) { - $self->{'attempt_time'} = datetime_from($self->attempt_ts)->epoch; - } - return $self->{'attempt_time'}; + my ($self) = @_; + if (!exists $self->{'attempt_time'}) { + $self->{'attempt_time'} = datetime_from($self->attempt_ts)->epoch; + } + return $self->{'attempt_time'}; } # @@ -107,11 +107,11 @@ sub attempt_time { # sub inc_attempts { - my ($self, $error) = @_; - $self->{attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()'); - $self->{attempts} = $self->{attempts} + 1; - $self->{last_error} = $error; - $self->update; + my ($self, $error) = @_; + $self->{attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()'); + $self->{attempts} = $self->{attempts} + 1; + $self->{last_error} = $error; + $self->update; } # @@ -119,32 +119,35 @@ sub inc_attempts { # sub _check_payload { - my ($invocant, $value) = @_; - length($value) || ThrowCodeError('push_invalid_payload'); - return $value; + my ($invocant, $value) = @_; + length($value) || ThrowCodeError('push_invalid_payload'); + return $value; } sub _check_change_set { - my ($invocant, $value) = @_; - (defined($value) && length($value)) || ThrowCodeError('push_invalid_change_set'); - return $value; + my ($invocant, $value) = @_; + (defined($value) && length($value)) + || ThrowCodeError('push_invalid_change_set'); + return $value; } sub _check_routing_key { - my ($invocant, $value) = @_; - (defined($value) && length($value)) || ThrowCodeError('push_invalid_routing_key'); - return $value; + my ($invocant, $value) = @_; + (defined($value) && length($value)) + || ThrowCodeError('push_invalid_routing_key'); + return $value; } sub _check_connector { - my ($invocant, $value) = @_; - Bugzilla->push_ext->connectors->exists($value) || ThrowCodeError('push_invalid_connector'); - return $value; + my ($invocant, $value) = @_; + Bugzilla->push_ext->connectors->exists($value) + || ThrowCodeError('push_invalid_connector'); + return $value; } sub _check_attempts { - my ($invocant, $value) = @_; - return $value || 0; + my ($invocant, $value) = @_; + return $value || 0; } 1; diff --git a/extensions/Push/lib/BacklogQueue.pm b/extensions/Push/lib/BacklogQueue.pm index a7200c688..17d0a188f 100644 --- a/extensions/Push/lib/BacklogQueue.pm +++ b/extensions/Push/lib/BacklogQueue.pm @@ -15,74 +15,67 @@ use Bugzilla; use Bugzilla::Extension::Push::BacklogMessage; sub new { - my ($class, $connector) = @_; - my $self = {}; - bless($self, $class); - $self->{connector} = $connector; - return $self; + my ($class, $connector) = @_; + my $self = {}; + bless($self, $class); + $self->{connector} = $connector; + return $self; } sub count { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_array(" + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_array(" SELECT COUNT(*) FROM push_backlog - WHERE connector = ?", - undef, - $self->{connector}); + WHERE connector = ?", undef, $self->{connector}); } sub oldest { - my ($self) = @_; - my @messages = $self->list( - limit => 1, - filter => 'AND ((next_attempt_ts IS NULL) OR (next_attempt_ts <= NOW()))', - ); - return scalar(@messages) ? $messages[0] : undef; + my ($self) = @_; + my @messages = $self->list( + limit => 1, + filter => 'AND ((next_attempt_ts IS NULL) OR (next_attempt_ts <= NOW()))', + ); + return scalar(@messages) ? $messages[0] : undef; } sub by_id { - my ($self, $id) = @_; - my @messages = $self->list( - limit => 1, - filter => "AND (log.id = $id)", - ); - return scalar(@messages) ? $messages[0] : undef; + my ($self, $id) = @_; + my @messages = $self->list(limit => 1, filter => "AND (log.id = $id)",); + return scalar(@messages) ? $messages[0] : undef; } sub list { - my ($self, %args) = @_; - $args{limit} ||= 10; - $args{filter} ||= ''; - my @result; - my $dbh = Bugzilla->dbh; + my ($self, %args) = @_; + $args{limit} ||= 10; + $args{filter} ||= ''; + my @result; + my $dbh = Bugzilla->dbh; - my $filter_sql = $args{filter} || ''; - my $sth = $dbh->prepare(" + my $filter_sql = $args{filter} || ''; + my $sth = $dbh->prepare(" SELECT log.id, message_id, push_ts, payload, change_set, routing_key, attempt_ts, log.attempts FROM push_backlog log LEFT JOIN push_backoff off ON off.connector = log.connector - WHERE log.connector = ? ". - $args{filter} . " - ORDER BY push_ts " . - $dbh->sql_limit($args{limit}) - ); - $sth->execute($self->{connector}); - while (my $row = $sth->fetchrow_hashref()) { - push @result, Bugzilla::Extension::Push::BacklogMessage->new({ - id => $row->{id}, - message_id => $row->{message_id}, - push_ts => $row->{push_ts}, - payload => $row->{payload}, - change_set => $row->{change_set}, - routing_key => $row->{routing_key}, - connector => $self->{connector}, - attempt_ts => $row->{attempt_ts}, - attempts => $row->{attempts}, - }); - } - return @result; + WHERE log.connector = ? " . $args{filter} . " + ORDER BY push_ts " . $dbh->sql_limit($args{limit})); + $sth->execute($self->{connector}); + while (my $row = $sth->fetchrow_hashref()) { + push @result, + Bugzilla::Extension::Push::BacklogMessage->new({ + id => $row->{id}, + message_id => $row->{message_id}, + push_ts => $row->{push_ts}, + payload => $row->{payload}, + change_set => $row->{change_set}, + routing_key => $row->{routing_key}, + connector => $self->{connector}, + attempt_ts => $row->{attempt_ts}, + attempts => $row->{attempts}, + }); + } + return @result; } # @@ -90,39 +83,40 @@ sub list { # sub backoff { - my ($self) = @_; - if (!$self->{backoff}) { - my $ra = Bugzilla::Extension::Push::Backoff->match({ - connector => $self->{connector} + my ($self) = @_; + if (!$self->{backoff}) { + my $ra + = Bugzilla::Extension::Push::Backoff->match({connector => $self->{connector} + }); + if (@$ra) { + $self->{backoff} = $ra->[0]; + } + else { + $self->{backoff} + = Bugzilla::Extension::Push::Backoff->create({connector => $self->{connector} }); - if (@$ra) { - $self->{backoff} = $ra->[0]; - } else { - $self->{backoff} = Bugzilla::Extension::Push::Backoff->create({ - connector => $self->{connector} - }); - } } - return $self->{backoff}; + } + return $self->{backoff}; } sub reset_backoff { - my ($self) = @_; - my $backoff = $self->backoff; - $backoff->reset(); - $backoff->update(); + my ($self) = @_; + my $backoff = $self->backoff; + $backoff->reset(); + $backoff->update(); } sub inc_backoff { - my ($self) = @_; - my $backoff = $self->backoff; - $backoff->inc(); - $backoff->update(); + my ($self) = @_; + my $backoff = $self->backoff; + $backoff->inc(); + $backoff->update(); } sub connector { - my ($self) = @_; - return $self->{connector}; + my ($self) = @_; + return $self->{connector}; } 1; diff --git a/extensions/Push/lib/Backoff.pm b/extensions/Push/lib/Backoff.pm index 0436cdf14..070adfc29 100644 --- a/extensions/Push/lib/Backoff.pm +++ b/extensions/Push/lib/Backoff.pm @@ -26,21 +26,21 @@ use Bugzilla::Util; # initialisation # -use constant DB_TABLE => 'push_backoff'; +use constant DB_TABLE => 'push_backoff'; use constant DB_COLUMNS => qw( - id - connector - next_attempt_ts - attempts + id + connector + next_attempt_ts + attempts ); use constant UPDATE_COLUMNS => qw( - next_attempt_ts - attempts + next_attempt_ts + attempts ); use constant VALIDATORS => { - connector => \&_check_connector, - next_attempt_ts => \&_check_next_attempt_ts, - attempts => \&_check_attempts, + connector => \&_check_connector, + next_attempt_ts => \&_check_next_attempt_ts, + attempts => \&_check_attempts, }; use constant LIST_ORDER => 'next_attempt_ts'; @@ -48,16 +48,16 @@ use constant LIST_ORDER => 'next_attempt_ts'; # accessors # -sub connector { return $_[0]->{'connector'}; } +sub connector { return $_[0]->{'connector'}; } sub next_attempt_ts { return $_[0]->{'next_attempt_ts'}; } -sub attempts { return $_[0]->{'attempts'}; } +sub attempts { return $_[0]->{'attempts'}; } sub next_attempt_time { - my ($self) = @_; - if (!exists $self->{'next_attempt_time'}) { - $self->{'next_attempt_time'} = datetime_from($self->next_attempt_ts)->epoch; - } - return $self->{'next_attempt_time'}; + my ($self) = @_; + if (!exists $self->{'next_attempt_time'}) { + $self->{'next_attempt_time'} = datetime_from($self->next_attempt_ts)->epoch; + } + return $self->{'next_attempt_time'}; } # @@ -65,25 +65,26 @@ sub next_attempt_time { # sub reset { - my ($self) = @_; - $self->{next_attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()'); - $self->{attempts} = 0; - INFO( sprintf 'resetting backoff for %s', $self->connector ); + my ($self) = @_; + $self->{next_attempt_ts} = Bugzilla->dbh->selectrow_array('SELECT NOW()'); + $self->{attempts} = 0; + INFO(sprintf 'resetting backoff for %s', $self->connector); } sub inc { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - my $attempts = $self->attempts + 1; - my $seconds = $attempts <= 4 ? 5 ** $attempts : 15 * 60; - my ($date) = $dbh->selectrow_array("SELECT " . $dbh->sql_date_math('NOW()', '+', $seconds, 'SECOND')); - - $self->{next_attempt_ts} = $date; - $self->{attempts} = $attempts; - INFO( - sprintf 'setting next attempt for %s to %s (attempt %s)', $self->connector, $date, $attempts - ); + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + my $attempts = $self->attempts + 1; + my $seconds = $attempts <= 4 ? 5**$attempts : 15 * 60; + my ($date) + = $dbh->selectrow_array( + "SELECT " . $dbh->sql_date_math('NOW()', '+', $seconds, 'SECOND')); + + $self->{next_attempt_ts} = $date; + $self->{attempts} = $attempts; + INFO(sprintf 'setting next attempt for %s to %s (attempt %s)', + $self->connector, $date, $attempts); } # @@ -91,19 +92,20 @@ sub inc { # sub _check_connector { - my ($invocant, $value) = @_; - Bugzilla->push_ext->connectors->exists($value) || ThrowCodeError('push_invalid_connector'); - return $value; + my ($invocant, $value) = @_; + Bugzilla->push_ext->connectors->exists($value) + || ThrowCodeError('push_invalid_connector'); + return $value; } sub _check_next_attempt_ts { - my ($invocant, $value) = @_; - return $value || Bugzilla->dbh->selectrow_array('SELECT NOW()'); + my ($invocant, $value) = @_; + return $value || Bugzilla->dbh->selectrow_array('SELECT NOW()'); } sub _check_attempts { - my ($invocant, $value) = @_; - return $value || 0; + my ($invocant, $value) = @_; + return $value || 0; } 1; diff --git a/extensions/Push/lib/Config.pm b/extensions/Push/lib/Config.pm index 2db95b972..bb0d523ad 100644 --- a/extensions/Push/lib/Config.pm +++ b/extensions/Push/lib/Config.pm @@ -17,199 +17,201 @@ use Bugzilla::Extension::Push::Option; use Crypt::CBC; sub new { - my ($class, $name, @options) = @_; - my $self = { - _name => $name - }; - bless($self, $class); - - $self->{_options} = [@options]; - unshift @{$self->{_options}}, { - name => 'enabled', - label => 'Status', - help => '', - type => 'select', - values => [ 'Enabled', 'Disabled' ], - default => 'Disabled', + my ($class, $name, @options) = @_; + my $self = {_name => $name}; + bless($self, $class); + + $self->{_options} = [@options]; + unshift @{$self->{_options}}, + { + name => 'enabled', + label => 'Status', + help => '', + type => 'select', + values => ['Enabled', 'Disabled'], + default => 'Disabled', }; - return $self; + return $self; } sub options { - my ($self) = @_; - return @{$self->{_options}}; + my ($self) = @_; + return @{$self->{_options}}; } sub option { - my ($self, $name) = @_; - foreach my $option ($self->options) { - return $option if $option->{name} eq $name; - } - return undef; + my ($self, $name) = @_; + foreach my $option ($self->options) { + return $option if $option->{name} eq $name; + } + return undef; } sub load { - my ($self) = @_; - my $config = {}; - - # prime $config with defaults - foreach my $rh ($self->options) { - $config->{$rh->{name}} = $rh->{default}; + my ($self) = @_; + my $config = {}; + + # prime $config with defaults + foreach my $rh ($self->options) { + $config->{$rh->{name}} = $rh->{default}; + } + + # override defaults with values from database + my $options + = Bugzilla::Extension::Push::Option->match({connector => $self->{_name},}); + foreach my $option (@$options) { + my $option_config = $self->option($option->name) || next; + if ($option_config->{type} eq 'password') { + $config->{$option->name} = $self->_decrypt($option->value); } - - # override defaults with values from database - my $options = Bugzilla::Extension::Push::Option->match({ - connector => $self->{_name}, - }); - foreach my $option (@$options) { - my $option_config = $self->option($option->name) - || next; - if ($option_config->{type} eq 'password') { - $config->{$option->name} = $self->_decrypt($option->value); - } else { - $config->{$option->name} = $option->value; - } + else { + $config->{$option->name} = $option->value; } + } - # validate when running from the daemon - if (Bugzilla->push_ext->is_daemon) { - $self->_validate_config($config); - } - - # done, update self - foreach my $name (keys %$config) { - my $value = $self->option($name)->{type} eq 'password' ? '********' : $config->{$name}; - TRACE( sprintf "%s: set %s=%s\n", $self->{_name}, $name, $value || '' ); - $self->{$name} = $config->{$name}; - } + # validate when running from the daemon + if (Bugzilla->push_ext->is_daemon) { + $self->_validate_config($config); + } + + # done, update self + foreach my $name (keys %$config) { + my $value + = $self->option($name)->{type} eq 'password' ? '********' : $config->{$name}; + TRACE(sprintf "%s: set %s=%s\n", $self->{_name}, $name, $value || ''); + $self->{$name} = $config->{$name}; + } } sub validate { - my ($self, $config) = @_; - $self->_validate_mandatory($config); - $self->_validate_config($config); + my ($self, $config) = @_; + $self->_validate_mandatory($config); + $self->_validate_config($config); } sub update { - my ($self) = @_; - - my @valid_options = map { $_->{name} } $self->options; - - my %options; - my $options_list = Bugzilla::Extension::Push::Option->match({ - connector => $self->{_name}, - }); - foreach my $option (@$options_list) { - $options{$option->name} = $option; - } - - # delete options which are no longer valid - foreach my $name (keys %options) { - if (!grep { $_ eq $name } @valid_options) { - $options{$name}->remove_from_db(); - delete $options{$name}; - } + my ($self) = @_; + + my @valid_options = map { $_->{name} } $self->options; + + my %options; + my $options_list + = Bugzilla::Extension::Push::Option->match({connector => $self->{_name},}); + foreach my $option (@$options_list) { + $options{$option->name} = $option; + } + + # delete options which are no longer valid + foreach my $name (keys %options) { + if (!grep { $_ eq $name } @valid_options) { + $options{$name}->remove_from_db(); + delete $options{$name}; } + } - # update options - foreach my $name (keys %options) { - my $option = $options{$name}; - if ($self->option($name)->{type} eq 'password') { - $option->set_value($self->_encrypt($self->{$name})); - } else { - $option->set_value($self->{$name}); - } - $option->update(); + # update options + foreach my $name (keys %options) { + my $option = $options{$name}; + if ($self->option($name)->{type} eq 'password') { + $option->set_value($self->_encrypt($self->{$name})); } - - # add missing options - foreach my $name (@valid_options) { - next if exists $options{$name}; - Bugzilla::Extension::Push::Option->create({ - connector => $self->{_name}, - option_name => $name, - option_value => $self->{$name}, - }); + else { + $option->set_value($self->{$name}); } + $option->update(); + } + + # add missing options + foreach my $name (@valid_options) { + next if exists $options{$name}; + Bugzilla::Extension::Push::Option->create({ + connector => $self->{_name}, + option_name => $name, + option_value => $self->{$name}, + }); + } } sub _remove_invalid_options { - my ($self, $config) = @_; - my @names; - foreach my $rh ($self->options) { - push @names, $rh->{name}; - } - foreach my $name (keys %$config) { - if ($name =~ /^_/ || !grep { $_ eq $name } @names) { - delete $config->{$name}; - } + my ($self, $config) = @_; + my @names; + foreach my $rh ($self->options) { + push @names, $rh->{name}; + } + foreach my $name (keys %$config) { + if ($name =~ /^_/ || !grep { $_ eq $name } @names) { + delete $config->{$name}; } + } } sub _validate_mandatory { - my ($self, $config) = @_; - $self->_remove_invalid_options($config); - - my @missing; - foreach my $option ($self->options) { - next unless $option->{required}; - my $name = $option->{name}; - if (!exists $config->{$name} || !defined($config->{$name}) || $config->{$name} eq '') { - push @missing, $option; - } + my ($self, $config) = @_; + $self->_remove_invalid_options($config); + + my @missing; + foreach my $option ($self->options) { + next unless $option->{required}; + my $name = $option->{name}; + if ( !exists $config->{$name} + || !defined($config->{$name}) + || $config->{$name} eq '') + { + push @missing, $option; + } + } + if (@missing) { + my $connector = $self->{_name}; + @missing = map { $_->{label} } @missing; + if (scalar @missing == 1) { + die "The option '$missing[0]' for the connector '$connector' is mandatory\n"; } - if (@missing) { - my $connector = $self->{_name}; - @missing = map { $_->{label} } @missing; - if (scalar @missing == 1) { - die "The option '$missing[0]' for the connector '$connector' is mandatory\n"; - } else { - die "The following options for the connector '$connector' are mandatory:\n " - . join("\n ", @missing) . "\n"; - } + else { + die "The following options for the connector '$connector' are mandatory:\n " + . join("\n ", @missing) . "\n"; } + } } sub _validate_config { - my ($self, $config) = @_; - $self->_remove_invalid_options($config); - - my @errors; - foreach my $option ($self->options) { - my $name = $option->{name}; - next unless exists $config->{$name} && exists $option->{validate}; - eval { - $option->{validate}->($config->{$name}, $config); - }; - push @errors, $@ if $@; - } - die join("\n", @errors) if @errors; - - if ($self->{_name} ne 'global') { - my $class = 'Bugzilla::Extension::Push::Connector::' . $self->{_name}; - $class->options_validate($config); - } + my ($self, $config) = @_; + $self->_remove_invalid_options($config); + + my @errors; + foreach my $option ($self->options) { + my $name = $option->{name}; + next unless exists $config->{$name} && exists $option->{validate}; + eval { $option->{validate}->($config->{$name}, $config); }; + push @errors, $@ if $@; + } + die join("\n", @errors) if @errors; + + if ($self->{_name} ne 'global') { + my $class = 'Bugzilla::Extension::Push::Connector::' . $self->{_name}; + $class->options_validate($config); + } } sub _cipher { - my ($self) = @_; - $self->{_cipher} ||= Crypt::CBC->new( - -key => Bugzilla->localconfig->{'site_wide_secret'}, - -cipher => 'DES_EDE3'); - return $self->{_cipher}; + my ($self) = @_; + $self->{_cipher} ||= Crypt::CBC->new( + -key => Bugzilla->localconfig->{'site_wide_secret'}, + -cipher => 'DES_EDE3' + ); + return $self->{_cipher}; } sub _decrypt { - my ($self, $value) = @_; - my $result; - eval { $result = $self->_cipher->decrypt_hex($value) }; - return $@ ? '' : $result; + my ($self, $value) = @_; + my $result; + eval { $result = $self->_cipher->decrypt_hex($value) }; + return $@ ? '' : $result; } sub _encrypt { - my ($self, $value) = @_; - return $self->_cipher->encrypt_hex($value); + my ($self, $value) = @_; + return $self->_cipher->encrypt_hex($value); } 1; diff --git a/extensions/Push/lib/Connector.disabled/AMQP.pm b/extensions/Push/lib/Connector.disabled/AMQP.pm index 1ba365e21..dda73dade 100644 --- a/extensions/Push/lib/Connector.disabled/AMQP.pm +++ b/extensions/Push/lib/Connector.disabled/AMQP.pm @@ -20,211 +20,197 @@ use Bugzilla::Util qw(generate_random_password); use DateTime; sub init { - my ($self) = @_; - $self->{mq} = 0; - $self->{channel} = 1; - - if ($self->config->{queue}) { - $self->{queue_name} = $self->config->{queue}; - } else { - my $queue_name = Bugzilla->localconfig->{'urlbase'}; - $queue_name =~ s#^https?://##; - $queue_name =~ s#/$#|#; - $queue_name .= generate_random_password(16); - $self->{queue_name} = $queue_name; - } + my ($self) = @_; + $self->{mq} = 0; + $self->{channel} = 1; + + if ($self->config->{queue}) { + $self->{queue_name} = $self->config->{queue}; + } + else { + my $queue_name = Bugzilla->localconfig->{'urlbase'}; + $queue_name =~ s#^https?://##; + $queue_name =~ s#/$#|#; + $queue_name .= generate_random_password(16); + $self->{queue_name} = $queue_name; + } } sub options { - return ( - { - name => 'host', - label => 'AMQP Hostname', - type => 'string', - default => 'localhost', - required => 1, - }, - { - name => 'port', - label => 'AMQP Port', - type => 'string', - default => '5672', - required => 1, - validate => sub { - $_[0] =~ /\D/ && die "Invalid port (must be numeric)\n"; - }, - }, - { - name => 'username', - label => 'Username', - type => 'string', - default => 'guest', - required => 1, - }, - { - name => 'password', - label => 'Password', - type => 'password', - default => 'guest', - required => 1, - }, - { - name => 'vhost', - label => 'Virtual Host', - type => 'string', - default => '/', - required => 1, - }, - { - name => 'exchange', - label => 'Exchange', - type => 'string', - default => '', - required => 1, - }, - { - name => 'queue', - label => 'Queue', - type => 'string', - }, - ); + return ( + { + name => 'host', + label => 'AMQP Hostname', + type => 'string', + default => 'localhost', + required => 1, + }, + { + name => 'port', + label => 'AMQP Port', + type => 'string', + default => '5672', + required => 1, + validate => sub { + $_[0] =~ /\D/ && die "Invalid port (must be numeric)\n"; + }, + }, + { + name => 'username', + label => 'Username', + type => 'string', + default => 'guest', + required => 1, + }, + { + name => 'password', + label => 'Password', + type => 'password', + default => 'guest', + required => 1, + }, + { + name => 'vhost', + label => 'Virtual Host', + type => 'string', + default => '/', + required => 1, + }, + { + name => 'exchange', + label => 'Exchange', + type => 'string', + default => '', + required => 1, + }, + {name => 'queue', label => 'Queue', type => 'string',}, + ); } sub stop { - my ($self) = @_; - if ($self->{mq}) { - Bugzilla->push_ext->logger->debug('AMQP: disconnecting'); - $self->{mq}->disconnect(); - $self->{mq} = 0; - } + my ($self) = @_; + if ($self->{mq}) { + Bugzilla->push_ext->logger->debug('AMQP: disconnecting'); + $self->{mq}->disconnect(); + $self->{mq} = 0; + } } sub _connect { - my ($self) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - $self->stop(); - - $logger->debug('AMQP: Connecting to RabbitMQ ' . $config->{host} . ':' . $config->{port}); - require Net::RabbitMQ; - my $mq = Net::RabbitMQ->new(); - $mq->connect( - $config->{host}, - { - port => $config->{port}, - user => $config->{username}, - password => $config->{password}, - } - ); - $self->{mq} = $mq; - - $logger->debug('AMQP: Opening channel ' . $self->{channel}); - $self->{mq}->channel_open($self->{channel}); - - $logger->debug('AMQP: Declaring queue ' . $self->{queue_name}); - $self->{mq}->queue_declare( - $self->{channel}, - $self->{queue_name}, - { - passive => 0, - durable => 1, - exclusive => 0, - auto_delete => 0, - }, - ); + my ($self) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + + $self->stop(); + + $logger->debug( + 'AMQP: Connecting to RabbitMQ ' . $config->{host} . ':' . $config->{port}); + require Net::RabbitMQ; + my $mq = Net::RabbitMQ->new(); + $mq->connect( + $config->{host}, + { + port => $config->{port}, + user => $config->{username}, + password => $config->{password}, + } + ); + $self->{mq} = $mq; + + $logger->debug('AMQP: Opening channel ' . $self->{channel}); + $self->{mq}->channel_open($self->{channel}); + + $logger->debug('AMQP: Declaring queue ' . $self->{queue_name}); + $self->{mq}->queue_declare($self->{channel}, $self->{queue_name}, + {passive => 0, durable => 1, exclusive => 0, auto_delete => 0,}, + ); } sub _bind { - my ($self, $message) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - # bind to queue (also acts to verify the connection is still valid) - if ($self->{mq}) { - eval { - $logger->debug('AMQP: binding queue(' . $self->{queue_name} . ') with exchange(' . $config->{exchange} . ')'); - $self->{mq}->queue_bind( - $self->{channel}, - $self->{queue_name}, - $config->{exchange}, - $message->routing_key, - ); - }; - if ($@) { - $logger->debug('AMQP: ' . clean_error($@)); - $self->{mq} = 0; - } + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + + # bind to queue (also acts to verify the connection is still valid) + if ($self->{mq}) { + eval { + $logger->debug('AMQP: binding queue(' + . $self->{queue_name} + . ') with exchange(' + . $config->{exchange} + . ')'); + $self->{mq}->queue_bind( + $self->{channel}, $self->{queue_name}, + $config->{exchange}, $message->routing_key, + ); + }; + if ($@) { + $logger->debug('AMQP: ' . clean_error($@)); + $self->{mq} = 0; } + } } sub should_send { - my ($self, $message) = @_; - my $logger = Bugzilla->push_ext->logger; - - my $payload = $message->payload_decoded(); - my $target = $payload->{event}->{target}; - my $is_private = $payload->{$target}->{is_private} ? 1 : 0; - if (!$is_private && exists $payload->{$target}->{bug}) { - $is_private = $payload->{$target}->{bug}->{is_private} ? 1 : 0; - } - - if ($is_private) { - # we only want to push the is_private message from the change_set, as - # this is guaranteed to contain public information only - if ($message->routing_key !~ /\.modify:is_private$/) { - $logger->debug('AMQP: Ignoring private message'); - return 0; - } - $logger->debug('AMQP: Sending change of message to is_private'); + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + + my $payload = $message->payload_decoded(); + my $target = $payload->{event}->{target}; + my $is_private = $payload->{$target}->{is_private} ? 1 : 0; + if (!$is_private && exists $payload->{$target}->{bug}) { + $is_private = $payload->{$target}->{bug}->{is_private} ? 1 : 0; + } + + if ($is_private) { + + # we only want to push the is_private message from the change_set, as + # this is guaranteed to contain public information only + if ($message->routing_key !~ /\.modify:is_private$/) { + $logger->debug('AMQP: Ignoring private message'); + return 0; } - return 1; + $logger->debug('AMQP: Sending change of message to is_private'); + } + return 1; } sub send { - my ($self, $message) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - # don't push comments to pulse - if ($message->routing_key =~ /^comment\./) { - $logger->debug('AMQP: Ignoring comment'); - return PUSH_RESULT_IGNORED; - } + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; - # don't push private data - $self->should_push($message) - || return PUSH_RESULT_IGNORED; + # don't push comments to pulse + if ($message->routing_key =~ /^comment\./) { + $logger->debug('AMQP: Ignoring comment'); + return PUSH_RESULT_IGNORED; + } - $self->_bind($message); + # don't push private data + $self->should_push($message) || return PUSH_RESULT_IGNORED; - eval { - # reconnect if required - if (!$self->{mq}) { - $self->_connect(); - } - - # send message - $logger->debug('AMQP: Publishing message'); - $self->{mq}->publish( - $self->{channel}, - $message->routing_key, - $message->payload, - { - exchange => $config->{exchange}, - }, - { - content_type => 'text/plain', - content_encoding => '8bit', - }, - ); - }; - if ($@) { - return (PUSH_RESULT_TRANSIENT, clean_error($@)); + $self->_bind($message); + + eval { + # reconnect if required + if (!$self->{mq}) { + $self->_connect(); } - return PUSH_RESULT_OK; + # send message + $logger->debug('AMQP: Publishing message'); + $self->{mq}->publish( + $self->{channel}, $message->routing_key, $message->payload, + {exchange => $config->{exchange},}, + {content_type => 'text/plain', content_encoding => '8bit',}, + ); + }; + if ($@) { + return (PUSH_RESULT_TRANSIENT, clean_error($@)); + } + + return PUSH_RESULT_OK; } 1; diff --git a/extensions/Push/lib/Connector.disabled/ServiceNow.pm b/extensions/Push/lib/Connector.disabled/ServiceNow.pm index d0ebdcf10..032e47dde 100644 --- a/extensions/Push/lib/Connector.disabled/ServiceNow.pm +++ b/extensions/Push/lib/Connector.disabled/ServiceNow.pm @@ -32,403 +32,411 @@ use MIME::Base64; use Net::LDAP; use constant SEND_COMPONENTS => ( - { - product => 'mozilla.org', - component => 'Server Operations: Desktop Issues', - }, + {product => 'mozilla.org', component => 'Server Operations: Desktop Issues',}, ); sub options { - return ( - { - name => 'bugzilla_user', - label => 'Bugzilla Service-Now User', - type => 'string', - default => 'service.now@bugzilla.tld', - required => 1, - validate => sub { - Bugzilla::User->new({ name => $_[0] }) - || die "Invalid Bugzilla user ($_[0])\n"; - }, - }, - { - name => 'ldap_scheme', - label => 'Mozilla LDAP Scheme', - type => 'select', - values => [ 'LDAP', 'LDAPS' ], - default => 'LDAPS', - required => 1, - }, - { - name => 'ldap_host', - label => 'Mozilla LDAP Host', - type => 'string', - default => '', - required => 1, - }, - { - name => 'ldap_user', - label => 'Mozilla LDAP Bind Username', - type => 'string', - default => '', - required => 1, - }, - { - name => 'ldap_pass', - label => 'Mozilla LDAP Password', - type => 'password', - default => '', - required => 1, - }, - { - name => 'ldap_poll', - label => 'Mozilla LDAP Poll Frequency', - type => 'string', - default => '3', - required => 1, - help => 'minutes', - validate => sub { - $_[0] =~ /\D/ - && die "LDAP Poll Frequency must be an integer\n"; - $_[0] == 0 - && die "LDAP Poll Frequency cannot be less than one minute\n"; - }, - }, - { - name => 'service_now_url', - label => 'Service Now JSON URL', - type => 'string', - default => 'https://mozilladev.service-now.com', - required => 1, - help => "Must start with https:// and end with ?JSON", - validate => sub { - $_[0] =~ m#^https://[^\.\/]+\.service-now\.com\/# - || die "Invalid Service Now JSON URL\n"; - $_[0] =~ m#\?JSON$# - || die "Invalid Service Now JSON URL (must end with ?JSON)\n"; - }, - }, - { - name => 'service_now_user', - label => 'Service Now JSON Username', - type => 'string', - default => '', - required => 1, - }, - { - name => 'service_now_pass', - label => 'Service Now JSON Password', - type => 'password', - default => '', - required => 1, - }, - ); + return ( + { + name => 'bugzilla_user', + label => 'Bugzilla Service-Now User', + type => 'string', + default => 'service.now@bugzilla.tld', + required => 1, + validate => sub { + Bugzilla::User->new({name => $_[0]}) || die "Invalid Bugzilla user ($_[0])\n"; + }, + }, + { + name => 'ldap_scheme', + label => 'Mozilla LDAP Scheme', + type => 'select', + values => ['LDAP', 'LDAPS'], + default => 'LDAPS', + required => 1, + }, + { + name => 'ldap_host', + label => 'Mozilla LDAP Host', + type => 'string', + default => '', + required => 1, + }, + { + name => 'ldap_user', + label => 'Mozilla LDAP Bind Username', + type => 'string', + default => '', + required => 1, + }, + { + name => 'ldap_pass', + label => 'Mozilla LDAP Password', + type => 'password', + default => '', + required => 1, + }, + { + name => 'ldap_poll', + label => 'Mozilla LDAP Poll Frequency', + type => 'string', + default => '3', + required => 1, + help => 'minutes', + validate => sub { + $_[0] =~ /\D/ && die "LDAP Poll Frequency must be an integer\n"; + $_[0] == 0 && die "LDAP Poll Frequency cannot be less than one minute\n"; + }, + }, + { + name => 'service_now_url', + label => 'Service Now JSON URL', + type => 'string', + default => 'https://mozilladev.service-now.com', + required => 1, + help => "Must start with https:// and end with ?JSON", + validate => sub { + $_[0] =~ m#^https://[^\.\/]+\.service-now\.com\/# + || die "Invalid Service Now JSON URL\n"; + $_[0] =~ m#\?JSON$# + || die "Invalid Service Now JSON URL (must end with ?JSON)\n"; + }, + }, + { + name => 'service_now_user', + label => 'Service Now JSON Username', + type => 'string', + default => '', + required => 1, + }, + { + name => 'service_now_pass', + label => 'Service Now JSON Password', + type => 'password', + default => '', + required => 1, + }, + ); } sub options_validate { - my ($self, $config) = @_; - my $host = $config->{ldap_host}; - trick_taint($host); - my $scheme = lc($config->{ldap_scheme}); - eval { - my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die', timeout => 5) - or die $!; - $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass}); - }; - if ($@) { - die sprintf("Failed to connect to %s://%s/: %s\n", $scheme, $host, $@); - } + my ($self, $config) = @_; + my $host = $config->{ldap_host}; + trick_taint($host); + my $scheme = lc($config->{ldap_scheme}); + eval { + my $ldap + = Net::LDAP->new($host, scheme => $scheme, onerror => 'die', timeout => 5) + or die $!; + $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass}); + }; + if ($@) { + die sprintf("Failed to connect to %s://%s/: %s\n", $scheme, $host, $@); + } } my $_instance; sub init { - my ($self) = @_; - $_instance = $self; + my ($self) = @_; + $_instance = $self; } sub load_config { - my ($self) = @_; - $self->SUPER::load_config(@_); - $self->{bugzilla_user} ||= Bugzilla::User->new({ name => $self->config->{bugzilla_user} }); + my ($self) = @_; + $self->SUPER::load_config(@_); + $self->{bugzilla_user} + ||= Bugzilla::User->new({name => $self->config->{bugzilla_user}}); } sub should_send { - my ($self, $message) = @_; - - my $data = $message->payload_decoded; - my $bug_data = $self->_get_bug_data($data) - || return 0; - - # we don't want to send the initial comment in a separate message - # because we inject it into the inital message - if (exists $data->{comment} && $data->{comment}->{number} == 0) { - return 0; - } - - my $target = $data->{event}->{target}; - unless ($target eq 'bug' || $target eq 'comment' || $target eq 'attachment') { - return 0; - } - - # ensure the service-now user can see the bug - if (!$self->{bugzilla_user} || !$self->{bugzilla_user}->is_enabled) { - return 0; - } - $self->{bugzilla_user}->can_see_bug($bug_data->{id}) - || return 0; - - # don't push changes made by the service-now account - $data->{event}->{user}->{id} == $self->{bugzilla_user}->id - && return 0; - - # filter based on the component - my $bug = Bugzilla::Bug->new($bug_data->{id}); - my $send = 0; - foreach my $rh (SEND_COMPONENTS) { - if ($bug->product eq $rh->{product} && $bug->component eq $rh->{component}) { - $send = 1; - last; - } + my ($self, $message) = @_; + + my $data = $message->payload_decoded; + my $bug_data = $self->_get_bug_data($data) || return 0; + + # we don't want to send the initial comment in a separate message + # because we inject it into the inital message + if (exists $data->{comment} && $data->{comment}->{number} == 0) { + return 0; + } + + my $target = $data->{event}->{target}; + unless ($target eq 'bug' || $target eq 'comment' || $target eq 'attachment') { + return 0; + } + + # ensure the service-now user can see the bug + if (!$self->{bugzilla_user} || !$self->{bugzilla_user}->is_enabled) { + return 0; + } + $self->{bugzilla_user}->can_see_bug($bug_data->{id}) || return 0; + + # don't push changes made by the service-now account + $data->{event}->{user}->{id} == $self->{bugzilla_user}->id && return 0; + + # filter based on the component + my $bug = Bugzilla::Bug->new($bug_data->{id}); + my $send = 0; + foreach my $rh (SEND_COMPONENTS) { + if ($bug->product eq $rh->{product} && $bug->component eq $rh->{component}) { + $send = 1; + last; } - return $send; + } + return $send; } sub send { - my ($self, $message) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - # should_send intiailises bugzilla_user; make sure we return a useful error message - if (!$self->{bugzilla_user}) { - return (PUSH_RESULT_TRANSIENT, "Invalid bugzilla-user (" . $self->config->{bugzilla_user} . ")"); - } - - # load the bug - my $data = $message->payload_decoded; - my $bug_data = $self->_get_bug_data($data); - my $bug = Bugzilla::Bug->new($bug_data->{id}); - - if ($message->routing_key eq 'bug.create') { - # inject the comment into the data for new bugs - my $comment = shift @{ $bug->comments }; - if ($comment->body ne '') { - $bug_data->{comment} = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($comment, 1); - } - - } elsif ($message->routing_key eq 'attachment.create') { - # inject the attachment payload - my $attachment = Bugzilla::Attachment->new($data->{attachment}->{id}); - $data->{attachment}->{data} = encode_base64($attachment->data); - } - - # map bmo login to ldap login and insert into json payload - $self->_add_ldap_logins($data, {}); - - # flatten json data - $self->_flatten($data); - - # add sysparm_action - $data->{sysparm_action} = 'insert'; - - if ($logger->debugging) { - $logger->debug(to_json(ref($data) ? $data : from_json($data), 1)); - } - - # send to service-now - my $request = HTTP::Request->new(POST => $self->config->{service_now_url}); - $request->content_type('application/json'); - $request->content(to_json($data)); - $request->authorization_basic($self->config->{service_now_user}, $self->config->{service_now_pass}); - - $self->{lwp} ||= LWP::UserAgent->new(agent => Bugzilla->localconfig->{urlbase}); - my $result = $self->{lwp}->request($request); - - # http level errors - if (!$result->is_success) { - # treat these as transient - return (PUSH_RESULT_TRANSIENT, $result->status_line); - } - - # empty response - if (length($result->content) == 0) { - # malformed request, treat as transient to allow code to fix - # may also be misconfiguration on servicenow, also transient - return (PUSH_RESULT_TRANSIENT, "Empty response"); + my ($self, $message) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + +# should_send intiailises bugzilla_user; make sure we return a useful error message + if (!$self->{bugzilla_user}) { + return (PUSH_RESULT_TRANSIENT, + "Invalid bugzilla-user (" . $self->config->{bugzilla_user} . ")"); + } + + # load the bug + my $data = $message->payload_decoded; + my $bug_data = $self->_get_bug_data($data); + my $bug = Bugzilla::Bug->new($bug_data->{id}); + + if ($message->routing_key eq 'bug.create') { + + # inject the comment into the data for new bugs + my $comment = shift @{$bug->comments}; + if ($comment->body ne '') { + $bug_data->{comment} + = Bugzilla::Extension::Push::Serialise->instance->object_to_hash($comment, 1); } - # json errors - my $result_data; - eval { - $result_data = from_json($result->content); - }; - if ($@) { - return (PUSH_RESULT_TRANSIENT, clean_error($@)); - } - if ($logger->debugging) { - $logger->debug(to_json($result_data, 1)); - } - if (exists $result_data->{error}) { - return (PUSH_RESULT_ERROR, $result_data->{error}); - }; - - # malformed/unexpected json response - if (!exists $result_data->{records} - || ref($result_data->{records}) ne 'ARRAY' - || scalar(@{$result_data->{records}}) == 0 - ) { - return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: missing or empty 'records' array"); - } - - my $record = $result_data->{records}->[0]; - if (ref($record) ne 'HASH') { - return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: 'records' array does not contain an object"); - } + } + elsif ($message->routing_key eq 'attachment.create') { + + # inject the attachment payload + my $attachment = Bugzilla::Attachment->new($data->{attachment}->{id}); + $data->{attachment}->{data} = encode_base64($attachment->data); + } + + # map bmo login to ldap login and insert into json payload + $self->_add_ldap_logins($data, {}); + + # flatten json data + $self->_flatten($data); + + # add sysparm_action + $data->{sysparm_action} = 'insert'; + + if ($logger->debugging) { + $logger->debug(to_json(ref($data) ? $data : from_json($data), 1)); + } + + # send to service-now + my $request = HTTP::Request->new(POST => $self->config->{service_now_url}); + $request->content_type('application/json'); + $request->content(to_json($data)); + $request->authorization_basic($self->config->{service_now_user}, + $self->config->{service_now_pass}); + + $self->{lwp} ||= LWP::UserAgent->new(agent => Bugzilla->localconfig->{urlbase}); + my $result = $self->{lwp}->request($request); + + # http level errors + if (!$result->is_success) { + + # treat these as transient + return (PUSH_RESULT_TRANSIENT, $result->status_line); + } + + # empty response + if (length($result->content) == 0) { + + # malformed request, treat as transient to allow code to fix + # may also be misconfiguration on servicenow, also transient + return (PUSH_RESULT_TRANSIENT, "Empty response"); + } + + # json errors + my $result_data; + eval { $result_data = from_json($result->content); }; + if ($@) { + return (PUSH_RESULT_TRANSIENT, clean_error($@)); + } + if ($logger->debugging) { + $logger->debug(to_json($result_data, 1)); + } + if (exists $result_data->{error}) { + return (PUSH_RESULT_ERROR, $result_data->{error}); + } + + # malformed/unexpected json response + if (!exists $result_data->{records} + || ref($result_data->{records}) ne 'ARRAY' + || scalar(@{$result_data->{records}}) == 0) + { + return (PUSH_RESULT_ERROR, + "Malformed JSON response from ServiceNow: missing or empty 'records' array"); + } + + my $record = $result_data->{records}->[0]; + if (ref($record) ne 'HASH') { + return (PUSH_RESULT_ERROR, + "Malformed JSON response from ServiceNow: 'records' array does not contain an object" + ); + } - # sys_id is the unique identifier for this action - if (!exists $record->{sys_id} || $record->{sys_id} eq '') { - return (PUSH_RESULT_ERROR, "Malformed JSON response from ServiceNow: 'records object' does not contain a valid sys_id"); - } + # sys_id is the unique identifier for this action + if (!exists $record->{sys_id} || $record->{sys_id} eq '') { + return (PUSH_RESULT_ERROR, + "Malformed JSON response from ServiceNow: 'records object' does not contain a valid sys_id" + ); + } - # success - return (PUSH_RESULT_OK, "sys_id: " . $record->{sys_id}); + # success + return (PUSH_RESULT_OK, "sys_id: " . $record->{sys_id}); } sub _get_bug_data { - my ($self, $data) = @_; - my $target = $data->{event}->{target}; - if ($target eq 'bug') { - return $data->{bug}; - } elsif (exists $data->{$target}->{bug}) { - return $data->{$target}->{bug}; - } else { - return; - } + my ($self, $data) = @_; + my $target = $data->{event}->{target}; + if ($target eq 'bug') { + return $data->{bug}; + } + elsif (exists $data->{$target}->{bug}) { + return $data->{$target}->{bug}; + } + else { + return; + } } sub _flatten { - # service-now expects a flat json object - my ($self, $data) = @_; - my $target = $data->{event}->{target}; + # service-now expects a flat json object + my ($self, $data) = @_; - # delete unnecessary deep objects - if ($target eq 'comment' || $target eq 'attachment') { - $data->{$target}->{bug_id} = $data->{$target}->{bug}->{id}; - delete $data->{$target}->{bug}; - } - delete $data->{event}->{changes}; + my $target = $data->{event}->{target}; - $self->_flatten_hash($data, $data, 'u'); + # delete unnecessary deep objects + if ($target eq 'comment' || $target eq 'attachment') { + $data->{$target}->{bug_id} = $data->{$target}->{bug}->{id}; + delete $data->{$target}->{bug}; + } + delete $data->{event}->{changes}; + + $self->_flatten_hash($data, $data, 'u'); } sub _flatten_hash { - my ($self, $base_hash, $hash, $prefix) = @_; - foreach my $key (keys %$hash) { - if (ref($hash->{$key}) eq 'HASH') { - $self->_flatten_hash($base_hash, $hash->{$key}, $prefix . "_$key"); - } elsif (ref($hash->{$key}) ne 'ARRAY') { - $base_hash->{$prefix . "_$key"} = $hash->{$key}; - } - delete $hash->{$key}; + my ($self, $base_hash, $hash, $prefix) = @_; + foreach my $key (keys %$hash) { + if (ref($hash->{$key}) eq 'HASH') { + $self->_flatten_hash($base_hash, $hash->{$key}, $prefix . "_$key"); + } + elsif (ref($hash->{$key}) ne 'ARRAY') { + $base_hash->{$prefix . "_$key"} = $hash->{$key}; } + delete $hash->{$key}; + } } sub _add_ldap_logins { - my ($self, $rh, $cache) = @_; - if (exists $rh->{login}) { - my $login = $rh->{login}; - $cache->{$login} ||= $self->_bmo_to_ldap($login); - Bugzilla->push_ext->logger->debug("BMO($login) --> LDAP(" . $cache->{$login} . ")"); - $rh->{ldap} = $cache->{$login}; - } - foreach my $key (keys %$rh) { - next unless ref($rh->{$key}) eq 'HASH'; - $self->_add_ldap_logins($rh->{$key}, $cache); - } + my ($self, $rh, $cache) = @_; + if (exists $rh->{login}) { + my $login = $rh->{login}; + $cache->{$login} ||= $self->_bmo_to_ldap($login); + Bugzilla->push_ext->logger->debug( + "BMO($login) --> LDAP(" . $cache->{$login} . ")"); + $rh->{ldap} = $cache->{$login}; + } + foreach my $key (keys %$rh) { + next unless ref($rh->{$key}) eq 'HASH'; + $self->_add_ldap_logins($rh->{$key}, $cache); + } } sub _bmo_to_ldap { - my ($self, $login) = @_; - my $ldap = $self->_ldap_cache(); + my ($self, $login) = @_; + my $ldap = $self->_ldap_cache(); - return '' unless $login =~ /\@mozilla\.(?:com|org)$/; + return '' unless $login =~ /\@mozilla\.(?:com|org)$/; - foreach my $check ($login, canon_email($login)) { - # check for matching bugmail entry - foreach my $mail (keys %$ldap) { - next unless $ldap->{$mail}{bugmail_canon} eq $check; - return $mail; - } + foreach my $check ($login, canon_email($login)) { - # check for matching mail - if (exists $ldap->{$check}) { - return $check; - } + # check for matching bugmail entry + foreach my $mail (keys %$ldap) { + next unless $ldap->{$mail}{bugmail_canon} eq $check; + return $mail; + } - # check for matching email alias - foreach my $mail (sort keys %$ldap) { - next unless grep { $check eq $_ } @{$ldap->{$mail}{aliases}}; - return $mail; - } + # check for matching mail + if (exists $ldap->{$check}) { + return $check; + } + + # check for matching email alias + foreach my $mail (sort keys %$ldap) { + next unless grep { $check eq $_ } @{$ldap->{$mail}{aliases}}; + return $mail; } + } - return ''; + return ''; } sub _ldap_cache { - my ($self) = @_; - my $logger = Bugzilla->push_ext->logger; - my $config = $self->config; - - # cache of all ldap entries; updated infrequently - if (!$self->{ldap_cache_time} || (time) - $self->{ldap_cache_time} > $config->{ldap_poll} * 60) { - $logger->debug('refreshing LDAP cache'); - - my $cache = {}; - - my $host = $config->{ldap_host}; - trick_taint($host); - my $scheme = lc($config->{ldap_scheme}); - my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die') - or die $!; - $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass}); - foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') { - my $result = $ldap->search( - base => $ldap_base, - scope => 'sub', - filter => '(mail=*)', - attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'], - ); - foreach my $entry ($result->entries) { - my ($name, $bugMail, $mail, $type) = - map { $entry->get_value($_) || '' } - qw(cn bugzillaEmail mail employeeType); - next if $type eq 'DISABLED'; - $mail = lc $mail; - $bugMail = '' if $bugMail !~ /\@/; - $bugMail = trim($bugMail); - if ($bugMail =~ / /) { - $bugMail = (grep { /\@/ } split / /, $bugMail)[0]; - } - $name =~ s/\s+/ /g; - $cache->{$mail}{name} = trim($name); - $cache->{$mail}{bugmail} = $bugMail; - $cache->{$mail}{bugmail_canon} = canon_email($bugMail); - $cache->{$mail}{aliases} = []; - foreach my $alias ( - @{$entry->get_value('emailAlias', asref => 1) || []} - ) { - push @{$cache->{$mail}{aliases}}, canon_email($alias); - } - } - } + my ($self) = @_; + my $logger = Bugzilla->push_ext->logger; + my $config = $self->config; + + # cache of all ldap entries; updated infrequently + if (!$self->{ldap_cache_time} + || (time) - $self->{ldap_cache_time} > $config->{ldap_poll} * 60) + { + $logger->debug('refreshing LDAP cache'); - $self->{ldap_cache} = $cache; - $self->{ldap_cache_time} = (time); + my $cache = {}; + + my $host = $config->{ldap_host}; + trick_taint($host); + my $scheme = lc($config->{ldap_scheme}); + my $ldap = Net::LDAP->new($host, scheme => $scheme, onerror => 'die') or die $!; + $ldap->bind($config->{ldap_user}, password => $config->{ldap_pass}); + foreach my $ldap_base ('o=com,dc=mozilla', 'o=org,dc=mozilla') { + my $result = $ldap->search( + base => $ldap_base, + scope => 'sub', + filter => '(mail=*)', + attrs => ['mail', 'bugzillaEmail', 'emailAlias', 'cn', 'employeeType'], + ); + foreach my $entry ($result->entries) { + my ($name, $bugMail, $mail, $type) + = map { $entry->get_value($_) || '' } qw(cn bugzillaEmail mail employeeType); + next if $type eq 'DISABLED'; + $mail = lc $mail; + $bugMail = '' if $bugMail !~ /\@/; + $bugMail = trim($bugMail); + if ($bugMail =~ / /) { + $bugMail = (grep {/\@/} split / /, $bugMail)[0]; + } + $name =~ s/\s+/ /g; + $cache->{$mail}{name} = trim($name); + $cache->{$mail}{bugmail} = $bugMail; + $cache->{$mail}{bugmail_canon} = canon_email($bugMail); + $cache->{$mail}{aliases} = []; + foreach my $alias (@{$entry->get_value('emailAlias', asref => 1) || []}) { + push @{$cache->{$mail}{aliases}}, canon_email($alias); + } + } } - return $self->{ldap_cache}; + $self->{ldap_cache} = $cache; + $self->{ldap_cache_time} = (time); + } + + return $self->{ldap_cache}; } 1; diff --git a/extensions/Push/lib/Connector/Base.pm b/extensions/Push/lib/Connector/Base.pm index ee41bd160..bd46fe6b4 100644 --- a/extensions/Push/lib/Connector/Base.pm +++ b/extensions/Push/lib/Connector/Base.pm @@ -18,59 +18,65 @@ use Bugzilla::Extension::Push::BacklogQueue; use Bugzilla::Extension::Push::Backoff; sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - ($self->{name}) = $class =~ /^.+:(.+)$/; - $self->init(); - return $self; + my ($class) = @_; + my $self = {}; + bless($self, $class); + ($self->{name}) = $class =~ /^.+:(.+)$/; + $self->init(); + return $self; } sub name { - my $self = shift; - return $self->{name}; + my $self = shift; + return $self->{name}; } sub init { - my ($self) = @_; - # abstract - # perform any initialisation here - # will be run when created by the web pages or by the daemon - # and also when the configuration needs to be reloaded + my ($self) = @_; + + # abstract + # perform any initialisation here + # will be run when created by the web pages or by the daemon + # and also when the configuration needs to be reloaded } sub stop { - my ($self) = @_; - # abstract - # run from the daemon only; disconnect from remote hosts, etc + my ($self) = @_; + + # abstract + # run from the daemon only; disconnect from remote hosts, etc } sub should_send { - my ($self, $message) = @_; - # abstract - # return boolean indicating if the connector will be sending the message. - # this will be called each message, and should be a very quick simple test. - # the connector can perform a more exhaustive test in the send() method. - return 0; + my ($self, $message) = @_; + + # abstract + # return boolean indicating if the connector will be sending the message. + # this will be called each message, and should be a very quick simple test. + # the connector can perform a more exhaustive test in the send() method. + return 0; } sub send { - my ($self, $message) = @_; - # abstract - # deliver the message, daemon only + my ($self, $message) = @_; + + # abstract + # deliver the message, daemon only } sub options { - my ($self) = @_; - # abstract - # return an array of configuration variables - return (); + my ($self) = @_; + + # abstract + # return an array of configuration variables + return (); } sub options_validate { - my ($class, $config) = @_; - # abstract, static - # die if a combination of options in $config is invalid + my ($class, $config) = @_; + + # abstract, static + # die if a combination of options in $config is invalid } # @@ -78,29 +84,30 @@ sub options_validate { # sub config { - my ($self) = @_; - if (!$self->{config}) { - $self->load_config(); - } - return $self->{config}; + my ($self) = @_; + if (!$self->{config}) { + $self->load_config(); + } + return $self->{config}; } sub load_config { - my ($self) = @_; - my $config = Bugzilla::Extension::Push::Config->new($self->name, $self->options); - $config->load(); - $self->{config} = $config; + my ($self) = @_; + my $config + = Bugzilla::Extension::Push::Config->new($self->name, $self->options); + $config->load(); + $self->{config} = $config; } sub enabled { - my ($self) = @_; - return $self->config->{enabled} eq 'Enabled'; + my ($self) = @_; + return $self->config->{enabled} eq 'Enabled'; } sub backlog { - my ($self) = @_; - $self->{backlog} ||= Bugzilla::Extension::Push::BacklogQueue->new($self->name); - return $self->{backlog}; + my ($self) = @_; + $self->{backlog} ||= Bugzilla::Extension::Push::BacklogQueue->new($self->name); + return $self->{backlog}; } 1; diff --git a/extensions/Push/lib/Connector/File.pm b/extensions/Push/lib/Connector/File.pm index ae249ffe5..7d86953f8 100644 --- a/extensions/Push/lib/Connector/File.pm +++ b/extensions/Push/lib/Connector/File.pm @@ -20,51 +20,46 @@ use Encode; use FileHandle; sub init { - my ($self) = @_; + my ($self) = @_; } sub options { - return ( - { - name => 'filename', - label => 'Filename', - type => 'string', - default => 'push.log', - required => 1, - validate => sub { - my $filename = shift; - $filename =~ m#^/# - && die "Absolute paths are not permitted\n"; - $filename =~ m#\.\.# - && die "Relative paths are not permitted\n"; - }, - }, - ); + return ( + { + name => 'filename', + label => 'Filename', + type => 'string', + default => 'push.log', + required => 1, + validate => sub { + my $filename = shift; + $filename =~ m#^/# && die "Absolute paths are not permitted\n"; + $filename =~ m#\.\.# && die "Relative paths are not permitted\n"; + }, + }, + ); } sub should_send { - my ($self, $message) = @_; - return 1; + my ($self, $message) = @_; + return 1; } sub send { - my ($self, $message) = @_; + my ($self, $message) = @_; - # pretty-format json payload - my $payload = $message->payload_decoded; - $payload = to_json($payload, 1); + # pretty-format json payload + my $payload = $message->payload_decoded; + $payload = to_json($payload, 1); - my $filename = bz_locations()->{'datadir'} . '/' . $self->config->{filename}; - Bugzilla->push_ext->logger->debug("File: Appending to $filename"); - my $fh = FileHandle->new(">>$filename"); - $fh->binmode(':utf8'); - $fh->print( - "[" . scalar(localtime) . "]\n" . - $payload . "\n\n" - ); - $fh->close; + my $filename = bz_locations()->{'datadir'} . '/' . $self->config->{filename}; + Bugzilla->push_ext->logger->debug("File: Appending to $filename"); + my $fh = FileHandle->new(">>$filename"); + $fh->binmode(':utf8'); + $fh->print("[" . scalar(localtime) . "]\n" . $payload . "\n\n"); + $fh->close; - return PUSH_RESULT_OK; + return PUSH_RESULT_OK; } 1; diff --git a/extensions/Push/lib/Connector/Phabricator.pm b/extensions/Push/lib/Connector/Phabricator.pm index 33e2bb6ad..61a39e32b 100644 --- a/extensions/Push/lib/Connector/Phabricator.pm +++ b/extensions/Push/lib/Connector/Phabricator.pm @@ -29,105 +29,96 @@ use Bugzilla::Extension::Push::Constants; use Bugzilla::Extension::Push::Util qw(is_public); sub options { - return ( - { - name => 'phabricator_url', - label => 'Phabricator URL', - type => 'string', - default => '', - required => 1, - } - ); + return ({ + name => 'phabricator_url', + label => 'Phabricator URL', + type => 'string', + default => '', + required => 1, + }); } sub should_send { - my ( $self, $message ) = @_; + my ($self, $message) = @_; - return 0 unless Bugzilla->params->{phabricator_enabled}; + return 0 unless Bugzilla->params->{phabricator_enabled}; - # We are only interested currently in bug group, assignee, qa-contact, or cc changes. - return 0 - unless $message->routing_key =~ - /^(?:attachment|bug)\.modify:.*\b(bug_group|assigned_to|qa_contact|cc)\b/; +# We are only interested currently in bug group, assignee, qa-contact, or cc changes. + return 0 + unless $message->routing_key + =~ /^(?:attachment|bug)\.modify:.*\b(bug_group|assigned_to|qa_contact|cc)\b/; - my $bug = $self->_get_bug_by_data( $message->payload_decoded ) || return 0; + my $bug = $self->_get_bug_by_data($message->payload_decoded) || return 0; - return $bug->has_attachment_with_mimetype(PHAB_CONTENT_TYPE); + return $bug->has_attachment_with_mimetype(PHAB_CONTENT_TYPE); } sub send { - my ( $self, $message ) = @_; - - my $logger = Bugzilla->push_ext->logger; - - my $data = $message->payload_decoded; - - my $bug = $self->_get_bug_by_data($data) || return PUSH_RESULT_OK; - - my $is_public = is_public($bug); - - my $revisions = get_attachment_revisions($bug); - - my $group_change = - ($message->routing_key =~ /^(?:attachment|bug)\.modify:.*\bbug_group\b/) - ? 1 - : 0; - - foreach my $revision (@$revisions) { - if ( $is_public && $group_change ) { - Bugzilla->audit(sprintf( - 'Making revision %s public for bug %s', - $revision->id, - $bug->id - )); - $revision->make_public(); - } - elsif ( !$is_public && $group_change ) { - Bugzilla->audit(sprintf( - 'Giving revision %s a custom policy for bug %s', - $revision->id, - $bug->id - )); - my $set_project_names = [ map { "bmo-" . $_->name } @{ $bug->groups_in } ]; - $revision->make_private($set_project_names); - } - - # Subscriber list of the private revision should always match - # the bug roles such as assignee, qa contact, and cc members. - if (!$is_public) { - Bugzilla->audit(sprintf( - 'Updating subscribers for %s for bug %s', - $revision->id, - $bug->id - )); - my $subscribers = get_bug_role_phids($bug); - $revision->set_subscribers($subscribers) if $subscribers; - } - - $revision->update(); + my ($self, $message) = @_; + + my $logger = Bugzilla->push_ext->logger; + + my $data = $message->payload_decoded; + + my $bug = $self->_get_bug_by_data($data) || return PUSH_RESULT_OK; + + my $is_public = is_public($bug); + + my $revisions = get_attachment_revisions($bug); + + my $group_change + = ($message->routing_key =~ /^(?:attachment|bug)\.modify:.*\bbug_group\b/) + ? 1 + : 0; + + foreach my $revision (@$revisions) { + if ($is_public && $group_change) { + Bugzilla->audit( + sprintf('Making revision %s public for bug %s', $revision->id, $bug->id)); + $revision->make_public(); } + elsif (!$is_public && $group_change) { + Bugzilla->audit(sprintf( + 'Giving revision %s a custom policy for bug %s', + $revision->id, $bug->id + )); + my $set_project_names = [map { "bmo-" . $_->name } @{$bug->groups_in}]; + $revision->make_private($set_project_names); + } + + # Subscriber list of the private revision should always match + # the bug roles such as assignee, qa contact, and cc members. + if (!$is_public) { + Bugzilla->audit( + sprintf('Updating subscribers for %s for bug %s', $revision->id, $bug->id)); + my $subscribers = get_bug_role_phids($bug); + $revision->set_subscribers($subscribers) if $subscribers; + } + + $revision->update(); + } - return PUSH_RESULT_OK; + return PUSH_RESULT_OK; } sub _get_bug_by_data { - my ( $self, $data ) = @_; - my $bug_data = $self->_get_bug_data($data) || return 0; - my $bug = Bugzilla::Bug->new( { id => $bug_data->{id} } ); + my ($self, $data) = @_; + my $bug_data = $self->_get_bug_data($data) || return 0; + my $bug = Bugzilla::Bug->new({id => $bug_data->{id}}); } sub _get_bug_data { - my ( $self, $data ) = @_; - my $target = $data->{event}->{target}; - if ( $target eq 'bug' ) { - return $data->{bug}; - } - elsif ( exists $data->{$target}->{bug} ) { - return $data->{$target}->{bug}; - } - else { - return; - } + my ($self, $data) = @_; + my $target = $data->{event}->{target}; + if ($target eq 'bug') { + return $data->{bug}; + } + elsif (exists $data->{$target}->{bug}) { + return $data->{$target}->{bug}; + } + else { + return; + } } 1; diff --git a/extensions/Push/lib/Connector/Spark.pm b/extensions/Push/lib/Connector/Spark.pm index e58ddfbe4..1eb6f22c6 100644 --- a/extensions/Push/lib/Connector/Spark.pm +++ b/extensions/Push/lib/Connector/Spark.pm @@ -25,150 +25,154 @@ use LWP::UserAgent; use List::MoreUtils qw(any); sub options { - return ( - { - name => 'spark_endpoint', - label => 'Spark API Endpoint', - type => 'string', - default => 'https://api.ciscospark.com/v1', - required => 1, - }, - { - name => 'spark_room_id', - label => 'Spark Room ID', - type => 'string', - default => 'bugzilla', - required => 1, - }, - { - name => 'spark_api_key', - label => 'Spark API Key', - type => 'string', - default => '', - required => 1, - }, - ); + return ( + { + name => 'spark_endpoint', + label => 'Spark API Endpoint', + type => 'string', + default => 'https://api.ciscospark.com/v1', + required => 1, + }, + { + name => 'spark_room_id', + label => 'Spark Room ID', + type => 'string', + default => 'bugzilla', + required => 1, + }, + { + name => 'spark_api_key', + label => 'Spark API Key', + type => 'string', + default => '', + required => 1, + }, + ); } sub stop { - my ($self) = @_; + my ($self) = @_; } sub should_send { - my ($self, $message) = @_; - - my $data = $message->payload_decoded; - my $bug_data = $self->_get_bug_data($data) - || return 0; - - # Send if bug has cisco-spark keyword - my $bug = Bugzilla::Bug->new({ id => $bug_data->{id}, cache => 1 }); - return 0 unless $bug->has_keyword('cisco-spark'); - - if ($message->routing_key eq 'bug.create') { - return 1; - } - else { - foreach my $change (@{ $data->{event}->{changes} }) { - # send status and resolution updates - return 1 if $change->{field} eq 'bug_status' || $change->{field} eq 'resolution'; - # also send if the right keyword has been added to this bug - if ($change->{field} eq 'keywords' && $change->{added}) { - my @added = split(/, /, $change->{added}); - return 1 if any { $_ eq 'cisco-spark' } @added; - } - } + my ($self, $message) = @_; + + my $data = $message->payload_decoded; + my $bug_data = $self->_get_bug_data($data) || return 0; + + # Send if bug has cisco-spark keyword + my $bug = Bugzilla::Bug->new({id => $bug_data->{id}, cache => 1}); + return 0 unless $bug->has_keyword('cisco-spark'); + + if ($message->routing_key eq 'bug.create') { + return 1; + } + else { + foreach my $change (@{$data->{event}->{changes}}) { + + # send status and resolution updates + return 1 + if $change->{field} eq 'bug_status' || $change->{field} eq 'resolution'; + + # also send if the right keyword has been added to this bug + if ($change->{field} eq 'keywords' && $change->{added}) { + my @added = split(/, /, $change->{added}); + return 1 if any { $_ eq 'cisco-spark' } @added; + } } + } - # and nothing else - return 0; + # and nothing else + return 0; } sub send { - my ($self, $message) = @_; + my ($self, $message) = @_; - eval { - my $data = $message->payload_decoded(); - my $bug_data = $self->_get_bug_data($data); - my $bug = Bugzilla::Bug->new({ id => $bug_data->{id}, cache => 1 }); + eval { + my $data = $message->payload_decoded(); + my $bug_data = $self->_get_bug_data($data); + my $bug = Bugzilla::Bug->new({id => $bug_data->{id}, cache => 1}); - my $text = "Bug " . $bug->id . " - " . $bug->short_desc . "\n"; - if ($message->routing_key eq 'bug.create') { - $text = "New " . $text; + my $text = "Bug " . $bug->id . " - " . $bug->short_desc . "\n"; + if ($message->routing_key eq 'bug.create') { + $text = "New " . $text; + } + else { + foreach my $change (@{$data->{event}->{changes}}) { + if ($change->{field} eq 'bug_status') { + $text + .= "Status changed: " . $change->{removed} . " -> " . $change->{added} . "\n"; } - else { - foreach my $change (@{ $data->{event}->{changes} }) { - if ($change->{field} eq 'bug_status') { - $text .= "Status changed: " . - $change->{removed} . " -> " . $change->{added} . "\n"; - } - if ($change->{field} eq 'resolution') { - $text .= "Resolution changed: " . - ($change->{removed} ? $change->{removed} . " -> " : "") . $change->{added} . "\n"; - } - } + if ($change->{field} eq 'resolution') { + $text + .= "Resolution changed: " + . ($change->{removed} ? $change->{removed} . " -> " : "") + . $change->{added} . "\n"; } - $text .= Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=" . $bug->id; + } + } + $text .= Bugzilla->localconfig->{urlbase} . "show_bug.cgi?id=" . $bug->id; - my $room_id = $self->config->{spark_room_id}; - my $message_uri = $self->_spark_uri('messages'); + my $room_id = $self->config->{spark_room_id}; + my $message_uri = $self->_spark_uri('messages'); - my $json_data = { roomId => $room_id, text => $text }; + my $json_data = {roomId => $room_id, text => $text}; - my $headers = HTTP::Headers->new( - Content_Type => 'application/json' - ); - my $request = HTTP::Request->new('POST', $message_uri, $headers, encode_json($json_data)); - my $resp = $self->_user_agent->request($request); + my $headers = HTTP::Headers->new(Content_Type => 'application/json'); + my $request + = HTTP::Request->new('POST', $message_uri, $headers, encode_json($json_data)); + my $resp = $self->_user_agent->request($request); - if ($resp->code != 200) { - die "Expected HTTP 200 response, got " . $resp->code; - } - }; - if ($@) { - return (PUSH_RESULT_TRANSIENT, clean_error($@)); + if ($resp->code != 200) { + die "Expected HTTP 200 response, got " . $resp->code; } + }; + if ($@) { + return (PUSH_RESULT_TRANSIENT, clean_error($@)); + } - return PUSH_RESULT_OK; + return PUSH_RESULT_OK; } # Private methods sub _get_bug_data { - my ($self, $data) = @_; - my $target = $data->{event}->{target}; - if ($target eq 'bug') { - return $data->{bug}; - } elsif (exists $data->{$target}->{bug}) { - return $data->{$target}->{bug}; - } else { - return; - } + my ($self, $data) = @_; + my $target = $data->{event}->{target}; + if ($target eq 'bug') { + return $data->{bug}; + } + elsif (exists $data->{$target}->{bug}) { + return $data->{$target}->{bug}; + } + else { + return; + } } sub _user_agent { - my ($self) = @_; - my $ua = LWP::UserAgent->new(agent => 'Bugzilla'); - $ua->timeout(10); - $ua->protocols_allowed(['http', 'https']); - - if (my $proxy_url = Bugzilla->params->{proxy_url}) { - $ua->proxy(['http', 'https'], $proxy_url); - } - else { - $ua->env_proxy(); - } - - $ua->default_header( - 'Authorization' => 'Bearer ' . $self->config->{spark_api_key} - ); - - return $ua; + my ($self) = @_; + my $ua = LWP::UserAgent->new(agent => 'Bugzilla'); + $ua->timeout(10); + $ua->protocols_allowed(['http', 'https']); + + if (my $proxy_url = Bugzilla->params->{proxy_url}) { + $ua->proxy(['http', 'https'], $proxy_url); + } + else { + $ua->env_proxy(); + } + + $ua->default_header( + 'Authorization' => 'Bearer ' . $self->config->{spark_api_key}); + + return $ua; } sub _spark_uri { - my ($self, $path) = @_; - return URI->new($self->config->{spark_endpoint} . "/" . $path); + my ($self, $path) = @_; + return URI->new($self->config->{spark_endpoint} . "/" . $path); } 1; diff --git a/extensions/Push/lib/Connectors.pm b/extensions/Push/lib/Connectors.pm index d3c55d3ca..9a3856c02 100644 --- a/extensions/Push/lib/Connectors.pm +++ b/extensions/Push/lib/Connectors.pm @@ -19,94 +19,97 @@ use File::Basename; use Try::Tiny; sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - - $self->{names} = []; - $self->{objects} = {}; - $self->{path} = bz_locations->{'extensionsdir'} . '/Push/lib/Connector'; - - foreach my $file (glob($self->{path} . '/*.pm')) { - my $name = basename($file); - $name =~ s/\.pm$//; - next if $name eq 'Base'; - if (length($name) > 32) { - WARN("Ignoring connector '$name': Name longer than 32 characters"); - } - push @{$self->{names}}, $name; - TRACE("Found connector '$name'"); + my ($class) = @_; + my $self = {}; + bless($self, $class); + + $self->{names} = []; + $self->{objects} = {}; + $self->{path} = bz_locations->{'extensionsdir'} . '/Push/lib/Connector'; + + foreach my $file (glob($self->{path} . '/*.pm')) { + my $name = basename($file); + $name =~ s/\.pm$//; + next if $name eq 'Base'; + if (length($name) > 32) { + WARN("Ignoring connector '$name': Name longer than 32 characters"); } + push @{$self->{names}}, $name; + TRACE("Found connector '$name'"); + } - return $self; + return $self; } sub _load { - my ($self) = @_; - return if scalar keys %{$self->{objects}}; - - foreach my $name (@{$self->{names}}) { - next if exists $self->{objects}->{$name}; - my $file = $self->{path} . "/$name.pm"; - trick_taint($file); - require $file; - my $package = "Bugzilla::Extension::Push::Connector::$name"; - - TRACE("Loading connector '$name'"); - my $old_error_mode = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - try { - my $connector = $package->new(); - $connector->load_config(); - $self->{objects}->{$name} = $connector; - } catch { - ERROR("Connector '$name' failed to load: " . clean_error($_)); - }; - Bugzilla->error_mode($old_error_mode); + my ($self) = @_; + return if scalar keys %{$self->{objects}}; + + foreach my $name (@{$self->{names}}) { + next if exists $self->{objects}->{$name}; + my $file = $self->{path} . "/$name.pm"; + trick_taint($file); + require $file; + my $package = "Bugzilla::Extension::Push::Connector::$name"; + + TRACE("Loading connector '$name'"); + my $old_error_mode = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + try { + my $connector = $package->new(); + $connector->load_config(); + $self->{objects}->{$name} = $connector; } + catch { + ERROR("Connector '$name' failed to load: " . clean_error($_)); + }; + Bugzilla->error_mode($old_error_mode); + } } sub stop { - my ($self) = @_; - foreach my $connector ($self->list) { - next unless $connector->enabled; - TRACE("Stopping '" . $connector->name . "'"); - try { - $connector->stop(); - } catch { - ERROR("Connector '" . $connector->name . "' failed to stop: " . clean_error($_)); - }; + my ($self) = @_; + foreach my $connector ($self->list) { + next unless $connector->enabled; + TRACE("Stopping '" . $connector->name . "'"); + try { + $connector->stop(); } + catch { + ERROR( + "Connector '" . $connector->name . "' failed to stop: " . clean_error($_)); + }; + } } sub reload { - my ($self) = @_; - $self->stop(); - $self->{objects} = {}; - $self->_load(); + my ($self) = @_; + $self->stop(); + $self->{objects} = {}; + $self->_load(); } sub names { - my ($self) = @_; - return @{$self->{names}}; + my ($self) = @_; + return @{$self->{names}}; } sub list { - my ($self) = @_; - $self->_load(); - return sort { $a->name cmp $b->name } values %{$self->{objects}}; + my ($self) = @_; + $self->_load(); + return sort { $a->name cmp $b->name } values %{$self->{objects}}; } sub exists { - my ($self, $name) = @_; - $self->by_name($name) ? 1 : 0; + my ($self, $name) = @_; + $self->by_name($name) ? 1 : 0; } sub by_name { - my ($self, $name) = @_; - $self->_load(); - return unless exists $self->{objects}->{$name}; - return $self->{objects}->{$name}; + my ($self, $name) = @_; + $self->_load(); + return unless exists $self->{objects}->{$name}; + return $self->{objects}->{$name}; } 1; diff --git a/extensions/Push/lib/Constants.pm b/extensions/Push/lib/Constants.pm index 09c5e464c..c021b32f4 100644 --- a/extensions/Push/lib/Constants.pm +++ b/extensions/Push/lib/Constants.pm @@ -14,14 +14,14 @@ use warnings; use base 'Exporter'; our @EXPORT = qw( - PUSH_RESULT_OK - PUSH_RESULT_IGNORED - PUSH_RESULT_TRANSIENT - PUSH_RESULT_ERROR - PUSH_RESULT_UNKNOWN - push_result_to_string - - POLL_INTERVAL_SECONDS + PUSH_RESULT_OK + PUSH_RESULT_IGNORED + PUSH_RESULT_TRANSIENT + PUSH_RESULT_ERROR + PUSH_RESULT_UNKNOWN + push_result_to_string + + POLL_INTERVAL_SECONDS ); use constant PUSH_RESULT_OK => 1; @@ -31,12 +31,12 @@ use constant PUSH_RESULT_ERROR => 4; use constant PUSH_RESULT_UNKNOWN => 5; sub push_result_to_string { - my ($result) = @_; - return 'OK' if $result == PUSH_RESULT_OK; - return 'OK-IGNORED' if $result == PUSH_RESULT_IGNORED; - return 'TRANSIENT-ERROR' if $result == PUSH_RESULT_TRANSIENT; - return 'FATAL-ERROR' if $result == PUSH_RESULT_ERROR; - return 'UNKNOWN' if $result == PUSH_RESULT_UNKNOWN; + my ($result) = @_; + return 'OK' if $result == PUSH_RESULT_OK; + return 'OK-IGNORED' if $result == PUSH_RESULT_IGNORED; + return 'TRANSIENT-ERROR' if $result == PUSH_RESULT_TRANSIENT; + return 'FATAL-ERROR' if $result == PUSH_RESULT_ERROR; + return 'UNKNOWN' if $result == PUSH_RESULT_UNKNOWN; } use constant POLL_INTERVAL_SECONDS => 30; diff --git a/extensions/Push/lib/Daemon.pm b/extensions/Push/lib/Daemon.pm index 7f2459a95..7fb5352ca 100644 --- a/extensions/Push/lib/Daemon.pm +++ b/extensions/Push/lib/Daemon.pm @@ -20,7 +20,7 @@ use File::Basename; use Pod::Usage; sub start { - newdaemon(); + newdaemon(); } # @@ -28,70 +28,71 @@ sub start { # sub gd_preconfig { - my $self = shift; - my $pidfile = $self->{gd_args}{pidfile}; - if (!$pidfile) { - $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".pid"; - } - return (pidfile => $pidfile); + my $self = shift; + my $pidfile = $self->{gd_args}{pidfile}; + if (!$pidfile) { + $pidfile = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".pid"; + } + return (pidfile => $pidfile); } sub gd_getopt { - my $self = shift; - $self->SUPER::gd_getopt(); - if ($self->{gd_args}{progname}) { - $self->{gd_progname} = $self->{gd_args}{progname}; - } else { - $self->{gd_progname} = basename($0); - } - $self->{_original_zero} = $0; - $0 = $self->{gd_progname}; + my $self = shift; + $self->SUPER::gd_getopt(); + if ($self->{gd_args}{progname}) { + $self->{gd_progname} = $self->{gd_args}{progname}; + } + else { + $self->{gd_progname} = basename($0); + } + $self->{_original_zero} = $0; + $0 = $self->{gd_progname}; } sub gd_postconfig { - my $self = shift; - $0 = delete $self->{_original_zero}; + my $self = shift; + $0 = delete $self->{_original_zero}; } sub gd_more_opt { - my $self = shift; - return ( - 'pidfile=s' => \$self->{gd_args}{pidfile}, - 'n=s' => \$self->{gd_args}{progname}, - ); + my $self = shift; + return ( + 'pidfile=s' => \$self->{gd_args}{pidfile}, + 'n=s' => \$self->{gd_args}{progname}, + ); } sub gd_usage { - pod2usage({ -verbose => 0, -exitval => 'NOEXIT' }); - return 0; -}; + pod2usage({-verbose => 0, -exitval => 'NOEXIT'}); + return 0; +} sub gd_redirect_output { - my $self = shift; + my $self = shift; - my $filename = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".log"; + my $filename = bz_locations()->{datadir} . '/' . $self->{gd_progname} . ".log"; + open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); + close(STDOUT); + open(STDOUT, ">&", STDERR) or die "redirect STDOUT -> STDERR: $!"; + $SIG{HUP} = sub { + close(STDERR); open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); - close(STDOUT); - open(STDOUT, ">&", STDERR) or die "redirect STDOUT -> STDERR: $!"; - $SIG{HUP} = sub { - close(STDERR); - open(STDERR, ">>", $filename) or (print "could not open stderr: $!" && exit(1)); - }; + }; } sub gd_setup_signals { - my $self = shift; - $self->SUPER::gd_setup_signals(); - $SIG{TERM} = sub { $self->gd_quit_event(); } + my $self = shift; + $self->SUPER::gd_setup_signals(); + $SIG{TERM} = sub { $self->gd_quit_event(); } } sub gd_run { - my $self = shift; - $::SIG{__DIE__} = \&Carp::confess if $self->{debug}; - my $push = Bugzilla->push_ext; - $push->logger->{debug} = $self->{debug}; - $push->is_daemon(1); - $push->start(); + my $self = shift; + $::SIG{__DIE__} = \&Carp::confess if $self->{debug}; + my $push = Bugzilla->push_ext; + $push->logger->{debug} = $self->{debug}; + $push->is_daemon(1); + $push->start(); } 1; diff --git a/extensions/Push/lib/Log.pm b/extensions/Push/lib/Log.pm index 8a35a6cf5..7358477ed 100644 --- a/extensions/Push/lib/Log.pm +++ b/extensions/Push/lib/Log.pm @@ -15,32 +15,30 @@ use Bugzilla; use Bugzilla::Extension::Push::Message; sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - return $self; + my ($class) = @_; + my $self = {}; + bless($self, $class); + return $self; } sub count { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_array("SELECT COUNT(*) FROM push_log"); + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_array("SELECT COUNT(*) FROM push_log"); } sub list { - my ($self, %args) = @_; - $args{limit} ||= 10; - $args{filter} ||= ''; - my @result; - my $dbh = Bugzilla->dbh; + my ($self, %args) = @_; + $args{limit} ||= 10; + $args{filter} ||= ''; + my @result; + my $dbh = Bugzilla->dbh; - my $ids = $dbh->selectcol_arrayref(" + my $ids = $dbh->selectcol_arrayref(" SELECT id FROM push_log - ORDER BY processed_ts DESC " . - $dbh->sql_limit(100) - ); - return Bugzilla::Extension::Push::LogEntry->new_from_list($ids); + ORDER BY processed_ts DESC " . $dbh->sql_limit(100)); + return Bugzilla::Extension::Push::LogEntry->new_from_list($ids); } 1; diff --git a/extensions/Push/lib/LogEntry.pm b/extensions/Push/lib/LogEntry.pm index f4e894b94..0d9770a8a 100644 --- a/extensions/Push/lib/LogEntry.pm +++ b/extensions/Push/lib/LogEntry.pm @@ -26,21 +26,19 @@ use Bugzilla::Extension::Push::Constants; # initialisation # -use constant DB_TABLE => 'push_log'; +use constant DB_TABLE => 'push_log'; use constant DB_COLUMNS => qw( - id - message_id - change_set - routing_key - connector - push_ts - processed_ts - result - data + id + message_id + change_set + routing_key + connector + push_ts + processed_ts + result + data ); -use constant VALIDATORS => { - data => \&_check_data, -}; +use constant VALIDATORS => {data => \&_check_data,}; use constant NAME_FIELD => ''; use constant LIST_ORDER => 'processed_ts DESC'; @@ -48,14 +46,14 @@ use constant LIST_ORDER => 'processed_ts DESC'; # accessors # -sub message_id { return $_[0]->{'message_id'}; } -sub change_set { return $_[0]->{'change_set'}; } -sub routing_key { return $_[0]->{'routing_key'}; } -sub connector { return $_[0]->{'connector'}; } -sub push_ts { return $_[0]->{'push_ts'}; } +sub message_id { return $_[0]->{'message_id'}; } +sub change_set { return $_[0]->{'change_set'}; } +sub routing_key { return $_[0]->{'routing_key'}; } +sub connector { return $_[0]->{'connector'}; } +sub push_ts { return $_[0]->{'push_ts'}; } sub processed_ts { return $_[0]->{'processed_ts'}; } -sub result { return $_[0]->{'result'}; } -sub data { return $_[0]->{'data'}; } +sub result { return $_[0]->{'result'}; } +sub data { return $_[0]->{'data'}; } sub result_string { return push_result_to_string($_[0]->result) } @@ -64,8 +62,8 @@ sub result_string { return push_result_to_string($_[0]->result) } # sub _check_data { - my ($invocant, $value) = @_; - return $value eq '' ? undef : $value; + my ($invocant, $value) = @_; + return $value eq '' ? undef : $value; } 1; diff --git a/extensions/Push/lib/Logger.pm b/extensions/Push/lib/Logger.pm index 5d92010ee..ec6dbe497 100644 --- a/extensions/Push/lib/Logger.pm +++ b/extensions/Push/lib/Logger.pm @@ -20,42 +20,38 @@ use Bugzilla::Extension::Push::LogEntry; Log::Log4perl->wrapper_register(__PACKAGE__); sub info { - my ($this, $message) = @_; - INFO($message); + my ($this, $message) = @_; + INFO($message); } sub error { - my ($this, $message) = @_; - ERROR($message); + my ($this, $message) = @_; + ERROR($message); } sub debug { - my ($this, $message) = @_; - DEBUG($message); + my ($this, $message) = @_; + DEBUG($message); } sub result { - my ($self, $connector, $message, $result, $data) = @_; - $data ||= ''; - - my $log_msg = sprintf - '%s: Message #%s: %s %s', - $connector->name, - $message->message_id, - push_result_to_string($result), - $data; - $self->info($log_msg); - - Bugzilla::Extension::Push::LogEntry->create({ - message_id => $message->message_id, - change_set => $message->change_set, - routing_key => $message->routing_key, - connector => $connector->name, - push_ts => $message->push_ts, - processed_ts => Bugzilla->dbh->selectrow_array('SELECT NOW()'), - result => $result, - data => $data, - }); + my ($self, $connector, $message, $result, $data) = @_; + $data ||= ''; + + my $log_msg = sprintf '%s: Message #%s: %s %s', $connector->name, + $message->message_id, push_result_to_string($result), $data; + $self->info($log_msg); + + Bugzilla::Extension::Push::LogEntry->create({ + message_id => $message->message_id, + change_set => $message->change_set, + routing_key => $message->routing_key, + connector => $connector->name, + push_ts => $message->push_ts, + processed_ts => Bugzilla->dbh->selectrow_array('SELECT NOW()'), + result => $result, + data => $data, + }); } sub _build_logger { Log::Log4perl->get_logger(__PACKAGE__); } diff --git a/extensions/Push/lib/Message.pm b/extensions/Push/lib/Message.pm index 1beb18ef0..3587de1d9 100644 --- a/extensions/Push/lib/Message.pm +++ b/extensions/Push/lib/Message.pm @@ -27,50 +27,50 @@ use Encode; # initialisation # -use constant DB_TABLE => 'push'; +use constant DB_TABLE => 'push'; use constant DB_COLUMNS => qw( - id - push_ts - payload - change_set - routing_key + id + push_ts + payload + change_set + routing_key ); use constant LIST_ORDER => 'push_ts'; use constant VALIDATORS => { - push_ts => \&_check_push_ts, - payload => \&_check_payload, - change_set => \&_check_change_set, - routing_key => \&_check_routing_key, + push_ts => \&_check_push_ts, + payload => \&_check_payload, + change_set => \&_check_change_set, + routing_key => \&_check_routing_key, }; # this creates an object which doesn't exist on the database sub new_transient { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my $object = shift; - bless($object, $class) if $object; - return $object; + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my $object = shift; + bless($object, $class) if $object; + return $object; } # take a transient object and commit sub create_from_transient { - my ($self) = @_; - return $self->create($self); + my ($self) = @_; + return $self->create($self); } # # accessors # -sub push_ts { return $_[0]->{'push_ts'}; } -sub payload { return $_[0]->{'payload'}; } -sub change_set { return $_[0]->{'change_set'}; } +sub push_ts { return $_[0]->{'push_ts'}; } +sub payload { return $_[0]->{'payload'}; } +sub change_set { return $_[0]->{'change_set'}; } sub routing_key { return $_[0]->{'routing_key'}; } -sub message_id { return $_[0]->id; } +sub message_id { return $_[0]->id; } sub payload_decoded { - my ($self) = @_; - return from_json($self->{'payload'}); + my ($self) = @_; + return from_json($self->{'payload'}); } # @@ -78,27 +78,29 @@ sub payload_decoded { # sub _check_push_ts { - my ($invocant, $value) = @_; - $value ||= Bugzilla->dbh->selectrow_array('SELECT NOW()'); - return $value; + my ($invocant, $value) = @_; + $value ||= Bugzilla->dbh->selectrow_array('SELECT NOW()'); + return $value; } sub _check_payload { - my ($invocant, $value) = @_; - length($value) || ThrowCodeError('push_invalid_payload'); - return $value; + my ($invocant, $value) = @_; + length($value) || ThrowCodeError('push_invalid_payload'); + return $value; } sub _check_change_set { - my ($invocant, $value) = @_; - (defined($value) && length($value)) || ThrowCodeError('push_invalid_change_set'); - return $value; + my ($invocant, $value) = @_; + (defined($value) && length($value)) + || ThrowCodeError('push_invalid_change_set'); + return $value; } sub _check_routing_key { - my ($invocant, $value) = @_; - (defined($value) && length($value)) || ThrowCodeError('push_invalid_routing_key'); - return $value; + my ($invocant, $value) = @_; + (defined($value) && length($value)) + || ThrowCodeError('push_invalid_routing_key'); + return $value; } 1; diff --git a/extensions/Push/lib/Option.pm b/extensions/Push/lib/Option.pm index a08e4c11d..a8e67714c 100644 --- a/extensions/Push/lib/Option.pm +++ b/extensions/Push/lib/Option.pm @@ -21,27 +21,25 @@ use Bugzilla::Util; # initialisation # -use constant DB_TABLE => 'push_options'; +use constant DB_TABLE => 'push_options'; use constant DB_COLUMNS => qw( - id - connector - option_name - option_value + id + connector + option_name + option_value ); use constant UPDATE_COLUMNS => qw( - option_value + option_value ); -use constant VALIDATORS => { - connector => \&_check_connector, -}; +use constant VALIDATORS => {connector => \&_check_connector,}; use constant LIST_ORDER => 'connector'; # # accessors # -sub connector { return $_[0]->{'connector'}; } -sub name { return $_[0]->{'option_name'}; } +sub connector { return $_[0]->{'connector'}; } +sub name { return $_[0]->{'option_name'}; } sub value { return $_[0]->{'option_value'}; } # @@ -55,12 +53,12 @@ sub set_value { $_[0]->{'option_value'} = $_[1]; } # sub _check_connector { - my ($invocant, $value) = @_; - $value eq '*' - || $value eq 'global' - || Bugzilla->push_ext->connectors->exists($value) - || ThrowCodeError('push_invalid_connector'); - return $value; + my ($invocant, $value) = @_; + $value eq '*' + || $value eq 'global' + || Bugzilla->push_ext->connectors->exists($value) + || ThrowCodeError('push_invalid_connector'); + return $value; } 1; diff --git a/extensions/Push/lib/Push.pm b/extensions/Push/lib/Push.pm index ab640da81..97bac942b 100644 --- a/extensions/Push/lib/Push.pm +++ b/extensions/Push/lib/Push.pm @@ -24,269 +24,273 @@ use Bugzilla::Extension::Push::Util; use DateTime; use Try::Tiny; -has 'is_daemon' => ( - is => 'rw', - default => 0, -); +has 'is_daemon' => (is => 'rw', default => 0,); sub start { - my ($self) = @_; - my $connectors = $self->connectors; - $self->{config_last_modified} = $self->get_config_last_modified(); - $self->{config_last_checked} = (time); - - foreach my $connector ($connectors->list) { - $connector->backlog->reset_backoff(); - } - - my $pushd_loop = IO::Async::Loop->new; - my $main_timer = IO::Async::Timer::Periodic->new( - first_interval => 0, - interval => POLL_INTERVAL_SECONDS, - reschedule => 'drift', - on_tick => sub { - if ( $self->_dbh_check() ) { - $self->_reload(); - try { - $self->push(); - } - catch { - FATAL($_); - }; - } - }, + my ($self) = @_; + my $connectors = $self->connectors; + $self->{config_last_modified} = $self->get_config_last_modified(); + $self->{config_last_checked} = (time); + + foreach my $connector ($connectors->list) { + $connector->backlog->reset_backoff(); + } + + my $pushd_loop = IO::Async::Loop->new; + my $main_timer = IO::Async::Timer::Periodic->new( + first_interval => 0, + interval => POLL_INTERVAL_SECONDS, + reschedule => 'drift', + on_tick => sub { + if ($self->_dbh_check()) { + $self->_reload(); + try { + $self->push(); + } + catch { + FATAL($_); + }; + } + }, + ); + if (Bugzilla->datadog) { + my $dog_timer = IO::Async::Timer::Periodic->new( + interval => 120, + reschedule => 'drift', + on_tick => sub { $self->heartbeat }, ); - if ( Bugzilla->datadog ) { - my $dog_timer = IO::Async::Timer::Periodic->new( - interval => 120, - reschedule => 'drift', - on_tick => sub { $self->heartbeat }, - ); - $pushd_loop->add($dog_timer); - $dog_timer->start; - } + $pushd_loop->add($dog_timer); + $dog_timer->start; + } - $pushd_loop->add($main_timer); - $main_timer->start; - $pushd_loop->run; + $pushd_loop->add($main_timer); + $main_timer->start; + $pushd_loop->run; } sub heartbeat { - my ($self) = @_; - my $dd = Bugzilla->datadog('bugzilla.pushd'); - - $dd->gauge('scheduled_jobs', Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM push')); - - foreach my $connector ($self->connectors->list) { - if ($connector->enabled) { - my $lcname = lc $connector->name; - $dd->gauge("${lcname}.backlog", Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM push_backlog WHERE connector = ?', undef, $connector->name)); - } + my ($self) = @_; + my $dd = Bugzilla->datadog('bugzilla.pushd'); + + $dd->gauge('scheduled_jobs', + Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM push')); + + foreach my $connector ($self->connectors->list) { + if ($connector->enabled) { + my $lcname = lc $connector->name; + $dd->gauge( + "${lcname}.backlog", + Bugzilla->dbh->selectrow_array( + 'SELECT COUNT(*) FROM push_backlog WHERE connector = ?', undef, + $connector->name + ) + ); } + } } sub push { - my ($self) = @_; - my $logger = $self->logger; - my $connectors = $self->connectors; + my ($self) = @_; + my $logger = $self->logger; + my $connectors = $self->connectors; + + my $enabled = 0; + foreach my $connector ($connectors->list) { + if ($connector->enabled) { + $enabled = 1; + last; + } + } + return unless $enabled; - my $enabled = 0; + $logger->debug("polling"); + + # process each message + while (my $message = $self->queue->oldest) { foreach my $connector ($connectors->list) { - if ($connector->enabled) { - $enabled = 1; - last; + next unless $connector->enabled; + next unless $connector->should_send($message); + $logger->debug("pushing to " . $connector->name); + + my $is_backlogged = $connector->backlog->count; + + if (!$is_backlogged) { + + # connector isn't backlogged, immediate send + $logger->debug("immediate send"); + my ($result, $data); + eval { ($result, $data) = $connector->send($message); }; + if ($@) { + $result = PUSH_RESULT_TRANSIENT; + $data = clean_error($@); } - } - return unless $enabled; - - $logger->debug("polling"); - - # process each message - while(my $message = $self->queue->oldest) { - foreach my $connector ($connectors->list) { - next unless $connector->enabled; - next unless $connector->should_send($message); - $logger->debug("pushing to " . $connector->name); - - my $is_backlogged = $connector->backlog->count; - - if (!$is_backlogged) { - # connector isn't backlogged, immediate send - $logger->debug("immediate send"); - my ($result, $data); - eval { - ($result, $data) = $connector->send($message); - }; - if ($@) { - $result = PUSH_RESULT_TRANSIENT; - $data = clean_error($@); - } - if (!$result) { - $logger->error($connector->name . " failed to return a result code"); - $result = PUSH_RESULT_UNKNOWN; - } - $logger->result($connector, $message, $result, $data); - - if ($result == PUSH_RESULT_TRANSIENT) { - $is_backlogged = 1; - } - } - - # if the connector is backlogged, push to the backlog queue - if ($is_backlogged) { - INFO('connector is backlogged'); - my $backlog = Bugzilla::Extension::Push::BacklogMessage->create_from_message($message, $connector); - } + if (!$result) { + $logger->error($connector->name . " failed to return a result code"); + $result = PUSH_RESULT_UNKNOWN; } + $logger->result($connector, $message, $result, $data); - # message processed - $message->remove_from_db(); + if ($result == PUSH_RESULT_TRANSIENT) { + $is_backlogged = 1; + } + } + + # if the connector is backlogged, push to the backlog queue + if ($is_backlogged) { + INFO('connector is backlogged'); + my $backlog + = Bugzilla::Extension::Push::BacklogMessage->create_from_message($message, + $connector); + } } - # process backlog - foreach my $connector ($connectors->list) { - next unless $connector->enabled; - my $message = $connector->backlog->oldest(); - next unless $message; - - $logger->debug("processing backlog for " . $connector->name); - while ($message) { - my ($result, $data); - eval { - ($result, $data) = $connector->send($message); - }; - if ($@) { - $result = PUSH_RESULT_TRANSIENT; - $data = $@; - } - $message->inc_attempts($result == PUSH_RESULT_OK ? '' : $data); - if (!$result) { - $logger->error($connector->name . " failed to return a result code"); - $result = PUSH_RESULT_UNKNOWN; - } - $logger->result($connector, $message, $result, $data); - - if ($result == PUSH_RESULT_TRANSIENT) { - # connector is still down, stop trying - $connector->backlog->inc_backoff(); - last; - } - - # message was processed - $message->remove_from_db(); - - $message = $connector->backlog->oldest(); - } + # message processed + $message->remove_from_db(); + } + + # process backlog + foreach my $connector ($connectors->list) { + next unless $connector->enabled; + my $message = $connector->backlog->oldest(); + next unless $message; + + $logger->debug("processing backlog for " . $connector->name); + while ($message) { + my ($result, $data); + eval { ($result, $data) = $connector->send($message); }; + if ($@) { + $result = PUSH_RESULT_TRANSIENT; + $data = $@; + } + $message->inc_attempts($result == PUSH_RESULT_OK ? '' : $data); + if (!$result) { + $logger->error($connector->name . " failed to return a result code"); + $result = PUSH_RESULT_UNKNOWN; + } + $logger->result($connector, $message, $result, $data); + + if ($result == PUSH_RESULT_TRANSIENT) { + + # connector is still down, stop trying + $connector->backlog->inc_backoff(); + last; + } + + # message was processed + $message->remove_from_db(); + + $message = $connector->backlog->oldest(); } + } } sub _reload { - my ($self) = @_; - - # check for updated config every 60 seconds - my $now = (time); - if ($now - $self->{config_last_checked} < 60) { - return; - } - $self->{config_last_checked} = $now; - - $self->logger->debug('Checking for updated configuration'); - if ($self->get_config_last_modified eq $self->{config_last_modified}) { - return; - } - $self->{config_last_modified} = $self->get_config_last_modified(); - - $self->logger->debug('Configuration has been updated'); - $self->connectors->reload(); + my ($self) = @_; + + # check for updated config every 60 seconds + my $now = (time); + if ($now - $self->{config_last_checked} < 60) { + return; + } + $self->{config_last_checked} = $now; + + $self->logger->debug('Checking for updated configuration'); + if ($self->get_config_last_modified eq $self->{config_last_modified}) { + return; + } + $self->{config_last_modified} = $self->get_config_last_modified(); + + $self->logger->debug('Configuration has been updated'); + $self->connectors->reload(); } sub get_config_last_modified { - my ($self) = @_; - my $options_list = Bugzilla::Extension::Push::Option->match({ - connector => '*', - option_name => 'last-modified', + my ($self) = @_; + my $options_list + = Bugzilla::Extension::Push::Option->match({ + connector => '*', option_name => 'last-modified', }); - if (@$options_list) { - return $options_list->[0]->value; - } else { - return $self->set_config_last_modified(); - } + if (@$options_list) { + return $options_list->[0]->value; + } + else { + return $self->set_config_last_modified(); + } } sub set_config_last_modified { - my ($self) = @_; - my $options_list = Bugzilla::Extension::Push::Option->match({ - connector => '*', - option_name => 'last-modified', + my ($self) = @_; + my $options_list + = Bugzilla::Extension::Push::Option->match({ + connector => '*', option_name => 'last-modified', }); - my $now = DateTime->now->datetime(); - if (@$options_list) { - $options_list->[0]->set_value($now); - $options_list->[0]->update(); - } else { - Bugzilla::Extension::Push::Option->create({ - connector => '*', - option_name => 'last-modified', - option_value => $now, - }); - } - return $now; + my $now = DateTime->now->datetime(); + if (@$options_list) { + $options_list->[0]->set_value($now); + $options_list->[0]->update(); + } + else { + Bugzilla::Extension::Push::Option->create({ + connector => '*', option_name => 'last-modified', option_value => $now, + }); + } + return $now; } sub config { - my ($self) = @_; - if (!$self->{config}) { - $self->{config} = Bugzilla::Extension::Push::Config->new( - 'global', - { - name => 'log_purge', - label => 'Purge logs older than (days)', - type => 'string', - default => '7', - required => '1', - validate => sub { $_[0] =~ /\D/ && die "Invalid purge duration (must be numeric)\n"; }, - }, - ); - $self->{config}->load(); - } - return $self->{config}; + my ($self) = @_; + if (!$self->{config}) { + $self->{config} = Bugzilla::Extension::Push::Config->new( + 'global', + { + name => 'log_purge', + label => 'Purge logs older than (days)', + type => 'string', + default => '7', + required => '1', + validate => + sub { $_[0] =~ /\D/ && die "Invalid purge duration (must be numeric)\n"; }, + }, + ); + $self->{config}->load(); + } + return $self->{config}; } sub logger { - my ($self, $value) = @_; - $self->{logger} = $value if $value; - return $self->{logger}; + my ($self, $value) = @_; + $self->{logger} = $value if $value; + return $self->{logger}; } sub connectors { - my ($self, $value) = @_; - $self->{connectors} = $value if $value; - return $self->{connectors}; + my ($self, $value) = @_; + $self->{connectors} = $value if $value; + return $self->{connectors}; } sub queue { - my ($self) = @_; - $self->{queue} ||= Bugzilla::Extension::Push::Queue->new(); - return $self->{queue}; + my ($self) = @_; + $self->{queue} ||= Bugzilla::Extension::Push::Queue->new(); + return $self->{queue}; } sub log { - my ($self) = @_; - $self->{log} ||= Bugzilla::Extension::Push::Log->new(); - return $self->{log}; + my ($self) = @_; + $self->{log} ||= Bugzilla::Extension::Push::Log->new(); + return $self->{log}; } sub _dbh_check { - my ($self) = @_; - eval { - Bugzilla->dbh->selectrow_array("SELECT 1 FROM push"); - }; - if ($@) { - $self->logger->error(clean_error($@)); - return 0; - } else { - return 1; - } + my ($self) = @_; + eval { Bugzilla->dbh->selectrow_array("SELECT 1 FROM push"); }; + if ($@) { + $self->logger->error(clean_error($@)); + return 0; + } + else { + return 1; + } } 1; diff --git a/extensions/Push/lib/Queue.pm b/extensions/Push/lib/Queue.pm index 3ee0321d9..f59423e6a 100644 --- a/extensions/Push/lib/Queue.pm +++ b/extensions/Push/lib/Queue.pm @@ -15,59 +15,54 @@ use Bugzilla; use Bugzilla::Extension::Push::Message; sub new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - return $self; + my ($class) = @_; + my $self = {}; + bless($self, $class); + return $self; } sub count { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - return $dbh->selectrow_array("SELECT COUNT(*) FROM push"); + my ($self) = @_; + my $dbh = Bugzilla->dbh; + return $dbh->selectrow_array("SELECT COUNT(*) FROM push"); } sub oldest { - my ($self) = @_; - my @messages = $self->list(limit => 1); - return scalar(@messages) ? $messages[0] : undef; + my ($self) = @_; + my @messages = $self->list(limit => 1); + return scalar(@messages) ? $messages[0] : undef; } sub by_id { - my ($self, $id) = @_; - my @messages = $self->list( - limit => 1, - filter => "AND (push.id = $id)", - ); - return scalar(@messages) ? $messages[0] : undef; + my ($self, $id) = @_; + my @messages = $self->list(limit => 1, filter => "AND (push.id = $id)",); + return scalar(@messages) ? $messages[0] : undef; } sub list { - my ($self, %args) = @_; - $args{limit} ||= 10; - $args{filter} ||= ''; - my @result; - my $dbh = Bugzilla->dbh; + my ($self, %args) = @_; + $args{limit} ||= 10; + $args{filter} ||= ''; + my @result; + my $dbh = Bugzilla->dbh; - my $sth = $dbh->prepare(" + my $sth = $dbh->prepare(" SELECT id, push_ts, payload, change_set, routing_key FROM push - WHERE (1 = 1) " . - $args{filter} . " - ORDER BY push_ts " . - $dbh->sql_limit($args{limit}) - ); - $sth->execute(); - while (my $row = $sth->fetchrow_hashref()) { - push @result, Bugzilla::Extension::Push::Message->new({ - id => $row->{id}, - push_ts => $row->{push_ts}, - payload => $row->{payload}, - change_set => $row->{change_set}, - routing_key => $row->{routing_key}, - }); - } - return @result; + WHERE (1 = 1) " . $args{filter} . " + ORDER BY push_ts " . $dbh->sql_limit($args{limit})); + $sth->execute(); + while (my $row = $sth->fetchrow_hashref()) { + push @result, + Bugzilla::Extension::Push::Message->new({ + id => $row->{id}, + push_ts => $row->{push_ts}, + payload => $row->{payload}, + change_set => $row->{change_set}, + routing_key => $row->{routing_key}, + }); + } + return @result; } 1; diff --git a/extensions/Push/lib/Serialise.pm b/extensions/Push/lib/Serialise.pm index bb6834c13..c878ff4d9 100644 --- a/extensions/Push/lib/Serialise.pm +++ b/extensions/Push/lib/Serialise.pm @@ -19,140 +19,145 @@ use Scalar::Util 'blessed'; use JSON (); my $_instance; + sub instance { - $_instance ||= Bugzilla::Extension::Push::Serialise->_new(); - return $_instance; + $_instance ||= Bugzilla::Extension::Push::Serialise->_new(); + return $_instance; } sub _new { - my ($class) = @_; - my $self = {}; - bless($self, $class); - return $self; + my ($class) = @_; + my $self = {}; + bless($self, $class); + return $self; } # given an object, serliase to a hash sub object_to_hash { - my ($self, $object, $is_shallow) = @_; - - my $method = lc(blessed($object)); - $method =~ s/::/_/g; - $method =~ s/^bugzilla//; - return unless $self->can($method); - (my $name = $method) =~ s/^_//; - - # check for a cached hash - my $cache = Bugzilla->request_cache; - my $cache_id = "push." . ($is_shallow ? 'shallow.' : 'deep.') . $object; - if (exists($cache->{$cache_id})) { - return wantarray ? ($cache->{$cache_id}, $name) : $cache->{$cache_id}; - } - - # call the right method to serialise to a hash - my $rh = $self->$method($object, $is_shallow); - - # store in cache - if ($cache_id) { - $cache->{$cache_id} = $rh; - } - - return wantarray ? ($rh, $name) : $rh; + my ($self, $object, $is_shallow) = @_; + + my $method = lc(blessed($object)); + $method =~ s/::/_/g; + $method =~ s/^bugzilla//; + return unless $self->can($method); + (my $name = $method) =~ s/^_//; + + # check for a cached hash + my $cache = Bugzilla->request_cache; + my $cache_id = "push." . ($is_shallow ? 'shallow.' : 'deep.') . $object; + if (exists($cache->{$cache_id})) { + return wantarray ? ($cache->{$cache_id}, $name) : $cache->{$cache_id}; + } + + # call the right method to serialise to a hash + my $rh = $self->$method($object, $is_shallow); + + # store in cache + if ($cache_id) { + $cache->{$cache_id} = $rh; + } + + return wantarray ? ($rh, $name) : $rh; } # given a changes hash, return an event hash sub changes_to_event { - my ($self, $changes) = @_; - - my $event = {}; - - # create common (created and modified) fields - $event->{'user'} = $self->object_to_hash(Bugzilla->user); - my $timestamp = - $changes->{'timestamp'} - || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $event->{'time'} = datetime_to_timestamp($timestamp); - - foreach my $change (@{$changes->{'changes'}}) { - if (exists $change->{'field'}) { - # map undef to emtpy - hash_undef_to_empty($change); - - # custom_fields change from undef to empty, ignore these changes - return if ($change->{'added'} || "") eq "" && - ($change->{'removed'} || "") eq ""; - - # use saner field serialisation - my $field = $change->{'field'}; - $change->{'field'} = $field; - - if ($field eq 'priority' || $field eq 'target_milestone') { - $change->{'added'} = _select($change->{'added'}); - $change->{'removed'} = _select($change->{'removed'}); - - } elsif ($field =~ /^cf_/) { - $change->{'added'} = _custom_field($field, $change->{'added'}); - $change->{'removed'} = _custom_field($field, $change->{'removed'}); - } - - $event->{'changes'} = [] unless exists $event->{'changes'}; - push @{$event->{'changes'}}, $change; - } + my ($self, $changes) = @_; + + my $event = {}; + + # create common (created and modified) fields + $event->{'user'} = $self->object_to_hash(Bugzilla->user); + my $timestamp = $changes->{'timestamp'} + || Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $event->{'time'} = datetime_to_timestamp($timestamp); + + foreach my $change (@{$changes->{'changes'}}) { + if (exists $change->{'field'}) { + + # map undef to emtpy + hash_undef_to_empty($change); + + # custom_fields change from undef to empty, ignore these changes + return + if ($change->{'added'} || "") eq "" && ($change->{'removed'} || "") eq ""; + + # use saner field serialisation + my $field = $change->{'field'}; + $change->{'field'} = $field; + + if ($field eq 'priority' || $field eq 'target_milestone') { + $change->{'added'} = _select($change->{'added'}); + $change->{'removed'} = _select($change->{'removed'}); + + } + elsif ($field =~ /^cf_/) { + $change->{'added'} = _custom_field($field, $change->{'added'}); + $change->{'removed'} = _custom_field($field, $change->{'removed'}); + } + + $event->{'changes'} = [] unless exists $event->{'changes'}; + push @{$event->{'changes'}}, $change; } + } - return $event; + return $event; } # bugzilla returns '---' or '--' for single-select fields that have no value # selected. it makes more sense to return an empty string. sub _select { - my ($value) = @_; - return '' if $value eq '---' or $value eq '--'; - return $value; + my ($value) = @_; + return '' if $value eq '---' or $value eq '--'; + return $value; } # return an object which serialises to a json boolean, but still acts as a perl # boolean sub _boolean { - my ($value) = @_; - return $value ? JSON::true : JSON::false; + my ($value) = @_; + return $value ? JSON::true : JSON::false; } sub _string { - my ($value) = @_; - return defined($value) ? $value : ''; + my ($value) = @_; + return defined($value) ? $value : ''; } sub _time { - my ($value) = @_; - return defined($value) ? datetime_to_timestamp($value) : undef; + my ($value) = @_; + return defined($value) ? datetime_to_timestamp($value) : undef; } sub _integer { - my ($value) = @_; - return defined($value) ? $value + 0 : undef; + my ($value) = @_; + return defined($value) ? $value + 0 : undef; } sub _array { - my ($value) = @_; - return defined($value) ? $value : []; + my ($value) = @_; + return defined($value) ? $value : []; } sub _custom_field { - my ($field, $value) = @_; - $field = Bugzilla::Field->new({ name => $field }) unless blessed $field; + my ($field, $value) = @_; + $field = Bugzilla::Field->new({name => $field}) unless blessed $field; - if ($field->type == FIELD_TYPE_DATETIME) { - return _time($value); + if ($field->type == FIELD_TYPE_DATETIME) { + return _time($value); - } elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) { - return _select($value); + } + elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) { + return _select($value); - } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - return _array($value); + } + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + return _array($value); - } else { - return _string($value); - } + } + else { + return _string($value); + } } # @@ -162,158 +167,148 @@ sub _custom_field { # sub _bug { - my ($self, $bug) = @_; - - my $version = $bug->can('version_obj') - ? $bug->version_obj - : Bugzilla::Version->new({ name => $bug->version, product => $bug->product_obj }); - - my $milestone; - if (_select($bug->target_milestone) ne '') { - $milestone = $bug->can('target_milestone_obj') - ? $bug->target_milestone_obj - : Bugzilla::Milestone->new({ name => $bug->target_milestone, product => $bug->product_obj }); - } - - my $status = $bug->can('status_obj') - ? $bug->status_obj - : Bugzilla::Status->new({ name => $bug->bug_status }); - - my $rh = { - id => _integer($bug->bug_id), - alias => _string($bug->alias), - assigned_to => $self->_user($bug->assigned_to), - classification => _string($bug->classification), - component => $self->_component($bug->component_obj), - creation_time => _time($bug->creation_ts || $bug->delta_ts), - flags => (mapr { $self->_flag($_) } $bug->flags), - is_private => _boolean(!is_public($bug)), - keywords => (mapr { _string($_->name) } $bug->keyword_objects), - last_change_time => _time($bug->delta_ts), - operating_system => _string($bug->op_sys), - platform => _string($bug->rep_platform), - priority => _select($bug->priority), - product => $self->_product($bug->product_obj), - qa_contact => $self->_user($bug->qa_contact), - reporter => $self->_user($bug->reporter), - resolution => _string($bug->resolution), - severity => _string($bug->bug_severity), - status => $self->_status($status), - summary => _string($bug->short_desc), - target_milestone => $self->_milestone($milestone), - url => _string($bug->bug_file_loc), - version => $self->_version($version), - whiteboard => _string($bug->status_whiteboard), - }; - - # add custom fields - my @custom_fields = Bugzilla->active_custom_fields( - { product => $bug->product_obj, component => $bug->component_obj }); - foreach my $field (@custom_fields) { - my $name = $field->name; - $rh->{$name} = _custom_field($field, $bug->$name); - } - - return $rh; + my ($self, $bug) = @_; + + my $version + = $bug->can('version_obj') + ? $bug->version_obj + : Bugzilla::Version->new( + {name => $bug->version, product => $bug->product_obj}); + + my $milestone; + if (_select($bug->target_milestone) ne '') { + $milestone + = $bug->can('target_milestone_obj') + ? $bug->target_milestone_obj + : Bugzilla::Milestone->new( + {name => $bug->target_milestone, product => $bug->product_obj}); + } + + my $status + = $bug->can('status_obj') + ? $bug->status_obj + : Bugzilla::Status->new({name => $bug->bug_status}); + + my $rh = { + id => _integer($bug->bug_id), + alias => _string($bug->alias), + assigned_to => $self->_user($bug->assigned_to), + classification => _string($bug->classification), + component => $self->_component($bug->component_obj), + creation_time => _time($bug->creation_ts || $bug->delta_ts), + flags => (mapr { $self->_flag($_) } $bug->flags), + is_private => _boolean(!is_public($bug)), + keywords => (mapr { _string($_->name) } $bug->keyword_objects), + last_change_time => _time($bug->delta_ts), + operating_system => _string($bug->op_sys), + platform => _string($bug->rep_platform), + priority => _select($bug->priority), + product => $self->_product($bug->product_obj), + qa_contact => $self->_user($bug->qa_contact), + reporter => $self->_user($bug->reporter), + resolution => _string($bug->resolution), + severity => _string($bug->bug_severity), + status => $self->_status($status), + summary => _string($bug->short_desc), + target_milestone => $self->_milestone($milestone), + url => _string($bug->bug_file_loc), + version => $self->_version($version), + whiteboard => _string($bug->status_whiteboard), + }; + + # add custom fields + my @custom_fields = Bugzilla->active_custom_fields( + {product => $bug->product_obj, component => $bug->component_obj}); + foreach my $field (@custom_fields) { + my $name = $field->name; + $rh->{$name} = _custom_field($field, $bug->$name); + } + + return $rh; } sub _user { - my ($self, $user) = @_; - return undef unless $user; - return { - id => _integer($user->id), - login => _string($user->login), - real_name => _string($user->name), - }; + my ($self, $user) = @_; + return undef unless $user; + return { + id => _integer($user->id), + login => _string($user->login), + real_name => _string($user->name), + }; } sub _component { - my ($self, $component) = @_; - return { - id => _integer($component->id), - name => _string($component->name), - }; + my ($self, $component) = @_; + return {id => _integer($component->id), name => _string($component->name),}; } sub _attachment { - my ($self, $attachment, $is_shallow) = @_; - my $rh = { - id => _integer($attachment->id), - content_type => _string($attachment->contenttype), - creation_time => _time($attachment->attached), - description => _string($attachment->description), - file_name => _string($attachment->filename), - flags => (mapr { $self->_flag($_) } $attachment->flags), - is_obsolete => _boolean($attachment->isobsolete), - is_patch => _boolean($attachment->ispatch), - is_private => _boolean(!is_public($attachment)), - last_change_time => _time($attachment->modification_time), - }; - if (!$is_shallow) { - $rh->{bug} = $self->_bug($attachment->bug); - } - return $rh; + my ($self, $attachment, $is_shallow) = @_; + my $rh = { + id => _integer($attachment->id), + content_type => _string($attachment->contenttype), + creation_time => _time($attachment->attached), + description => _string($attachment->description), + file_name => _string($attachment->filename), + flags => (mapr { $self->_flag($_) } $attachment->flags), + is_obsolete => _boolean($attachment->isobsolete), + is_patch => _boolean($attachment->ispatch), + is_private => _boolean(!is_public($attachment)), + last_change_time => _time($attachment->modification_time), + }; + if (!$is_shallow) { + $rh->{bug} = $self->_bug($attachment->bug); + } + return $rh; } sub _comment { - my ($self, $comment, $is_shallow) = @_; - my $rh = { - id => _integer($comment->bug_id), - body => _string($comment->body), - creation_time => _time($comment->creation_ts), - is_private => _boolean($comment->is_private), - number => _integer($comment->count), - }; - if (!$is_shallow) { - $rh->{bug} = $self->_bug($comment->bug); - } - return $rh; + my ($self, $comment, $is_shallow) = @_; + my $rh = { + id => _integer($comment->bug_id), + body => _string($comment->body), + creation_time => _time($comment->creation_ts), + is_private => _boolean($comment->is_private), + number => _integer($comment->count), + }; + if (!$is_shallow) { + $rh->{bug} = $self->_bug($comment->bug); + } + return $rh; } sub _product { - my ($self, $product) = @_; - return { - id => _integer($product->id), - name => _string($product->name), - }; + my ($self, $product) = @_; + return {id => _integer($product->id), name => _string($product->name),}; } sub _flag { - my ($self, $flag) = @_; - my $rh = { - id => _integer($flag->id), - name => _string($flag->type->name), - value => _string($flag->status), - }; - if ($flag->requestee) { - $rh->{'requestee'} = $self->_user($flag->requestee); - } - return $rh; + my ($self, $flag) = @_; + my $rh = { + id => _integer($flag->id), + name => _string($flag->type->name), + value => _string($flag->status), + }; + if ($flag->requestee) { + $rh->{'requestee'} = $self->_user($flag->requestee); + } + return $rh; } sub _version { - my ($self, $version) = @_; - return { - id => _integer($version->id), - name => _string($version->name), - }; + my ($self, $version) = @_; + return {id => _integer($version->id), name => _string($version->name),}; } sub _milestone { - my ($self, $milestone) = @_; - return undef unless $milestone; - return { - id => _integer($milestone->id), - name => _string($milestone->name), - }; + my ($self, $milestone) = @_; + return undef unless $milestone; + return {id => _integer($milestone->id), name => _string($milestone->name),}; } sub _status { - my ($self, $status) = @_; - return { - id => _integer($status->id), - name => _string($status->name), - }; + my ($self, $status) = @_; + return {id => _integer($status->id), name => _string($status->name),}; } 1; diff --git a/extensions/Push/lib/Util.pm b/extensions/Push/lib/Util.pm index bda6331bf..34a0879ea 100644 --- a/extensions/Push/lib/Util.pm +++ b/extensions/Push/lib/Util.pm @@ -22,142 +22,147 @@ use Time::HiRes; use base qw(Exporter); our @EXPORT = qw( - datetime_to_timestamp - debug_dump - get_first_value - hash_undef_to_empty - is_public - mapr - clean_error - change_set_id - canon_email - to_json from_json + datetime_to_timestamp + debug_dump + get_first_value + hash_undef_to_empty + is_public + mapr + clean_error + change_set_id + canon_email + to_json from_json ); # returns true if the specified object is public sub is_public { - my ($object) = @_; + my ($object) = @_; - my $default_user = Bugzilla::User->new(); + my $default_user = Bugzilla::User->new(); - if ($object->isa('Bugzilla::Bug')) { - return unless $default_user->can_see_bug($object->bug_id); - return 1; + if ($object->isa('Bugzilla::Bug')) { + return unless $default_user->can_see_bug($object->bug_id); + return 1; - } elsif ($object->isa('Bugzilla::Comment')) { - return if $object->is_private; - return unless $default_user->can_see_bug($object->bug_id); - return 1; + } + elsif ($object->isa('Bugzilla::Comment')) { + return if $object->is_private; + return unless $default_user->can_see_bug($object->bug_id); + return 1; - } elsif ($object->isa('Bugzilla::Attachment')) { - return if $object->isprivate; - return unless $default_user->can_see_bug($object->bug_id); - return 1; + } + elsif ($object->isa('Bugzilla::Attachment')) { + return if $object->isprivate; + return unless $default_user->can_see_bug($object->bug_id); + return 1; - } else { - warn "Unsupported class " . blessed($object) . " passed to is_public()\n"; - } + } + else { + warn "Unsupported class " . blessed($object) . " passed to is_public()\n"; + } - return 1; + return 1; } # return the first existing value from the hashref for the given list of keys sub get_first_value { - my ($rh, @keys) = @_; - foreach my $field (@keys) { - return $rh->{$field} if exists $rh->{$field}; - } - return; + my ($rh, @keys) = @_; + foreach my $field (@keys) { + return $rh->{$field} if exists $rh->{$field}; + } + return; } # wrapper for map that works on array references sub mapr(&$) { - my ($filter, $ra) = @_; - my @result = map(&$filter, @$ra); - return \@result; + my ($filter, $ra) = @_; + my @result = map(&$filter, @$ra); + return \@result; } # convert datetime string (from db) to a UTC json friendly datetime sub datetime_to_timestamp { - my ($datetime_string) = @_; - return '' unless $datetime_string; - return datetime_from($datetime_string, 'UTC')->datetime(); + my ($datetime_string) = @_; + return '' unless $datetime_string; + return datetime_from($datetime_string, 'UTC')->datetime(); } # replaces all undef values in a hashref with an empty string (deep) sub hash_undef_to_empty { - my ($rh) = @_; - foreach my $key (keys %$rh) { - my $value = $rh->{$key}; - if (!defined($value)) { - $rh->{$key} = ''; - } elsif (ref($value) eq 'HASH') { - hash_undef_to_empty($value); - } + my ($rh) = @_; + foreach my $key (keys %$rh) { + my $value = $rh->{$key}; + if (!defined($value)) { + $rh->{$key} = ''; + } + elsif (ref($value) eq 'HASH') { + hash_undef_to_empty($value); } + } } # debugging methods sub debug_dump { - my ($object) = @_; - local $Data::Dumper::Sortkeys = 1; - my $output = Dumper($object); - $output =~ s/$output
"; + my ($object) = @_; + local $Data::Dumper::Sortkeys = 1; + my $output = Dumper($object); + $output =~ s/$output
"; } # removes stacktrace and "at /some/path ..." from errors sub clean_error { - my ($error) = @_; - my $path = bz_locations->{'extensionsdir'}; - $error = $1 if $error =~ /^(.+?) at \Q$path/s; - $path = '/loader/0x'; - $error = $1 if $error =~ /^(.+?) at \Q$path/s; - $error =~ s/(^\s+|\s+$)//g; - return $error; + my ($error) = @_; + my $path = bz_locations->{'extensionsdir'}; + $error = $1 if $error =~ /^(.+?) at \Q$path/s; + $path = '/loader/0x'; + $error = $1 if $error =~ /^(.+?) at \Q$path/s; + $error =~ s/(^\s+|\s+$)//g; + return $error; } # generate a new change_set id sub change_set_id { - return "$$." . Time::HiRes::time(); + return "$$." . Time::HiRes::time(); } # remove guff from email addresses sub clean_email { - my $email = shift; - $email = trim($email); - $email = $1 if $email =~ /^(\S+)/; - $email =~ s/@/@/; - $email = lc $email; - return $email; + my $email = shift; + $email = trim($email); + $email = $1 if $email =~ /^(\S+)/; + $email =~ s/@/@/; + $email = lc $email; + return $email; } # resolve to canonised email form # eg. glob+bmo@mozilla.com --> glob@mozilla.com sub canon_email { - my $email = shift; - $email = clean_email($email); - $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/; - return $email; + my $email = shift; + $email = clean_email($email); + $email =~ s/^([^\+]+)\+[^\@]+(\@.+)$/$1$2/; + return $email; } # json helpers sub to_json { - my ($object, $pretty) = @_; - if ($pretty) { - return decode('utf8', JSON->new->utf8(1)->pretty(1)->encode($object)); - } else { - return JSON->new->ascii(1)->shrink(1)->encode($object); - } + my ($object, $pretty) = @_; + if ($pretty) { + return decode('utf8', JSON->new->utf8(1)->pretty(1)->encode($object)); + } + else { + return JSON->new->ascii(1)->shrink(1)->encode($object); + } } sub from_json { - my ($json) = @_; - if (utf8::is_utf8($json)) { - $json = encode('utf8', $json); - } - return JSON->new->utf8(1)->decode($json); + my ($json) = @_; + if (utf8::is_utf8($json)) { + $json = encode('utf8', $json); + } + return JSON->new->utf8(1)->decode($json); } 1; diff --git a/extensions/Push/template/en/default/setup/strings.txt.pl b/extensions/Push/template/en/default/setup/strings.txt.pl index 6f41f26d5..7bdab77bd 100644 --- a/extensions/Push/template/en/default/setup/strings.txt.pl +++ b/extensions/Push/template/en/default/setup/strings.txt.pl @@ -10,6 +10,6 @@ use warnings; use 5.10.1; %strings = ( - feature_push_amqp => 'Push: AMQP Support', - feature_push_stomp => 'Push: STOMP Support', + feature_push_amqp => 'Push: AMQP Support', + feature_push_stomp => 'Push: STOMP Support', ); diff --git a/extensions/REMO/Config.pm b/extensions/REMO/Config.pm index a679d64a0..a1d8327de 100644 --- a/extensions/REMO/Config.pm +++ b/extensions/REMO/Config.pm @@ -28,10 +28,8 @@ use warnings; use constant NAME => 'REMO'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/REMO/Extension.pm b/extensions/REMO/Extension.pm index df4e70c80..7ca74e081 100644 --- a/extensions/REMO/Extension.pm +++ b/extensions/REMO/Extension.pm @@ -37,304 +37,311 @@ use List::Util qw(first); our $VERSION = '0.01'; sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; - if ($page eq 'remo-form-payment.html') { - _remo_form_payment($vars); - } + if ($page eq 'remo-form-payment.html') { + _remo_form_payment($vars); + } } sub _remo_form_payment { - my ($vars) = @_; - my $input = Bugzilla->input_params; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - - if ($input->{'action'} eq 'commit') { - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - - my $bug_id = $input->{'bug_id'}; - detaint_natural($bug_id); - my $bug = Bugzilla::Bug->check($bug_id); - - # Detect if the user already used the same form to submit again - my $token = trim($input->{'token'}); - if ($token) { - my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token); - if (!$creator_id - || $creator_id != $user->id - || $old_attach_id !~ "^remo_form_payment:") - { - # The token is invalid. - ThrowUserError('token_does_not_exist'); - } - - $old_attach_id =~ s/^remo_form_payment://; - if ($old_attach_id) { - ThrowUserError('remo_payment_cancel_dupe', - { bugid => $bug_id, attachid => $old_attach_id }); - } - } - - # Make sure the user can attach to this bug - if (!$bug->user->{'canedit'}) { - ThrowUserError("remo_payment_bug_edit_denied", - { bug_id => $bug->id }); - } - - # Make sure the bug is under the correct product/component - if ($bug->product ne 'Mozilla Reps' - || $bug->component ne 'Budget Requests') - { - ThrowUserError('remo_payment_invalid_product'); - } - - my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); - - $dbh->bz_start_transaction; - - # Create the comment to be added based on the form fields from rep-payment-form - my $comment; - $template->process("pages/comment-remo-form-payment.txt.tmpl", $vars, \$comment) - || ThrowTemplateError($template->error()); - $bug->add_comment($comment, { isprivate => 0 }); - - # Attach expense report - # FIXME: Would be nice to be able to have the above prefilled comment and - # the following attachments all show up under a single comment. But the longdescs - # table can only handle one attach_id per comment currently. At least only one - # email is sent the way it is done below. - my $attachment; - if (defined $cgi->upload('expenseform')) { - # Determine content-type - my $content_type = $cgi->uploadInfo($cgi->param('expenseform'))->{'Content-Type'}; - - $attachment = Bugzilla::Attachment->create( - { bug => $bug, - creation_ts => $timestamp, - data => $cgi->upload('expenseform'), - description => 'Expense Form', - filename => scalar $cgi->upload('expenseform'), - ispatch => 0, - isprivate => 0, - mimetype => $content_type, - }); - - # Insert comment for attachment - $bug->add_comment('', { isprivate => 0, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - } - - # Attach receipts file - if (defined $cgi->upload("receipts")) { - # Determine content-type - my $content_type = $cgi->uploadInfo($cgi->param("receipts"))->{'Content-Type'}; - - $attachment = Bugzilla::Attachment->create( - { bug => $bug, - creation_ts => $timestamp, - data => $cgi->upload('receipts'), - description => "Receipts", - filename => scalar $cgi->upload("receipts"), - ispatch => 0, - isprivate => 0, - mimetype => $content_type, - }); - - # Insert comment for attachment - $bug->add_comment('', { isprivate => 0, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - } - - $bug->update($timestamp); - - if ($token) { - trick_taint($token); - $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', undef, - ("remo_form_payment:" . $attachment->id, $token)); - } - - $dbh->bz_commit_transaction; - - # Define the variables and functions that will be passed to the UI template. - $vars->{'attachment'} = $attachment; - $vars->{'bugs'} = [ new Bugzilla::Bug($bug_id) ]; - $vars->{'header_done'} = 1; - $vars->{'contenttypemethod'} = 'autodetect'; - - my $recipients = { 'changer' => $user }; - $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bug_id, $recipients); - - print $cgi->header(); - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("attachment/created.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my ($vars) = @_; + my $input = Bugzilla->input_params; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + if ($input->{'action'} eq 'commit') { + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + + my $bug_id = $input->{'bug_id'}; + detaint_natural($bug_id); + my $bug = Bugzilla::Bug->check($bug_id); + + # Detect if the user already used the same form to submit again + my $token = trim($input->{'token'}); + if ($token) { + my ($creator_id, $date, $old_attach_id) = Bugzilla::Token::GetTokenData($token); + if (!$creator_id + || $creator_id != $user->id + || $old_attach_id !~ "^remo_form_payment:") + { + # The token is invalid. + ThrowUserError('token_does_not_exist'); + } + + $old_attach_id =~ s/^remo_form_payment://; + if ($old_attach_id) { + ThrowUserError('remo_payment_cancel_dupe', + {bugid => $bug_id, attachid => $old_attach_id}); + } } - else { - $vars->{'token'} = issue_session_token('remo_form_payment:'); + + # Make sure the user can attach to this bug + if (!$bug->user->{'canedit'}) { + ThrowUserError("remo_payment_bug_edit_denied", {bug_id => $bug->id}); + } + + # Make sure the bug is under the correct product/component + if ($bug->product ne 'Mozilla Reps' || $bug->component ne 'Budget Requests') { + ThrowUserError('remo_payment_invalid_product'); + } + + my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + + $dbh->bz_start_transaction; + + # Create the comment to be added based on the form fields from rep-payment-form + my $comment; + $template->process("pages/comment-remo-form-payment.txt.tmpl", $vars, \$comment) + || ThrowTemplateError($template->error()); + $bug->add_comment($comment, {isprivate => 0}); + + # Attach expense report + # FIXME: Would be nice to be able to have the above prefilled comment and + # the following attachments all show up under a single comment. But the longdescs + # table can only handle one attach_id per comment currently. At least only one + # email is sent the way it is done below. + my $attachment; + if (defined $cgi->upload('expenseform')) { + + # Determine content-type + my $content_type + = $cgi->uploadInfo($cgi->param('expenseform'))->{'Content-Type'}; + + $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $cgi->upload('expenseform'), + description => 'Expense Form', + filename => scalar $cgi->upload('expenseform'), + ispatch => 0, + isprivate => 0, + mimetype => $content_type, + }); + + # Insert comment for attachment + $bug->add_comment('', + {isprivate => 0, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id} + ); + } + + # Attach receipts file + if (defined $cgi->upload("receipts")) { + + # Determine content-type + my $content_type = $cgi->uploadInfo($cgi->param("receipts"))->{'Content-Type'}; + + $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $cgi->upload('receipts'), + description => "Receipts", + filename => scalar $cgi->upload("receipts"), + ispatch => 0, + isprivate => 0, + mimetype => $content_type, + }); + + # Insert comment for attachment + $bug->add_comment('', + {isprivate => 0, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id} + ); + } + + $bug->update($timestamp); + + if ($token) { + trick_taint($token); + $dbh->do('UPDATE tokens SET eventdata = ? WHERE token = ?', + undef, ("remo_form_payment:" . $attachment->id, $token)); } + + $dbh->bz_commit_transaction; + + # Define the variables and functions that will be passed to the UI template. + $vars->{'attachment'} = $attachment; + $vars->{'bugs'} = [new Bugzilla::Bug($bug_id)]; + $vars->{'header_done'} = 1; + $vars->{'contenttypemethod'} = 'autodetect'; + + my $recipients = {'changer' => $user}; + $vars->{'sent_bugmail'} = Bugzilla::BugMail::Send($bug_id, $recipients); + + print $cgi->header(); + + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("attachment/created.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + else { + $vars->{'token'} = issue_session_token('remo_form_payment:'); + } } my %CSV_COLUMNS = ( - "Date Required" => { pos => 1, value => '%cf_due_date' }, - "Requester" => { pos => 2, value => 'Rizki Kelimutu' }, - "Email 1" => { pos => 3, value => 'rkelimutu@mozilla.com' }, - "Mozilla Space" => { pos => 4, value => 'Remote' }, - "Team" => { pos => 5, value => 'Participation' }, - "Department Code" => { pos => 6, value => '1002' }, - "Purpose" => { pos => 7, value => 'Rep event: %eventpage' }, - "Item 1" => { pos => 8 }, - "Item 2" => { pos => 9 }, - "Item 3" => { pos => 10 }, - "Item 4" => { pos => 11 }, - "Item 5" => { pos => 12 }, - "Item 6" => { pos => 13 }, - "Item 7" => { pos => 14 }, - "Item 8" => { pos => 15 }, - "Item 9" => { pos => 16 }, - "Item 10" => { pos => 17 }, - "Item 11" => { pos => 18 }, - "Item 12" => { pos => 19 }, - "Item 13" => { pos => 20 }, - "Item 14" => { pos => 21 }, - "Recipient Name" => { pos => 22, value => '%shiptofirstname %shiptolastname' }, - "Email 2" => { pos => 23, value => sub { Bugzilla->user->email } }, - "Address 1" => { pos => 24, value => '%shiptoaddress1' }, - "Address 2" => { pos => 25, value => '%shiptoaddress2' }, - "City" => { pos => 26, value => '%shiptocity' }, - "State" => { pos => 27, value => '%shiptostate' }, - "Zip" => { pos => 28, value => '%shiptopcode' }, - "Country" => { pos => 29, value => '%shiptocountry' }, - "Phone number" => { pos => 30, value => '%shiptophone' }, - "Notes" => { pos => 31, value => '%shipadditional' }, + "Date Required" => {pos => 1, value => '%cf_due_date'}, + "Requester" => {pos => 2, value => 'Rizki Kelimutu'}, + "Email 1" => {pos => 3, value => 'rkelimutu@mozilla.com'}, + "Mozilla Space" => {pos => 4, value => 'Remote'}, + "Team" => {pos => 5, value => 'Participation'}, + "Department Code" => {pos => 6, value => '1002'}, + "Purpose" => {pos => 7, value => 'Rep event: %eventpage'}, + "Item 1" => {pos => 8}, + "Item 2" => {pos => 9}, + "Item 3" => {pos => 10}, + "Item 4" => {pos => 11}, + "Item 5" => {pos => 12}, + "Item 6" => {pos => 13}, + "Item 7" => {pos => 14}, + "Item 8" => {pos => 15}, + "Item 9" => {pos => 16}, + "Item 10" => {pos => 17}, + "Item 11" => {pos => 18}, + "Item 12" => {pos => 19}, + "Item 13" => {pos => 20}, + "Item 14" => {pos => 21}, + "Recipient Name" => {pos => 22, value => '%shiptofirstname %shiptolastname'}, + "Email 2" => { + pos => 23, + value => sub { Bugzilla->user->email } + }, + "Address 1" => {pos => 24, value => '%shiptoaddress1'}, + "Address 2" => {pos => 25, value => '%shiptoaddress2'}, + "City" => {pos => 26, value => '%shiptocity'}, + "State" => {pos => 27, value => '%shiptostate'}, + "Zip" => {pos => 28, value => '%shiptopcode'}, + "Country" => {pos => 29, value => '%shiptocountry'}, + "Phone number" => {pos => 30, value => '%shiptophone'}, + "Notes" => {pos => 31, value => '%shipadditional'}, ); sub _expand_value { - my $value = shift; - if (ref $value && ref $value eq 'CODE') { - return $value->(); - } - else { - my $cgi = Bugzilla->cgi; - $value =~ s/%(\w+)/$cgi->param($1)/ge; - return $value; - } + my $value = shift; + if (ref $value && ref $value eq 'CODE') { + return $value->(); + } + else { + my $cgi = Bugzilla->cgi; + $value =~ s/%(\w+)/$cgi->param($1)/ge; + return $value; + } } sub _csv_quote { - my $s = shift; - $s =~ s/"/""/g; - return qq{"$s"}; + my $s = shift; + $s =~ s/"/""/g; + return qq{"$s"}; } sub _csv_line { - return join(",", map { _csv_quote($_) } @_); + return join(",", map { _csv_quote($_) } @_); } sub _csv_encode { - return join("\r\n", map { _csv_line(@$_) } @_) . "\r\n"; + return join("\r\n", map { _csv_line(@$_) } @_) . "\r\n"; } sub post_bug_after_creation { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $bug = $vars->{bug}; - my $template = Bugzilla->template; - - my $format = Bugzilla->input_params->{format}; - - return unless defined $format; - - if ($format eq 'remo-swag') { - # If the attachment cannot be successfully added to the bug, - # we notify the user, but we don't interrupt the bug creation process. - my $error_mode_cache = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - - my @attachments; - eval { - my $xml; - $template->process("bug/create/create-remo-swag.xml.tmpl", {}, \$xml) - || ThrowTemplateError($template->error()); - - push @attachments, Bugzilla::Attachment->create( - { bug => $bug, - creation_ts => $bug->creation_ts, - data => $xml, - description => 'Remo Swag Request (XML)', - filename => 'remo-swag.xml', - ispatch => 0, - isprivate => 0, - mimetype => 'text/xml', - }); - - my @columns_raw = sort { $CSV_COLUMNS{$a}{pos} <=> $CSV_COLUMNS{$b}{pos} } keys %CSV_COLUMNS; - my @data = map { _expand_value( $CSV_COLUMNS{$_}{value} ) } @columns_raw; - my @columns = map { s/^(Item|Email) \d+$/$1/g; $_ } @columns_raw; - my $csv = _csv_encode(\@columns, \@data); - - push @attachments, Bugzilla::Attachment->create({ - bug => $bug, - creation_ts => $bug->creation_ts, - data => $csv, - description => 'Remo Swag Request (CSV)', - filename => 'remo-swag.csv', - ispatch => 0, - isprivate => 0, - mimetype => 'text/csv', - }); - }; - if ($@) { - warn "$@"; - } - - if (@attachments) { - # Insert comment for attachment - foreach my $attachment (@attachments) { - $bug->add_comment('', { isprivate => 0, - type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - } - $bug->update($bug->creation_ts); - delete $bug->{attachments}; - } - else { - $vars->{'message'} = 'attachment_creation_failed'; - } - - Bugzilla->error_mode($error_mode_cache); + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $bug = $vars->{bug}; + my $template = Bugzilla->template; + + my $format = Bugzilla->input_params->{format}; + + return unless defined $format; + + if ($format eq 'remo-swag') { + + # If the attachment cannot be successfully added to the bug, + # we notify the user, but we don't interrupt the bug creation process. + my $error_mode_cache = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + + my @attachments; + eval { + my $xml; + $template->process("bug/create/create-remo-swag.xml.tmpl", {}, \$xml) + || ThrowTemplateError($template->error()); + + push @attachments, + Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $bug->creation_ts, + data => $xml, + description => 'Remo Swag Request (XML)', + filename => 'remo-swag.xml', + ispatch => 0, + isprivate => 0, + mimetype => 'text/xml', + }); + + my @columns_raw + = sort { $CSV_COLUMNS{$a}{pos} <=> $CSV_COLUMNS{$b}{pos} } keys %CSV_COLUMNS; + my @data = map { _expand_value($CSV_COLUMNS{$_}{value}) } @columns_raw; + my @columns = map { s/^(Item|Email) \d+$/$1/g; $_ } @columns_raw; + my $csv = _csv_encode(\@columns, \@data); + + push @attachments, + Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $bug->creation_ts, + data => $csv, + description => 'Remo Swag Request (CSV)', + filename => 'remo-swag.csv', + ispatch => 0, + isprivate => 0, + mimetype => 'text/csv', + }); + }; + if ($@) { + warn "$@"; } - elsif ($format eq 'mozreps') { - my $needinfo_type = first { $_->name eq 'needinfo' } @{$bug->flag_types}; - return unless $needinfo_type; - my %original_cc = map { $_ => 1 } Bugzilla->cgi->param('cc'); - my @cc_users = grep { $_->is_enabled && $original_cc{$_->login}} @{$bug->cc_users}; - my @new_flags = map { - { type_id => $needinfo_type->id, - status => '?', - requestee => $_->login } - } @cc_users; - ThrowUserError('remo_missing_voucher') unless @cc_users; - - $bug->set_flags(\@new_flags, []) if @new_flags; - $bug->add_comment( - join(", ", map { $_->name || $_->login } @cc_users) . - ": You have been added as a voucher to this Reps application.\n" . - "Please provide a comment describing why you endorse this application.\n" . - "Thanks!" - ); + if (@attachments) { - $bug->update($bug->creation_ts); - Bugzilla::BugMail::Send($bug->id, { changer => Bugzilla->user }); + # Insert comment for attachment + foreach my $attachment (@attachments) { + $bug->add_comment('', + {isprivate => 0, type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id} + ); + } + $bug->update($bug->creation_ts); + delete $bug->{attachments}; } + else { + $vars->{'message'} = 'attachment_creation_failed'; + } + + Bugzilla->error_mode($error_mode_cache); + } + + elsif ($format eq 'mozreps') { + my $needinfo_type = first { $_->name eq 'needinfo' } @{$bug->flag_types}; + return unless $needinfo_type; + my %original_cc = map { $_ => 1 } Bugzilla->cgi->param('cc'); + my @cc_users + = grep { $_->is_enabled && $original_cc{$_->login} } @{$bug->cc_users}; + my @new_flags + = map { {type_id => $needinfo_type->id, status => '?', requestee => $_->login + } } @cc_users; + ThrowUserError('remo_missing_voucher') unless @cc_users; + + $bug->set_flags(\@new_flags, []) if @new_flags; + $bug->add_comment( + join(", ", map { $_->name || $_->login } @cc_users) + . ": You have been added as a voucher to this Reps application.\n" + . "Please provide a comment describing why you endorse this application.\n" + . "Thanks!"); + + $bug->update($bug->creation_ts); + Bugzilla::BugMail::Send($bug->id, {changer => Bugzilla->user}); + } } __PACKAGE__->NAME; diff --git a/extensions/RequestNagger/Config.pm b/extensions/RequestNagger/Config.pm index a338cd441..7bcaf3013 100644 --- a/extensions/RequestNagger/Config.pm +++ b/extensions/RequestNagger/Config.pm @@ -11,8 +11,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'RequestNagger'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'RequestNagger'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/RequestNagger/Extension.pm b/extensions/RequestNagger/Extension.pm index e0f97c9f7..54a11ff5b 100644 --- a/extensions/RequestNagger/Extension.pm +++ b/extensions/RequestNagger/Extension.pm @@ -25,241 +25,239 @@ use DateTime; our $VERSION = '1'; BEGIN { - *Bugzilla::Flag::age = \&_flag_age; - *Bugzilla::Flag::deferred = \&_flag_deferred; - *Bugzilla::Product::nag_interval = \&_product_nag_interval; + *Bugzilla::Flag::age = \&_flag_age; + *Bugzilla::Flag::deferred = \&_flag_deferred; + *Bugzilla::Product::nag_interval = \&_product_nag_interval; } sub _flag_age { - return time_ago(datetime_from($_[0]->modification_date)); + return time_ago(datetime_from($_[0]->modification_date)); } sub _flag_deferred { - my ($self) = @_; - if (!exists $self->{deferred}) { - my $dbh = Bugzilla->dbh; - my ($defer_until) = $dbh->selectrow_array( - "SELECT defer_until FROM nag_defer WHERE flag_id=?", - undef, - $self->id - ); - $self->{deferred} = $defer_until ? datetime_from($defer_until) : undef; - } - return $self->{deferred}; + my ($self) = @_; + if (!exists $self->{deferred}) { + my $dbh = Bugzilla->dbh; + my ($defer_until) + = $dbh->selectrow_array("SELECT defer_until FROM nag_defer WHERE flag_id=?", + undef, $self->id); + $self->{deferred} = $defer_until ? datetime_from($defer_until) : undef; + } + return $self->{deferred}; } sub _product_nag_interval { $_[0]->{nag_interval} } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Product')) { - push @$columns, 'nag_interval'; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Product')) { + push @$columns, 'nag_interval'; + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::Product')) { - push @$columns, 'nag_interval'; - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push @$columns, 'nag_interval'; + } } sub object_before_create { - my ($self, $args) = @_; - my ($class, $params) = @$args{qw(class params)}; - return unless $class->isa('Bugzilla::Product'); - my $input = Bugzilla->input_params; - if (exists $input->{nag_interval}) { - my $interval = _check_nag_interval($input->{nag_interval}); - $params->{nag_interval} = $interval; - } + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + return unless $class->isa('Bugzilla::Product'); + my $input = Bugzilla->input_params; + if (exists $input->{nag_interval}) { + my $interval = _check_nag_interval($input->{nag_interval}); + $params->{nag_interval} = $interval; + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my ($object, $params) = @$args{qw(object params)}; - return unless $object->isa('Bugzilla::Product'); - my $input = Bugzilla->input_params; - if (exists $input->{nag_interval}) { - my $interval = _check_nag_interval($input->{nag_interval}); - $object->set('nag_interval', $interval); - } + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + return unless $object->isa('Bugzilla::Product'); + my $input = Bugzilla->input_params; + if (exists $input->{nag_interval}) { + my $interval = _check_nag_interval($input->{nag_interval}); + $object->set('nag_interval', $interval); + } } sub _check_nag_interval { - my ($value) = @_; - detaint_natural($value) - || ThrowUserError('invalid_parameter', { name => 'request reminding interval', err => 'must be numeric' }); - return $value < 0 ? 0 : $value * 24; + my ($value) = @_; + detaint_natural($value) + || ThrowUserError('invalid_parameter', + {name => 'request reminding interval', err => 'must be numeric'}); + return $value < 0 ? 0 : $value * 24; } sub page_before_template { - my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; - return unless $page eq 'request_defer.html'; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $input = Bugzilla->input_params; - - # load flag - my $flag_id = scalar($input->{flag}) - || ThrowUserError('request_nagging_flag_invalid'); - detaint_natural($flag_id) - || ThrowUserError('request_nagging_flag_invalid'); - my $flag = Bugzilla::Flag->new({ id => $flag_id, cache => 1 }) - || ThrowUserError('request_nagging_flag_invalid'); - - # you can only defer flags directed at you - $user->can_see_bug($flag->bug->id) - || ThrowUserError("bug_access_denied", { bug_id => $flag->bug->id }); - $flag->status eq '?' - || ThrowUserError('request_nagging_flag_set'); - $flag->requestee - || ThrowUserError('request_nagging_flag_wind'); - $flag->requestee->id == $user->id - || ThrowUserError('request_nagging_flag_not_owned'); - - my $date = DateTime->now()->truncate(to => 'day'); - my $defer_until; - if ($input->{'defer-until'} - && $input->{'defer-until'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) - { - $defer_until = DateTime->new(year => $1, month => $2, day => $3); - if ($defer_until > $date->clone->add(days => 7)) { - $defer_until = undef; - } + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'request_defer.html'; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $input = Bugzilla->input_params; + + # load flag + my $flag_id + = scalar($input->{flag}) || ThrowUserError('request_nagging_flag_invalid'); + detaint_natural($flag_id) || ThrowUserError('request_nagging_flag_invalid'); + my $flag = Bugzilla::Flag->new({id => $flag_id, cache => 1}) + || ThrowUserError('request_nagging_flag_invalid'); + + # you can only defer flags directed at you + $user->can_see_bug($flag->bug->id) + || ThrowUserError("bug_access_denied", {bug_id => $flag->bug->id}); + $flag->status eq '?' || ThrowUserError('request_nagging_flag_set'); + $flag->requestee || ThrowUserError('request_nagging_flag_wind'); + $flag->requestee->id == $user->id + || ThrowUserError('request_nagging_flag_not_owned'); + + my $date = DateTime->now()->truncate(to => 'day'); + my $defer_until; + if ( $input->{'defer-until'} + && $input->{'defer-until'} =~ /^(\d\d\d\d)-(\d\d)-(\d\d)$/) + { + $defer_until = DateTime->new(year => $1, month => $2, day => $3); + if ($defer_until > $date->clone->add(days => 7)) { + $defer_until = undef; } - - if ($input->{save} && $defer_until) { - $self->_defer_until($flag_id, $defer_until); - $vars->{saved} = "1"; - $vars->{defer_until} = $defer_until; - } - else { - my @dates; - foreach my $i (1..7) { - $date->add(days => 1); - unshift @dates, { days => $i, date => $date->clone }; - } - $vars->{defer_until} = \@dates; + } + + if ($input->{save} && $defer_until) { + $self->_defer_until($flag_id, $defer_until); + $vars->{saved} = "1"; + $vars->{defer_until} = $defer_until; + } + else { + my @dates; + foreach my $i (1 .. 7) { + $date->add(days => 1); + unshift @dates, {days => $i, date => $date->clone}; } + $vars->{defer_until} = \@dates; + } - $vars->{flag} = $flag; + $vars->{flag} = $flag; } sub _defer_until { - my ($self, $flag_id, $defer_until) = @_; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - my ($defer_id) = $dbh->selectrow_array("SELECT id FROM nag_defer WHERE flag_id=?", undef, $flag_id); - if ($defer_id) { - $dbh->do("UPDATE nag_defer SET defer_until=? WHERE id=?", undef, $defer_until->ymd, $flag_id); - } else { - $dbh->do("INSERT INTO nag_defer(flag_id, defer_until) VALUES (?, ?)", undef, $flag_id, $defer_until->ymd); - } - - $dbh->bz_commit_transaction(); + my ($self, $flag_id, $defer_until) = @_; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + my ($defer_id) + = $dbh->selectrow_array("SELECT id FROM nag_defer WHERE flag_id=?", + undef, $flag_id); + if ($defer_id) { + $dbh->do("UPDATE nag_defer SET defer_until=? WHERE id=?", + undef, $defer_until->ymd, $flag_id); + } + else { + $dbh->do("INSERT INTO nag_defer(flag_id, defer_until) VALUES (?, ?)", + undef, $flag_id, $defer_until->ymd); + } + + $dbh->bz_commit_transaction(); } sub object_end_of_update { - my ($self, $args) = @_; - if ($args->{object}->isa("Bugzilla::Flag") && exists $args->{changes}) { - # any change to the flag (setting, clearing, or retargetting) will clear the deferals - my $flag = $args->{object}; - Bugzilla->dbh->do("DELETE FROM nag_defer WHERE flag_id=?", undef, $flag->id); - } + my ($self, $args) = @_; + if ($args->{object}->isa("Bugzilla::Flag") && exists $args->{changes}) { + +# any change to the flag (setting, clearing, or retargetting) will clear the deferals + my $flag = $args->{object}; + Bugzilla->dbh->do("DELETE FROM nag_defer WHERE flag_id=?", undef, $flag->id); + } } sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{'current_tab'}; - return unless $tab eq 'request_nagging'; - - my $save = $args->{'save_changes'}; - my $vars = $args->{'vars'}; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - - my %watching = - map { $_ => 1 } - @{ $dbh->selectcol_arrayref( - "SELECT profiles.login_name + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + return unless $tab eq 'request_nagging'; + + my $save = $args->{'save_changes'}; + my $vars = $args->{'vars'}; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + + my %watching = map { $_ => 1 } @{ + $dbh->selectcol_arrayref( + "SELECT profiles.login_name FROM nag_watch INNER JOIN profiles ON nag_watch.nagged_id = profiles.userid WHERE nag_watch.watcher_id = ? - ORDER BY profiles.login_name", - undef, - $user->id - ) }; - - my $nag_settings = Bugzilla::Extension::RequestNagger::Settings->new($user->id); - - if ($save) { - my $input = Bugzilla->input_params; - Bugzilla::User::match_field({ 'add_watching' => {'type' => 'multi'} }); - - $dbh->bz_start_transaction(); - - # user preference - if (my $value = $input->{request_nagging}) { - my $settings = $user->settings; - my $setting = new Bugzilla::User::Setting('request_nagging'); - if ($value eq 'default') { - $settings->{request_nagging}->reset_to_default; - } - else { - $setting->validate_value($value); - $settings->{request_nagging}->set($value); - } - } - - # watching - if ($input->{remove_watched_users}) { - my $del_watching = ref($input->{del_watching}) ? $input->{del_watching} : [ $input->{del_watching} ]; - foreach my $login (@$del_watching) { - my $u = Bugzilla::User->new({ name => $login, cache => 1 }) - || next; - next unless exists $watching{$u->login}; - $dbh->do( - "DELETE FROM nag_watch WHERE watcher_id=? AND nagged_id=?", - undef, - $user->id, $u->id - ); - delete $watching{$u->login}; - } - } - if ($input->{add_watching}) { - my $add_watching = ref($input->{add_watching}) ? $input->{add_watching} : [ $input->{add_watching} ]; - foreach my $login (@$add_watching) { - my $u = Bugzilla::User->new({ name => $login, cache => 1 }) - || next; - next if exists $watching{$u->login}; - $dbh->do( - "INSERT INTO nag_watch(watcher_id, nagged_id) VALUES(?, ?)", - undef, - $user->id, $u->id - ); - $watching{$u->login} = 1; - } - } - - # watching settings - foreach my $field (Bugzilla::Extension::RequestNagger::Settings::FIELDS()) { - $nag_settings->set($field, $input->{$field}); - } - - $dbh->bz_commit_transaction(); + ORDER BY profiles.login_name", undef, $user->id + ) + }; + + my $nag_settings = Bugzilla::Extension::RequestNagger::Settings->new($user->id); + + if ($save) { + my $input = Bugzilla->input_params; + Bugzilla::User::match_field({'add_watching' => {'type' => 'multi'}}); + + $dbh->bz_start_transaction(); + + # user preference + if (my $value = $input->{request_nagging}) { + my $settings = $user->settings; + my $setting = new Bugzilla::User::Setting('request_nagging'); + if ($value eq 'default') { + $settings->{request_nagging}->reset_to_default; + } + else { + $setting->validate_value($value); + $settings->{request_nagging}->set($value); + } } - $vars->{watching} = [ sort keys %watching ]; - $vars->{settings} = $nag_settings; + # watching + if ($input->{remove_watched_users}) { + my $del_watching + = ref($input->{del_watching}) + ? $input->{del_watching} + : [$input->{del_watching}]; + foreach my $login (@$del_watching) { + my $u = Bugzilla::User->new({name => $login, cache => 1}) || next; + next unless exists $watching{$u->login}; + $dbh->do("DELETE FROM nag_watch WHERE watcher_id=? AND nagged_id=?", + undef, $user->id, $u->id); + delete $watching{$u->login}; + } + } + if ($input->{add_watching}) { + my $add_watching + = ref($input->{add_watching}) + ? $input->{add_watching} + : [$input->{add_watching}]; + foreach my $login (@$add_watching) { + my $u = Bugzilla::User->new({name => $login, cache => 1}) || next; + next if exists $watching{$u->login}; + $dbh->do("INSERT INTO nag_watch(watcher_id, nagged_id) VALUES(?, ?)", + undef, $user->id, $u->id); + $watching{$u->login} = 1; + } + } + + # watching settings + foreach my $field (Bugzilla::Extension::RequestNagger::Settings::FIELDS()) { + $nag_settings->set($field, $input->{$field}); + } - my $handled = $args->{'handled'}; - $$handled = 1; + $dbh->bz_commit_transaction(); + } + + $vars->{watching} = [sort keys %watching]; + $vars->{settings} = $nag_settings; + + my $handled = $args->{'handled'}; + $$handled = 1; } # @@ -267,125 +265,77 @@ sub user_preferences { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'nag_watch'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - nagged_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - watcher_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - nag_watch_idx => { - FIELDS => [ 'nagged_id', 'watcher_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'nag_defer'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'flags', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - defer_until => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - ], - INDEXES => [ - nag_defer_idx => { - FIELDS => [ 'flag_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'nag_settings'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - setting_name => { - TYPE => 'VARCHAR(16)', - NOTNULL => 1, - }, - setting_value => { - TYPE => 'VARCHAR(16)', - NOTNULL => 1, - }, - ], - INDEXES => [ - nag_setting_idx => { - FIELDS => [ 'user_id', 'setting_name' ], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'nag_watch'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + nagged_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + watcher_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + nag_watch_idx => {FIELDS => ['nagged_id', 'watcher_id'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'nag_defer'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'flags', COLUMN => 'id', DELETE => 'CASCADE',} + }, + defer_until => {TYPE => 'DATETIME', NOTNULL => 1,}, + ], + INDEXES => [nag_defer_idx => {FIELDS => ['flag_id'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'nag_settings'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + setting_name => {TYPE => 'VARCHAR(16)', NOTNULL => 1,}, + setting_value => {TYPE => 'VARCHAR(16)', NOTNULL => 1,}, + ], + INDEXES => [ + nag_setting_idx => {FIELDS => ['user_id', 'setting_name'], TYPE => 'UNIQUE',}, + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('products', 'nag_interval', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 7 * 24 }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('products', 'nag_interval', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 7 * 24}); } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; - my $extensions_dir = bz_locations()->{'extensionsdir'}; - my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/send-request-nags.pl"; - $files->{$script_name} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{'files'}; + my $extensions_dir = bz_locations()->{'extensionsdir'}; + my $script_name + = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/send-request-nags.pl"; + $files->{$script_name} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'request_nagging', - options => ['on', 'off'], - default => 'on', - category => 'Reviews and Needinfo' - }); + my ($self, $args) = @_; + add_setting({ + name => 'request_nagging', + options => ['on', 'off'], + default => 'on', + category => 'Reviews and Needinfo' + }); } __PACKAGE__->NAME; diff --git a/extensions/RequestNagger/bin/send-request-nags.pl b/extensions/RequestNagger/bin/send-request-nags.pl index f823fc197..33c49d2b6 100755 --- a/extensions/RequestNagger/bin/send-request-nags.pl +++ b/extensions/RequestNagger/bin/send-request-nags.pl @@ -39,8 +39,8 @@ my $DO_NOT_NAG = grep { $_ eq '-d' } @ARGV; @ARGV = grep { !/^-/ } @ARGV; if (my $filename = shift @ARGV) { - _send_email(decode_json(scalar read_file($filename))); - exit; + _send_email(decode_json(scalar read_file($filename))); + exit; } my $dbh = Bugzilla->dbh; @@ -53,269 +53,272 @@ Bugzilla->switch_to_shadow_db(); # send nags to requestees send_nags( - reports => [ 'requestee' ], - requestee_sql => REQUESTEE_NAG_SQL, - setter_sql => SETTER_NAG_SQL, - template => 'user', - date => $date, + reports => ['requestee'], + requestee_sql => REQUESTEE_NAG_SQL, + setter_sql => SETTER_NAG_SQL, + template => 'user', + date => $date, ); # send nags to watchers send_nags( - reports => [ 'requestee', 'setter' ], - requestee_sql => WATCHING_REQUESTEE_NAG_SQL, - setter_sql => WATCHING_SETTER_NAG_SQL, - template => 'watching', - date => $date, + reports => ['requestee', 'setter'], + requestee_sql => WATCHING_REQUESTEE_NAG_SQL, + setter_sql => WATCHING_SETTER_NAG_SQL, + template => 'watching', + date => $date, ); sub send_nags { - my (%args) = @_; - my $requests = {}; - my $watching = $args{template} eq 'watching'; - - # get requests - - foreach my $report (@{ $args{reports} }) { - - # collate requests - my $rows = $dbh->selectall_arrayref($args{$report . '_sql'}, { Slice => {} }); - foreach my $request (@$rows) { - next unless _include_request($request, $report); - - my $target = Bugzilla::User->new({ id => $request->{target_id}, cache => 1 }); - push @{ - $requests - ->{$request->{recipient_id}} - ->{$target->login} - ->{$report} - ->{$request->{flag_type}} - }, $request; - push @{ - $requests - ->{$request->{recipient_id}} - ->{$target->login} - ->{bug_ids} - ->{$report} - }, $request->{bug_id}; - } + my (%args) = @_; + my $requests = {}; + my $watching = $args{template} eq 'watching'; - # process requests here to avoid doing it in the templates - foreach my $recipient_id (keys %$requests) { - foreach my $target_login (keys %{ $requests->{$recipient_id} }) { - my $rh = $requests->{$recipient_id}->{$target_login}; - - # build a list of valid types in the correct order - $rh->{types}->{$report} = []; - foreach my $type (map { $_->{type} } FLAG_TYPES) { - next unless exists $rh->{$report}->{$type}; - push @{ $rh->{types}->{$report} }, $type; - } - - # build a summary - $rh->{summary}->{$report} = join(', ', - map { scalar(@{ $rh->{$report}->{$_} }) . ' ' . $_ } - @{ $rh->{types}->{$report} } - ); - - if ($watching && $report eq 'setter') { - # remove links to reports with too many items to display - my $total = 0; - foreach my $type (@{ $rh->{types}->{$report} }) { - $total += scalar(@{ $rh->{$report}->{$type} }); - } - if ($total > MAX_SETTER_COUNT) { - $rh->{types}->{$report} = []; - } - } - } - } - } + # get requests - # send emails - - foreach my $recipient_id (sort keys %$requests) { - # send the email in a separate process to avoid excessive memory usage - my $params = { - recipient_id => $recipient_id, - template => $args{template}, - date => $args{date}, - reports => $args{reports}, - requests => $requests->{$recipient_id}, - }; - my ($fh, $filename) = tempfile(); - print $fh encode_json($params); - close($fh); - - my @command = ($0, $filename); - push @command, '-d' if $DO_NOT_NAG; - system(@command); - unlink($filename); - } -} - -sub _include_request { - my ($request, $report) = @_; - state $now = datetime_from($db_date, 'UTC')->truncate( to => 'day' ); - - my $recipient = Bugzilla::User->new({ id => $request->{recipient_id}, cache => 1 }); - - if ($report eq 'requestee') { - # check recipient group membership - my $group; - foreach my $type (FLAG_TYPES) { - next unless $type->{type} eq $request->{flag_type}; - $group = $type->{group}; - last; - } - return 0 unless $recipient->in_group($group); - } + foreach my $report (@{$args{reports}}) { - # check bug visibility - return 0 unless $recipient->can_see_bug($request->{bug_id}); + # collate requests + my $rows = $dbh->selectall_arrayref($args{$report . '_sql'}, {Slice => {}}); + foreach my $request (@$rows) { + next unless _include_request($request, $report); - # check attachment visibility - if ($request->{attach_id}) { - my $attachment = Bugzilla::Attachment->new({ id => $request->{attach_id}, cache => 1 }); - return 0 if $attachment->isprivate && !$recipient->is_insider; + my $target = Bugzilla::User->new({id => $request->{target_id}, cache => 1}); + push @{$requests->{$request->{recipient_id}}->{$target->login}->{$report} + ->{$request->{flag_type}}}, $request; + push @{$requests->{$request->{recipient_id}}->{$target->login}->{bug_ids} + ->{$report}}, $request->{bug_id}; } - # exclude weekends and re-check nag-interval - my $date = datetime_from($request->{modification_date}, 'UTC'); - my $hours = 0; - $hours += 24 - $date->hour if $date->day_of_week <= 5; - $date->add( days => 1 )->truncate( to => 'day' ); - while ($date < $now) { - $hours += 24 if $date->day_of_week <= 5; - $date->add( days => 1 ); - } - return 0 if $hours < ($request->{extended_period} ? $request->{nag_interval} + 24 : $request->{nag_interval}); + # process requests here to avoid doing it in the templates + foreach my $recipient_id (keys %$requests) { + foreach my $target_login (keys %{$requests->{$recipient_id}}) { + my $rh = $requests->{$recipient_id}->{$target_login}; - return 1; -} + # build a list of valid types in the correct order + $rh->{types}->{$report} = []; + foreach my $type (map { $_->{type} } FLAG_TYPES) { + next unless exists $rh->{$report}->{$type}; + push @{$rh->{types}->{$report}}, $type; + } -sub _send_email { - my ($params) = @_; - - my @reports = @{ $params->{reports} }; - my $recipient_id = $params->{recipient_id}; - my $requests = $params->{requests}; - my $watching = $params->{template} eq 'watching'; - my $recipient = Bugzilla::User->new({ id => $recipient_id, cache => 1 }); - my $securemail = Bugzilla::User->can('public_key'); - my $has_key = $securemail && $recipient->public_key; - my $has_private_bug = 0; - - my $settings = Bugzilla::Extension::RequestNagger::Settings->new($recipient_id); - if ($watching && $settings->no_encryption) { - $has_key = 0; - } + # build a summary + $rh->{summary}->{$report} = join(', ', + map { scalar(@{$rh->{$report}->{$_}}) . ' ' . $_ } @{$rh->{types}->{$report}}); + + if ($watching && $report eq 'setter') { - foreach my $target_login (keys %$requests) { - my $rh = $requests->{$target_login}; - $rh->{target} = Bugzilla::User->new({ name => $target_login, cache => 1 }); - foreach my $report (@reports) { - foreach my $type (keys %{ $rh->{$report} }) { - foreach my $request (@{ $rh->{$report}->{$type} }) { - - _create_objects($request); - - # we need to encrypt or censor emails which contain - # non-public bugs - if ($request->{bug}->is_private) { - $has_private_bug = 1; - $request->{bug}->{sanitise_bug} = !$securemail || !$has_key; - } - else { - $request->{bug}->{sanitise_bug} = 0; - } - } - } + # remove links to reports with too many items to display + my $total = 0; + foreach my $type (@{$rh->{types}->{$report}}) { + $total += scalar(@{$rh->{$report}->{$type}}); + } + if ($total > MAX_SETTER_COUNT) { + $rh->{types}->{$report} = []; + } } + } } - my $encrypt = $securemail && $has_private_bug && $has_key; - - # generate email - my $template = Bugzilla->template_inner($recipient->setting('lang')); - my $template_file = $params->{template}; - my $vars = { - recipient => $recipient, - requests => $requests, - date => $params->{date}, - }; + } - my ($header, $text); - $template->process("email/request_nagging-$template_file-header.txt.tmpl", $vars, \$header) - || ThrowTemplateError($template->error()); - $header .= "\n"; - $template->process("email/request_nagging-$template_file.txt.tmpl", $vars, \$text) - || ThrowTemplateError($template->error()); - - my @parts = ( - Email::MIME->create( - attributes => { - content_type => 'text/plain', - charset => 'UTF-8', - encoding => 'quoted-printable', - }, - body_str => $text, - ) - ); - if ($recipient->setting('email_format') eq 'html') { - my $html; - $template->process("email/request_nagging-$template_file.html.tmpl", $vars, \$html) - || ThrowTemplateError($template->error()); - push @parts, Email::MIME->create( - attributes => { - content_type => 'text/html', - charset => 'UTF-8', - encoding => 'quoted-printable', - }, - body_str => $html, - ); - } + # send emails - my $email = Email::MIME->new($header); - $email->header_set('X-Generated-By' => hostname()); - if (scalar(@parts) == 1) { - $email->content_type_set($parts[0]->content_type); - } - else { - $email->content_type_set('multipart/alternative'); - } - $email->parts_set(\@parts); - if ($encrypt) { - $email->header_set('X-Bugzilla-Encrypt' => '1'); - } + foreach my $recipient_id (sort keys %$requests) { - # send - if ($DO_NOT_NAG) { - # uncomment the following line to enable other extensions to - # process this email, including encryption - # Bugzilla::Hook::process('mailer_before_send', { email => $email }); - print $email->as_string, "\n"; - } - else { - MessageToMTA($email); - } + # send the email in a separate process to avoid excessive memory usage + my $params = { + recipient_id => $recipient_id, + template => $args{template}, + date => $args{date}, + reports => $args{reports}, + requests => $requests->{$recipient_id}, + }; + my ($fh, $filename) = tempfile(); + print $fh encode_json($params); + close($fh); + + my @command = ($0, $filename); + push @command, '-d' if $DO_NOT_NAG; + system(@command); + unlink($filename); + } } -sub _create_objects { - my ($request) = @_; +sub _include_request { + my ($request, $report) = @_; + state $now = datetime_from($db_date, 'UTC')->truncate(to => 'day'); - $request->{recipient} = Bugzilla::User->new({ id => $request->{recipient_id}, cache => 1 }); - $request->{setter} = Bugzilla::User->new({ id => $request->{setter_id}, cache => 1 }); + my $recipient + = Bugzilla::User->new({id => $request->{recipient_id}, cache => 1}); - if (defined $request->{requestee_id}) { - $request->{requestee} = Bugzilla::User->new({ id => $request->{requestee_id}, cache => 1 }); - } - if (exists $request->{watcher_id}) { - $request->{watcher} = Bugzilla::User->new({ id => $request->{watcher_id}, cache => 1 }); + if ($report eq 'requestee') { + + # check recipient group membership + my $group; + foreach my $type (FLAG_TYPES) { + next unless $type->{type} eq $request->{flag_type}; + $group = $type->{group}; + last; } + return 0 unless $recipient->in_group($group); + } + + # check bug visibility + return 0 unless $recipient->can_see_bug($request->{bug_id}); + + # check attachment visibility + if ($request->{attach_id}) { + my $attachment + = Bugzilla::Attachment->new({id => $request->{attach_id}, cache => 1}); + return 0 if $attachment->isprivate && !$recipient->is_insider; + } + + # exclude weekends and re-check nag-interval + my $date = datetime_from($request->{modification_date}, 'UTC'); + my $hours = 0; + $hours += 24 - $date->hour if $date->day_of_week <= 5; + $date->add(days => 1)->truncate(to => 'day'); + while ($date < $now) { + $hours += 24 if $date->day_of_week <= 5; + $date->add(days => 1); + } + return 0 + if $hours < ($request->{extended_period} + ? $request->{nag_interval} + 24 + : $request->{nag_interval}); + + return 1; +} - $request->{bug} = Bugzilla::Extension::RequestNagger::Bug->new({ id => $request->{bug_id}, cache => 1 }); - $request->{flag} = Bugzilla::Flag->new({ id => $request->{flag_id}, cache => 1 }); - if (defined $request->{attach_id}) { - $request->{attachment} = Bugzilla::Attachment->new({ id => $request->{attach_id}, cache => 1 }); +sub _send_email { + my ($params) = @_; + + my @reports = @{$params->{reports}}; + my $recipient_id = $params->{recipient_id}; + my $requests = $params->{requests}; + my $watching = $params->{template} eq 'watching'; + my $recipient = Bugzilla::User->new({id => $recipient_id, cache => 1}); + my $securemail = Bugzilla::User->can('public_key'); + my $has_key = $securemail && $recipient->public_key; + my $has_private_bug = 0; + + my $settings = Bugzilla::Extension::RequestNagger::Settings->new($recipient_id); + if ($watching && $settings->no_encryption) { + $has_key = 0; + } + + foreach my $target_login (keys %$requests) { + my $rh = $requests->{$target_login}; + $rh->{target} = Bugzilla::User->new({name => $target_login, cache => 1}); + foreach my $report (@reports) { + foreach my $type (keys %{$rh->{$report}}) { + foreach my $request (@{$rh->{$report}->{$type}}) { + + _create_objects($request); + + # we need to encrypt or censor emails which contain + # non-public bugs + if ($request->{bug}->is_private) { + $has_private_bug = 1; + $request->{bug}->{sanitise_bug} = !$securemail || !$has_key; + } + else { + $request->{bug}->{sanitise_bug} = 0; + } + } + } } + } + my $encrypt = $securemail && $has_private_bug && $has_key; + + # generate email + my $template = Bugzilla->template_inner($recipient->setting('lang')); + my $template_file = $params->{template}; + my $vars + = {recipient => $recipient, requests => $requests, date => $params->{date},}; + + my ($header, $text); + $template->process("email/request_nagging-$template_file-header.txt.tmpl", + $vars, \$header) + || ThrowTemplateError($template->error()); + $header .= "\n"; + $template->process("email/request_nagging-$template_file.txt.tmpl", + $vars, \$text) + || ThrowTemplateError($template->error()); + + my @parts = (Email::MIME->create( + attributes => { + content_type => 'text/plain', + charset => 'UTF-8', + encoding => 'quoted-printable', + }, + body_str => $text, + )); + + if ($recipient->setting('email_format') eq 'html') { + my $html; + $template->process("email/request_nagging-$template_file.html.tmpl", + $vars, \$html) + || ThrowTemplateError($template->error()); + push @parts, + Email::MIME->create( + attributes => { + content_type => 'text/html', + charset => 'UTF-8', + encoding => 'quoted-printable', + }, + body_str => $html, + ); + } + + my $email = Email::MIME->new($header); + $email->header_set('X-Generated-By' => hostname()); + if (scalar(@parts) == 1) { + $email->content_type_set($parts[0]->content_type); + } + else { + $email->content_type_set('multipart/alternative'); + } + $email->parts_set(\@parts); + if ($encrypt) { + $email->header_set('X-Bugzilla-Encrypt' => '1'); + } + + # send + if ($DO_NOT_NAG) { + + # uncomment the following line to enable other extensions to + # process this email, including encryption + # Bugzilla::Hook::process('mailer_before_send', { email => $email }); + print $email->as_string, "\n"; + } + else { + MessageToMTA($email); + } +} + +sub _create_objects { + my ($request) = @_; + + $request->{recipient} + = Bugzilla::User->new({id => $request->{recipient_id}, cache => 1}); + $request->{setter} + = Bugzilla::User->new({id => $request->{setter_id}, cache => 1}); + + if (defined $request->{requestee_id}) { + $request->{requestee} + = Bugzilla::User->new({id => $request->{requestee_id}, cache => 1}); + } + if (exists $request->{watcher_id}) { + $request->{watcher} + = Bugzilla::User->new({id => $request->{watcher_id}, cache => 1}); + } + + $request->{bug} = Bugzilla::Extension::RequestNagger::Bug->new( + {id => $request->{bug_id}, cache => 1}); + $request->{flag} = Bugzilla::Flag->new({id => $request->{flag_id}, cache => 1}); + if (defined $request->{attach_id}) { + $request->{attachment} + = Bugzilla::Attachment->new({id => $request->{attach_id}, cache => 1}); + } } diff --git a/extensions/RequestNagger/lib/Bug.pm b/extensions/RequestNagger/lib/Bug.pm index 974a688ea..dc510d486 100644 --- a/extensions/RequestNagger/lib/Bug.pm +++ b/extensions/RequestNagger/lib/Bug.pm @@ -17,29 +17,29 @@ use feature 'state'; use Bugzilla::User; sub short_desc { - my ($self) = @_; - return $self->{sanitise_bug} ? '(Secure bug)' : $self->SUPER::short_desc; + my ($self) = @_; + return $self->{sanitise_bug} ? '(Secure bug)' : $self->SUPER::short_desc; } sub is_private { - my ($self) = @_; - if (!exists $self->{is_private}) { - state $default_user //= Bugzilla::User->new(); - $self->{is_private} = !$default_user->can_see_bug($self); - } - return $self->{is_private}; + my ($self) = @_; + if (!exists $self->{is_private}) { + state $default_user //= Bugzilla::User->new(); + $self->{is_private} = !$default_user->can_see_bug($self); + } + return $self->{is_private}; } sub tooltip { - my ($self) = @_; - my $tooltip = $self->bug_status; - if ($self->bug_status eq 'RESOLVED') { - $tooltip .= '/' . $self->resolution; - } - if (!$self->{sanitise_bug}) { - $tooltip .= ' ' . $self->product . ' :: ' . $self->component; - } - return $tooltip; + my ($self) = @_; + my $tooltip = $self->bug_status; + if ($self->bug_status eq 'RESOLVED') { + $tooltip .= '/' . $self->resolution; + } + if (!$self->{sanitise_bug}) { + $tooltip .= ' ' . $self->product . ' :: ' . $self->component; + } + return $tooltip; } 1; diff --git a/extensions/RequestNagger/lib/Constants.pm b/extensions/RequestNagger/lib/Constants.pm index bc6cf3371..1309e06c9 100644 --- a/extensions/RequestNagger/lib/Constants.pm +++ b/extensions/RequestNagger/lib/Constants.pm @@ -14,13 +14,13 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - MAX_SETTER_COUNT - MAX_REQUEST_AGE - FLAG_TYPES - REQUESTEE_NAG_SQL - SETTER_NAG_SQL - WATCHING_REQUESTEE_NAG_SQL - WATCHING_SETTER_NAG_SQL + MAX_SETTER_COUNT + MAX_REQUEST_AGE + FLAG_TYPES + REQUESTEE_NAG_SQL + SETTER_NAG_SQL + WATCHING_REQUESTEE_NAG_SQL + WATCHING_SETTER_NAG_SQL ); # if there are more than this many requests that a user is waiting on, show a @@ -29,33 +29,24 @@ use constant MAX_SETTER_COUNT => 7; # ignore any request older than this many days in the requestee emails # massively overdue requests will still be included in the 'watching' emails -use constant MAX_REQUEST_AGE => 90; # about three months +use constant MAX_REQUEST_AGE => 90; # about three months # the order of this array determines the order used in email use constant FLAG_TYPES => ( - { - type => 'review', # flag_type.name - group => 'everyone', # the user must be a member of this group to receive reminders - }, - { - type => 'superview', - group => 'everyone', - }, - { - type => 'feedback', - group => 'everyone', - }, - { - type => 'needinfo', - group => 'editbugs', - }, + { + type => 'review', # flag_type.name + group => 'everyone', # the user must be a member of this group to receive reminders + }, + {type => 'superview', group => 'everyone',}, + {type => 'feedback', group => 'everyone',}, + {type => 'needinfo', group => 'editbugs',}, ); sub REQUESTEE_NAG_SQL { - my $dbh = Bugzilla->dbh; - my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; - return " + return " SELECT flagtypes.name AS flag_type, flags.id AS flag_id, @@ -84,7 +75,8 @@ sub REQUESTEE_NAG_SQL { AND flags.status = '?' AND products.nag_interval != 0 AND TIMESTAMPDIFF(HOUR, flags.modification_date, CURRENT_DATE()) >= products.nag_interval - AND TIMESTAMPDIFF(DAY, flags.modification_date, CURRENT_DATE()) <= " . MAX_REQUEST_AGE . " + AND TIMESTAMPDIFF(DAY, flags.modification_date, CURRENT_DATE()) <= " + . MAX_REQUEST_AGE . " AND (profile_setting.setting_value IS NULL OR profile_setting.setting_value = 'on') AND requestee.disable_mail = 0 AND nag_defer.id IS NULL @@ -96,10 +88,10 @@ sub REQUESTEE_NAG_SQL { } sub SETTER_NAG_SQL { - my $dbh = Bugzilla->dbh; - my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; - return " + return " SELECT flagtypes.name AS flag_type, flags.id AS flag_id, @@ -128,7 +120,8 @@ sub SETTER_NAG_SQL { AND flags.status = '?' AND products.nag_interval != 0 AND TIMESTAMPDIFF(HOUR, flags.modification_date, CURRENT_DATE()) >= products.nag_interval - AND TIMESTAMPDIFF(DAY, flags.modification_date, CURRENT_DATE()) <= " . MAX_REQUEST_AGE . " + AND TIMESTAMPDIFF(DAY, flags.modification_date, CURRENT_DATE()) <= " + . MAX_REQUEST_AGE . " AND (profile_setting.setting_value IS NULL OR profile_setting.setting_value = 'on') AND setter.disable_mail = 0 AND nag_defer.id IS NULL @@ -140,10 +133,10 @@ sub SETTER_NAG_SQL { } sub WATCHING_REQUESTEE_NAG_SQL { - my $dbh = Bugzilla->dbh; - my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; - return " + return " SELECT nag_watch.watcher_id, flagtypes.name AS flag_type, @@ -192,10 +185,10 @@ sub WATCHING_REQUESTEE_NAG_SQL { } sub WATCHING_SETTER_NAG_SQL { - my $dbh = Bugzilla->dbh; - my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; + my $dbh = Bugzilla->dbh; + my @flag_types_sql = map { $dbh->quote($_->{type}) } FLAG_TYPES; - return " + return " SELECT nag_watch.watcher_id, flagtypes.name AS flag_type, diff --git a/extensions/RequestNagger/lib/Settings.pm b/extensions/RequestNagger/lib/Settings.pm index 393d224ba..839c37485 100644 --- a/extensions/RequestNagger/lib/Settings.pm +++ b/extensions/RequestNagger/lib/Settings.pm @@ -17,47 +17,48 @@ use List::MoreUtils qw( any ); use constant FIELDS => qw( reviews_only extended_period no_encryption ); sub new { - my ($class, $user_id) = @_; - - my $dbh = Bugzilla->dbh; - my $self = { user_id => $user_id }; - foreach my $row (@{ $dbh->selectall_arrayref( - "SELECT setting_name,setting_value FROM nag_settings WHERE user_id = ?", - { Slice => {} }, - $user_id - ) }) { - $self->{$row->{setting_name}} = $row->{setting_value}; - } - - return bless($self, $class); + my ($class, $user_id) = @_; + + my $dbh = Bugzilla->dbh; + my $self = {user_id => $user_id}; + foreach my $row (@{ + $dbh->selectall_arrayref( + "SELECT setting_name,setting_value FROM nag_settings WHERE user_id = ?", + {Slice => {}}, $user_id) + }) + { + $self->{$row->{setting_name}} = $row->{setting_value}; + } + + return bless($self, $class); } -sub reviews_only { exists $_[0]->{reviews_only} ? $_[0]->{reviews_only} : 0 } -sub extended_period { exists $_[0]->{extended_period} ? $_[0]->{extended_period} : 0 } -sub no_encryption { exists $_[0]->{no_encryption} ? $_[0]->{no_encryption} : 0 } +sub reviews_only { exists $_[0]->{reviews_only} ? $_[0]->{reviews_only} : 0 } + +sub extended_period { + exists $_[0]->{extended_period} ? $_[0]->{extended_period} : 0; +} +sub no_encryption { exists $_[0]->{no_encryption} ? $_[0]->{no_encryption} : 0 } sub set { - my ($self, $field, $value) = @_; - return unless any { $_ eq $field } FIELDS; - $value = $value ? 1 : 0; - - my $dbh = Bugzilla->dbh; - if (exists $self->{$field}) { - $dbh->do( - "UPDATE nag_settings SET setting_value=? WHERE user_id=? AND setting_name=?", - undef, - $value, $self->{user_id}, $field - ); - } - else { - $dbh->do( - "INSERT INTO nag_settings(user_id, setting_name, setting_value) VALUES (?, ?, ?)", - undef, - $self->{user_id}, $field, $value - ); - } - - $self->{$field} = $value; + my ($self, $field, $value) = @_; + return unless any { $_ eq $field } FIELDS; + $value = $value ? 1 : 0; + + my $dbh = Bugzilla->dbh; + if (exists $self->{$field}) { + $dbh->do( + "UPDATE nag_settings SET setting_value=? WHERE user_id=? AND setting_name=?", + undef, $value, $self->{user_id}, $field); + } + else { + $dbh->do( + "INSERT INTO nag_settings(user_id, setting_name, setting_value) VALUES (?, ?, ?)", + undef, $self->{user_id}, $field, $value + ); + } + + $self->{$field} = $value; } 1; diff --git a/extensions/RestrictComments/Config.pm b/extensions/RestrictComments/Config.pm index be703bed7..eb99e5a94 100644 --- a/extensions/RestrictComments/Config.pm +++ b/extensions/RestrictComments/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'RestrictComments'; +use constant NAME => 'RestrictComments'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/RestrictComments/Extension.pm b/extensions/RestrictComments/Extension.pm index e93540d5a..83075ac23 100644 --- a/extensions/RestrictComments/Extension.pm +++ b/extensions/RestrictComments/Extension.pm @@ -17,84 +17,86 @@ use Bugzilla::Constants; use Bugzilla::Util qw(i_am_webservice); BEGIN { - *Bugzilla::Bug::restrict_comments = \&_bug_restrict_comments; + *Bugzilla::Bug::restrict_comments = \&_bug_restrict_comments; } sub _bug_restrict_comments { - my ($self) = @_; - return $self->{restrict_comments}; + my ($self) = @_; + return $self->{restrict_comments}; } sub bug_check_can_change_field { - my ($self, $args) = @_; - my ($bug, $priv_results) = @$args{qw(bug priv_results)}; - my $user = Bugzilla->user; - - if ($user->id - && $bug->restrict_comments - && !$user->in_group(Bugzilla->params->{'restrict_comments_group'})) - { - push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - return; - } + my ($self, $args) = @_; + my ($bug, $priv_results) = @$args{qw(bug priv_results)}; + my $user = Bugzilla->user; + + if ( $user->id + && $bug->restrict_comments + && !$user->in_group(Bugzilla->params->{'restrict_comments_group'})) + { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + return; + } } sub _can_restrict_comments { - my ($self, $object) = @_; - return unless $object->isa('Bugzilla::Bug'); - $self->{setter_group} ||= Bugzilla->params->{'restrict_comments_enable_group'}; - return Bugzilla->user->in_group($self->{setter_group}); + my ($self, $object) = @_; + return unless $object->isa('Bugzilla::Bug'); + $self->{setter_group} ||= Bugzilla->params->{'restrict_comments_enable_group'}; + return Bugzilla->user->in_group($self->{setter_group}); } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{object}; - my $input = Bugzilla->input_params; - my $update_restrict_comments = !i_am_webservice() || exists $input->{restrict_comments}; - if ($update_restrict_comments && $self->_can_restrict_comments($object)) { - $object->set('restrict_comments', $input->{restrict_comments} ? 1 : undef); - } + my ($self, $args) = @_; + my $object = $args->{object}; + my $input = Bugzilla->input_params; + my $update_restrict_comments + = !i_am_webservice() || exists $input->{restrict_comments}; + if ($update_restrict_comments && $self->_can_restrict_comments($object)) { + $object->set('restrict_comments', $input->{restrict_comments} ? 1 : undef); + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($self->_can_restrict_comments($object)) { - push(@$columns, 'restrict_comments'); - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($self->_can_restrict_comments($object)) { + push(@$columns, 'restrict_comments'); + } } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Bug')) { - if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'restrict_comments')) { - push @$columns, 'restrict_comments'; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Bug')) { + if (Bugzilla->dbh->bz_column_info($class->DB_TABLE, 'restrict_comments')) { + push @$columns, 'restrict_comments'; } + } } sub bug_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - push (@$fields, 'restrict_comments') + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + push(@$fields, 'restrict_comments'); } sub config_add_panels { - my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{RestrictComments} = "Bugzilla::Extension::RestrictComments::Config"; + my ($self, $args) = @_; + my $modules = $args->{panel_modules}; + $modules->{RestrictComments} = "Bugzilla::Extension::RestrictComments::Config"; } sub install_update_db { - my $dbh = Bugzilla->dbh; + my $dbh = Bugzilla->dbh; - my $field = new Bugzilla::Field({ name => 'restrict_comments' }); - if (!$field) { - Bugzilla::Field->create({ name => 'restrict_comments', description => 'Restrict Comments' }); - } + my $field = new Bugzilla::Field({name => 'restrict_comments'}); + if (!$field) { + Bugzilla::Field->create( + {name => 'restrict_comments', description => 'Restrict Comments'}); + } - $dbh->bz_add_column('bugs', 'restrict_comments', { TYPE => 'BOOLEAN' }); + $dbh->bz_add_column('bugs', 'restrict_comments', {TYPE => 'BOOLEAN'}); } __PACKAGE__->NAME; diff --git a/extensions/RestrictComments/lib/Config.pm b/extensions/RestrictComments/lib/Config.pm index e5dbc518c..c1f9829f9 100644 --- a/extensions/RestrictComments/lib/Config.pm +++ b/extensions/RestrictComments/lib/Config.pm @@ -17,26 +17,26 @@ use Bugzilla::Group; our $sortkey = 510; sub get_param_list { - my ($class) = @_; - - my @param_list = ( - { - name => 'restrict_comments_group', - type => 's', - choices => \&get_all_group_names, - default => '', - checker => \&check_group - }, - { - name => 'restrict_comments_enable_group', - type => 's', - choices => \&get_all_group_names, - default => '', - checker => \&check_group - }, - ); - - return @param_list; + my ($class) = @_; + + my @param_list = ( + { + name => 'restrict_comments_group', + type => 's', + choices => \&get_all_group_names, + default => '', + checker => \&check_group + }, + { + name => 'restrict_comments_enable_group', + type => 's', + choices => \&get_all_group_names, + default => '', + checker => \&check_group + }, + ); + + return @param_list; } 1; diff --git a/extensions/Review/Config.pm b/extensions/Review/Config.pm index ea7e8a725..aa9a8c32d 100644 --- a/extensions/Review/Config.pm +++ b/extensions/Review/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'Review'; +use constant NAME => 'Review'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/Review/Extension.pm b/extensions/Review/Extension.pm index a918a5ca5..975857cf7 100644 --- a/extensions/Review/Extension.pm +++ b/extensions/Review/Extension.pm @@ -36,95 +36,96 @@ use constant MENTOR_LIMIT => 10; # BEGIN { - *Bugzilla::Product::reviewers = \&_product_reviewers; - *Bugzilla::Product::reviewers_objs = \&_product_reviewers_objs; - *Bugzilla::Product::reviewer_required = \&_product_reviewer_required; - *Bugzilla::Component::reviewers = \&_component_reviewers; - *Bugzilla::Component::reviewers_objs = \&_component_reviewers_objs; - *Bugzilla::Bug::mentors = \&_bug_mentors; - *Bugzilla::Bug::bug_mentors = \&_bug_mentors; - *Bugzilla::Bug::bug_mentor = \&_bug_mentors; - *Bugzilla::Bug::is_mentor = \&_bug_is_mentor; - *Bugzilla::Bug::set_bug_mentors = \&_bug_set_bug_mentors; - *Bugzilla::User::review_count = \&_user_review_count; - *Bugzilla::User::reviews_blocked = \&_user_reviews_blocked; - *Bugzilla::User::is_active = \&_user_is_active; + *Bugzilla::Product::reviewers = \&_product_reviewers; + *Bugzilla::Product::reviewers_objs = \&_product_reviewers_objs; + *Bugzilla::Product::reviewer_required = \&_product_reviewer_required; + *Bugzilla::Component::reviewers = \&_component_reviewers; + *Bugzilla::Component::reviewers_objs = \&_component_reviewers_objs; + *Bugzilla::Bug::mentors = \&_bug_mentors; + *Bugzilla::Bug::bug_mentors = \&_bug_mentors; + *Bugzilla::Bug::bug_mentor = \&_bug_mentors; + *Bugzilla::Bug::is_mentor = \&_bug_is_mentor; + *Bugzilla::Bug::set_bug_mentors = \&_bug_set_bug_mentors; + *Bugzilla::User::review_count = \&_user_review_count; + *Bugzilla::User::reviews_blocked = \&_user_reviews_blocked; + *Bugzilla::User::is_active = \&_user_is_active; } # # monkey-patched methods # -sub _product_reviewers { _reviewers($_[0], 'product', $_[1]) } -sub _product_reviewers_objs { _reviewers_objs($_[0], 'product', $_[1]) } -sub _component_reviewers { _reviewers($_[0], 'component', $_[1]) } -sub _component_reviewers_objs { _reviewers_objs($_[0], 'component', $_[1]) } +sub _product_reviewers { _reviewers($_[0], 'product', $_[1]) } +sub _product_reviewers_objs { _reviewers_objs($_[0], 'product', $_[1]) } +sub _component_reviewers { _reviewers($_[0], 'component', $_[1]) } +sub _component_reviewers_objs { _reviewers_objs($_[0], 'component', $_[1]) } sub _reviewers { - my ($object, $type, $unfiltered) = @_; - return join(', ', map { $_->login } @{ _reviewers_objs($object, $type, $unfiltered) }); + my ($object, $type, $unfiltered) = @_; + return + join(', ', map { $_->login } @{_reviewers_objs($object, $type, $unfiltered)}); } sub _reviewers_objs { - my ($object, $type, $unfiltered) = @_; - if (!$object->{reviewers}) { - my $dbh = Bugzilla->dbh; - my $user_ids = $dbh->selectcol_arrayref( - "SELECT user_id FROM ${type}_reviewers WHERE ${type}_id = ? ORDER BY sortkey", - undef, - $object->id, - ); - # new_from_list always sorts according to the object's definition, - # so we have to reorder the list - my $users = Bugzilla::User->new_from_list($user_ids); - my %user_map = map { $_->id => $_ } @$users; - my @reviewers = map { $user_map{$_} } @$user_ids; - if (!$unfiltered) { - @reviewers = grep { - $_->is_enabled - && $_->is_active - && $_->name !~ UNAVAILABLE_RE - && !$_->reviews_blocked - } @reviewers; - } - $object->{reviewers} = \@reviewers; + my ($object, $type, $unfiltered) = @_; + if (!$object->{reviewers}) { + my $dbh = Bugzilla->dbh; + my $user_ids + = $dbh->selectcol_arrayref( + "SELECT user_id FROM ${type}_reviewers WHERE ${type}_id = ? ORDER BY sortkey", + undef, $object->id,); + + # new_from_list always sorts according to the object's definition, + # so we have to reorder the list + my $users = Bugzilla::User->new_from_list($user_ids); + my %user_map = map { $_->id => $_ } @$users; + my @reviewers = map { $user_map{$_} } @$user_ids; + if (!$unfiltered) { + @reviewers = grep { + $_->is_enabled + && $_->is_active + && $_->name !~ UNAVAILABLE_RE + && !$_->reviews_blocked + } @reviewers; } - return $object->{reviewers}; + $object->{reviewers} = \@reviewers; + } + return $object->{reviewers}; } sub _user_is_active { - my ($self) = @_; + my ($self) = @_; - # never consider .bugs or .tld addresses as inactive. - return 1 if $self->login =~ /\.(?:bugs|tld)$/; - return 1 unless Bugzilla->params->{max_reviewer_last_seen}; - return 0 if !defined($self->last_seen_date); + # never consider .bugs or .tld addresses as inactive. + return 1 if $self->login =~ /\.(?:bugs|tld)$/; + return 1 unless Bugzilla->params->{max_reviewer_last_seen}; + return 0 if !defined($self->last_seen_date); - my $dt = datetime_from($self->last_seen_date); - my $days_ago = $dt->delta_days(DateTime->now())->in_units('days'); + my $dt = datetime_from($self->last_seen_date); + my $days_ago = $dt->delta_days(DateTime->now())->in_units('days'); - return $days_ago <= Bugzilla->params->{max_reviewer_last_seen}; + return $days_ago <= Bugzilla->params->{max_reviewer_last_seen}; } sub _user_review_count { - my ($self) = @_; - if (!exists $self->{review_count}) { - my $dbh = Bugzilla->dbh; - ($self->{review_count}) = $dbh->selectrow_array( - "SELECT COUNT(*) + my ($self) = @_; + if (!exists $self->{review_count}) { + my $dbh = Bugzilla->dbh; + ($self->{review_count}) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM flags INNER JOIN flagtypes ON flagtypes.id = flags.type_id WHERE flags.requestee_id = ? - AND " . $dbh->sql_in('flagtypes.name', [ "'review'", "'feedback'" ]), - undef, - $self->id, - ); - } - return $self->{review_count}; + AND " + . $dbh->sql_in('flagtypes.name', ["'review'", "'feedback'"]), undef, + $self->id, + ); + } + return $self->{review_count}; } sub _user_reviews_blocked { - return $_[0]->settings->{block_reviews}->{value} eq 'on'; + return $_[0]->settings->{block_reviews}->{value} eq 'on'; } # @@ -132,148 +133,143 @@ sub _user_reviews_blocked { # sub _bug_mentors { - my ($self, $options) = @_; - $options //= {}; - my $dbh = Bugzilla->dbh; - if (!$self->{bug_mentors}) { - my $mentor_ids = $dbh->selectcol_arrayref(" - SELECT user_id FROM bug_mentors WHERE bug_id = ?", - undef, - $self->id); - $self->{bug_mentors} = []; - foreach my $mentor_id (@$mentor_ids) { - push(@{ $self->{bug_mentors} }, Bugzilla::User->new({ id => $mentor_id, cache => 1 })); - } - $self->{bug_mentors} = [ - sort { $a->login cmp $b->login } @{ $self->{bug_mentors} } - ]; - } - my @result = @{ $self->{bug_mentors} }; - if ($options->{exclude_needinfo_blocked}) { - @result = grep { !$_->needinfo_blocked } @result; - } - if ($options->{exclude_review_blocked}) { - @result = grep { !$_->reviews_blocked } @result; + my ($self, $options) = @_; + $options //= {}; + my $dbh = Bugzilla->dbh; + if (!$self->{bug_mentors}) { + my $mentor_ids = $dbh->selectcol_arrayref(" + SELECT user_id FROM bug_mentors WHERE bug_id = ?", undef, $self->id); + $self->{bug_mentors} = []; + foreach my $mentor_id (@$mentor_ids) { + push( + @{$self->{bug_mentors}}, + Bugzilla::User->new({id => $mentor_id, cache => 1}) + ); } - return \@result; + $self->{bug_mentors} + = [sort { $a->login cmp $b->login } @{$self->{bug_mentors}}]; + } + my @result = @{$self->{bug_mentors}}; + if ($options->{exclude_needinfo_blocked}) { + @result = grep { !$_->needinfo_blocked } @result; + } + if ($options->{exclude_review_blocked}) { + @result = grep { !$_->reviews_blocked } @result; + } + return \@result; } sub _bug_is_mentor { - my ($self, $user) = @_; - my $user_id = ($user || Bugzilla->user)->id; - return (grep { $_->id == $user_id} @{ $self->mentors }) ? 1 : 0; + my ($self, $user) = @_; + my $user_id = ($user || Bugzilla->user)->id; + return (grep { $_->id == $user_id } @{$self->mentors}) ? 1 : 0; } sub _bug_set_bug_mentors { - my ($self, $value) = @_; - $self->set('bug_mentors', $value); + my ($self, $value) = @_; + $self->set('bug_mentors', $value); } sub object_validators { - my ($self, $args) = @_; - return unless $args->{class} eq 'Bugzilla::Bug'; - $args->{validators}->{bug_mentors} = \&_bug_check_bug_mentors; + my ($self, $args) = @_; + return unless $args->{class} eq 'Bugzilla::Bug'; + $args->{validators}->{bug_mentors} = \&_bug_check_bug_mentors; } sub _bug_check_bug_mentors { - my ($self, $value) = @_; - my %seen; - my $mentors = [ - grep { !$seen{$_->id}++ } - map { Bugzilla::User->check({ name => $_, cache => 1 }) } - ref($value) ? @$value : ($value) - ]; - if (scalar(@$mentors) > MENTOR_LIMIT) { - ThrowUserError('mentor_limit_exceeded', { limit => MENTOR_LIMIT }); - } - return $mentors; + my ($self, $value) = @_; + my %seen; + my $mentors + = [grep { !$seen{$_->id}++ } + map { Bugzilla::User->check({name => $_, cache => 1}) } + ref($value) ? @$value : ($value)]; + if (scalar(@$mentors) > MENTOR_LIMIT) { + ThrowUserError('mentor_limit_exceeded', {limit => MENTOR_LIMIT}); + } + return $mentors; } sub bug_user_match_fields { - my ($self, $args) = @_; - $args->{fields}->{bug_mentors} = { type => 'multi' }; + my ($self, $args) = @_; + $args->{fields}->{bug_mentors} = {type => 'multi'}; } sub bug_before_create { - my ($self, $args) = @_; - my $params = $args->{params}; - my $stash = $args->{stash}; - $stash->{bug_mentors} = delete $params->{bug_mentors}; + my ($self, $args) = @_; + my $params = $args->{params}; + my $stash = $args->{stash}; + $stash->{bug_mentors} = delete $params->{bug_mentors}; } sub bug_end_of_create { - my ($self, $args) = @_; - my $bug = $args->{bug}; - my $stash = $args->{stash}; - if (my $mentors = $stash->{bug_mentors}) { - $self->_update_user_table({ - object => $bug, - old_users => [], - new_users => $self->_bug_check_bug_mentors($mentors), - table => 'bug_mentors', - id_field => 'bug_id', - }); - } + my ($self, $args) = @_; + my $bug = $args->{bug}; + my $stash = $args->{stash}; + if (my $mentors = $stash->{bug_mentors}) { + $self->_update_user_table({ + object => $bug, + old_users => [], + new_users => $self->_bug_check_bug_mentors($mentors), + table => 'bug_mentors', + id_field => 'bug_id', + }); + } } sub _update_user_table { - my ($self, $args) = @_; - my ($object, $old_users, $new_users, $table, $id_field, $has_sortkey, $return) = - @$args{qw(object old_users new_users table id_field has_sortkey return)}; - my $dbh = Bugzilla->dbh; - my (@removed, @added); - - # remove deleted users - foreach my $old_user (@$old_users) { - if (!grep { $_->id == $old_user->id } @$new_users) { - $dbh->do( - "DELETE FROM $table WHERE $id_field = ? AND user_id = ?", - undef, - $object->id, $old_user->id, - ); - push @removed, $old_user; - } + my ($self, $args) = @_; + my ($object, $old_users, $new_users, $table, $id_field, $has_sortkey, $return) + = @$args{qw(object old_users new_users table id_field has_sortkey return)}; + my $dbh = Bugzilla->dbh; + my (@removed, @added); + + # remove deleted users + foreach my $old_user (@$old_users) { + if (!grep { $_->id == $old_user->id } @$new_users) { + $dbh->do("DELETE FROM $table WHERE $id_field = ? AND user_id = ?", + undef, $object->id, $old_user->id,); + push @removed, $old_user; } - # add new users - foreach my $new_user (@$new_users) { - if (!grep { $_->id == $new_user->id } @$old_users) { - $dbh->do( - "INSERT INTO $table ($id_field, user_id) VALUES (?, ?)", - undef, - $object->id, $new_user->id, - ); - push @added, $new_user; - } + } + + # add new users + foreach my $new_user (@$new_users) { + if (!grep { $_->id == $new_user->id } @$old_users) { + $dbh->do("INSERT INTO $table ($id_field, user_id) VALUES (?, ?)", + undef, $object->id, $new_user->id,); + push @added, $new_user; } + } - return unless @removed || @added; - - if ($has_sortkey) { - # update the sortkey for all users - for (my $i = 0; $i < scalar(@$new_users); $i++) { - $dbh->do( - "UPDATE $table SET sortkey=? WHERE $id_field = ? AND user_id = ?", - undef, - ($i + 1) * 10, $object->id, $new_users->[$i]->id, - ); - } - } + return unless @removed || @added; - if (!$return) { - return undef; - } - elsif ($return eq 'diff') { - return [ - @removed ? join(', ', map { $_->login } @removed) : undef, - @added ? join(', ', map { $_->login } @added) : undef, - ]; - } - elsif ($return eq 'old-new') { - return [ - @$old_users ? join(', ', map { $_->login } @$old_users) : '', - @$new_users ? join(', ', map { $_->login } @$new_users) : '', - ]; + if ($has_sortkey) { + + # update the sortkey for all users + for (my $i = 0; $i < scalar(@$new_users); $i++) { + $dbh->do( + "UPDATE $table SET sortkey=? WHERE $id_field = ? AND user_id = ?", + undef, ($i + 1) * 10, + $object->id, $new_users->[$i]->id, + ); } + } + + if (!$return) { + return undef; + } + elsif ($return eq 'diff') { + return [ + @removed ? join(', ', map { $_->login } @removed) : undef, + @added ? join(', ', map { $_->login } @added) : undef, + ]; + } + elsif ($return eq 'old-new') { + return [ + @$old_users ? join(', ', map { $_->login } @$old_users) : '', + @$new_users ? join(', ', map { $_->login } @$new_users) : '', + ]; + } } # @@ -283,44 +279,46 @@ sub _update_user_table { sub _product_reviewer_required { $_[0]->{reviewer_required} } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Product')) { - my $dbh = Bugzilla->dbh; - my @new_columns = qw(reviewer_required); - push @$columns, grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; - } - elsif ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - my @new_columns = qw(review_request_count feedback_request_count needinfo_request_count); - push @$columns, grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Product')) { + my $dbh = Bugzilla->dbh; + my @new_columns = qw(reviewer_required); + push @$columns, + grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; + } + elsif ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + my @new_columns + = qw(review_request_count feedback_request_count needinfo_request_count); + push @$columns, + grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::Product')) { - push @$columns, 'reviewer_required'; - } - elsif ($object->isa('Bugzilla::User')) { - push @$columns, qw(review_request_count feedback_request_count needinfo_request_count); - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push @$columns, 'reviewer_required'; + } + elsif ($object->isa('Bugzilla::User')) { + push @$columns, + qw(review_request_count feedback_request_count needinfo_request_count); + } } sub _new_users_from_input { - my ($field) = @_; - my $input_params = Bugzilla->input_params; - return undef unless exists $input_params->{$field}; - return [] unless $input_params->{$field}; - Bugzilla::User::match_field({ $field => {'type' => 'multi'} });; - my $value = $input_params->{$field}; - my %seen; - return [ - grep { !$seen{$_->id}++ } - map { Bugzilla::User->check({ name => $_, cache => 1 }) } - ref($value) ? @$value : ($value) - ]; + my ($field) = @_; + my $input_params = Bugzilla->input_params; + return undef unless exists $input_params->{$field}; + return [] unless $input_params->{$field}; + Bugzilla::User::match_field({$field => {'type' => 'multi'}}); + my $value = $input_params->{$field}; + my %seen; + return [grep { !$seen{$_->id}++ } + map { Bugzilla::User->check({name => $_, cache => 1}) } + ref($value) ? @$value : ($value)]; } # @@ -328,311 +326,329 @@ sub _new_users_from_input { # sub object_before_create { - my ($self, $args) = @_; - my ($class, $params) = @$args{qw(class params)}; - return unless $class->isa('Bugzilla::Product'); + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + return unless $class->isa('Bugzilla::Product'); - $params->{reviewer_required} = Bugzilla->cgi->param('reviewer_required') ? 1 : 0; + $params->{reviewer_required} + = Bugzilla->cgi->param('reviewer_required') ? 1 : 0; } sub object_end_of_set_all { - my ($self, $args) = @_; - my ($object, $params) = @$args{qw(object params)}; - return unless $object->isa('Bugzilla::Product'); + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + return unless $object->isa('Bugzilla::Product'); - $object->set('reviewer_required', Bugzilla->cgi->param('reviewer_required') ? 1 : 0); + $object->set('reviewer_required', + Bugzilla->cgi->param('reviewer_required') ? 1 : 0); } sub object_end_of_create { - my ($self, $args) = @_; - my ($object, $params) = @$args{qw(object params)}; - - if ($object->isa('Bugzilla::Product')) { - $self->_update_user_table({ - object => $object, - old_users => [], - new_users => _new_users_from_input('reviewers'), - table => 'product_reviewers', - id_field => 'product_id', - has_sortkey => 1, - }); - } - elsif ($object->isa('Bugzilla::Component')) { - $self->_update_user_table({ - object => $object, - old_users => [], - new_users => _new_users_from_input('reviewers'), - table => 'component_reviewers', - id_field => 'component_id', - has_sortkey => 1, - }); - } - elsif (_is_countable_flag($object) && $object->requestee_id && $object->status eq '?') { - _check_requestee($object); - _adjust_request_count($object, +1); - } - if (_is_countable_flag($object)) { - $self->_log_flag_state_activity($object, $object->status, $object->modification_date); - } + my ($self, $args) = @_; + my ($object, $params) = @$args{qw(object params)}; + + if ($object->isa('Bugzilla::Product')) { + $self->_update_user_table({ + object => $object, + old_users => [], + new_users => _new_users_from_input('reviewers'), + table => 'product_reviewers', + id_field => 'product_id', + has_sortkey => 1, + }); + } + elsif ($object->isa('Bugzilla::Component')) { + $self->_update_user_table({ + object => $object, + old_users => [], + new_users => _new_users_from_input('reviewers'), + table => 'component_reviewers', + id_field => 'component_id', + has_sortkey => 1, + }); + } + elsif (_is_countable_flag($object) + && $object->requestee_id + && $object->status eq '?') + { + _check_requestee($object); + _adjust_request_count($object, +1); + } + if (_is_countable_flag($object)) { + $self->_log_flag_state_activity($object, $object->status, + $object->modification_date); + } } sub object_end_of_update { - my ($self, $args) = @_; - my ($object, $old_object, $changes) = @$args{qw(object old_object changes)}; - - if ($object->isa('Bugzilla::Product') && exists Bugzilla->input_params->{reviewers}) { - my $diff = $self->_update_user_table({ - object => $object, - old_users => $old_object->reviewers_objs(1), - new_users => _new_users_from_input('reviewers'), - table => 'product_reviewers', - id_field => 'product_id', - has_sortkey => 1, - return => 'old-new', - }); - $changes->{reviewers} = $diff if $diff; + my ($self, $args) = @_; + my ($object, $old_object, $changes) = @$args{qw(object old_object changes)}; + + if ($object->isa('Bugzilla::Product') + && exists Bugzilla->input_params->{reviewers}) + { + my $diff = $self->_update_user_table({ + object => $object, + old_users => $old_object->reviewers_objs(1), + new_users => _new_users_from_input('reviewers'), + table => 'product_reviewers', + id_field => 'product_id', + has_sortkey => 1, + return => 'old-new', + }); + $changes->{reviewers} = $diff if $diff; + } + elsif ($object->isa('Bugzilla::Component')) { + my $diff = $self->_update_user_table({ + object => $object, + old_users => $old_object->reviewers_objs(1), + new_users => _new_users_from_input('reviewers'), + table => 'component_reviewers', + id_field => 'component_id', + has_sortkey => 1, + return => 'old-new', + }); + $changes->{reviewers} = $diff if $diff; + } + elsif ($object->isa('Bugzilla::Bug')) { + my $diff = $self->_update_user_table({ + object => $object, + old_users => $old_object->mentors, + new_users => $object->mentors, + table => 'bug_mentors', + id_field => 'bug_id', + return => 'diff', + }); + $changes->{bug_mentor} = $diff if $diff; + } + elsif (_is_countable_flag($object)) { + my ($old_status, $new_status) = ($old_object->status, $object->status); + if ($old_status ne '?' && $new_status eq '?') { + + # setting flag to ? + _adjust_request_count($object, +1); + if ($object->requestee_id) { + _check_requestee($object); + } } - elsif ($object->isa('Bugzilla::Component')) { - my $diff = $self->_update_user_table({ - object => $object, - old_users => $old_object->reviewers_objs(1), - new_users => _new_users_from_input('reviewers'), - table => 'component_reviewers', - id_field => 'component_id', - has_sortkey => 1, - return => 'old-new', - }); - $changes->{reviewers} = $diff if $diff; + elsif ($old_status eq '?' && $new_status ne '?') { + + # setting flag from ? + _adjust_request_count($old_object, -1); } - elsif ($object->isa('Bugzilla::Bug')) { - my $diff = $self->_update_user_table({ - object => $object, - old_users => $old_object->mentors, - new_users => $object->mentors, - table => 'bug_mentors', - id_field => 'bug_id', - return => 'diff', - }); - $changes->{bug_mentor} = $diff if $diff; + elsif ($old_object->requestee_id && !$object->requestee_id) { + + # removing requestee + _adjust_request_count($old_object, -1); } - elsif (_is_countable_flag($object)) { - my ($old_status, $new_status) = ($old_object->status, $object->status); - if ($old_status ne '?' && $new_status eq '?') { - # setting flag to ? - _adjust_request_count($object, +1); - if ($object->requestee_id) { - _check_requestee($object); - } - } - elsif ($old_status eq '?' && $new_status ne '?') { - # setting flag from ? - _adjust_request_count($old_object, -1); - } - elsif ($old_object->requestee_id && !$object->requestee_id) { - # removing requestee - _adjust_request_count($old_object, -1); - } - elsif (!$old_object->requestee_id && $object->requestee_id) { - # setting requestee - _check_requestee($object); - _adjust_request_count($object, +1); - } - elsif ($old_object->requestee_id && $object->requestee_id - && $old_object->requestee_id != $object->requestee_id) - { - # changing requestee - _check_requestee($object); - _adjust_request_count($old_object, -1); - _adjust_request_count($object, +1); - } + elsif (!$old_object->requestee_id && $object->requestee_id) { + + # setting requestee + _check_requestee($object); + _adjust_request_count($object, +1); } + elsif ($old_object->requestee_id + && $object->requestee_id + && $old_object->requestee_id != $object->requestee_id) + { + # changing requestee + _check_requestee($object); + _adjust_request_count($old_object, -1); + _adjust_request_count($object, +1); + } + } } sub flag_updated { - my ($self, $args) = @_; - my $flag = $args->{flag}; - my $timestamp = $args->{timestamp}; - my $changes = $args->{changes}; - - return unless scalar(keys %$changes); - if (_is_countable_flag($flag)) { - $self->_log_flag_state_activity($flag, $flag->status, $timestamp); - } + my ($self, $args) = @_; + my $flag = $args->{flag}; + my $timestamp = $args->{timestamp}; + my $changes = $args->{changes}; + + return unless scalar(keys %$changes); + if (_is_countable_flag($flag)) { + $self->_log_flag_state_activity($flag, $flag->status, $timestamp); + } } sub flag_deleted { - my ($self, $args) = @_; - my $flag = $args->{flag}; - my $timestamp = $args->{timestamp}; + my ($self, $args) = @_; + my $flag = $args->{flag}; + my $timestamp = $args->{timestamp}; - if (_is_countable_flag($flag) && $flag->requestee_id && $flag->status eq '?') { - _adjust_request_count($flag, -1); - } + if (_is_countable_flag($flag) && $flag->requestee_id && $flag->status eq '?') { + _adjust_request_count($flag, -1); + } - if (_is_countable_flag($flag)) { - $self->_log_flag_state_activity($flag, 'X', $timestamp, Bugzilla->user->id); - } + if (_is_countable_flag($flag)) { + $self->_log_flag_state_activity($flag, 'X', $timestamp, Bugzilla->user->id); + } } sub _is_countable_flag { - my ($object) = @_; - return unless $object->isa('Bugzilla::Flag'); - my $type_name = $object->type->name; - return $type_name eq 'review' || $type_name eq 'feedback' || $type_name eq 'needinfo'; + my ($object) = @_; + return unless $object->isa('Bugzilla::Flag'); + my $type_name = $object->type->name; + return + $type_name eq 'review' + || $type_name eq 'feedback' + || $type_name eq 'needinfo'; } sub _check_requestee { - my ($flag) = @_; - return unless $flag->type->name eq 'review' || $flag->type->name eq 'feedback'; - if ($flag->requestee->reviews_blocked) { - ThrowUserError('reviews_blocked', - { requestee => $flag->requestee, flagtype => $flag->type->name }); - } + my ($flag) = @_; + return unless $flag->type->name eq 'review' || $flag->type->name eq 'feedback'; + if ($flag->requestee->reviews_blocked) { + ThrowUserError('reviews_blocked', + {requestee => $flag->requestee, flagtype => $flag->type->name}); + } } sub _log_flag_state_activity { - my ($self, $flag, $status, $timestamp, $setter_id) = @_; - - $setter_id //= $flag->setter_id; - - Bugzilla::Extension::Review::FlagStateActivity->create({ - flag_when => $timestamp, - setter_id => $setter_id, - status => $status, - type_id => $flag->type_id, - flag_id => $flag->id, - requestee_id => $flag->requestee_id, - bug_id => $flag->bug_id, - attachment_id => $flag->attach_id, - }); + my ($self, $flag, $status, $timestamp, $setter_id) = @_; + + $setter_id //= $flag->setter_id; + + Bugzilla::Extension::Review::FlagStateActivity->create({ + flag_when => $timestamp, + setter_id => $setter_id, + status => $status, + type_id => $flag->type_id, + flag_id => $flag->id, + requestee_id => $flag->requestee_id, + bug_id => $flag->bug_id, + attachment_id => $flag->attach_id, + }); } sub _adjust_request_count { - my ($flag, $add) = @_; - return unless my $requestee_id = $flag->requestee_id; - my $field = $flag->type->name . '_request_count'; - - # update the current user's object so things are display correctly on the - # post-processing page - my $user = Bugzilla->user; - if ($requestee_id == $user->id) { - $user->{$field} += $add; - } - - # update database directly to avoid creating audit_log entries - $add = $add == -1 ? ' - 1' : ' + 1'; - Bugzilla->dbh->do( - "UPDATE profiles SET $field = $field $add WHERE userid = ?", - undef, - $requestee_id - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $requestee_id }); + my ($flag, $add) = @_; + return unless my $requestee_id = $flag->requestee_id; + my $field = $flag->type->name . '_request_count'; + + # update the current user's object so things are display correctly on the + # post-processing page + my $user = Bugzilla->user; + if ($requestee_id == $user->id) { + $user->{$field} += $add; + } + + # update database directly to avoid creating audit_log entries + $add = $add == -1 ? ' - 1' : ' + 1'; + Bugzilla->dbh->do("UPDATE profiles SET $field = $field $add WHERE userid = ?", + undef, $requestee_id); + Bugzilla->memcached->clear({table => 'profiles', id => $requestee_id}); } # bugzilla's handling of requestee matching when creating bugs is "if it's # wrong, or matches too many, default to empty", which breaks mandatory # reviewer requirements. instead we just throw an error. sub post_bug_attachment_flags { - my ($self, $args) = @_; - $self->_check_review_flag($args); + my ($self, $args) = @_; + $self->_check_review_flag($args); } sub create_attachment_flags { - my ($self, $args) = @_; - $self->_check_review_flag($args); + my ($self, $args) = @_; + $self->_check_review_flag($args); } sub _check_review_flag { - my ($self, $args) = @_; - my ($bug, $attachment) = @$args{qw( bug attachment )}; - my $cgi = Bugzilla->cgi; - - # extract the set flag-types - my @flagtype_ids = map { /^flag_type-(\d+)$/ ? $1 : () } $cgi->param(); - @flagtype_ids = grep { $cgi->param("flag_type-$_") eq '?' } @flagtype_ids; - return unless scalar(@flagtype_ids); - - # find valid review flagtypes - my $flag_types = Bugzilla::FlagType::match({ - product_id => $bug->product_id, - component_id => $bug->component_id, - is_active => 1 - }); - foreach my $flag_type (@$flag_types) { - next unless $flag_type->name eq 'review' - && $flag_type->target_type eq 'attachment'; - my $type_id = $flag_type->id; - next unless scalar(grep { $_ == $type_id } @flagtype_ids); - - my $reviewers = clean_text($cgi->param("requestee_type-$type_id") || ''); - if ($reviewers eq '' && $bug->product_obj->reviewer_required) { - ThrowUserError('reviewer_required'); - } - - foreach my $reviewer (split(/[,;]+/, $reviewers)) { - # search on the reviewer - my $users = Bugzilla::User::match($reviewer, 2, 1); - - # no matches - if (scalar(@$users) == 0) { - ThrowUserError('user_match_failed', { name => $reviewer }); - } - - # more than one match, throw error - if (scalar(@$users) > 1) { - ThrowUserError('user_match_too_many', { fields => [ 'review' ] }); - } - - # we want to throw an error if the requestee does not have access - # to the bug. bugzilla's default behaviour is to sliently drop the - # requestee, which results in a confusing 'reviewer required' - # error. - # fake it by creating a flag and try to set the requestee. - # bugzilla's flags don't have a normal constructor or property - # setters, so we have to bless it directly then call the internal - # check_requestee method. urgh. - my $flag = bless({ - type_id => $flag_type->id, - status => '?', - bug_id => $bug->id, - attach_id => $attachment->id - }, 'Bugzilla::Flag'); - $flag->_check_requestee($users->[0]->login, $bug, $attachment); - } + my ($self, $args) = @_; + my ($bug, $attachment) = @$args{qw( bug attachment )}; + my $cgi = Bugzilla->cgi; + + # extract the set flag-types + my @flagtype_ids = map { /^flag_type-(\d+)$/ ? $1 : () } $cgi->param(); + @flagtype_ids = grep { $cgi->param("flag_type-$_") eq '?' } @flagtype_ids; + return unless scalar(@flagtype_ids); + + # find valid review flagtypes + my $flag_types = Bugzilla::FlagType::match({ + product_id => $bug->product_id, + component_id => $bug->component_id, + is_active => 1 + }); + foreach my $flag_type (@$flag_types) { + next + unless $flag_type->name eq 'review' + && $flag_type->target_type eq 'attachment'; + my $type_id = $flag_type->id; + next unless scalar(grep { $_ == $type_id } @flagtype_ids); + + my $reviewers = clean_text($cgi->param("requestee_type-$type_id") || ''); + if ($reviewers eq '' && $bug->product_obj->reviewer_required) { + ThrowUserError('reviewer_required'); } + + foreach my $reviewer (split(/[,;]+/, $reviewers)) { + + # search on the reviewer + my $users = Bugzilla::User::match($reviewer, 2, 1); + + # no matches + if (scalar(@$users) == 0) { + ThrowUserError('user_match_failed', {name => $reviewer}); + } + + # more than one match, throw error + if (scalar(@$users) > 1) { + ThrowUserError('user_match_too_many', {fields => ['review']}); + } + + # we want to throw an error if the requestee does not have access + # to the bug. bugzilla's default behaviour is to sliently drop the + # requestee, which results in a confusing 'reviewer required' + # error. + # fake it by creating a flag and try to set the requestee. + # bugzilla's flags don't have a normal constructor or property + # setters, so we have to bless it directly then call the internal + # check_requestee method. urgh. + my $flag = bless( + { + type_id => $flag_type->id, + status => '?', + bug_id => $bug->id, + attach_id => $attachment->id + }, + 'Bugzilla::Flag' + ); + $flag->_check_requestee($users->[0]->login, $bug, $attachment); + } + } } sub flag_end_of_update { - my ($self, $args) = @_; - my ($object, $old_flags, $new_flags) = @$args{qw(object old_flags new_flags)}; - my $bug = $object->isa('Bugzilla::Attachment') ? $object->bug : $object; - - my (undef, $added) = diff_arrays($old_flags, $new_flags); - foreach my $change (@$added) { - $change =~ s/^[^:]+://; - my $reviewer = ''; - if ($change =~ s/\(([^\)]+)\)$//) { - $reviewer = $1; - } - my ($name, $value) = $change =~ /^(.+)(.)$/; - - if ($name eq 'review' && $value eq '?') { - if ($reviewer eq '') { - ThrowUserError('reviewer_required') if $bug->product_obj->reviewer_required; - } - else { - my $reviewer_obj = Bugzilla::User->check({ - name => $reviewer, - cache => 1 - }); - - ThrowUserError('reviewer_inactive', { - reviewer => $reviewer_obj, - timeout => Bugzilla->params->{max_reviewer_last_seen} - }) unless $reviewer_obj->is_active; - } - } + my ($self, $args) = @_; + my ($object, $old_flags, $new_flags) = @$args{qw(object old_flags new_flags)}; + my $bug = $object->isa('Bugzilla::Attachment') ? $object->bug : $object; + + my (undef, $added) = diff_arrays($old_flags, $new_flags); + foreach my $change (@$added) { + $change =~ s/^[^:]+://; + my $reviewer = ''; + if ($change =~ s/\(([^\)]+)\)$//) { + $reviewer = $1; + } + my ($name, $value) = $change =~ /^(.+)(.)$/; + + if ($name eq 'review' && $value eq '?') { + if ($reviewer eq '') { + ThrowUserError('reviewer_required') if $bug->product_obj->reviewer_required; + } + else { + my $reviewer_obj = Bugzilla::User->check({name => $reviewer, cache => 1}); + + ThrowUserError( + 'reviewer_inactive', + { + reviewer => $reviewer_obj, + timeout => Bugzilla->params->{max_reviewer_last_seen} + } + ) unless $reviewer_obj->is_active; + } } + } } # @@ -640,44 +656,45 @@ sub flag_end_of_update { # sub buglist_columns { - my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - my $columns = $args->{columns}; - $columns->{bug_mentor} = { title => 'Mentor' }; - if (Bugzilla->user->id) { - $columns->{bug_mentor}->{name} - = $dbh->sql_group_concat('map_mentors_names.login_name'); - } - else { - $columns->{bug_mentor}->{name} - = $dbh->sql_group_concat('map_mentors_names.realname'); - - } + my ($self, $args) = @_; + my $dbh = Bugzilla->dbh; + my $columns = $args->{columns}; + $columns->{bug_mentor} = {title => 'Mentor'}; + if (Bugzilla->user->id) { + $columns->{bug_mentor}->{name} + = $dbh->sql_group_concat('map_mentors_names.login_name'); + } + else { + $columns->{bug_mentor}->{name} + = $dbh->sql_group_concat('map_mentors_names.realname'); + + } } sub buglist_column_joins { - my ($self, $args) = @_; - my $column_joins = $args->{column_joins}; - $column_joins->{bug_mentor} = { - as => 'map_mentors', - table => 'bug_mentors', - then_to => { - as => 'map_mentors_names', - table => 'profiles', - from => 'map_mentors.user_id', - to => 'userid', - }, + my ($self, $args) = @_; + my $column_joins = $args->{column_joins}; + $column_joins->{bug_mentor} = { + as => 'map_mentors', + table => 'bug_mentors', + then_to => { + as => 'map_mentors_names', + table => 'profiles', + from => 'map_mentors.user_id', + to => 'userid', }, + }, + ; } sub search_operator_field_override { - my ($self, $args) = @_; - my $operators = $args->{operators}; - $operators->{bug_mentor} = { - _non_changed => sub { - Bugzilla::Search::_user_nonchanged(@_) - } - }; + my ($self, $args) = @_; + my $operators = $args->{operators}; + $operators->{bug_mentor} = { + _non_changed => sub { + Bugzilla::Search::_user_nonchanged(@_); + } + }; } # @@ -685,105 +702,103 @@ sub search_operator_field_override { # sub webservice { - my ($self, $args) = @_; - my $dispatch = $args->{dispatch}; - $dispatch->{Review} = "Bugzilla::Extension::Review::WebService"; + my ($self, $args) = @_; + my $dispatch = $args->{dispatch}; + $dispatch->{Review} = "Bugzilla::Extension::Review::WebService"; } sub user_preferences { - my ($self, $args) = @_; - return unless - $args->{current_tab} eq 'account' - && $args->{save_changes}; - - my $input = Bugzilla->input_params; - my $settings = Bugzilla->user->settings; - - my $value = $input->{block_reviews} ? 'on' : 'off'; - $settings->{block_reviews}->validate_value($value); - $settings->{block_reviews}->set($value); - clear_settings_cache(Bugzilla->user->id); + my ($self, $args) = @_; + return unless $args->{current_tab} eq 'account' && $args->{save_changes}; + + my $input = Bugzilla->input_params; + my $settings = Bugzilla->user->settings; + + my $value = $input->{block_reviews} ? 'on' : 'off'; + $settings->{block_reviews}->validate_value($value); + $settings->{block_reviews}->set($value); + clear_settings_cache(Bugzilla->user->id); } sub page_before_template { - my ($self, $args) = @_; - - if ($args->{page_id} eq 'review_suggestions.html') { - $self->review_suggestions_report($args); - } - elsif ($args->{page_id} eq 'review_requests_rebuild.html') { - $self->review_requests_rebuild($args); - } - elsif ($args->{page_id} eq 'review_history.html') { - $self->review_history($args); - } + my ($self, $args) = @_; + + if ($args->{page_id} eq 'review_suggestions.html') { + $self->review_suggestions_report($args); + } + elsif ($args->{page_id} eq 'review_requests_rebuild.html') { + $self->review_requests_rebuild($args); + } + elsif ($args->{page_id} eq 'review_history.html') { + $self->review_history($args); + } } sub review_suggestions_report { - my ($self, $args) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $products = []; - my @products = sort { lc($a->name) cmp lc($b->name) } - @{ Bugzilla->user->get_accessible_products }; - foreach my $product_obj (@products) { - my $has_reviewers = 0; - my $product = { - name => $product_obj->name, - components => [], - reviewers => $product_obj->reviewers_objs(1), - }; - $has_reviewers = scalar @{ $product->{reviewers} }; - - foreach my $component_obj (@{ $product_obj->components }) { - my $component = { - name => $component_obj->name, - reviewers => $component_obj->reviewers_objs(1), - }; - if (@{ $component->{reviewers} }) { - push @{ $product->{components} }, $component; - $has_reviewers = 1; - } - } - - if ($has_reviewers) { - push @$products, $product; - } + my ($self, $args) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $products = []; + my @products = sort { lc($a->name) cmp lc($b->name) } + @{Bugzilla->user->get_accessible_products}; + foreach my $product_obj (@products) { + my $has_reviewers = 0; + my $product = { + name => $product_obj->name, + components => [], + reviewers => $product_obj->reviewers_objs(1), + }; + $has_reviewers = scalar @{$product->{reviewers}}; + + foreach my $component_obj (@{$product_obj->components}) { + my $component = { + name => $component_obj->name, + reviewers => $component_obj->reviewers_objs(1), + }; + if (@{$component->{reviewers}}) { + push @{$product->{components}}, $component; + $has_reviewers = 1; + } + } + + if ($has_reviewers) { + push @$products, $product; } - $args->{vars}->{products} = $products; + } + $args->{vars}->{products} = $products; } sub review_requests_rebuild { - my ($self, $args) = @_; - - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', { group => 'admin', - action => 'run', - object => 'review_requests_rebuild' }); - if (Bugzilla->cgi->param('rebuild')) { - my $processed_users = 0; - rebuild_review_counters(sub { - my ($count, $total) = @_; - $processed_users = $total; - }); - $args->{vars}->{rebuild} = 1; - $args->{vars}->{total} = $processed_users; - } + my ($self, $args) = @_; + + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'run', object => 'review_requests_rebuild'}); + if (Bugzilla->cgi->param('rebuild')) { + my $processed_users = 0; + rebuild_review_counters(sub { + my ($count, $total) = @_; + $processed_users = $total; + }); + $args->{vars}->{rebuild} = 1; + $args->{vars}->{total} = $processed_users; + } } sub review_history { - my ($self, $args) = @_; - - my $user = Bugzilla->login(LOGIN_REQUIRED); - - Bugzilla::User::match_field({ 'requestee' => { 'type' => 'single' } }); - my $requestee = Bugzilla->input_params->{requestee}; - if ($requestee) { - $args->{vars}{requestee} = Bugzilla::User->check({ name => $requestee, cache => 1 }); - } - else { - $args->{vars}{requestee} = $user; - } + my ($self, $args) = @_; + + my $user = Bugzilla->login(LOGIN_REQUIRED); + + Bugzilla::User::match_field({'requestee' => {'type' => 'single'}}); + my $requestee = Bugzilla->input_params->{requestee}; + if ($requestee) { + $args->{vars}{requestee} + = Bugzilla::User->check({name => $requestee, cache => 1}); + } + else { + $args->{vars}{requestee} = $user; + } } # @@ -791,282 +806,178 @@ sub review_history { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'product_reviewers'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - display_name => { - TYPE => 'VARCHAR(64)', - }, - product_id => { - TYPE => 'INT2', - NOTNULL => 1, - REFERENCES => { - TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - sortkey => { - TYPE => 'INT2', - NOTNULL => 1, - DEFAULT => 0, - }, - ], - INDEXES => [ - product_reviewers_idx => { - FIELDS => [ 'user_id', 'product_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'component_reviewers'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - display_name => { - TYPE => 'VARCHAR(64)', - }, - component_id => { - TYPE => 'INT2', - NOTNULL => 1, - REFERENCES => { - TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE', - } - }, - sortkey => { - TYPE => 'INT2', - NOTNULL => 1, - DEFAULT => 0, - }, - ], - INDEXES => [ - component_reviewers_idx => { - FIELDS => [ 'user_id', 'component_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - - $args->{'schema'}->{'flag_state_activity'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - - flag_when => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - - type_id => { - TYPE => 'INT2', - NOTNULL => 1, - REFERENCES => { - TABLE => 'flagtypes', - COLUMN => 'id', - DELETE => 'CASCADE' - } - }, - - flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - }, - - setter_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - }, - }, - - requestee_id => { - TYPE => 'INT3', - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - }, - }, - - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' - } - }, - - attachment_id => { - TYPE => 'INT3', - REFERENCES => { - TABLE => 'attachments', - COLUMN => 'attach_id', - DELETE => 'CASCADE' - } - }, - - status => { - TYPE => 'CHAR(1)', - NOTNULL => 1, - }, - ], - }; - - $args->{'schema'}->{'bug_mentors'} = { - FIELDS => [ - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE', - }, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - bug_mentors_idx => { - FIELDS => [ 'bug_id', 'user_id' ], - TYPE => 'UNIQUE', - }, - bug_mentors_bug_id_idx => [ 'bug_id' ], - ], - }; - - $args->{'schema'}->{'bug_mentors'} = { - FIELDS => [ - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE', - }, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - bug_mentors_idx => { - FIELDS => [ 'bug_id', 'user_id' ], - TYPE => 'UNIQUE', - }, - bug_mentors_bug_id_idx => [ 'bug_id' ], - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'product_reviewers'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + display_name => {TYPE => 'VARCHAR(64)',}, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE',} + }, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0,}, + ], + INDEXES => [ + product_reviewers_idx => + {FIELDS => ['user_id', 'product_id'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'component_reviewers'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + display_name => {TYPE => 'VARCHAR(64)',}, + component_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE',} + }, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0,}, + ], + INDEXES => [ + component_reviewers_idx => + {FIELDS => ['user_id', 'component_id'], TYPE => 'UNIQUE',}, + ], + }; + + $args->{'schema'}->{'flag_state_activity'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + + flag_when => {TYPE => 'DATETIME', NOTNULL => 1,}, + + type_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'flagtypes', COLUMN => 'id', DELETE => 'CASCADE'} + }, + + flag_id => {TYPE => 'INT3', NOTNULL => 1,}, + + setter_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid',}, + }, + + requestee_id => + {TYPE => 'INT3', REFERENCES => {TABLE => 'profiles', COLUMN => 'userid',},}, + + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'} + }, + + attachment_id => { + TYPE => 'INT3', + REFERENCES => + {TABLE => 'attachments', COLUMN => 'attach_id', DELETE => 'CASCADE'} + }, + + status => {TYPE => 'CHAR(1)', NOTNULL => 1,}, + ], + }; + + $args->{'schema'}->{'bug_mentors'} = { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}, + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + bug_mentors_idx => {FIELDS => ['bug_id', 'user_id'], TYPE => 'UNIQUE',}, + bug_mentors_bug_id_idx => ['bug_id'], + ], + }; + + $args->{'schema'}->{'bug_mentors'} = { + FIELDS => [ + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}, + }, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + bug_mentors_idx => {FIELDS => ['bug_id', 'user_id'], TYPE => 'UNIQUE',}, + bug_mentors_bug_id_idx => ['bug_id'], + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column( - 'products', - 'reviewer_required', { TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE' } - ); - $dbh->bz_add_column( - 'profiles', - 'review_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } - ); - $dbh->bz_add_column( - 'profiles', - 'feedback_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } - ); - $dbh->bz_add_column( - 'profiles', - 'needinfo_request_count', { TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0 } - ); - - my $field = Bugzilla::Field->new({ name => 'bug_mentor' }); - if (!$field) { - Bugzilla::Field->create({ - name => 'bug_mentor', - description => 'Mentor', - mailhead => 1 - }); - } - elsif (!$field->in_new_bugmail) { - $field->set_in_new_bugmail(1); - $field->update(); - } + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('products', 'reviewer_required', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'FALSE'}); + $dbh->bz_add_column('profiles', 'review_request_count', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('profiles', 'feedback_request_count', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('profiles', 'needinfo_request_count', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + + my $field = Bugzilla::Field->new({name => 'bug_mentor'}); + if (!$field) { + Bugzilla::Field->create({ + name => 'bug_mentor', description => 'Mentor', mailhead => 1 + }); + } + elsif (!$field->in_new_bugmail) { + $field->set_in_new_bugmail(1); + $field->update(); + } } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{files}; - my $extensions_dir = bz_locations()->{extensionsdir}; - $files->{"$extensions_dir/Review/bin/review_requests_rebuild.pl"} = { - perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{files}; + my $extensions_dir = bz_locations()->{extensionsdir}; + $files->{"$extensions_dir/Review/bin/review_requests_rebuild.pl"} + = {perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE}; } sub install_before_final_checks { - my ($self, $args) = @_; - add_setting({ - name => 'block_reviews', - options => ['on', 'off'], - default => 'off', - category => 'Reviews and Needinfo' - }); + my ($self, $args) = @_; + add_setting({ + name => 'block_reviews', + options => ['on', 'off'], + default => 'off', + category => 'Reviews and Needinfo' + }); } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{advanced}->{params} }, { - name => 'max_reviewer_last_seen', - type => 't', - default => '', - default => 0, - checker => \&check_numeric, + my ($self, $args) = @_; + push @{$args->{panels}->{advanced}->{params}}, + { + name => 'max_reviewer_last_seen', + type => 't', + default => '', + default => 0, + checker => \&check_numeric, }; } @@ -1075,46 +986,44 @@ sub config_modify_panels { # sub webservice_user_get { - my ($self, $args) = @_; - my ($webservice, $params, $users) = @$args{qw(webservice params users)}; + my ($self, $args) = @_; + my ($webservice, $params, $users) = @$args{qw(webservice params users)}; - return unless filter_wants($params, 'requests'); + return unless filter_wants($params, 'requests'); - my $ids = [ - map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } - grep { exists $_->{id} } - @$users - ]; + my $ids = [map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } + grep { exists $_->{id} } @$users]; - return unless @$ids; - - my %user_map = map { $_->id => $_ } @{ Bugzilla::User->new_from_list($ids) }; - - foreach my $user (@$users) { - my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; - my $user_obj = $user_map{$id}; - - $user->{requests} = { - review => { - blocked => $webservice->type('boolean', $user_obj->reviews_blocked), - pending => $webservice->type('int', $user_obj->{review_request_count}), - }, - feedback => { - # reviews_blocked includes feedback as well - blocked => $webservice->type('boolean', $user_obj->reviews_blocked), - pending => $webservice->type('int', $user_obj->{feedback_request_count}), - }, - needinfo => { - blocked => $webservice->type('boolean', $user_obj->needinfo_blocked), - pending => $webservice->type('int', $user_obj->{needinfo_request_count}), - }, - }; - } + return unless @$ids; + + my %user_map = map { $_->id => $_ } @{Bugzilla::User->new_from_list($ids)}; + + foreach my $user (@$users) { + my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; + my $user_obj = $user_map{$id}; + + $user->{requests} = { + review => { + blocked => $webservice->type('boolean', $user_obj->reviews_blocked), + pending => $webservice->type('int', $user_obj->{review_request_count}), + }, + feedback => { + + # reviews_blocked includes feedback as well + blocked => $webservice->type('boolean', $user_obj->reviews_blocked), + pending => $webservice->type('int', $user_obj->{feedback_request_count}), + }, + needinfo => { + blocked => $webservice->type('boolean', $user_obj->needinfo_blocked), + pending => $webservice->type('int', $user_obj->{needinfo_request_count}), + }, + }; + } } sub webservice_user_suggest { - my ($self, $args) = @_; - $self->webservice_user_get($args); + my ($self, $args) = @_; + $self->webservice_user_get($args); } __PACKAGE__->NAME; diff --git a/extensions/Review/bin/migrate_mentor_from_whiteboard.pl b/extensions/Review/bin/migrate_mentor_from_whiteboard.pl index debf173a7..19837779e 100755 --- a/extensions/Review/bin/migrate_mentor_from_whiteboard.pl +++ b/extensions/Review/bin/migrate_mentor_from_whiteboard.pl @@ -36,11 +36,11 @@ EOF <>; # we need to be logged in to do user searching and update bugs -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); -$nobody->{groups} = [ Bugzilla::Group->get_all ]; +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); +$nobody->{groups} = [Bugzilla::Group->get_all]; Bugzilla->set_user($nobody); -my $mentor_field = Bugzilla::Field->check({ name => 'bug_mentor' }); +my $mentor_field = Bugzilla::Field->check({name => 'bug_mentor'}); my $dbh = Bugzilla->dbh; # fix broken migration @@ -54,48 +54,40 @@ my $sth = $dbh->prepare(" $sth->execute($mentor_field->id); my %pair; while (my $row = $sth->fetchrow_hashref) { - if ($row->{added} && $row->{removed}) { - %pair = (); - next; - } - if ($row->{added}) { - $pair{bug_id} = $row->{bug_id}; - $pair{bug_when} = $row->{bug_when}; - $pair{who} = $row->{added}; - next; - } - if (!$pair{bug_id}) { - next; - } - if ($row->{removed}) { - if ($row->{bug_id} == $pair{bug_id} - && $row->{bug_when} eq $pair{bug_when} - && $row->{removed} eq $pair{who}) - { - print "Fixing mentor on bug $row->{bug_id}\n"; - my $user = Bugzilla::User->check({ name => $row->{removed} }); - $dbh->bz_start_transaction; - $dbh->do( - "DELETE FROM bugs_activity WHERE id = ?", - undef, - $row->{id} - ); - my ($exists) = $dbh->selectrow_array( - "SELECT 1 FROM bug_mentors WHERE bug_id = ? AND user_id = ?", - undef, - $row->{bug_id}, $user->id - ); - if (!$exists) { - $dbh->do( - "INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)", - undef, - $row->{bug_id}, $user->id, - ); - } - $dbh->bz_commit_transaction; - %pair = (); - } + if ($row->{added} && $row->{removed}) { + %pair = (); + next; + } + if ($row->{added}) { + $pair{bug_id} = $row->{bug_id}; + $pair{bug_when} = $row->{bug_when}; + $pair{who} = $row->{added}; + next; + } + if (!$pair{bug_id}) { + next; + } + if ($row->{removed}) { + if ( $row->{bug_id} == $pair{bug_id} + && $row->{bug_when} eq $pair{bug_when} + && $row->{removed} eq $pair{who}) + { + print "Fixing mentor on bug $row->{bug_id}\n"; + my $user = Bugzilla::User->check({name => $row->{removed}}); + $dbh->bz_start_transaction; + $dbh->do("DELETE FROM bugs_activity WHERE id = ?", undef, $row->{id}); + my ($exists) + = $dbh->selectrow_array( + "SELECT 1 FROM bug_mentors WHERE bug_id = ? AND user_id = ?", + undef, $row->{bug_id}, $user->id); + if (!$exists) { + $dbh->do("INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)", + undef, $row->{bug_id}, $user->id,); + } + $dbh->bz_commit_transaction; + %pair = (); } + } } # migrate remaining bugs @@ -110,119 +102,95 @@ my $bug_ids = $dbh->selectcol_arrayref(" print "Bugs found: " . scalar(@$bug_ids) . "\n"; my $bugs = Bugzilla::Bug->new_from_list($bug_ids); foreach my $bug (@$bugs) { - my $whiteboard = $bug->status_whiteboard; - my $orig_whiteboard = $whiteboard; - my ($mentors, $errors) = extract_mentors($whiteboard); - - printf "%7s %s\n", $bug->id, $whiteboard; - foreach my $error (@$errors) { - print " $error\n"; + my $whiteboard = $bug->status_whiteboard; + my $orig_whiteboard = $whiteboard; + my ($mentors, $errors) = extract_mentors($whiteboard); + + printf "%7s %s\n", $bug->id, $whiteboard; + foreach my $error (@$errors) { + print " $error\n"; + } + foreach my $user (@$mentors) { + print " Mentor: " . $user->identity . "\n"; + } + next if @$errors; + $whiteboard =~ s/\[mentor=[^\]]+\]//g; + + my $migrated + = $dbh->selectcol_arrayref("SELECT user_id FROM bug_mentors WHERE bug_id = ?", + undef, $bug->id); + if (@$migrated) { + foreach my $migrated_id (@$migrated) { + $mentors = [grep { $_->id != $migrated_id } @$mentors]; } - foreach my $user (@$mentors) { - print " Mentor: " . $user->identity . "\n"; + if (!@$mentors) { + print " mentor(s) already migrated\n"; + next; } - next if @$errors; - $whiteboard =~ s/\[mentor=[^\]]+\]//g; - - my $migrated = $dbh->selectcol_arrayref( - "SELECT user_id FROM bug_mentors WHERE bug_id = ?", - undef, - $bug->id - ); - if (@$migrated) { - foreach my $migrated_id (@$migrated) { - $mentors = [ - grep { $_->id != $migrated_id } - @$mentors - ]; - } - if (!@$mentors) { - print " mentor(s) already migrated\n"; - next; - } - } - - my $delta_ts = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - $dbh->bz_start_transaction; - $dbh->do( - "UPDATE bugs SET status_whiteboard=? WHERE bug_id=?", - undef, - $whiteboard, $bug->id - ); - Bugzilla::Bug::LogActivityEntry( - $bug->id, - 'status_whiteboard', - $orig_whiteboard, - $whiteboard, - $nobody->id, - $delta_ts, - ); - foreach my $mentor (@$mentors) { - $dbh->do( - "INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)", - undef, - $bug->id, $mentor->id, - ); - Bugzilla::Bug::LogActivityEntry( - $bug->id, - 'bug_mentor', - '', - $mentor->login, - $nobody->id, - $delta_ts, - ); - } - $dbh->do( - "UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?", - undef, - $bug->id, - ); - $dbh->bz_commit_transaction; + } + + my $delta_ts = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + $dbh->bz_start_transaction; + $dbh->do("UPDATE bugs SET status_whiteboard=? WHERE bug_id=?", + undef, $whiteboard, $bug->id); + Bugzilla::Bug::LogActivityEntry($bug->id, 'status_whiteboard', + $orig_whiteboard, $whiteboard, $nobody->id, $delta_ts,); + foreach my $mentor (@$mentors) { + $dbh->do("INSERT INTO bug_mentors (bug_id, user_id) VALUES (?, ?)", + undef, $bug->id, $mentor->id,); + Bugzilla::Bug::LogActivityEntry($bug->id, 'bug_mentor', '', $mentor->login, + $nobody->id, $delta_ts,); + } + $dbh->do("UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?", + undef, $bug->id,); + $dbh->bz_commit_transaction; } sub extract_mentors { - my ($whiteboard) = @_; - - my (@mentors, @errors); - my $logout = 0; - while ($whiteboard =~ /\[mentor=([^\]]+)\]/g) { - my $mentor_string = $1; - $mentor_string =~ s/(^\s+|\s+$)//g; - if ($mentor_string =~ /\@/) { - # assume it's a full username if it contains an @ - my $user = Bugzilla::User->new({ name => $mentor_string }); - if (!$user) { - push @errors, "'$mentor_string' failed to match any users"; - } else { - push @mentors, $user; - } - } else { - # otherwise assume it's a : prefixed nick - - $mentor_string =~ s/^://; - my $matches = find_users(":$mentor_string"); - if (!@$matches) { - $matches = find_users($mentor_string); - } - - if (!$matches || !@$matches) { - push @errors, "'$mentor_string' failed to match any users"; - } elsif (scalar(@$matches) > 1) { - push @errors, "'$mentor_string' matches more than one user: " . - join(', ', map { $_->identity } @$matches); - } else { - push @mentors, $matches->[0]; - } - } + my ($whiteboard) = @_; + + my (@mentors, @errors); + my $logout = 0; + while ($whiteboard =~ /\[mentor=([^\]]+)\]/g) { + my $mentor_string = $1; + $mentor_string =~ s/(^\s+|\s+$)//g; + if ($mentor_string =~ /\@/) { + + # assume it's a full username if it contains an @ + my $user = Bugzilla::User->new({name => $mentor_string}); + if (!$user) { + push @errors, "'$mentor_string' failed to match any users"; + } + else { + push @mentors, $user; + } + } + else { + # otherwise assume it's a : prefixed nick + + $mentor_string =~ s/^://; + my $matches = find_users(":$mentor_string"); + if (!@$matches) { + $matches = find_users($mentor_string); + } + + if (!$matches || !@$matches) { + push @errors, "'$mentor_string' failed to match any users"; + } + elsif (scalar(@$matches) > 1) { + push @errors, "'$mentor_string' matches more than one user: " + . join(', ', map { $_->identity } @$matches); + } + else { + push @mentors, $matches->[0]; + } } - return (\@mentors, \@errors); + } + return (\@mentors, \@errors); } sub find_users { - my ($query) = @_; - my $matches = Bugzilla::User::match("*$query*", 2); - return [ - grep { $_->name =~ /:?\Q$query\E\b/i } - @$matches - ]; + my ($query) = @_; + my $matches = Bugzilla::User::match("*$query*", 2); + return [grep { $_->name =~ /:?\Q$query\E\b/i } @$matches]; } diff --git a/extensions/Review/bin/review_requests_rebuild.pl b/extensions/Review/bin/review_requests_rebuild.pl index 03d25d045..8bda4119c 100755 --- a/extensions/Review/bin/review_requests_rebuild.pl +++ b/extensions/Review/bin/review_requests_rebuild.pl @@ -24,7 +24,7 @@ use Bugzilla::Extension::Review::Util; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); -rebuild_review_counters(sub{ - my ($count, $total) = @_; - indicate_progress({ current => $count, total => $total, every => 5 }); +rebuild_review_counters(sub { + my ($count, $total) = @_; + indicate_progress({current => $count, total => $total, every => 5}); }); diff --git a/extensions/Review/lib/FlagStateActivity.pm b/extensions/Review/lib/FlagStateActivity.pm index 35da42351..92efb6c02 100644 --- a/extensions/Review/lib/FlagStateActivity.pm +++ b/extensions/Review/lib/FlagStateActivity.pm @@ -24,65 +24,58 @@ use constant AUDIT_UPDATES => 0; use constant AUDIT_REMOVES => 0; use constant DB_COLUMNS => qw( - id - flag_when - type_id - flag_id - setter_id - requestee_id - bug_id - attachment_id - status + id + flag_when + type_id + flag_id + setter_id + requestee_id + bug_id + attachment_id + status ); sub _check_param_required { - my ($param) = @_; - - return sub { - my ($invocant, $value) = @_; - $value = trim($value) - or ThrowCodeError('param_required', {param => $param}); - return $value; - }, + my ($param) = @_; + + return sub { + my ($invocant, $value) = @_; + $value = trim($value) or ThrowCodeError('param_required', {param => $param}); + return $value; + },; } sub _check_date { - my ($invocant, $date) = @_; + my ($invocant, $date) = @_; - $date = trim($date); - datetime_from($date) - or ThrowUserError('illegal_date', { date => $date, - format => 'YYYY-MM-DD HH24:MI:SS' }); - return $date; + $date = trim($date); + datetime_from($date) + or ThrowUserError('illegal_date', + {date => $date, format => 'YYYY-MM-DD HH24:MI:SS'}); + return $date; } 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 (none { $status eq $_ } qw( X + - ? )) { - 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 (none { $status eq $_ } qw( X + - ? )) { + ThrowUserError('flag_status_invalid', {id => $self->id, status => $status}); + } + return $status; } use constant VALIDATORS => { - flag_when => \&_check_date, - type_id => _check_param_required('type_id'), - flag_id => _check_param_required('flag_id'), - setter_id => _check_param_required('setter_id'), - bug_id => _check_param_required('bug_id'), - status => \&_check_status, + flag_when => \&_check_date, + type_id => _check_param_required('type_id'), + flag_id => _check_param_required('flag_id'), + setter_id => _check_param_required('setter_id'), + bug_id => _check_param_required('bug_id'), + status => \&_check_status, }; sub flag_when { return $_[0]->{flag_when} } @@ -95,30 +88,33 @@ sub attachment_id { return $_[0]->{attachment_id} } sub status { return $_[0]->{status} } sub type { - my ($self) = @_; - return $self->{type} //= Bugzilla::FlagType->new({ id => $self->type_id, cache => 1 }); + my ($self) = @_; + return $self->{type} + //= Bugzilla::FlagType->new({id => $self->type_id, cache => 1}); } sub setter { - my ($self) = @_; - return $self->{setter} //= Bugzilla::User->new({ id => $self->setter_id, cache => 1 }); + my ($self) = @_; + return $self->{setter} + //= Bugzilla::User->new({id => $self->setter_id, cache => 1}); } sub requestee { - my ($self) = @_; - return undef unless defined $self->requestee_id; - return $self->{requestee} //= Bugzilla::User->new({ id => $self->requestee_id, cache => 1 }); + my ($self) = @_; + return undef unless defined $self->requestee_id; + return $self->{requestee} + //= Bugzilla::User->new({id => $self->requestee_id, cache => 1}); } sub bug { - my ($self) = @_; - return $self->{bug} //= Bugzilla::Bug->new({ id => $self->bug_id, cache => 1 }); + my ($self) = @_; + return $self->{bug} //= Bugzilla::Bug->new({id => $self->bug_id, cache => 1}); } sub attachment { - my ($self) = @_; - return $self->{attachment} //= - Bugzilla::Attachment->new({ id => $self->attachment_id, cache => 1 }); + my ($self) = @_; + return $self->{attachment} + //= Bugzilla::Attachment->new({id => $self->attachment_id, cache => 1}); } 1; diff --git a/extensions/Review/lib/Util.pm b/extensions/Review/lib/Util.pm index a8744079d..61d4e9117 100644 --- a/extensions/Review/lib/Util.pm +++ b/extensions/Review/lib/Util.pm @@ -17,12 +17,12 @@ use Bugzilla; our @EXPORT = qw( rebuild_review_counters ); sub rebuild_review_counters { - my ($callback) = @_; - my $dbh = Bugzilla->dbh; + my ($callback) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction; + $dbh->bz_start_transaction; - my $rows = $dbh->selectall_arrayref(" + my $rows = $dbh->selectall_arrayref(" SELECT flags.requestee_id AS user_id, flagtypes.name AS flagtype, COUNT(*) as count @@ -32,55 +32,48 @@ sub rebuild_review_counters { WHERE flags.status = '?' AND flagtypes.name IN ('review', 'feedback', 'needinfo') GROUP BY flags.requestee_id, flagtypes.name - ", { Slice => {} }); + ", {Slice => {}}); - my ($count, $total, $current) = (1, scalar(@$rows), { id => 0 }); - foreach my $row (@$rows) { - $callback->($count++, $total) if $callback; - if ($row->{user_id} != $current->{id}) { - _update_profile($dbh, $current) if $current->{id}; - $current = { id => $row->{user_id} }; - } - $current->{$row->{flagtype}} = $row->{count}; + my ($count, $total, $current) = (1, scalar(@$rows), {id => 0}); + foreach my $row (@$rows) { + $callback->($count++, $total) if $callback; + if ($row->{user_id} != $current->{id}) { + _update_profile($dbh, $current) if $current->{id}; + $current = {id => $row->{user_id}}; } - _update_profile($dbh, $current) if $current->{id}; + $current->{$row->{flagtype}} = $row->{count}; + } + _update_profile($dbh, $current) if $current->{id}; - foreach my $field (qw( review feedback needinfo )) { - _fix_negatives($dbh, $field); - } + foreach my $field (qw( review feedback needinfo )) { + _fix_negatives($dbh, $field); + } - $dbh->bz_commit_transaction; + $dbh->bz_commit_transaction; } sub _fix_negatives { - my ($dbh, $field) = @_; - my $user_ids = $dbh->selectcol_arrayref( - "SELECT userid FROM profiles WHERE ${field}_request_count < 0" - ); - return unless @$user_ids; - $dbh->do( - "UPDATE profiles SET ${field}_request_count = 0 WHERE " . $dbh->sql_in('userid', $user_ids) - ); - foreach my $user_id (@$user_ids) { - Bugzilla->memcached->clear({ table => 'profiles', id => $user_id }); - } + my ($dbh, $field) = @_; + my $user_ids = $dbh->selectcol_arrayref( + "SELECT userid FROM profiles WHERE ${field}_request_count < 0"); + return unless @$user_ids; + $dbh->do("UPDATE profiles SET ${field}_request_count = 0 WHERE " + . $dbh->sql_in('userid', $user_ids)); + foreach my $user_id (@$user_ids) { + Bugzilla->memcached->clear({table => 'profiles', id => $user_id}); + } } sub _update_profile { - my ($dbh, $data) = @_; - $dbh->do(" + my ($dbh, $data) = @_; + $dbh->do(" UPDATE profiles SET review_request_count = ?, feedback_request_count = ?, needinfo_request_count = ? - WHERE userid = ?", - undef, - $data->{review} || 0, - $data->{feedback} || 0, - $data->{needinfo} || 0, - $data->{id} - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $data->{id} }); + WHERE userid = ?", undef, $data->{review} || 0, $data->{feedback} || 0, + $data->{needinfo} || 0, $data->{id}); + Bugzilla->memcached->clear({table => 'profiles', id => $data->{id}}); } 1; diff --git a/extensions/Review/lib/WebService.pm b/extensions/Review/lib/WebService.pm index 0c54d725a..79843cf2c 100644 --- a/extensions/Review/lib/WebService.pm +++ b/extensions/Review/lib/WebService.pm @@ -20,277 +20,295 @@ use Bugzilla::Util qw(detaint_natural trick_taint); use Bugzilla::WebService::Util 'filter'; use constant PUBLIC_METHODS => qw( - flag_activity - suggestions + flag_activity + suggestions ); sub suggestions { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - - my ($bug, $product, $component); - if (exists $params->{bug_id}) { - $bug = Bugzilla::Bug->check($params->{bug_id}); - $product = $bug->product_obj; - $component = $bug->component_obj; - } - elsif (exists $params->{product}) { - $product = Bugzilla::Product->check($params->{product}); - if (exists $params->{component}) { - $component = Bugzilla::Component->check({ - product => $product, name => $params->{component} - }); - } - } - else { - ThrowUserError("reviewer_suggestions_param_required"); + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + + my ($bug, $product, $component); + if (exists $params->{bug_id}) { + $bug = Bugzilla::Bug->check($params->{bug_id}); + $product = $bug->product_obj; + $component = $bug->component_obj; + } + elsif (exists $params->{product}) { + $product = Bugzilla::Product->check($params->{product}); + if (exists $params->{component}) { + $component + = Bugzilla::Component->check({ + product => $product, name => $params->{component} + }); } + } + else { + ThrowUserError("reviewer_suggestions_param_required"); + } - my @reviewers; - if ($bug) { - # we always need to be authentiated to perform user matching - my $user = Bugzilla->user; - if (!$user->id) { - Bugzilla->set_user(Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} })); - push @reviewers, @{ $bug->mentors }; - Bugzilla->set_user($user); - } else { - push @reviewers, @{ $bug->mentors }; - } - } - if ($component) { - push @reviewers, @{ $component->reviewers_objs }; - } - if (!$component || !@{ $component->reviewers_objs }) { - push @reviewers, @{ $product->reviewers_objs }; - } + my @reviewers; + if ($bug) { - my @result; - foreach my $reviewer (@reviewers) { - push @result, { - id => $self->type('int', $reviewer->id), - email => $self->type('email', $reviewer->login), - name => $self->type('string', $reviewer->name), - review_count => $self->type('int', $reviewer->review_count), - }; + # we always need to be authentiated to perform user matching + my $user = Bugzilla->user; + if (!$user->id) { + Bugzilla->set_user(Bugzilla::User->check( + {name => Bugzilla->params->{'nobody_user'}})); + push @reviewers, @{$bug->mentors}; + Bugzilla->set_user($user); + } + else { + push @reviewers, @{$bug->mentors}; } - return \@result; + } + if ($component) { + push @reviewers, @{$component->reviewers_objs}; + } + if (!$component || !@{$component->reviewers_objs}) { + push @reviewers, @{$product->reviewers_objs}; + } + + my @result; + foreach my $reviewer (@reviewers) { + push @result, + { + id => $self->type('int', $reviewer->id), + email => $self->type('email', $reviewer->login), + name => $self->type('string', $reviewer->name), + review_count => $self->type('int', $reviewer->review_count), + }; + } + return \@result; } sub flag_activity { - my ($self, $params) = @_; - my $dbh = Bugzilla->switch_to_shadow_db(); - my %match_criteria; + my ($self, $params) = @_; + my $dbh = Bugzilla->switch_to_shadow_db(); + my %match_criteria; - if (my $flag_id = $params->{flag_id}) { - detaint_natural($flag_id) - or ThrowUserError('invalid_flag_id', { flag_id => $flag_id }); + if (my $flag_id = $params->{flag_id}) { + detaint_natural($flag_id) + or ThrowUserError('invalid_flag_id', {flag_id => $flag_id}); - $match_criteria{flag_id} = $flag_id; - } - - if (my $flag_ids = $params->{flag_ids}) { - foreach my $flag_id (@$flag_ids) { - detaint_natural($flag_id) - or ThrowUserError('invalid_flag_id', { flag_id => $flag_id }); - } + $match_criteria{flag_id} = $flag_id; + } - $match_criteria{flag_id} = $flag_ids; + if (my $flag_ids = $params->{flag_ids}) { + foreach my $flag_id (@$flag_ids) { + detaint_natural($flag_id) + or ThrowUserError('invalid_flag_id', {flag_id => $flag_id}); } - if (my $type_id = $params->{type_id}) { - detaint_natural($type_id) - or ThrowUserError('invalid_flag_type_id', { type_id => $type_id }); + $match_criteria{flag_id} = $flag_ids; + } - $match_criteria{type_id} = $type_id; - } + if (my $type_id = $params->{type_id}) { + detaint_natural($type_id) + or ThrowUserError('invalid_flag_type_id', {type_id => $type_id}); - if (my $type_name = $params->{type_name}) { - trick_taint($type_name); - my $flag_types = Bugzilla::FlagType::match({ name => $type_name }); - $match_criteria{type_id} = [map { $_->id } @$flag_types]; - } + $match_criteria{type_id} = $type_id; + } - foreach my $user_field (qw( requestee setter )) { - if (my $user_name = $params->{$user_field}) { - my $user = Bugzilla::User->check({ name => $user_name, cache => 1, _error => 'invalid_username' }); + if (my $type_name = $params->{type_name}) { + trick_taint($type_name); + my $flag_types = Bugzilla::FlagType::match({name => $type_name}); + $match_criteria{type_id} = [map { $_->id } @$flag_types]; + } - $match_criteria{ $user_field . "_id" } = $user->id; - } - } + foreach my $user_field (qw( requestee setter )) { + if (my $user_name = $params->{$user_field}) { + my $user = Bugzilla::User->check( + {name => $user_name, cache => 1, _error => 'invalid_username'}); - foreach my $field (qw( bug_id status )) { - if (exists $params->{$field}) { - $match_criteria{$field} = $params->{$field}; - } + $match_criteria{$user_field . "_id"} = $user->id; } + } - ThrowCodeError('param_required', { param => 'limit', function => 'Review.flag_activity()' }) - if defined $params->{offset} && !defined $params->{limit}; - - my $limit = delete $params->{limit}; - my $offset = delete $params->{offset}; - my $after = delete $params->{after}; - my $before = delete $params->{before}; - my $max_results = Bugzilla->params->{max_search_results}; - - if (!$limit || $limit > $max_results) { - $limit = $max_results; + foreach my $field (qw( bug_id status )) { + if (exists $params->{$field}) { + $match_criteria{$field} = $params->{$field}; } - - if ($after && $after =~ /^(\d{4}-\d{1,2}-\d{1,2})$/) { - $after = $1; - } - else { - my $now = DateTime->now; - $now->subtract(days => 30); - $after = $now->ymd('-'); - } - - if ($before && $before =~ /^(\d{4}-\d{1,2}-\d{1,2})$/) { - $before = $1; - } - else { - my $now = DateTime->now; - $before = $now->ymd('-'); - } - - $match_criteria{LIMIT} = $limit; - $match_criteria{OFFSET} = $offset if defined $offset; - $match_criteria{WHERE} = { 'date(flag_when) BETWEEN ? AND ?' => [$after, $before] }; - - # Throw error if no other parameters have been passed other than limit and offset - if (!grep(!/^(LIMIT|OFFSET)$/, keys %match_criteria)) { - ThrowUserError('flag_activity_parameters_required'); - } - - my $matches = Bugzilla::Extension::Review::FlagStateActivity->match(\%match_criteria); - my $user = Bugzilla->user; - $user->visible_bugs([ map { $_->bug_id } @$matches ]); - my @results = map { $self->_flag_state_activity_to_hash($_, $params) } - grep { $user->can_see_bug($_->bug_id) && _can_see_attachment($user, $_) } - @$matches; - return \@results; + } + + ThrowCodeError('param_required', + {param => 'limit', function => 'Review.flag_activity()'}) + if defined $params->{offset} && !defined $params->{limit}; + + my $limit = delete $params->{limit}; + my $offset = delete $params->{offset}; + my $after = delete $params->{after}; + my $before = delete $params->{before}; + my $max_results = Bugzilla->params->{max_search_results}; + + if (!$limit || $limit > $max_results) { + $limit = $max_results; + } + + if ($after && $after =~ /^(\d{4}-\d{1,2}-\d{1,2})$/) { + $after = $1; + } + else { + my $now = DateTime->now; + $now->subtract(days => 30); + $after = $now->ymd('-'); + } + + if ($before && $before =~ /^(\d{4}-\d{1,2}-\d{1,2})$/) { + $before = $1; + } + else { + my $now = DateTime->now; + $before = $now->ymd('-'); + } + + $match_criteria{LIMIT} = $limit; + $match_criteria{OFFSET} = $offset if defined $offset; + $match_criteria{WHERE} + = {'date(flag_when) BETWEEN ? AND ?' => [$after, $before]}; + + # Throw error if no other parameters have been passed other than limit and offset + if (!grep(!/^(LIMIT|OFFSET)$/, keys %match_criteria)) { + ThrowUserError('flag_activity_parameters_required'); + } + + my $matches + = Bugzilla::Extension::Review::FlagStateActivity->match(\%match_criteria); + my $user = Bugzilla->user; + $user->visible_bugs([map { $_->bug_id } @$matches]); + my @results + = map { $self->_flag_state_activity_to_hash($_, $params) } + grep { $user->can_see_bug($_->bug_id) && _can_see_attachment($user, $_) } + @$matches; + return \@results; } sub _can_see_attachment { - my ($user, $flag_state_activity) = @_; + my ($user, $flag_state_activity) = @_; - return 1 if !$flag_state_activity->attachment_id; - return 0 if $flag_state_activity->attachment->isprivate && !$user->is_insider; - return 1; + return 1 if !$flag_state_activity->attachment_id; + return 0 if $flag_state_activity->attachment->isprivate && !$user->is_insider; + return 1; } sub rest_resources { - return [ - # bug-id - qr{^/review/suggestions/(\d+)$}, { - GET => { - method => 'suggestions', - params => sub { - return { bug_id => $_[0] }; - }, - }, - }, - # product/component - qr{^/review/suggestions/([^/]+)/(.+)$}, { - GET => { - method => 'suggestions', - params => sub { - return { product => $_[0], component => $_[1] }; - }, - }, + return [ + # bug-id + qr{^/review/suggestions/(\d+)$}, + { + GET => { + method => 'suggestions', + params => sub { + return {bug_id => $_[0]}; }, - # just product - qr{^/review/suggestions/([^/]+)$}, { - GET => { - method => 'suggestions', - params => sub { - return { product => $_[0] }; - }, - }, + }, + }, + + # product/component + qr{^/review/suggestions/([^/]+)/(.+)$}, + { + GET => { + method => 'suggestions', + params => sub { + return {product => $_[0], component => $_[1]}; }, - # named parameters - qr{^/review/suggestions$}, { - GET => { - method => 'suggestions', - }, + }, + }, + + # just product + qr{^/review/suggestions/([^/]+)$}, + { + GET => { + method => 'suggestions', + params => sub { + return {product => $_[0]}; }, - # flag activity by flag id - qr{^/review/flag_activity/(\d+)$}, { - GET => { - method => 'flag_activity', - params => sub { - return { flag_id => $_[0] } - }, - }, + }, + }, + + # named parameters + qr{^/review/suggestions$}, + {GET => {method => 'suggestions',},}, + + # flag activity by flag id + qr{^/review/flag_activity/(\d+)$}, + { + GET => { + method => 'flag_activity', + params => sub { + return {flag_id => $_[0]}; }, - qr{^/review/flag_activity/type_name/(\w+)$}, { - GET => { - method => 'flag_activity', - params => sub { - return { type_name => $_[0] } - }, - }, + }, + }, + qr{^/review/flag_activity/type_name/(\w+)$}, + { + GET => { + method => 'flag_activity', + params => sub { + return {type_name => $_[0]}; }, - # flag activity by user - qr{^/review/flag_activity/(requestee|setter|type_id)/(.*)$}, { - GET => { - method => 'flag_activity', - params => sub { - return { $_[0] => $_[1] }; - }, - }, + }, + }, + + # flag activity by user + qr{^/review/flag_activity/(requestee|setter|type_id)/(.*)$}, + { + GET => { + method => 'flag_activity', + params => sub { + return {$_[0] => $_[1]}; }, - # flag activity with only query strings - qr{^/review/flag_activity$}, { - GET => { method => 'flag_activity' }, - }, - ]; + }, + }, + + # flag activity with only query strings + qr{^/review/flag_activity$}, + {GET => {method => 'flag_activity'},}, + ]; } sub _flag_state_activity_to_hash { - my ($self, $fsa, $params) = @_; - - my %flag = ( - id => $self->type('int', $fsa->id), - creation_time => $self->type('string', $fsa->flag_when), - type => $self->_flagtype_to_hash($fsa->type), - setter => $self->_user_to_hash($fsa->setter), - bug_id => $self->type('int', $fsa->bug_id), - attachment_id => $self->type('int', $fsa->attachment_id), - status => $self->type('string', $fsa->status), - ); - - $flag{requestee} = $self->_user_to_hash($fsa->requestee) if $fsa->requestee; - $flag{flag_id} = $self->type('int', $fsa->flag_id) unless $params->{flag_id}; - - return filter($params, \%flag); + my ($self, $fsa, $params) = @_; + + my %flag = ( + id => $self->type('int', $fsa->id), + creation_time => $self->type('string', $fsa->flag_when), + type => $self->_flagtype_to_hash($fsa->type), + setter => $self->_user_to_hash($fsa->setter), + bug_id => $self->type('int', $fsa->bug_id), + attachment_id => $self->type('int', $fsa->attachment_id), + status => $self->type('string', $fsa->status), + ); + + $flag{requestee} = $self->_user_to_hash($fsa->requestee) if $fsa->requestee; + $flag{flag_id} = $self->type('int', $fsa->flag_id) unless $params->{flag_id}; + + return filter($params, \%flag); } sub _flagtype_to_hash { - my ($self, $flagtype) = @_; - my $user = Bugzilla->user; - - return { - id => $self->type('int', $flagtype->id), - name => $self->type('string', $flagtype->name), - description => $self->type('string', $flagtype->description), - type => $self->type('string', $flagtype->target_type), - is_active => $self->type('boolean', $flagtype->is_active), - is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), - is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), - }; + my ($self, $flagtype) = @_; + my $user = Bugzilla->user; + + return { + id => $self->type('int', $flagtype->id), + name => $self->type('string', $flagtype->name), + description => $self->type('string', $flagtype->description), + type => $self->type('string', $flagtype->target_type), + is_active => $self->type('boolean', $flagtype->is_active), + is_requesteeble => $self->type('boolean', $flagtype->is_requesteeble), + is_multiplicable => $self->type('boolean', $flagtype->is_multiplicable), + }; } sub _user_to_hash { - my ($self, $user) = @_; + my ($self, $user) = @_; - return { - id => $self->type('int', $user->id), - real_name => $self->type('string', $user->name), - name => $self->type('email', $user->login), - }; + return { + id => $self->type('int', $user->id), + real_name => $self->type('string', $user->name), + name => $self->type('email', $user->login), + }; } 1; diff --git a/extensions/SecureMail/Config.pm b/extensions/SecureMail/Config.pm index 8d877a253..5c2dc615a 100644 --- a/extensions/SecureMail/Config.pm +++ b/extensions/SecureMail/Config.pm @@ -28,25 +28,19 @@ use warnings; use constant NAME => 'SecureMail'; use constant REQUIRED_MODULES => [ - { - package => 'Crypt-OpenPGP', - module => 'Crypt::OpenPGP', - # 1.02 added the ability for new() to take KeyRing objects for the - # PubRing argument. - version => '1.02', - # 1.04 hangs - https://rt.cpan.org/Public/Bug/Display.html?id=68018 - # blacklist => [ '1.04' ], - }, - { - package => 'Crypt-SMIME', - module => 'Crypt::SMIME', - version => 0, - }, - { - package => 'HTML-Tree', - module => 'HTML::Tree', - version => 0, - } + { + package => 'Crypt-OpenPGP', + module => 'Crypt::OpenPGP', + + # 1.02 added the ability for new() to take KeyRing objects for the + # PubRing argument. + version => '1.02', + + # 1.04 hangs - https://rt.cpan.org/Public/Bug/Display.html?id=68018 + # blacklist => [ '1.04' ], + }, + {package => 'Crypt-SMIME', module => 'Crypt::SMIME', version => 0,}, + {package => 'HTML-Tree', module => 'HTML::Tree', version => 0,} ]; __PACKAGE__->NAME; diff --git a/extensions/SecureMail/Extension.pm b/extensions/SecureMail/Extension.pm index 2b5e1bdd6..9790c0828 100644 --- a/extensions/SecureMail/Extension.pm +++ b/extensions/SecureMail/Extension.pm @@ -59,12 +59,12 @@ use constant SECURE_ALL => 2; # public_key text in the 'profiles' table - stores public key ############################################################################## sub install_update_db { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('groups', 'secure_mail', - {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_column('profiles', 'public_key', { TYPE => 'LONGTEXT' }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('groups', 'secure_mail', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('profiles', 'public_key', {TYPE => 'LONGTEXT'}); } ############################################################################## @@ -72,194 +72,193 @@ sub install_update_db { ############################################################################## BEGIN { - *Bugzilla::Group::secure_mail = \&_group_secure_mail; - *Bugzilla::User::public_key = \&_user_public_key; - *Bugzilla::securemail_groups = \&_securemail_groups; + *Bugzilla::Group::secure_mail = \&_group_secure_mail; + *Bugzilla::User::public_key = \&_user_public_key; + *Bugzilla::securemail_groups = \&_securemail_groups; } sub _group_secure_mail { return $_[0]->{'secure_mail'}; } sub _securemail_groups { - return Bugzilla->dbh->selectcol_arrayref("SELECT name FROM groups WHERE secure_mail = 1") // []; + return Bugzilla->dbh->selectcol_arrayref( + "SELECT name FROM groups WHERE secure_mail = 1") // []; } # We want to lazy-load the public_key. sub _user_public_key { - my $self = shift; - if (!exists $self->{public_key}) { - ($self->{public_key}) = Bugzilla->dbh->selectrow_array( - "SELECT public_key FROM profiles WHERE userid = ?", - undef, - $self->id - ); - } - return $self->{public_key}; + my $self = shift; + if (!exists $self->{public_key}) { + ($self->{public_key}) + = Bugzilla->dbh->selectrow_array( + "SELECT public_key FROM profiles WHERE userid = ?", + undef, $self->id); + } + return $self->{public_key}; } # Make sure generic functions know about the additional fields in the user # and group objects. sub object_columns { - my ($self, $args) = @_; - my $class = $args->{'class'}; - my $columns = $args->{'columns'}; - - if ($class->isa('Bugzilla::Group')) { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_column_info($class->DB_TABLE, 'secure_mail')) { - push @$columns, 'secure_mail'; - } + my ($self, $args) = @_; + my $class = $args->{'class'}; + my $columns = $args->{'columns'}; + + if ($class->isa('Bugzilla::Group')) { + my $dbh = Bugzilla->dbh; + if ($dbh->bz_column_info($class->DB_TABLE, 'secure_mail')) { + push @$columns, 'secure_mail'; } + } } # Plug appropriate validators so we can check the validity of the two # fields created by this extension, when new values are submitted. sub object_validators { - my ($self, $args) = @_; - my %args = %{ $args }; - my ($invocant, $validators) = @args{qw(class validators)}; + my ($self, $args) = @_; + my %args = %{$args}; + my ($invocant, $validators) = @args{qw(class validators)}; - if ($invocant->isa('Bugzilla::Group')) { - $validators->{'secure_mail'} = \&Bugzilla::Object::check_boolean; - } - elsif ($invocant->isa('Bugzilla::User')) { - $validators->{'public_key'} = sub { - my ($self, $value) = @_; - $value = trim($value) || ''; - - return $value if $value eq ''; - - if ($value =~ /PUBLIC KEY/) { - # PGP keys must be ASCII-armoured. - my $tct = Bugzilla::Extension::SecureMail::TCT->new( - public_key => $value, - command => Bugzilla->localconfig->{tct_bin}, - ); - unless ($tct->is_valid->get) { - ThrowUserError( 'securemail_invalid_key', { errstr => 'key is invalid or expired' } ); - } - } - elsif ($value =~ /BEGIN CERTIFICATE/) { - # S/MIME Keys must be in PEM format (Base64-encoded X.509) - # - # Crypt::SMIME seems not to like tainted values - it claims - # they aren't scalars! - trick_taint($value); - - my $smime = Crypt::SMIME->new(); - eval { - $smime->setPublicKey([$value]); - }; - if ($@) { - ThrowUserError('securemail_invalid_key', - { errstr => $@ }); - } - } - else { - ThrowUserError('securemail_invalid_key'); - } - - return $value; - }; - } + if ($invocant->isa('Bugzilla::Group')) { + $validators->{'secure_mail'} = \&Bugzilla::Object::check_boolean; + } + elsif ($invocant->isa('Bugzilla::User')) { + $validators->{'public_key'} = sub { + my ($self, $value) = @_; + $value = trim($value) || ''; + + return $value if $value eq ''; + + if ($value =~ /PUBLIC KEY/) { + + # PGP keys must be ASCII-armoured. + my $tct = Bugzilla::Extension::SecureMail::TCT->new( + public_key => $value, + command => Bugzilla->localconfig->{tct_bin}, + ); + unless ($tct->is_valid->get) { + ThrowUserError('securemail_invalid_key', + {errstr => 'key is invalid or expired'}); + } + } + elsif ($value =~ /BEGIN CERTIFICATE/) { + + # S/MIME Keys must be in PEM format (Base64-encoded X.509) + # + # Crypt::SMIME seems not to like tainted values - it claims + # they aren't scalars! + trick_taint($value); + + my $smime = Crypt::SMIME->new(); + eval { $smime->setPublicKey([$value]); }; + if ($@) { + ThrowUserError('securemail_invalid_key', {errstr => $@}); + } + } + else { + ThrowUserError('securemail_invalid_key'); + } + + return $value; + }; + } } # When creating a 'group' object, set up the secure_mail field appropriately. sub object_before_create { - my ($self, $args) = @_; - my $class = $args->{'class'}; - my $params = $args->{'params'}; + my ($self, $args) = @_; + my $class = $args->{'class'}; + my $params = $args->{'params'}; - if ($class->isa('Bugzilla::Group')) { - $params->{secure_mail} = Bugzilla->cgi->param('secure_mail'); - } + if ($class->isa('Bugzilla::Group')) { + $params->{secure_mail} = Bugzilla->cgi->param('secure_mail'); + } } # On update, make sure the updating process knows about our new columns. sub object_update_columns { - my ($self, $args) = @_; - my $object = $args->{'object'}; - my $columns = $args->{'columns'}; + my ($self, $args) = @_; + my $object = $args->{'object'}; + my $columns = $args->{'columns'}; - if ($object->isa('Bugzilla::Group')) { - # This seems like a convenient moment to extract this value... - $object->set('secure_mail', Bugzilla->cgi->param('secure_mail')); + if ($object->isa('Bugzilla::Group')) { - push(@$columns, 'secure_mail'); - } - elsif ($object->isa('Bugzilla::User')) { - push(@$columns, 'public_key'); - } + # This seems like a convenient moment to extract this value... + $object->set('secure_mail', Bugzilla->cgi->param('secure_mail')); + + push(@$columns, 'secure_mail'); + } + elsif ($object->isa('Bugzilla::User')) { + push(@$columns, 'public_key'); + } } # Handle the setting and changing of the public key. sub user_preferences { - my ($self, $args) = @_; - my $tab = $args->{'current_tab'}; - my $save = $args->{'save_changes'}; - my $handled = $args->{'handled'}; - my $vars = $args->{'vars'}; - my $params = Bugzilla->input_params; - - return unless $tab eq 'securemail'; - - # Create a new user object so we don't mess with the main one, as we - # don't know where it's been... - my $user = new Bugzilla::User(Bugzilla->user->id); - - if ($save) { - $user->set('public_key', $params->{'public_key'}); - $user->update(); - - # Send user a test email - if ($user->public_key) { - _send_test_email($user); - $vars->{'test_email_sent'} = 1; - } + my ($self, $args) = @_; + my $tab = $args->{'current_tab'}; + my $save = $args->{'save_changes'}; + my $handled = $args->{'handled'}; + my $vars = $args->{'vars'}; + my $params = Bugzilla->input_params; + + return unless $tab eq 'securemail'; + + # Create a new user object so we don't mess with the main one, as we + # don't know where it's been... + my $user = new Bugzilla::User(Bugzilla->user->id); + + if ($save) { + $user->set('public_key', $params->{'public_key'}); + $user->update(); + + # Send user a test email + if ($user->public_key) { + _send_test_email($user); + $vars->{'test_email_sent'} = 1; } + } - $vars->{'public_key'} = $user->public_key; + $vars->{'public_key'} = $user->public_key; - # Set the 'handled' scalar reference to true so that the caller - # knows the panel name is valid and that an extension took care of it. - $$handled = 1; + # Set the 'handled' scalar reference to true so that the caller + # knows the panel name is valid and that an extension took care of it. + $$handled = 1; } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - # Bug dependency emails contain the subject of the dependent bug - # right before the diffs when a status has gone from open/closed - # or closed/open. We need to sanitize the subject of change.blocker - # similar to how we do referenced bugs - return unless - $file eq 'email/bugmail.html.tmpl' - || $file eq 'email/bugmail.txt.tmpl'; - - if (defined $vars->{diffs}) { - foreach my $change (@{ $vars->{diffs} }) { - next if !defined $change->{blocker}; - if (grep($_->secure_mail, @{ $change->{blocker}->groups_in })) { - $change->{blocker}->{short_desc} = "(Secure bug)"; - } - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + # Bug dependency emails contain the subject of the dependent bug + # right before the diffs when a status has gone from open/closed + # or closed/open. We need to sanitize the subject of change.blocker + # similar to how we do referenced bugs + return + unless $file eq 'email/bugmail.html.tmpl' + || $file eq 'email/bugmail.txt.tmpl'; + + if (defined $vars->{diffs}) { + foreach my $change (@{$vars->{diffs}}) { + next if !defined $change->{blocker}; + if (grep($_->secure_mail, @{$change->{blocker}->groups_in})) { + $change->{blocker}->{short_desc} = "(Secure bug)"; + } } + } } sub _send_test_email { - my ($user) = @_; - my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); + my ($user) = @_; + my $template = Bugzilla->template_inner($user->settings->{'lang'}->{'value'}); - my $vars = { - to_user => $user->email, - }; + my $vars = {to_user => $user->email,}; - my $msg = ""; - $template->process("account/email/securemail-test.txt.tmpl", $vars, \$msg) - || ThrowTemplateError($template->error()); + my $msg = ""; + $template->process("account/email/securemail-test.txt.tmpl", $vars, \$msg) + || ThrowTemplateError($template->error()); - MessageToMTA($msg); + MessageToMTA($msg); } ############################################################################## @@ -268,365 +267,382 @@ sub _send_test_email { # determine if the bug should be encrypted at the time it is generated sub bugmail_enqueue { - my ($self, $args) = @_; - my $vars = $args->{vars}; - if (_should_secure_bug($vars->{bug})) { - $vars->{bugzilla_encrypt} = 1; - } + my ($self, $args) = @_; + my $vars = $args->{vars}; + if (_should_secure_bug($vars->{bug})) { + $vars->{bugzilla_encrypt} = 1; + } } sub bugmail_generate { - my ($self, $args) = @_; - my $vars = $args->{vars}; - my $email = $args->{email}; - if ($vars->{bugzilla_encrypt}) { - $email->header_set('X-Bugzilla-Encrypt', 1); - } + my ($self, $args) = @_; + my $vars = $args->{vars}; + my $email = $args->{email}; + if ($vars->{bugzilla_encrypt}) { + $email->header_set('X-Bugzilla-Encrypt', 1); + } } sub mailer_before_send { - my ($self, $args) = @_; - - my $email = $args->{'email'}; - my $body = $email->body; - - # Decide whether to make secure. - # This is a bit of a hack; it would be nice if it were more clear - # what sort a particular email is. - my $is_bugmail = $email->header('X-Bugzilla-Status') || - $email->header('X-Bugzilla-Type') eq 'request'; - my $is_passwordmail = !$is_bugmail && ($body =~ /cfmpw.*cxlpw/s); - my $is_test_email = $email->header('X-Bugzilla-Type') =~ /securemail-test/ ? 1 : 0; - my $is_whine_email = $email->header('X-Bugzilla-Type') eq 'whine' ? 1 : 0; - my $encrypt_header = $email->header('X-Bugzilla-Encrypt') ? 1 : 0; - - if ($is_bugmail - || $is_passwordmail - || $is_test_email - || $is_whine_email - || $encrypt_header - ) { - # Convert the email's To address into a User object - my $login = $email->header('To'); - my $emailsuffix = Bugzilla->params->{'emailsuffix'}; - $login =~ s/$emailsuffix$//; - my $user = new Bugzilla::User({ name => $login }); - - # Default to secure. (Of course, this means if this extension has a - # bug, lots of people are going to get bugmail falsely claiming their - # bugs are secure and they need to add a key...) - my $make_secure = SECURE_ALL; - - if ($is_bugmail) { - # This is also a bit of a hack, but there's no header with the - # bug ID in. So we take the first number in the subject. - my ($bug_id) = ($email->header('Subject') =~ /\[\D+(\d+)\]/); - my $bug = new Bugzilla::Bug($bug_id); - if (!_should_secure_bug($bug)) { - $make_secure = SECURE_NONE; - } - # If the insider group has securemail enabled.. - my $insider_group = Bugzilla::Group->new({ name => Bugzilla->params->{'insidergroup'} }); - if ($insider_group - && $insider_group->secure_mail - && $make_secure == SECURE_NONE) - { - my $comment_is_private = Bugzilla->dbh->selectcol_arrayref( - "SELECT isprivate FROM longdescs WHERE bug_id=? ORDER BY bug_when", - undef, $bug_id); - # Encrypt if there are private comments on an otherwise public bug - if (scalar $email->parts > 1) { - $email->walk_parts(sub { - my $part = shift; - my $content_type = $part->content_type; - $body = $part->body if $content_type && $content_type =~ /^text\/plain/; - }); - } - while ($body =~ /[\r\n]--- Comment #(\d+)/g) { - my $comment_number = $1; - if ($comment_number && $comment_is_private->[$comment_number]) { - $make_secure = SECURE_BODY; - last; - } - } - # Encrypt if updating a private attachment without a comment - if ($email->header('X-Bugzilla-Changed-Fields') - && $email->header('X-Bugzilla-Attach-ID')) - { - my $attachment = Bugzilla::Attachment->new($email->header('X-Bugzilla-Attach-ID')); - if ($attachment && $attachment->isprivate) { - $make_secure = SECURE_BODY; - } - } - } - } - elsif ($is_passwordmail) { - # Mail is made unsecure only if the user does not have a public - # key and is not in any security groups. So specifying a public - # key OR being in a security group means the mail is kept secure - # (but, as noted above, the check is the other way around because - # we default to secure). - if ($user && - !$user->public_key && - !grep($_->secure_mail, @{ $user->groups })) - { - $make_secure = SECURE_NONE; - } - } - elsif ($is_whine_email) { - # When a whine email has one or more secure bugs in the body, then - # encrypt the entire email body. Subject can be left alone as it - # comes from the whine settings. - $make_secure = _should_secure_whine($email) ? SECURE_BODY : SECURE_NONE; + my ($self, $args) = @_; + + my $email = $args->{'email'}; + my $body = $email->body; + + # Decide whether to make secure. + # This is a bit of a hack; it would be nice if it were more clear + # what sort a particular email is. + my $is_bugmail = $email->header('X-Bugzilla-Status') + || $email->header('X-Bugzilla-Type') eq 'request'; + my $is_passwordmail = !$is_bugmail && ($body =~ /cfmpw.*cxlpw/s); + my $is_test_email + = $email->header('X-Bugzilla-Type') =~ /securemail-test/ ? 1 : 0; + my $is_whine_email = $email->header('X-Bugzilla-Type') eq 'whine' ? 1 : 0; + my $encrypt_header = $email->header('X-Bugzilla-Encrypt') ? 1 : 0; + + if ( $is_bugmail + || $is_passwordmail + || $is_test_email + || $is_whine_email + || $encrypt_header) + { + # Convert the email's To address into a User object + my $login = $email->header('To'); + my $emailsuffix = Bugzilla->params->{'emailsuffix'}; + $login =~ s/$emailsuffix$//; + my $user = new Bugzilla::User({name => $login}); + + # Default to secure. (Of course, this means if this extension has a + # bug, lots of people are going to get bugmail falsely claiming their + # bugs are secure and they need to add a key...) + my $make_secure = SECURE_ALL; + + if ($is_bugmail) { + + # This is also a bit of a hack, but there's no header with the + # bug ID in. So we take the first number in the subject. + my ($bug_id) = ($email->header('Subject') =~ /\[\D+(\d+)\]/); + my $bug = new Bugzilla::Bug($bug_id); + if (!_should_secure_bug($bug)) { + $make_secure = SECURE_NONE; + } + + # If the insider group has securemail enabled.. + my $insider_group + = Bugzilla::Group->new({name => Bugzilla->params->{'insidergroup'}}); + if ( $insider_group + && $insider_group->secure_mail + && $make_secure == SECURE_NONE) + { + my $comment_is_private + = Bugzilla->dbh->selectcol_arrayref( + "SELECT isprivate FROM longdescs WHERE bug_id=? ORDER BY bug_when", + undef, $bug_id); + + # Encrypt if there are private comments on an otherwise public bug + if (scalar $email->parts > 1) { + $email->walk_parts(sub { + my $part = shift; + my $content_type = $part->content_type; + $body = $part->body if $content_type && $content_type =~ /^text\/plain/; + }); } - elsif ($encrypt_header) { - # Templates or code may set the X-Bugzilla-Encrypt header to - # trigger encryption of emails. Remove that header from the email. - $email->header_set('X-Bugzilla-Encrypt'); + while ($body =~ /[\r\n]--- Comment #(\d+)/g) { + my $comment_number = $1; + if ($comment_number && $comment_is_private->[$comment_number]) { + $make_secure = SECURE_BODY; + last; + } } - # If finding the user fails for some reason, but we determine we - # should be encrypting, we want to make the mail safe. An empty key - # does that. - my $public_key = $user ? $user->public_key : ''; - - # Check if the new bugmail prefix should be added to the subject. - my $add_new = ($email->header('X-Bugzilla-Type') eq 'new' && - $user && - $user->settings->{'bugmail_new_prefix'}->{'value'} eq 'on') ? 1 : 0; - - if ($make_secure == SECURE_NONE) { - # Filter the bug_links in HTML email in case the bugs the links - # point are "secured" bugs and the user may not be able to see - # the summaries. - _filter_bug_links($email); - } - else { - _make_secure($email, $public_key, $is_bugmail && $make_secure == SECURE_ALL, $add_new); + # Encrypt if updating a private attachment without a comment + if ( $email->header('X-Bugzilla-Changed-Fields') + && $email->header('X-Bugzilla-Attach-ID')) + { + my $attachment + = Bugzilla::Attachment->new($email->header('X-Bugzilla-Attach-ID')); + if ($attachment && $attachment->isprivate) { + $make_secure = SECURE_BODY; + } } + } + } + elsif ($is_passwordmail) { + + # Mail is made unsecure only if the user does not have a public + # key and is not in any security groups. So specifying a public + # key OR being in a security group means the mail is kept secure + # (but, as noted above, the check is the other way around because + # we default to secure). + if ($user && !$user->public_key && !grep($_->secure_mail, @{$user->groups})) { + $make_secure = SECURE_NONE; + } + } + elsif ($is_whine_email) { + + # When a whine email has one or more secure bugs in the body, then + # encrypt the entire email body. Subject can be left alone as it + # comes from the whine settings. + $make_secure = _should_secure_whine($email) ? SECURE_BODY : SECURE_NONE; + } + elsif ($encrypt_header) { + + # Templates or code may set the X-Bugzilla-Encrypt header to + # trigger encryption of emails. Remove that header from the email. + $email->header_set('X-Bugzilla-Encrypt'); + } + + # If finding the user fails for some reason, but we determine we + # should be encrypting, we want to make the mail safe. An empty key + # does that. + my $public_key = $user ? $user->public_key : ''; + + # Check if the new bugmail prefix should be added to the subject. + my $add_new + = ( $email->header('X-Bugzilla-Type') eq 'new' + && $user + && $user->settings->{'bugmail_new_prefix'}->{'value'} eq 'on') ? 1 : 0; + + if ($make_secure == SECURE_NONE) { + + # Filter the bug_links in HTML email in case the bugs the links + # point are "secured" bugs and the user may not be able to see + # the summaries. + _filter_bug_links($email); + } + else { + _make_secure($email, $public_key, $is_bugmail && $make_secure == SECURE_ALL, + $add_new); } + } } # Custom hook for bugzilla.mozilla.org (see bug 752400) sub bugmail_referenced_bugs { - my ($self, $args) = @_; - # Sanitise subjects of referenced bugs. - my $referenced_bugs = $args->{'referenced_bugs'}; - # No need to sanitise subjects if the entire email will be secured. - return if _should_secure_bug($args->{'updated_bug'}); - # Replace the subject if required - foreach my $ref (@$referenced_bugs) { - if (grep($_->secure_mail, @{ $ref->{'bug'}->groups_in })) { - $ref->{'short_desc'} = "(Secure bug)"; - } + my ($self, $args) = @_; + + # Sanitise subjects of referenced bugs. + my $referenced_bugs = $args->{'referenced_bugs'}; + + # No need to sanitise subjects if the entire email will be secured. + return if _should_secure_bug($args->{'updated_bug'}); + + # Replace the subject if required + foreach my $ref (@$referenced_bugs) { + if (grep($_->secure_mail, @{$ref->{'bug'}->groups_in})) { + $ref->{'short_desc'} = "(Secure bug)"; } + } } sub _should_secure_bug { - my ($bug) = @_; - # If there's a problem with the bug, err on the side of caution and mark it - # as secure. - return - !$bug - || $bug->{'error'} - || grep($_->secure_mail, @{ $bug->groups_in }); + my ($bug) = @_; + + # If there's a problem with the bug, err on the side of caution and mark it + # as secure. + return !$bug || $bug->{'error'} || grep($_->secure_mail, @{$bug->groups_in}); } sub _should_secure_whine { - my ($email) = @_; - my $should_secure = 0; - $email->walk_parts(sub { - my $part = shift; - my $content_type = $part->content_type; - return if !$content_type || $content_type !~ /^text\/plain/; - my $body = $part->body; - my @bugids = $body =~ /Bug (\d+):/g; - foreach my $id (@bugids) { - $id = trim($id); - next if !$id; - my $bug = new Bugzilla::Bug($id); - if ($bug && _should_secure_bug($bug)) { - $should_secure = 1; - last; - } - } - }); - return $should_secure ? 1 : 0; + my ($email) = @_; + my $should_secure = 0; + $email->walk_parts(sub { + my $part = shift; + my $content_type = $part->content_type; + return if !$content_type || $content_type !~ /^text\/plain/; + my $body = $part->body; + my @bugids = $body =~ /Bug (\d+):/g; + foreach my $id (@bugids) { + $id = trim($id); + next if !$id; + my $bug = new Bugzilla::Bug($id); + if ($bug && _should_secure_bug($bug)) { + $should_secure = 1; + last; + } + } + }); + return $should_secure ? 1 : 0; } sub _make_secure { - my ($email, $key, $sanitise_subject, $add_new) = @_; - - # Add header showing this email has been secured - $email->header_set('X-Bugzilla-Secure-Email', 'Yes'); - - my $subject = $email->header('Subject'); - my ($bug_id) = $subject =~ /\[\D+(\d+)\]/; - - my $key_type = 0; - if ($key && $key =~ /PUBLIC KEY/) { - $key_type = 'PGP'; + my ($email, $key, $sanitise_subject, $add_new) = @_; + + # Add header showing this email has been secured + $email->header_set('X-Bugzilla-Secure-Email', 'Yes'); + + my $subject = $email->header('Subject'); + my ($bug_id) = $subject =~ /\[\D+(\d+)\]/; + + my $key_type = 0; + if ($key && $key =~ /PUBLIC KEY/) { + $key_type = 'PGP'; + } + elsif ($key && $key =~ /BEGIN CERTIFICATE/) { + $key_type = 'S/MIME'; + } + + if ($key_type eq 'PGP') { + ################## + # PGP Encryption # + ################## + + my $tct = Bugzilla::Extension::SecureMail::TCT->new( + public_key => $key, + command => Bugzilla->localconfig->{tct_bin}, + ); + + if (scalar $email->parts > 1) { + my $old_boundary = $email->{ct}{attributes}{boundary}; + my $to_encrypt = "Content-Type: " . $email->content_type . "\n\n"; + + # We need to do some fix up of each part for proper encoding and then + # stringify all parts for encrypting. We have to retain the old + # boundaries as well so that the email client can reconstruct the + # original message properly. + $email->walk_parts(\&_fix_encoding); + + $email->walk_parts(sub { + my ($part) = @_; + if ($sanitise_subject) { + _insert_subject($part, $subject); + } + return if $part->parts > 1; # Top-level + $to_encrypt .= "--$old_boundary\n" . $part->as_string . "\n"; + }); + $to_encrypt .= "--$old_boundary--"; + + # Now create the new properly formatted PGP parts containing the + # encrypted original message + my @new_parts = ( + Email::MIME->create( + attributes => + {content_type => 'application/pgp-encrypted', encoding => '7bit',}, + body => "Version: 1\n", + ), + Email::MIME->create( + attributes => { + content_type => 'application/octet-stream', + filename => 'encrypted.asc', + disposition => 'inline', + encoding => '7bit', + }, + body => _tct_encrypt($tct, $to_encrypt, $bug_id) + ), + ); + $email->parts_set(\@new_parts); + my $new_boundary = $email->{ct}{attributes}{boundary}; + + # Redo the old content type header with the new boundaries + # and other information needed for PGP + $email->header_set("Content-Type", + "multipart/encrypted; " + . "protocol=\"application/pgp-encrypted\"; " + . "boundary=\"$new_boundary\""); } - elsif ($key && $key =~ /BEGIN CERTIFICATE/) { - $key_type = 'S/MIME'; + else { + _fix_encoding($email); + if ($sanitise_subject) { + _insert_subject($email, $subject); + } + $email->body_set(_tct_encrypt($tct, $email->body, $bug_id)); } + } - if ($key_type eq 'PGP') { - ################## - # PGP Encryption # - ################## + elsif ($key_type eq 'S/MIME') { + ##################### + # S/MIME Encryption # + ##################### - my $tct = Bugzilla::Extension::SecureMail::TCT->new( - public_key => $key, - command => Bugzilla->localconfig->{tct_bin}, - ); + $email->walk_parts(\&_fix_encoding); - if (scalar $email->parts > 1) { - my $old_boundary = $email->{ct}{attributes}{boundary}; - my $to_encrypt = "Content-Type: " . $email->content_type . "\n\n"; - - # We need to do some fix up of each part for proper encoding and then - # stringify all parts for encrypting. We have to retain the old - # boundaries as well so that the email client can reconstruct the - # original message properly. - $email->walk_parts(\&_fix_encoding); - - $email->walk_parts(sub { - my ($part) = @_; - if ($sanitise_subject) { - _insert_subject($part, $subject); - } - return if $part->parts > 1; # Top-level - $to_encrypt .= "--$old_boundary\n" . $part->as_string . "\n"; - }); - $to_encrypt .= "--$old_boundary--"; - - # Now create the new properly formatted PGP parts containing the - # encrypted original message - my @new_parts = ( - Email::MIME->create( - attributes => { - content_type => 'application/pgp-encrypted', - encoding => '7bit', - }, - body => "Version: 1\n", - ), - Email::MIME->create( - attributes => { - content_type => 'application/octet-stream', - filename => 'encrypted.asc', - disposition => 'inline', - encoding => '7bit', - }, - body => _tct_encrypt($tct, $to_encrypt, $bug_id) - ), - ); - $email->parts_set(\@new_parts); - my $new_boundary = $email->{ct}{attributes}{boundary}; - # Redo the old content type header with the new boundaries - # and other information needed for PGP - $email->header_set("Content-Type", - "multipart/encrypted; " . - "protocol=\"application/pgp-encrypted\"; " . - "boundary=\"$new_boundary\""); - } - else { - _fix_encoding($email); - if ($sanitise_subject) { - _insert_subject($email, $subject); - } - $email->body_set(_tct_encrypt($tct, $email->body, $bug_id)); - } + if ($sanitise_subject) { + $email->walk_parts(sub { _insert_subject($_[0], $subject) }); } - elsif ($key_type eq 'S/MIME') { - ##################### - # S/MIME Encryption # - ##################### + my $smime = Crypt::SMIME->new(); + my $encrypted; - $email->walk_parts(\&_fix_encoding); + eval { + $smime->setPublicKey([$key]); + $encrypted = $smime->encrypt($email->as_string()); + }; - if ($sanitise_subject) { - $email->walk_parts(sub { _insert_subject($_[0], $subject) }); - } + if (!$@) { - my $smime = Crypt::SMIME->new(); - my $encrypted; - - eval { - $smime->setPublicKey([$key]); - $encrypted = $smime->encrypt($email->as_string()); - }; - - if (!$@) { - # We can't replace the Email::MIME object, so we have to swap - # out its component parts. - my $enc_obj = new Email::MIME($encrypted); - $email->header_obj_set($enc_obj->header_obj()); - $email->parts_set([]); - $email->body_set($enc_obj->body()); - $email->content_type_set('application/pkcs7-mime'); - $email->charset_set('UTF-8') if Bugzilla->params->{'utf8'}; - } - else { - $email->body_set('Error during Encryption: ' . $@); - } + # We can't replace the Email::MIME object, so we have to swap + # out its component parts. + my $enc_obj = new Email::MIME($encrypted); + $email->header_obj_set($enc_obj->header_obj()); + $email->parts_set([]); + $email->body_set($enc_obj->body()); + $email->content_type_set('application/pkcs7-mime'); + $email->charset_set('UTF-8') if Bugzilla->params->{'utf8'}; } else { - # No encryption key provided; send a generic, safe email. - my $template = Bugzilla->template; - my $message; - my $email_type = $email->header('X-Bugzilla-Type'); - my $vars = { - 'urlbase' => Bugzilla->localconfig->{urlbase}, - 'bug_id' => $bug_id, - 'maintainer' => Bugzilla->params->{'maintainer'}, - 'email_type' => $email_type - }; - - $template->process('account/email/encryption-required.txt.tmpl', - $vars, \$message) - || ThrowTemplateError($template->error()); - - $email->parts_set([]); - $email->content_type_set('text/plain'); - $email->body_set($message); + $email->body_set('Error during Encryption: ' . $@); } + } + else { + # No encryption key provided; send a generic, safe email. + my $template = Bugzilla->template; + my $message; + my $email_type = $email->header('X-Bugzilla-Type'); + my $vars = { + 'urlbase' => Bugzilla->localconfig->{urlbase}, + 'bug_id' => $bug_id, + 'maintainer' => Bugzilla->params->{'maintainer'}, + 'email_type' => $email_type + }; - if ($sanitise_subject) { - # This is designed to still work if the admin changes the word - # 'bug' to something else. However, it could break if they change - # the format of the subject line in another way. - my $new = $add_new ? ' New:' : ''; - my $product = $email->header('X-Bugzilla-Product'); - my $component = $email->header('X-Bugzilla-Component'); - # Note: the $bug_id is required within the parentheses in order to keep - # gmail's threading algorithm happy. - $subject =~ s/($bug_id\])\s+(.*)$/$1$new (Secure bug $bug_id in $product :: $component)/; - { - # avoid excessive line wrapping done by Encode. - local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998; - $email->header_set('Subject', encode('MIME-Q', $subject)); - } + $template->process('account/email/encryption-required.txt.tmpl', + $vars, \$message) + || ThrowTemplateError($template->error()); + + $email->parts_set([]); + $email->content_type_set('text/plain'); + $email->body_set($message); + } + + if ($sanitise_subject) { + + # This is designed to still work if the admin changes the word + # 'bug' to something else. However, it could break if they change + # the format of the subject line in another way. + my $new = $add_new ? ' New:' : ''; + my $product = $email->header('X-Bugzilla-Product'); + my $component = $email->header('X-Bugzilla-Component'); + + # Note: the $bug_id is required within the parentheses in order to keep + # gmail's threading algorithm happy. + $subject + =~ s/($bug_id\])\s+(.*)$/$1$new (Secure bug $bug_id in $product :: $component)/; + { + # avoid excessive line wrapping done by Encode. + local $Encode::Encoding{'MIME-Q'}->{'bpl'} = 998; + $email->header_set('Subject', encode('MIME-Q', $subject)); } + } } sub _tct_encrypt { - my ($tct, $text, $bug_id) = @_; - - my $comment = Bugzilla->localconfig->{urlbase} . ( $bug_id ? 'show_bug.cgi?id=' . $bug_id : '' ); - my $encrypted; - my $ok = eval { $encrypted = $tct->encrypt( $text, $comment )->get; 1 }; - if (!$ok) { - WARN("Error: $@"); - $encrypted = "$comment\nOpenPGP Encryption failed. Check if your key is expired."; - } - elsif (!$encrypted) { - WARN('message empty!'); - $encrypted = "$comment\nOpenPGP Encryption failed for unknown reason."; - } - - return $encrypted; + my ($tct, $text, $bug_id) = @_; + + my $comment = Bugzilla->localconfig->{urlbase} + . ($bug_id ? 'show_bug.cgi?id=' . $bug_id : ''); + my $encrypted; + my $ok = eval { $encrypted = $tct->encrypt($text, $comment)->get; 1 }; + if (!$ok) { + WARN("Error: $@"); + $encrypted + = "$comment\nOpenPGP Encryption failed. Check if your key is expired."; + } + elsif (!$encrypted) { + WARN('message empty!'); + $encrypted = "$comment\nOpenPGP Encryption failed for unknown reason."; + } + + return $encrypted; } # Insert the subject into the part's body, as the subject of the message will @@ -634,69 +650,67 @@ sub _tct_encrypt { # XXX this incorrectly assumes all parts of the message are the body # we should only alter parts whose parent is multipart/alternative sub _insert_subject { - my ($part, $subject) = @_; - my $content_type = $part->content_type or return; - if ($content_type =~ /^text\/plain/) { - $part->body_str_set("Subject: $subject\015\012\015\012" . $part->body_str); - } - elsif ($content_type =~ /^text\/html/) { - my $tree = HTML::Tree->new->parse_content($part->body_str); - my $body = $tree->look_down(qw(_tag body)); - $body->unshift_content(['strong', "Subject: $subject"], ['br']); - $part->body_str_set($tree->as_HTML); - $tree->delete; - } + my ($part, $subject) = @_; + my $content_type = $part->content_type or return; + if ($content_type =~ /^text\/plain/) { + $part->body_str_set("Subject: $subject\015\012\015\012" . $part->body_str); + } + elsif ($content_type =~ /^text\/html/) { + my $tree = HTML::Tree->new->parse_content($part->body_str); + my $body = $tree->look_down(qw(_tag body)); + $body->unshift_content(['strong', "Subject: $subject"], ['br']); + $part->body_str_set($tree->as_HTML); + $tree->delete; + } } sub _fix_encoding { - my $part = shift; - - # don't touch the top-level part of multi-part mail - return if $part->parts > 1; - - # nothing to do if the part already has a charset - my $ct = parse_content_type($part->content_type); - my $charset = $ct->{attributes}{charset} - ? $ct->{attributes}{charset} - : ''; - return unless !$charset || $charset eq 'us-ascii'; - - if (Bugzilla->params->{utf8}) { - $part->charset_set('UTF-8'); - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } + my $part = shift; + + # don't touch the top-level part of multi-part mail + return if $part->parts > 1; + + # nothing to do if the part already has a charset + my $ct = parse_content_type($part->content_type); + my $charset = $ct->{attributes}{charset} ? $ct->{attributes}{charset} : ''; + return unless !$charset || $charset eq 'us-ascii'; + + if (Bugzilla->params->{utf8}) { + $part->charset_set('UTF-8'); + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); } - $part->encoding_set('quoted-printable'); + } + $part->encoding_set('quoted-printable'); } sub _filter_bug_links { - my ($email) = @_; - $email->walk_parts(sub { - my $part = shift; - _fix_encoding($part); - my $content_type = $part->content_type; - return if !$content_type || $content_type !~ /text\/html/; - my $tree = HTML::Tree->new->parse_content($part->body_str); - my @links = $tree->look_down( _tag => q{a}, class => qr/bz_bug_link/ ); - my $updated = 0; - foreach my $link (@links) { - my $href = $link->attr('href'); - my ($bug_id) = $href =~ /\Qshow_bug.cgi?id=\E(\d+)/; - my $bug = new Bugzilla::Bug($bug_id); - if ($bug && _should_secure_bug($bug)) { - $link->attr('title', '(secure bug)'); - $link->attr('class', 'bz_bug_link'); - $updated = 1; - } - } - if ($updated) { - $part->body_str_set($tree->as_HTML); - } - $tree->delete; - }); + my ($email) = @_; + $email->walk_parts(sub { + my $part = shift; + _fix_encoding($part); + my $content_type = $part->content_type; + return if !$content_type || $content_type !~ /text\/html/; + my $tree = HTML::Tree->new->parse_content($part->body_str); + my @links = $tree->look_down(_tag => q{a}, class => qr/bz_bug_link/); + my $updated = 0; + foreach my $link (@links) { + my $href = $link->attr('href'); + my ($bug_id) = $href =~ /\Qshow_bug.cgi?id=\E(\d+)/; + my $bug = new Bugzilla::Bug($bug_id); + if ($bug && _should_secure_bug($bug)) { + $link->attr('title', '(secure bug)'); + $link->attr('class', 'bz_bug_link'); + $updated = 1; + } + } + if ($updated) { + $part->body_str_set($tree->as_HTML); + } + $tree->delete; + }); } __PACKAGE__->NAME; diff --git a/extensions/SecureMail/lib/TCT.pm b/extensions/SecureMail/lib/TCT.pm index 3a16309c2..f3de8ca39 100644 --- a/extensions/SecureMail/lib/TCT.pm +++ b/extensions/SecureMail/lib/TCT.pm @@ -15,76 +15,64 @@ use Future::Utils qw(call); use Future; use IO::Async::Process; -has 'public_key' => ( is => 'ro', required => 1 ); -has 'public_key_file' => ( is => 'lazy' ); -has 'is_valid' => ( is => 'lazy' ); -has 'command' => ( is => 'ro', default => 'tct' ); +has 'public_key' => (is => 'ro', required => 1); +has 'public_key_file' => (is => 'lazy'); +has 'is_valid' => (is => 'lazy'); +has 'command' => (is => 'ro', default => 'tct'); sub _build_public_key_file { - my ($self) = @_; - my $fh = File::Temp->new(SUFFIX => '.pubkey'); - $fh->print($self->public_key); - $fh->close; - return $fh; + my ($self) = @_; + my $fh = File::Temp->new(SUFFIX => '.pubkey'); + $fh->print($self->public_key); + $fh->close; + return $fh; } sub _build_is_valid { - my ($self) = @_; - - my $loop = IO::Async::Loop->new; - my $exit_f = $loop->new_future; - my ($stderr, $stdout); - my $process = IO::Async::Process->new( - command => [$self->command, 'check', '-k', $self->public_key_file ], - stderr => { - into => \$stderr, - }, - stdout => { - into => \$stdout, - }, - on_finish => on_finish($exit_f), - on_exception => on_exception($self->command, $exit_f), - ); - $loop->add($process); - - return $exit_f->then( - sub { - my ($rv) = @_; - Future->wrap($rv == 0); - } - ); + my ($self) = @_; + + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my ($stderr, $stdout); + my $process = IO::Async::Process->new( + command => [$self->command, 'check', '-k', $self->public_key_file], + stderr => {into => \$stderr,}, + stdout => {into => \$stdout,}, + on_finish => on_finish($exit_f), + on_exception => on_exception($self->command, $exit_f), + ); + $loop->add($process); + + return $exit_f->then(sub { + my ($rv) = @_; + Future->wrap($rv == 0); + }); } sub encrypt { - my ($self, $input, $comment) = @_; - $self->is_valid->then( - sub { - my ($is_valid) = @_; - call { - die 'invalid public key!' unless $is_valid; - - my $output; - my $loop = IO::Async::Loop->new; - my $exit_f = $loop->new_future; - my @command = ( $self->command, 'encrypt', '-k', $self->public_key_file ); - push @command, '--comment', $comment if $comment; - my $process = IO::Async::Process->new( - command => \@command, - stdin => { - from => $input, - }, - stdout => { - into => \$output, - }, - on_finish => on_finish($exit_f), - on_exception => on_exception($self->command, $exit_f), - ); - $loop->add($process); - - return $exit_f->then(sub { Future->wrap($output) }); - } - } - ); + my ($self, $input, $comment) = @_; + $self->is_valid->then(sub { + my ($is_valid) = @_; + call { + die 'invalid public key!' unless $is_valid; + + my $output; + my $loop = IO::Async::Loop->new; + my $exit_f = $loop->new_future; + my @command = ($self->command, 'encrypt', '-k', $self->public_key_file); + push @command, '--comment', $comment if $comment; + my $process = IO::Async::Process->new( + command => \@command, + stdin => {from => $input,}, + stdout => {into => \$output,}, + on_finish => on_finish($exit_f), + on_exception => on_exception($self->command, $exit_f), + ); + $loop->add($process); + + return $exit_f->then(sub { Future->wrap($output) }); + } + }); } 1; diff --git a/extensions/ShadowBugs/Config.pm b/extensions/ShadowBugs/Config.pm index 6999edaf3..a45948dd4 100644 --- a/extensions/ShadowBugs/Config.pm +++ b/extensions/ShadowBugs/Config.pm @@ -8,7 +8,7 @@ package Bugzilla::Extension::ShadowBugs; use strict; -use constant NAME => 'ShadowBugs'; +use constant NAME => 'ShadowBugs'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/ShadowBugs/Extension.pm b/extensions/ShadowBugs/Extension.pm index a9a1e0861..a1eb4a8c1 100644 --- a/extensions/ShadowBugs/Extension.pm +++ b/extensions/ShadowBugs/Extension.pm @@ -19,81 +19,83 @@ use Bugzilla::User; our $VERSION = '1'; BEGIN { - *Bugzilla::is_cf_shadow_bug_hidden = \&_is_cf_shadow_bug_hidden; - *Bugzilla::Bug::cf_shadow_bug_obj = \&_cf_shadow_bug_obj; + *Bugzilla::is_cf_shadow_bug_hidden = \&_is_cf_shadow_bug_hidden; + *Bugzilla::Bug::cf_shadow_bug_obj = \&_cf_shadow_bug_obj; } # Determine if the shadow-bug / shadowed-by fields are visibile on the # specified bug. sub _is_cf_shadow_bug_hidden { - my ($self, $bug) = @_; + my ($self, $bug) = @_; - # completely hide unless you're a member of the right group - return 1 unless Bugzilla->user->in_group('can_shadow_bugs'); + # completely hide unless you're a member of the right group + return 1 unless Bugzilla->user->in_group('can_shadow_bugs'); - my $is_public = Bugzilla::User->new()->can_see_bug($bug->id); - if ($is_public) { - # hide on public bugs, unless it's shadowed - my $related = $bug->related_bugs(Bugzilla->process_cache->{shadow_bug_field}); - return 1 if !@$related; - } + my $is_public = Bugzilla::User->new()->can_see_bug($bug->id); + if ($is_public) { + + # hide on public bugs, unless it's shadowed + my $related = $bug->related_bugs(Bugzilla->process_cache->{shadow_bug_field}); + return 1 if !@$related; + } } sub _cf_shadow_bug_obj { - my ($self) = @_; - return unless $self->cf_shadow_bug; - return $self->{cf_shadow_bug_obj} ||= Bugzilla::Bug->new($self->cf_shadow_bug); + my ($self) = @_; + return unless $self->cf_shadow_bug; + return $self->{cf_shadow_bug_obj} ||= Bugzilla::Bug->new($self->cf_shadow_bug); } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - Bugzilla->process_cache->{shadow_bug_field} ||= Bugzilla::Field->new({ name => 'cf_shadow_bug' }); - - return unless Bugzilla->user->in_group('can_shadow_bugs'); - return unless - $file eq 'bug/edit.html.tmpl' - || $file eq 'bug/show.html.tmpl' - || $file eq 'bug/show-header.html.tmpl'; - my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; - return unless $bug && $bug->cf_shadow_bug; - $vars->{is_shadow_bug} = 1; - - if ($file eq 'bug/edit.html.tmpl') { - # load comments from other bug - $vars->{shadow_comments} = $bug->cf_shadow_bug_obj->comments; - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + Bugzilla->process_cache->{shadow_bug_field} + ||= Bugzilla::Field->new({name => 'cf_shadow_bug'}); + + return unless Bugzilla->user->in_group('can_shadow_bugs'); + return + unless $file eq 'bug/edit.html.tmpl' + || $file eq 'bug/show.html.tmpl' + || $file eq 'bug/show-header.html.tmpl'; + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + return unless $bug && $bug->cf_shadow_bug; + $vars->{is_shadow_bug} = 1; + + if ($file eq 'bug/edit.html.tmpl') { + + # load comments from other bug + $vars->{shadow_comments} = $bug->cf_shadow_bug_obj->comments; + } } sub bug_end_of_update { - my ($self, $args) = @_; - - # don't allow shadowing non-public bugs - if (exists $args->{changes}->{cf_shadow_bug}) { - my ($old_id, $new_id) = @{ $args->{changes}->{cf_shadow_bug} }; - if ($new_id) { - if (!Bugzilla::User->new()->can_see_bug($new_id)) { - ThrowUserError('illegal_shadow_bug_public', { id => $new_id }); - } - } + my ($self, $args) = @_; + + # don't allow shadowing non-public bugs + if (exists $args->{changes}->{cf_shadow_bug}) { + my ($old_id, $new_id) = @{$args->{changes}->{cf_shadow_bug}}; + if ($new_id) { + if (!Bugzilla::User->new()->can_see_bug($new_id)) { + ThrowUserError('illegal_shadow_bug_public', {id => $new_id}); + } } + } + + # if a shadow bug is made public, clear the shadow_bug field + if (exists $args->{changes}->{bug_group}) { + my $bug = $args->{bug}; + return unless my $shadow_id = $bug->cf_shadow_bug; + my $is_public = Bugzilla::User->new()->can_see_bug($bug->id); + if ($is_public) { + Bugzilla->dbh->do("UPDATE bugs SET cf_shadow_bug=NULL WHERE bug_id=?", + undef, $bug->id); + LogActivityEntry($bug->id, 'cf_shadow_bug', $shadow_id, '', Bugzilla->user->id, + $args->{timestamp}); - # if a shadow bug is made public, clear the shadow_bug field - if (exists $args->{changes}->{bug_group}) { - my $bug = $args->{bug}; - return unless my $shadow_id = $bug->cf_shadow_bug; - my $is_public = Bugzilla::User->new()->can_see_bug($bug->id); - if ($is_public) { - Bugzilla->dbh->do( - "UPDATE bugs SET cf_shadow_bug=NULL WHERE bug_id=?", - undef, $bug->id); - LogActivityEntry($bug->id, 'cf_shadow_bug', $shadow_id, '', - Bugzilla->user->id, $args->{timestamp}); - - } } + } } __PACKAGE__->NAME; diff --git a/extensions/SiteMapIndex/Config.pm b/extensions/SiteMapIndex/Config.pm index 980938503..3ff922167 100644 --- a/extensions/SiteMapIndex/Config.pm +++ b/extensions/SiteMapIndex/Config.pm @@ -28,12 +28,8 @@ use warnings; use constant NAME => 'SiteMapIndex'; -use constant REQUIRED_MODULES => [ - { - package => 'IO-Compress-Gzip', - module => 'IO::Compress::Gzip', - version => 0, - } -]; +use constant REQUIRED_MODULES => + [{package => 'IO-Compress-Gzip', module => 'IO::Compress::Gzip', version => 0, + }]; __PACKAGE__->NAME; diff --git a/extensions/SiteMapIndex/Extension.pm b/extensions/SiteMapIndex/Extension.pm index 3d4342e0e..c606702ae 100644 --- a/extensions/SiteMapIndex/Extension.pm +++ b/extensions/SiteMapIndex/Extension.pm @@ -46,32 +46,32 @@ use POSIX; ######### sub template_before_process { - my ($self, $args) = @_; - my ($vars, $file) = @$args{qw(vars file)}; - - return if $file ne 'global/header.html.tmpl'; - return unless (exists $vars->{bug} || exists $vars->{bugs}); - my $bugs = exists $vars->{bugs} ? $vars->{bugs} : [$vars->{bug}]; - return if ref $bugs ne 'ARRAY'; - - foreach my $bug (@$bugs) { - if (!bug_is_ok_to_index($bug)) { - $vars->{sitemap_noindex} = 1; - last; - } + my ($self, $args) = @_; + my ($vars, $file) = @$args{qw(vars file)}; + + return if $file ne 'global/header.html.tmpl'; + return unless (exists $vars->{bug} || exists $vars->{bugs}); + my $bugs = exists $vars->{bugs} ? $vars->{bugs} : [$vars->{bug}]; + return if ref $bugs ne 'ARRAY'; + + foreach my $bug (@$bugs) { + if (!bug_is_ok_to_index($bug)) { + $vars->{sitemap_noindex} = 1; + last; } + } } sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{page_id}; - - if ($page =~ m{^sitemap/sitemap\.}) { - my $map = generate_sitemap(__PACKAGE__->NAME); - print Bugzilla->cgi->header('text/xml'); - print $map; - exit; - } + my ($self, $args) = @_; + my $page = $args->{page_id}; + + if ($page =~ m{^sitemap/sitemap\.}) { + my $map = generate_sitemap(__PACKAGE__->NAME); + print Bugzilla->cgi->header('text/xml'); + print $map; + exit; + } } ################ @@ -79,54 +79,54 @@ sub page_before_template { ################ sub install_before_final_checks { - my ($self) = @_; - if (!Bugzilla->localconfig->{urlbase}) { - print STDERR get_text('sitemap_no_urlbase'), "\n"; - return; - } - if (Bugzilla->params->{'requirelogin'}) { - print STDERR get_text('sitemap_requirelogin'), "\n"; - return; - } - - return if (Bugzilla->localconfig->{urlbase} ne 'https://bugzilla.mozilla.org/'); + my ($self) = @_; + if (!Bugzilla->localconfig->{urlbase}) { + print STDERR get_text('sitemap_no_urlbase'), "\n"; + return; + } + if (Bugzilla->params->{'requirelogin'}) { + print STDERR get_text('sitemap_requirelogin'), "\n"; + return; + } + + return if (Bugzilla->localconfig->{urlbase} ne 'https://bugzilla.mozilla.org/'); } sub install_filesystem { - my ($self, $args) = @_; - my $create_dirs = $args->{'create_dirs'}; - my $recurse_dirs = $args->{'recurse_dirs'}; - my $htaccess = $args->{'htaccess'}; - - # Create the sitemap directory to store the index and sitemap files - my $sitemap_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; - - $create_dirs->{$sitemap_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE - | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE; - - $recurse_dirs->{$sitemap_path} = { - files => Bugzilla::Install::Filesystem::CGI_WRITE - | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE, - dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE - | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE - }; - - # Create a htaccess file that allows the sitemap files to be served out - $htaccess->{"$sitemap_path/.htaccess"} = { - perms => Bugzilla::Install::Filesystem::WS_SERVE, - contents => <{'create_dirs'}; + my $recurse_dirs = $args->{'recurse_dirs'}; + my $htaccess = $args->{'htaccess'}; + + # Create the sitemap directory to store the index and sitemap files + my $sitemap_path = bz_locations->{'datadir'} . "/" . __PACKAGE__->NAME; + + $create_dirs->{$sitemap_path} = Bugzilla::Install::Filesystem::DIR_CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE; + + $recurse_dirs->{$sitemap_path} = { + files => Bugzilla::Install::Filesystem::CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE, + dirs => Bugzilla::Install::Filesystem::DIR_CGI_WRITE + | Bugzilla::Install::Filesystem::DIR_ALSO_WS_SERVE + }; + + # Create a htaccess file that allows the sitemap files to be served out + $htaccess->{"$sitemap_path/.htaccess"} = { + perms => Bugzilla::Install::Filesystem::WS_SERVE, + contents => < Allow from all Deny from all EOT - }; + }; } sub before_robots_txt { - my ($self, $args) = @_; - $args->{vars}{SITEMAP_URL} = Bugzilla->localconfig->{urlbase} . SITEMAP_URL; + my ($self, $args) = @_; + $args->{vars}{SITEMAP_URL} = Bugzilla->localconfig->{urlbase} . SITEMAP_URL; } __PACKAGE__->NAME; diff --git a/extensions/SiteMapIndex/lib/Constants.pm b/extensions/SiteMapIndex/lib/Constants.pm index 4f404c8b1..bd098d16a 100644 --- a/extensions/SiteMapIndex/lib/Constants.pm +++ b/extensions/SiteMapIndex/lib/Constants.pm @@ -27,10 +27,10 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - SITEMAP_AGE - SITEMAP_MAX - SITEMAP_DELAY - SITEMAP_URL + SITEMAP_AGE + SITEMAP_MAX + SITEMAP_DELAY + SITEMAP_URL ); # This is the amount of hours a sitemap index and it's files are considered diff --git a/extensions/SiteMapIndex/lib/Util.pm b/extensions/SiteMapIndex/lib/Util.pm index 4519461b4..fb945e324 100644 --- a/extensions/SiteMapIndex/lib/Util.pm +++ b/extensions/SiteMapIndex/lib/Util.pm @@ -28,8 +28,8 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - generate_sitemap - bug_is_ok_to_index + generate_sitemap + bug_is_ok_to_index ); use Bugzilla::Extension::SiteMapIndex::Constants; @@ -41,169 +41,176 @@ use Scalar::Util qw(blessed); use IO::Compress::Gzip qw(gzip $GzipError); sub too_young_date { - my $hours_ago = DateTime->now(time_zone => Bugzilla->local_timezone); - $hours_ago->subtract(hours => SITEMAP_DELAY); - return $hours_ago; + my $hours_ago = DateTime->now(time_zone => Bugzilla->local_timezone); + $hours_ago->subtract(hours => SITEMAP_DELAY); + return $hours_ago; } sub bug_is_ok_to_index { - my ($bug) = @_; - return 1 unless blessed($bug) && $bug->isa('Bugzilla::Bug') && !$bug->{error}; - my $creation_ts = datetime_from($bug->creation_ts); - return ($creation_ts && $creation_ts lt too_young_date()) ? 1 : 0; + my ($bug) = @_; + return 1 unless blessed($bug) && $bug->isa('Bugzilla::Bug') && !$bug->{error}; + my $creation_ts = datetime_from($bug->creation_ts); + return ($creation_ts && $creation_ts lt too_young_date()) ? 1 : 0; } # We put two things in the Sitemap: a list of Browse links for products, # and links to bugs. sub generate_sitemap { - my ($extension_name) = @_; - - # If file is less than SITEMAP_AGE hours old, then read in and send to caller. - # If greater, then regenerate and send the new version. - my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; - if (-e $index_file) { - my $index_mtime = (stat($index_file))[9]; - my $index_hours = sprintf("%d", (time() - $index_mtime) / 60 / 60); # in hours - if ($index_hours < SITEMAP_AGE) { - my $index_fh = new IO::File($index_file, 'r'); - $index_fh || die "Could not open current sitemap index: $!"; - my $index_xml; - { local $/; $index_xml = <$index_fh> } - $index_fh->close() || die "Could not close current sitemap index: $!"; - - return $index_xml; - } + my ($extension_name) = @_; + + # If file is less than SITEMAP_AGE hours old, then read in and send to caller. + # If greater, then regenerate and send the new version. + my $index_file + = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; + if (-e $index_file) { + my $index_mtime = (stat($index_file))[9]; + my $index_hours = sprintf("%d", (time() - $index_mtime) / 60 / 60); # in hours + if ($index_hours < SITEMAP_AGE) { + my $index_fh = new IO::File($index_file, 'r'); + $index_fh || die "Could not open current sitemap index: $!"; + my $index_xml; + { local $/; $index_xml = <$index_fh> } + $index_fh->close() || die "Could not close current sitemap index: $!"; + + return $index_xml; } - - # Set the atime and mtime of the index file to the current time - # in case another request is made before we finish. - utime(undef, undef, $index_file); - - # Sitemaps must never contain private data. - Bugzilla->logout_request(); - my $user = Bugzilla->user; - my $products = $user->get_accessible_products; - - my $num_bugs = SITEMAP_MAX - scalar(@$products); - # We do this date math outside of the database because databases - # usually do better with a straight comparison value. - my $hours_ago = too_young_date(); - - # We don't use Bugzilla::Bug objects, because this could be a tremendous - # amount of data, and we only want a little. Also, we only display - # bugs that are not in any group. We show the last $num_bugs - # most-recently-updated bugs. - my $dbh = Bugzilla->dbh; - my $bug_sth = $dbh->prepare( - 'SELECT bugs.bug_id, bugs.delta_ts + } + + # Set the atime and mtime of the index file to the current time + # in case another request is made before we finish. + utime(undef, undef, $index_file); + + # Sitemaps must never contain private data. + Bugzilla->logout_request(); + my $user = Bugzilla->user; + my $products = $user->get_accessible_products; + + my $num_bugs = SITEMAP_MAX - scalar(@$products); + + # We do this date math outside of the database because databases + # usually do better with a straight comparison value. + my $hours_ago = too_young_date(); + + # We don't use Bugzilla::Bug objects, because this could be a tremendous + # amount of data, and we only want a little. Also, we only display + # bugs that are not in any group. We show the last $num_bugs + # most-recently-updated bugs. + my $dbh = Bugzilla->dbh; + my $bug_sth = $dbh->prepare( + 'SELECT bugs.bug_id, bugs.delta_ts FROM bugs LEFT JOIN bug_group_map ON bugs.bug_id = bug_group_map.bug_id WHERE bug_group_map.bug_id IS NULL AND creation_ts < ? - ' . $dbh->sql_limit($num_bugs, '?')); + ' . $dbh->sql_limit($num_bugs, '?') + ); - my $filecount = 1; - my $filelist = []; - my $offset = 0; + my $filecount = 1; + my $filelist = []; + my $offset = 0; - while (1) { - my $bugs = []; + while (1) { + my $bugs = []; - $bug_sth->execute($hours_ago, $offset); + $bug_sth->execute($hours_ago, $offset); - while (my ($bug_id, $delta_ts) = $bug_sth->fetchrow_array()) { - push(@$bugs, { bug_id => $bug_id, delta_ts => $delta_ts }); - } + while (my ($bug_id, $delta_ts) = $bug_sth->fetchrow_array()) { + push(@$bugs, {bug_id => $bug_id, delta_ts => $delta_ts}); + } - last if !@$bugs; + last if !@$bugs; - # We only need the product links in the first sitemap file - $products = [] if $filecount > 1; + # We only need the product links in the first sitemap file + $products = [] if $filecount > 1; - push(@$filelist, _generate_sitemap_file($extension_name, $filecount, $products, $bugs)); + push(@$filelist, + _generate_sitemap_file($extension_name, $filecount, $products, $bugs)); - $filecount++; - $offset += $num_bugs; - } + $filecount++; + $offset += $num_bugs; + } - # Generate index file - return _generate_sitemap_index($extension_name, $filelist); + # Generate index file + return _generate_sitemap_index($extension_name, $filelist); } sub _generate_sitemap_index { - my ($extension_name, $filelist) = @_; + my ($extension_name, $filelist) = @_; - my $dbh = Bugzilla->dbh; - my $timestamp = $dbh->selectrow_array( - "SELECT " . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); + my $dbh = Bugzilla->dbh; + my $timestamp = $dbh->selectrow_array( + "SELECT " . $dbh->sql_date_format('NOW()', '%Y-%m-%d')); - my $index_xml = < END - foreach my $filename (@$filelist) { - $index_xml .= " + foreach my $filename (@$filelist) { + $index_xml .= " - " . Bugzilla->localconfig->{urlbase} . "data/$extension_name/$filename + " + . Bugzilla->localconfig->{urlbase} . "data/$extension_name/$filename $timestamp "; - } + } - $index_xml .= < END - my $index_file = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; - my $index_fh = new IO::File($index_file, 'w'); - $index_fh || die "Could not open new sitemap index: $!"; - print $index_fh $index_xml; - $index_fh->close() || die "Could not close new sitemap index: $!"; + my $index_file + = bz_locations->{'datadir'} . "/$extension_name/sitemap_index.xml"; + my $index_fh = new IO::File($index_file, 'w'); + $index_fh || die "Could not open new sitemap index: $!"; + print $index_fh $index_xml; + $index_fh->close() || die "Could not close new sitemap index: $!"; - return $index_xml; + return $index_xml; } sub _generate_sitemap_file { - my ($extension_name, $filecount, $products, $bugs) = @_; + my ($extension_name, $filecount, $products, $bugs) = @_; - my $bug_url = Bugzilla->localconfig->{urlbase} . 'show_bug.cgi?id='; - my $product_url = Bugzilla->localconfig->{urlbase} . 'describecomponents.cgi?product='; + my $bug_url = Bugzilla->localconfig->{urlbase} . 'show_bug.cgi?id='; + my $product_url + = Bugzilla->localconfig->{urlbase} . 'describecomponents.cgi?product='; - my $sitemap_xml = < END - foreach my $product (@$products) { - $sitemap_xml .= " + foreach my $product (@$products) { + $sitemap_xml .= " " . $product_url . url_quote($product->name) . " daily 0.4 "; - } + } - foreach my $bug (@$bugs) { - $sitemap_xml .= " + foreach my $bug (@$bugs) { + $sitemap_xml .= " " . $bug_url . $bug->{bug_id} . " " . datetime_from($bug->{delta_ts}, 'UTC')->iso8601 . 'Z' . " "; - } + } - $sitemap_xml .= < END - # Write the compressed sitemap data to a file in the cgi root so that they can - # be accessed by the search engines. - my $filename = "sitemap$filecount.xml.gz"; - gzip \$sitemap_xml => bz_locations->{'datadir'} . "/$extension_name/$filename" - || die "gzip failed: $GzipError\n"; + # Write the compressed sitemap data to a file in the cgi root so that they can + # be accessed by the search engines. + my $filename = "sitemap$filecount.xml.gz"; + gzip \$sitemap_xml => bz_locations->{'datadir'} . "/$extension_name/$filename" + || die "gzip failed: $GzipError\n"; - return $filename; + return $filename; } 1; diff --git a/extensions/Splinter/Extension.pm b/extensions/Splinter/Extension.pm index eb2006f47..f5a2f41cb 100644 --- a/extensions/Splinter/Extension.pm +++ b/extensions/Splinter/Extension.pm @@ -28,135 +28,140 @@ use Bugzilla::Extension::Splinter::Util; our $VERSION = '0.1'; BEGIN { - *Bugzilla::splinter_review_base = \&get_review_base; - *Bugzilla::splinter_review_url = \&_get_review_url; + *Bugzilla::splinter_review_base = \&get_review_base; + *Bugzilla::splinter_review_url = \&_get_review_url; } sub _get_review_url { - my ($class, $bug_id, $attach_id) = @_; - return get_review_url(Bugzilla::Bug->check({ id => $bug_id, cache => 1 }), $attach_id); + my ($class, $bug_id, $attach_id) = @_; + return get_review_url(Bugzilla::Bug->check({id => $bug_id, cache => 1}), + $attach_id); } sub page_before_template { - my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; - - if ($page eq 'splinter.html') { - my $user = Bugzilla->user; - - # We can either provide just a bug id to see a list - # of prior reviews by the user, or just an attachment - # id to go directly to a review page for the attachment. - # If both are give they will be checked later to make - # sure they are connected. - - my $input = Bugzilla->input_params; - if ($input->{'bug'}) { - $vars->{'bug_id'} = $input->{'bug'}; - $vars->{'attach_id'} = $input->{'attachment'}; - $vars->{'bug'} = Bugzilla::Bug->check({ id => $input->{'bug'}, cache => 1 }); - } - - if ($input->{'attachment'}) { - my $attachment = Bugzilla::Attachment->check({ id => $input->{'attachment'} }); - - # Check to see if the user can see the bug this attachment is connected to. - Bugzilla::Bug->check($attachment->bug_id); - if ($attachment->isprivate - && $user->id != $attachment->attacher->id - && !$user->is_insider) - { - ThrowUserError('auth_failure', {action => 'access', - object => 'attachment'}); - } - - # If the user provided both a bug id and an attachment id, they must - # be connected to each other - if ($input->{'bug'} && $input->{'bug'} != $attachment->bug_id) { - ThrowUserError('bug_attach_id_mismatch'); - } - - # The patch is going to be displayed in a HTML page and if the utf8 - # param is enabled, we have to encode attachment data as utf8. - if (Bugzilla->params->{'utf8'}) { - $attachment->data; # load data - utf8::decode($attachment->{data}); - } - - $vars->{'attach_id'} = $attachment->id; - if ($user->id && $attachment->contenttype eq "text/x-github-pull-request" && $attachment->can_review) { - $vars->{'attach_data'} = $attachment->fetch_github_pr_diff; - } - else { - $vars->{'attach_data'} = $attachment->data; - } - $vars->{'attach_is_crlf'} = $vars->{'attach_data'} =~ /\012\015/ ? 1 : 0; - } - - my $field_object = new Bugzilla::Field({ name => 'attachments.status' }); - my $statuses; - if ($field_object) { - $statuses = [map { $_->name } @{ $field_object->legal_values }]; - } else { - $statuses = []; - } - $vars->{'attachment_statuses'} = $statuses; + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + + if ($page eq 'splinter.html') { + my $user = Bugzilla->user; + + # We can either provide just a bug id to see a list + # of prior reviews by the user, or just an attachment + # id to go directly to a review page for the attachment. + # If both are give they will be checked later to make + # sure they are connected. + + my $input = Bugzilla->input_params; + if ($input->{'bug'}) { + $vars->{'bug_id'} = $input->{'bug'}; + $vars->{'attach_id'} = $input->{'attachment'}; + $vars->{'bug'} = Bugzilla::Bug->check({id => $input->{'bug'}, cache => 1}); } + + if ($input->{'attachment'}) { + my $attachment = Bugzilla::Attachment->check({id => $input->{'attachment'}}); + + # Check to see if the user can see the bug this attachment is connected to. + Bugzilla::Bug->check($attachment->bug_id); + if ( $attachment->isprivate + && $user->id != $attachment->attacher->id + && !$user->is_insider) + { + ThrowUserError('auth_failure', {action => 'access', object => 'attachment'}); + } + + # If the user provided both a bug id and an attachment id, they must + # be connected to each other + if ($input->{'bug'} && $input->{'bug'} != $attachment->bug_id) { + ThrowUserError('bug_attach_id_mismatch'); + } + + # The patch is going to be displayed in a HTML page and if the utf8 + # param is enabled, we have to encode attachment data as utf8. + if (Bugzilla->params->{'utf8'}) { + $attachment->data; # load data + utf8::decode($attachment->{data}); + } + + $vars->{'attach_id'} = $attachment->id; + if ( $user->id + && $attachment->contenttype eq "text/x-github-pull-request" + && $attachment->can_review) + { + $vars->{'attach_data'} = $attachment->fetch_github_pr_diff; + } + else { + $vars->{'attach_data'} = $attachment->data; + } + $vars->{'attach_is_crlf'} = $vars->{'attach_data'} =~ /\012\015/ ? 1 : 0; + } + + my $field_object = new Bugzilla::Field({name => 'attachments.status'}); + my $statuses; + if ($field_object) { + $statuses = [map { $_->name } @{$field_object->legal_values}]; + } + else { + $statuses = []; + } + $vars->{'attachment_statuses'} = $statuses; + } } sub bug_format_comment { - my ($self, $args) = @_; - - my $bug = $args->{'bug'}; - my $regexes = $args->{'regexes'}; - my $text = $args->{'text'}; - - # Add [review] link to the end of "Created attachment" comments - # - # We need to work around the way that the hook works, which is intended - # to avoid overlapping matches, since we *want* an overlapping match - # here (the normal handling of "Created attachment"), so we add in - # dummy text and then replace in the regular expression we return from - # the hook. - $$text =~ s~((?:^Created\ |\b)attachment\s*\#?\s*(\d+)(\s\[details\])?) + my ($self, $args) = @_; + + my $bug = $args->{'bug'}; + my $regexes = $args->{'regexes'}; + my $text = $args->{'text'}; + + # Add [review] link to the end of "Created attachment" comments + # + # We need to work around the way that the hook works, which is intended + # to avoid overlapping matches, since we *want* an overlapping match + # here (the normal handling of "Created attachment"), so we add in + # dummy text and then replace in the regular expression we return from + # the hook. + $$text =~ s~((?:^Created\ |\b)attachment\s*\#?\s*(\d+)(\s\[details\])?) ~(push(@$regexes, { match => qr/__REVIEW__$2/, replace => get_review_link("$2", "[review]") })) && (attachment_id_is_patch($2) ? "$1 __REVIEW__$2" : $1) ~egmx; - # And linkify "Review of attachment", this is less of a workaround since - # there is no issue with overlap; note that there is an assumption that - # there is only one match in the text we are linkifying, since they all - # get the same link. - my $REVIEW_RE = qr/Review\s+of\s+attachment\s+(\d+)\s*:/; - - if ($$text =~ $REVIEW_RE) { - my $attach_id = $1; - my $review_link = get_review_link($attach_id, "Review"); - my $attach_link = Bugzilla::Template::get_attachment_link($attach_id, "attachment $attach_id"); - - push(@$regexes, { match => $REVIEW_RE, - replace => "$review_link of $attach_link:"}); - } + # And linkify "Review of attachment", this is less of a workaround since + # there is no issue with overlap; note that there is an assumption that + # there is only one match in the text we are linkifying, since they all + # get the same link. + my $REVIEW_RE = qr/Review\s+of\s+attachment\s+(\d+)\s*:/; + + if ($$text =~ $REVIEW_RE) { + my $attach_id = $1; + my $review_link = get_review_link($attach_id, "Review"); + my $attach_link = Bugzilla::Template::get_attachment_link($attach_id, + "attachment $attach_id"); + + push(@$regexes, + {match => $REVIEW_RE, replace => "$review_link of $attach_link:"}); + } } sub config_add_panels { - my ($self, $args) = @_; + my ($self, $args) = @_; - my $modules = $args->{panel_modules}; - $modules->{Splinter} = "Bugzilla::Extension::Splinter::Config"; + my $modules = $args->{panel_modules}; + $modules->{Splinter} = "Bugzilla::Extension::Splinter::Config"; } sub mailer_before_send { - my ($self, $args) = @_; - - # Post-process bug mail to add review links to bug mail. - # It would be nice to be able to hook in earlier in the - # process when the email body is being formatted in the - # style of the bug-format_comment link for HTML but this - # is the only hook available as of Bugzilla-3.4. - add_review_links_to_email($args->{'email'}); + my ($self, $args) = @_; + + # Post-process bug mail to add review links to bug mail. + # It would be nice to be able to hook in earlier in the + # process when the email body is being formatted in the + # style of the bug-format_comment link for HTML but this + # is the only hook available as of Bugzilla-3.4. + add_review_links_to_email($args->{'email'}); } __PACKAGE__->NAME; diff --git a/extensions/Splinter/lib/Config.pm b/extensions/Splinter/lib/Config.pm index fb3c16074..d3675c111 100644 --- a/extensions/Splinter/lib/Config.pm +++ b/extensions/Splinter/lib/Config.pm @@ -31,17 +31,13 @@ use Bugzilla::Config::Common; our $sortkey = 1350; sub get_param_list { - my ($class) = @_; + my ($class) = @_; - my @param_list = ( - { - name => 'splinter_base', - type => 't', - default => 'page.cgi?id=splinter.html', - }, - ); + my @param_list = ( + {name => 'splinter_base', type => 't', default => 'page.cgi?id=splinter.html',}, + ); - return @param_list; + return @param_list; } 1; diff --git a/extensions/Splinter/lib/Util.pm b/extensions/Splinter/lib/Util.pm index c85bb9b3b..0b7b2ff12 100644 --- a/extensions/Splinter/lib/Util.pm +++ b/extensions/Splinter/lib/Util.pm @@ -32,12 +32,12 @@ use Email::MIME::ContentType qw(parse_content_type); use base qw(Exporter); @Bugzilla::Extension::Splinter::Util::EXPORT = qw( - attachment_is_visible - attachment_id_is_patch - get_review_base - get_review_url - get_review_link - add_review_links_to_email + attachment_is_visible + attachment_id_is_patch + get_review_base + get_review_url + get_review_link + add_review_links_to_email ); # Validates an attachment ID. @@ -51,81 +51,95 @@ use base qw(Exporter); # Returns an attachment object. # Based on code from attachment.cgi sub attachment_id_is_valid { - my ($attach_id, $dont_validate_access) = @_; + my ($attach_id, $dont_validate_access) = @_; - # Validate the specified attachment id. - detaint_natural($attach_id) || return 0; + # Validate the specified attachment id. + detaint_natural($attach_id) || return 0; - # Make sure the attachment exists in the database. - my $attachment = new Bugzilla::Attachment({ id => $attach_id, cache => 1 }) - || return 0; + # Make sure the attachment exists in the database. + my $attachment + = new Bugzilla::Attachment({id => $attach_id, cache => 1}) || return 0; - return $attachment - if ($dont_validate_access || attachment_is_visible($attachment)); + return $attachment + if ($dont_validate_access || attachment_is_visible($attachment)); } # Checks if the current user can see an attachment # Based on code from attachment.cgi sub attachment_is_visible { - my $attachment = shift; + my $attachment = shift; - $attachment->isa('Bugzilla::Attachment') || return 0; + $attachment->isa('Bugzilla::Attachment') || return 0; - return (Bugzilla->user->can_see_bug($attachment->bug->id) - && (!$attachment->isprivate - || Bugzilla->user->id == $attachment->attacher->id - || Bugzilla->user->is_insider)); + return ( + Bugzilla->user->can_see_bug($attachment->bug->id) + && (!$attachment->isprivate + || Bugzilla->user->id == $attachment->attacher->id + || Bugzilla->user->is_insider) + ); } sub attachment_id_is_patch { - my $attach_id = shift; - my $attachment = attachment_id_is_valid($attach_id); - return ($attachment - && ($attachment->ispatch - || ($attachment->contenttype eq "text/x-github-pull-request" && $attachment->external_redirect))); + my $attach_id = shift; + my $attachment = attachment_id_is_valid($attach_id); + return ( + $attachment + && ( + $attachment->ispatch + || ( $attachment->contenttype eq "text/x-github-pull-request" + && $attachment->external_redirect) + ) + ); } sub get_review_base { - my $base = Bugzilla->params->{'splinter_base'}; - $base =~ s!/$!!; - my $urlbase = Bugzilla->localconfig->{urlbase}; - $urlbase =~ s!/$!! if $base =~ "^/"; - $base = $urlbase . $base; - return $base; + my $base = Bugzilla->params->{'splinter_base'}; + $base =~ s!/$!!; + my $urlbase = Bugzilla->localconfig->{urlbase}; + $urlbase =~ s!/$!! if $base =~ "^/"; + $base = $urlbase . $base; + return $base; } sub get_review_url { - my ($bug, $attach_id) = @_; - my $base = get_review_base(); - my $bug_id = $bug->id; - return $base . ($base =~ /\?/ ? '&' : '?') . "bug=$bug_id&attachment=$attach_id"; + my ($bug, $attach_id) = @_; + my $base = get_review_base(); + my $bug_id = $bug->id; + return + $base + . ($base =~ /\?/ ? '&' : '?') + . "bug=$bug_id&attachment=$attach_id"; } sub get_review_link { - my ($attach_id, $link_text) = @_; - - my $attachment = attachment_id_is_valid($attach_id); - - if (attachment_id_is_patch($attach_id)) { - return "$link_text"; - } - else { - return $link_text; - } + my ($attach_id, $link_text) = @_; + + my $attachment = attachment_id_is_valid($attach_id); + + if (attachment_id_is_patch($attach_id)) { + return + "$link_text"; + } + else { + return $link_text; + } } sub munge_create_attachment { - my ($bug, $intro_text, $attach_id, $view_link) = @_; - - if (attachment_id_is_patch($attach_id)) { - return ("$intro_text" . - " View: $view_link\015\012" . - " Review: " . get_review_url($bug, $attach_id, 1) . "\015\012"); - } - else { - return ("$intro_text --> ($view_link)"); - } + my ($bug, $intro_text, $attach_id, $view_link) = @_; + + if (attachment_id_is_patch($attach_id)) { + return ("$intro_text" + . " View: $view_link\015\012" + . " Review: " + . get_review_url($bug, $attach_id, 1) + . "\015\012"); + } + else { + return ("$intro_text --> ($view_link)"); + } } # This adds review links into a bug mail before we send it out. @@ -133,64 +147,62 @@ sub munge_create_attachment { # RFC-2822 style \r\n, we need handle line ends carefully. # (\015 and \012 are used because Perl \n is platform-dependent) sub add_review_links_to_email { - my $email = shift; - return if $email->parts > 1; - return unless $email->content_type =~ m#^text/#; + my $email = shift; + return if $email->parts > 1; + return unless $email->content_type =~ m#^text/#; - _fix_encoding($email); - my $body = $email->body_str; + _fix_encoding($email); + my $body = $email->body_str; - my $new_body = 0; - my $bug; + my $new_body = 0; + my $bug; - if ($email->header('Subject') =~ /^\[Bug\s+(\d+)\]/ - && Bugzilla->user->can_see_bug($1)) - { - $bug = Bugzilla::Bug->new({ id => $1, cache => 1 }); - } + if ($email->header('Subject') =~ /^\[Bug\s+(\d+)\]/ + && Bugzilla->user->can_see_bug($1)) + { + $bug = Bugzilla::Bug->new({id => $1, cache => 1}); + } - return unless defined $bug; + return unless defined $bug; - if ($body =~ /Review\s+of\s+attachment\s+\d+\s*:/) { - $body =~ s~(Review\s+of\s+attachment\s+(\d+)\s*:) + if ($body =~ /Review\s+of\s+attachment\s+\d+\s*:/) { + $body =~ s~(Review\s+of\s+attachment\s+(\d+)\s*:) ~"$1\015\012 --> (" . get_review_url($bug, $2, 1) . ")" ~egx; - $new_body = 1; - } + $new_body = 1; + } - if ($body =~ /Created attachment \d+\015\012 --> /) { - $body =~ s~(Created\ attachment\ (\d+)\015\012) + if ($body =~ /Created attachment \d+\015\012 --> /) { + $body =~ s~(Created\ attachment\ (\d+)\015\012) \ -->\ \(([^\015\012]*)\)[^\015\012]* ~munge_create_attachment($bug, $1, $2, $3) ~egx; - $new_body = 1; - } + $new_body = 1; + } - $email->body_str_set($body) if $new_body; + $email->body_str_set($body) if $new_body; } sub _fix_encoding { - my $part = shift; - - # don't touch the top-level part of multi-part mail - return if $part->parts > 1; - - # nothing to do if the part already has a charset - my $ct = parse_content_type($part->content_type); - my $charset = $ct->{attributes}{charset} - ? $ct->{attributes}{charset} - : ''; - return unless !$charset || $charset eq 'us-ascii'; - - if (Bugzilla->params->{utf8}) { - $part->charset_set('UTF-8'); - my $raw = $part->body_raw; - if (utf8::is_utf8($raw)) { - utf8::encode($raw); - $part->body_set($raw); - } + my $part = shift; + + # don't touch the top-level part of multi-part mail + return if $part->parts > 1; + + # nothing to do if the part already has a charset + my $ct = parse_content_type($part->content_type); + my $charset = $ct->{attributes}{charset} ? $ct->{attributes}{charset} : ''; + return unless !$charset || $charset eq 'us-ascii'; + + if (Bugzilla->params->{utf8}) { + $part->charset_set('UTF-8'); + my $raw = $part->body_raw; + if (utf8::is_utf8($raw)) { + utf8::encode($raw); + $part->body_set($raw); } - $part->encoding_set('quoted-printable'); + } + $part->encoding_set('quoted-printable'); } 1; diff --git a/extensions/TagNewUsers/Config.pm b/extensions/TagNewUsers/Config.pm index c791e3a07..cc3676bff 100644 --- a/extensions/TagNewUsers/Config.pm +++ b/extensions/TagNewUsers/Config.pm @@ -11,8 +11,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'TagNewUsers'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'TagNewUsers'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/TagNewUsers/Extension.pm b/extensions/TagNewUsers/Extension.pm index 1810f204f..6280f1697 100644 --- a/extensions/TagNewUsers/Extension.pm +++ b/extensions/TagNewUsers/Extension.pm @@ -34,85 +34,88 @@ our $VERSION = '1'; # sub install_update_db { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - if (!$dbh->bz_column_info('profiles', 'comment_count')) { - $dbh->bz_add_column('profiles', 'comment_count', - {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); - my $sth = $dbh->prepare('UPDATE profiles SET comment_count=? WHERE userid=?'); - my $ra = $dbh->selectall_arrayref('SELECT who,COUNT(*) FROM longdescs GROUP BY who'); - my $count = 1; - my $total = scalar @$ra; - foreach my $ra_row (@$ra) { - indicate_progress({ current => $count++, total => $total, every => 25 }); - my ($user_id, $count) = @$ra_row; - $sth->execute($count, $user_id); - } + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + if (!$dbh->bz_column_info('profiles', 'comment_count')) { + $dbh->bz_add_column('profiles', 'comment_count', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); + my $sth = $dbh->prepare('UPDATE profiles SET comment_count=? WHERE userid=?'); + my $ra + = $dbh->selectall_arrayref('SELECT who,COUNT(*) FROM longdescs GROUP BY who'); + my $count = 1; + my $total = scalar @$ra; + foreach my $ra_row (@$ra) { + indicate_progress({current => $count++, total => $total, every => 25}); + my ($user_id, $count) = @$ra_row; + $sth->execute($count, $user_id); } + } - if (!$dbh->bz_column_info('profiles', 'creation_ts')) { - $dbh->bz_add_column('profiles', 'creation_ts', - {TYPE => 'DATETIME'}); - my $creation_date_fieldid = get_field_id('creation_ts'); - my $sth = $dbh->prepare('UPDATE profiles SET creation_ts=? WHERE userid=?'); - my $ra = $dbh->selectall_arrayref(" + if (!$dbh->bz_column_info('profiles', 'creation_ts')) { + $dbh->bz_add_column('profiles', 'creation_ts', {TYPE => 'DATETIME'}); + my $creation_date_fieldid = get_field_id('creation_ts'); + my $sth = $dbh->prepare('UPDATE profiles SET creation_ts=? WHERE userid=?'); + my $ra = $dbh->selectall_arrayref(" SELECT p.userid, a.profiles_when FROM profiles p LEFT JOIN profiles_activity a ON a.userid=p.userid AND a.fieldid=$creation_date_fieldid "); - my ($now) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); - my $count = 1; - my $total = scalar @$ra; - foreach my $ra_row (@$ra) { - indicate_progress({ current => $count++, total => $total, every => 25 }); - my ($user_id, $when) = @$ra_row; - if (!$when) { - ($when) = $dbh->selectrow_array( - "SELECT bug_when FROM bugs_activity WHERE who=? ORDER BY bug_when " . - $dbh->sql_limit(1), - undef, $user_id - ); - } - if (!$when) { - ($when) = $dbh->selectrow_array( - "SELECT bug_when FROM longdescs WHERE who=? ORDER BY bug_when " . - $dbh->sql_limit(1), - undef, $user_id - ); - } - if (!$when) { - ($when) = $dbh->selectrow_array( - "SELECT creation_ts FROM bugs WHERE reporter=? ORDER BY creation_ts " . - $dbh->sql_limit(1), - undef, $user_id - ); - } - if (!$when) { - $when = $now; - } - - $sth->execute($when, $user_id); - } - } - - if (!$dbh->bz_column_info('profiles', 'first_patch_bug_id')) { - $dbh->bz_add_column('profiles', 'first_patch_bug_id', {TYPE => 'INT3'}); - my $sth_update = $dbh->prepare('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?'); - my $sth_select = $dbh->prepare( - 'SELECT bug_id FROM attachments WHERE submitter_id=? AND ispatch=1 ORDER BY creation_ts ' . $dbh->sql_limit(1) + my ($now) = Bugzilla->dbh->selectrow_array("SELECT NOW()"); + my $count = 1; + my $total = scalar @$ra; + foreach my $ra_row (@$ra) { + indicate_progress({current => $count++, total => $total, every => 25}); + my ($user_id, $when) = @$ra_row; + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT bug_when FROM bugs_activity WHERE who=? ORDER BY bug_when " + . $dbh->sql_limit(1), + undef, $user_id + ); + } + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT bug_when FROM longdescs WHERE who=? ORDER BY bug_when " + . $dbh->sql_limit(1), + undef, $user_id + ); + } + if (!$when) { + ($when) = $dbh->selectrow_array( + "SELECT creation_ts FROM bugs WHERE reporter=? ORDER BY creation_ts " + . $dbh->sql_limit(1), + undef, $user_id ); - my $ra = $dbh->selectcol_arrayref('SELECT DISTINCT submitter_id FROM attachments WHERE ispatch=1'); - my $count = 1; - my $total = scalar @$ra; - foreach my $user_id (@$ra) { - indicate_progress({ current => $count++, total => $total, every => 25 }); - $sth_select->execute($user_id); - my ($bug_id) = $sth_select->fetchrow_array; - $sth_update->execute($bug_id, $user_id); - } + } + if (!$when) { + $when = $now; + } + + $sth->execute($when, $user_id); + } + } + + if (!$dbh->bz_column_info('profiles', 'first_patch_bug_id')) { + $dbh->bz_add_column('profiles', 'first_patch_bug_id', {TYPE => 'INT3'}); + my $sth_update + = $dbh->prepare('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?'); + my $sth_select + = $dbh->prepare( + 'SELECT bug_id FROM attachments WHERE submitter_id=? AND ispatch=1 ORDER BY creation_ts ' + . $dbh->sql_limit(1)); + my $ra = $dbh->selectcol_arrayref( + 'SELECT DISTINCT submitter_id FROM attachments WHERE ispatch=1'); + my $count = 1; + my $total = scalar @$ra; + foreach my $user_id (@$ra) { + indicate_progress({current => $count++, total => $total, every => 25}); + $sth_select->execute($user_id); + my ($bug_id) = $sth_select->fetchrow_array; + $sth_update->execute($bug_id, $user_id); } + } } # @@ -120,32 +123,34 @@ sub install_update_db { # sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - my @new_columns = qw(comment_count creation_ts first_patch_bug_id); - push @$columns, grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + my @new_columns = qw(comment_count creation_ts first_patch_bug_id); + push @$columns, + grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; + } } sub object_before_create { - my ($self, $args) = @_; - my ($class, $params) = @$args{qw(class params)}; - if ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); - if ($dbh->bz_column_info($class->DB_TABLE, 'comment_count')) { - $params->{comment_count} = 0; - } - if ($dbh->bz_column_info($class->DB_TABLE, 'creation_ts')) { - $params->{creation_ts} = $timestamp; - } - } elsif ($class->isa('Bugzilla::Attachment')) { - if ($params->{ispatch} && !Bugzilla->user->first_patch_bug_id) { - Bugzilla->user->first_patch_bug_id($params->{bug}->id); - } + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + if ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); + if ($dbh->bz_column_info($class->DB_TABLE, 'comment_count')) { + $params->{comment_count} = 0; + } + if ($dbh->bz_column_info($class->DB_TABLE, 'creation_ts')) { + $params->{creation_ts} = $timestamp; + } + } + elsif ($class->isa('Bugzilla::Attachment')) { + if ($params->{ispatch} && !Bugzilla->user->first_patch_bug_id) { + Bugzilla->user->first_patch_bug_id($params->{bug}->id); } + } } # @@ -153,74 +158,70 @@ sub object_before_create { # BEGIN { - *Bugzilla::User::comment_count = \&_comment_count; - *Bugzilla::User::creation_ts = \&_creation_ts; - *Bugzilla::User::update_comment_count = \&_update_comment_count; - *Bugzilla::User::first_patch_bug_id = \&_first_patch_bug_id; - *Bugzilla::User::is_new = \&_is_new; - *Bugzilla::User::creation_age = \&_creation_age; + *Bugzilla::User::comment_count = \&_comment_count; + *Bugzilla::User::creation_ts = \&_creation_ts; + *Bugzilla::User::update_comment_count = \&_update_comment_count; + *Bugzilla::User::first_patch_bug_id = \&_first_patch_bug_id; + *Bugzilla::User::is_new = \&_is_new; + *Bugzilla::User::creation_age = \&_creation_age; } sub _comment_count { return $_[0]->{comment_count} } -sub _creation_ts { return $_[0]->{creation_ts} } +sub _creation_ts { return $_[0]->{creation_ts} } sub _update_comment_count { - my $self = shift; - my $dbh = Bugzilla->dbh; - - # no need to update this counter for users which are no longer new - return unless $self->is_new; - - my $id = $self->id; - my ($count) = $dbh->selectrow_array( - "SELECT COUNT(*) FROM longdescs WHERE who=?", - undef, $id - ); - return if $self->{comment_count} == $count; - $dbh->do( - 'UPDATE profiles SET comment_count=? WHERE userid=?', - undef, $count, $id - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $id }); - $self->{comment_count} = $count; + my $self = shift; + my $dbh = Bugzilla->dbh; + + # no need to update this counter for users which are no longer new + return unless $self->is_new; + + my $id = $self->id; + my ($count) + = $dbh->selectrow_array("SELECT COUNT(*) FROM longdescs WHERE who=?", + undef, $id); + return if $self->{comment_count} == $count; + $dbh->do('UPDATE profiles SET comment_count=? WHERE userid=?', + undef, $count, $id); + Bugzilla->memcached->clear({table => 'profiles', id => $id}); + $self->{comment_count} = $count; } sub _first_patch_bug_id { - my ($self, $bug_id) = @_; - return $self->{first_patch_bug_id} unless defined $bug_id; - - Bugzilla->dbh->do( - 'UPDATE profiles SET first_patch_bug_id=? WHERE userid=?', - undef, $bug_id, $self->id - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); - $self->{first_patch_bug_id} = $bug_id; + my ($self, $bug_id) = @_; + return $self->{first_patch_bug_id} unless defined $bug_id; + + Bugzilla->dbh->do('UPDATE profiles SET first_patch_bug_id=? WHERE userid=?', + undef, $bug_id, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); + $self->{first_patch_bug_id} = $bug_id; } sub _is_new { - my ($self) = @_; - - if (!exists $self->{is_new}) { - if ($self->in_group('editbugs')) { - $self->{is_new} = 0; - } else { - $self->{is_new} = ($self->comment_count <= COMMENT_COUNT) - || ($self->creation_age <= PROFILE_AGE); - } + my ($self) = @_; + + if (!exists $self->{is_new}) { + if ($self->in_group('editbugs')) { + $self->{is_new} = 0; + } + else { + $self->{is_new} = ($self->comment_count <= COMMENT_COUNT) + || ($self->creation_age <= PROFILE_AGE); } + } - return $self->{is_new}; + return $self->{is_new}; } sub _creation_age { - my ($self) = @_; + my ($self) = @_; - if (!exists $self->{creation_age}) { - my $age = sprintf("%.0f", (time() - str2time($self->creation_ts)) / 86400); - $self->{creation_age} = $age; - } + if (!exists $self->{creation_age}) { + my $age = sprintf("%.0f", (time() - str2time($self->creation_ts)) / 86400); + $self->{creation_age} = $age; + } - return $self->{creation_age}; + return $self->{creation_age}; } # @@ -228,49 +229,48 @@ sub _creation_age { # sub bug_end_of_create { - Bugzilla->user->update_comment_count(); + Bugzilla->user->update_comment_count(); } sub bug_end_of_update { - Bugzilla->user->update_comment_count(); + Bugzilla->user->update_comment_count(); } sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{email}; - - my ($bug_id) = ($email->header('Subject') =~ /^[^\d]+(\d+)/); - my $changer_login = $email->header('X-Bugzilla-Who'); - my $changed_fields = $email->header('X-Bugzilla-Changed-Fields'); - - if ($bug_id - && $changer_login - && $changed_fields =~ /attachments.created/) + my ($self, $args) = @_; + my $email = $args->{email}; + + my ($bug_id) = ($email->header('Subject') =~ /^[^\d]+(\d+)/); + my $changer_login = $email->header('X-Bugzilla-Who'); + my $changed_fields = $email->header('X-Bugzilla-Changed-Fields'); + + if ($bug_id && $changer_login && $changed_fields =~ /attachments.created/) { + my $changer = Bugzilla::User->new({name => $changer_login}); + if ( $changer + && $changer->first_patch_bug_id + && $changer->first_patch_bug_id == $bug_id) { - my $changer = Bugzilla::User->new({ name => $changer_login }); - if ($changer - && $changer->first_patch_bug_id - && $changer->first_patch_bug_id == $bug_id) - { - $email->header_set('X-Bugzilla-FirstPatch' => $bug_id); - } + $email->header_set('X-Bugzilla-FirstPatch' => $bug_id); } + } } sub webservice_user_get { - my ($self, $args) = @_; - my ($webservice, $params, $users) = @$args{qw(webservice params users)}; - - return unless filter_wants($params, 'is_new'); - - foreach my $user (@$users) { - # Most of the time the hash values are XMLRPC::Data objects - my $email = blessed $user->{'email'} ? $user->{'email'}->value : $user->{'email'}; - if ($email) { - my $user_obj = Bugzilla::User->new({ name => $email }); - $user->{'is_new'} = $webservice->type('boolean', $user_obj->is_new ? 1 : 0); - } + my ($self, $args) = @_; + my ($webservice, $params, $users) = @$args{qw(webservice params users)}; + + return unless filter_wants($params, 'is_new'); + + foreach my $user (@$users) { + + # Most of the time the hash values are XMLRPC::Data objects + my $email + = blessed $user->{'email'} ? $user->{'email'}->value : $user->{'email'}; + if ($email) { + my $user_obj = Bugzilla::User->new({name => $email}); + $user->{'is_new'} = $webservice->type('boolean', $user_obj->is_new ? 1 : 0); } + } } __PACKAGE__->NAME; diff --git a/extensions/TrackingFlags/Config.pm b/extensions/TrackingFlags/Config.pm index d0bc5ca20..6f5b9be39 100644 --- a/extensions/TrackingFlags/Config.pm +++ b/extensions/TrackingFlags/Config.pm @@ -13,15 +13,9 @@ use warnings; use constant NAME => 'TrackingFlags'; -use constant REQUIRED_MODULES => [ - { - package => 'JSON-XS', - module => 'JSON::XS', - version => '2.0' - }, -]; +use constant REQUIRED_MODULES => + [{package => 'JSON-XS', module => 'JSON::XS', version => '2.0'},]; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/TrackingFlags/Extension.pm b/extensions/TrackingFlags/Extension.pm index 5f6715fc8..fea0240c8 100644 --- a/extensions/TrackingFlags/Extension.pm +++ b/extensions/TrackingFlags/Extension.pm @@ -34,823 +34,701 @@ our $VERSION = '1'; our @FLAG_CACHE; BEGIN { - *Bugzilla::tracking_flags = \&_tracking_flags; - *Bugzilla::tracking_flag_names = \&_tracking_flag_names; + *Bugzilla::tracking_flags = \&_tracking_flags; + *Bugzilla::tracking_flag_names = \&_tracking_flag_names; } sub _tracking_flags { - return Bugzilla::Extension::TrackingFlags::Flag->get_all(); + return Bugzilla::Extension::TrackingFlags::Flag->get_all(); } sub _tracking_flag_names { - return Bugzilla::Extension::TrackingFlags::Flag->get_all_names(); + return Bugzilla::Extension::TrackingFlags::Flag->get_all_names(); } sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{'page_id'}; - my $vars = $args->{'vars'}; - - if ($page eq 'tracking_flags_admin_list.html') { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_list($vars); - - } elsif ($page eq 'tracking_flags_admin_edit.html') { - Bugzilla->user->in_group('admin') - || ThrowUserError('auth_failure', - { group => 'admin', - action => 'access', - object => 'administrative_pages' }); - admin_edit($vars); - } + my ($self, $args) = @_; + my $page = $args->{'page_id'}; + my $vars = $args->{'vars'}; + + if ($page eq 'tracking_flags_admin_list.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_list($vars); + + } + elsif ($page eq 'tracking_flags_admin_edit.html') { + Bugzilla->user->in_group('admin') + || ThrowUserError('auth_failure', + {group => 'admin', action => 'access', object => 'administrative_pages'}); + admin_edit($vars); + } } sub template_before_process { - my ($self, $args) = @_; - my $file = $args->{'file'}; - my $vars = $args->{'vars'}; - - if ($file eq 'bug/create/create.html.tmpl') { - my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $vars->{'product'}->name, - enter_bug => 1, - is_active => 1, - }); - - $vars->{tracking_flags} = $flags; - $vars->{tracking_flags_json} = _flags_to_json($flags); - $vars->{tracking_flag_types} = FLAG_TYPES; - $vars->{tracking_flag_components} = _flags_to_components($flags, $vars->{product}); - $vars->{highest_status_firefox} = _get_highest_status_firefox($flags); - } - elsif ($file eq 'bug/edit.html.tmpl'|| $file eq 'bug/show.xml.tmpl' - || $file eq 'email/bugmail.html.tmpl' || $file eq 'email/bugmail.txt.tmpl') - { - # note: bug/edit.html.tmpl doesn't support multiple bugs - my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; - - if ($bug && !$bug->{error}) { - my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $bug->product, - component => $bug->component, - bug_id => $bug->id, - is_active => 1, - }); - - $vars->{tracking_flags} = $flags; - $vars->{tracking_flags_json} = _flags_to_json($flags); - } + my ($self, $args) = @_; + my $file = $args->{'file'}; + my $vars = $args->{'vars'}; + + if ($file eq 'bug/create/create.html.tmpl') { + my $flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $vars->{'product'}->name, enter_bug => 1, is_active => 1, + }); + + $vars->{tracking_flags} = $flags; + $vars->{tracking_flags_json} = _flags_to_json($flags); + $vars->{tracking_flag_types} = FLAG_TYPES; + $vars->{tracking_flag_components} + = _flags_to_components($flags, $vars->{product}); + $vars->{highest_status_firefox} = _get_highest_status_firefox($flags); + } + elsif ($file eq 'bug/edit.html.tmpl' + || $file eq 'bug/show.xml.tmpl' + || $file eq 'email/bugmail.html.tmpl' + || $file eq 'email/bugmail.txt.tmpl') + { + # note: bug/edit.html.tmpl doesn't support multiple bugs + my $bug = exists $vars->{'bugs'} ? $vars->{'bugs'}[0] : $vars->{'bug'}; + + if ($bug && !$bug->{error}) { + my $flags = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $bug->product, + component => $bug->component, + bug_id => $bug->id, + is_active => 1, + }); - $vars->{'tracking_flag_types'} = FLAG_TYPES; - } - elsif ($file eq 'list/edit-multiple.html.tmpl' && $vars->{'one_product'}) { - $vars->{'tracking_flags'} = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $vars->{'one_product'}->name, - is_active => 1 - }); + $vars->{tracking_flags} = $flags; + $vars->{tracking_flags_json} = _flags_to_json($flags); } + + $vars->{'tracking_flag_types'} = FLAG_TYPES; + } + elsif ($file eq 'list/edit-multiple.html.tmpl' && $vars->{'one_product'}) { + $vars->{'tracking_flags'} + = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $vars->{'one_product'}->name, is_active => 1 + }); + } } sub _flags_to_json { - my ($flags) = @_; + my ($flags) = @_; - my $json = { - flags => {}, - types => [], - comments => {}, - }; + my $json = {flags => {}, types => [], comments => {},}; - my %type_map = map { $_->{name} => $_ } @{ FLAG_TYPES() }; - foreach my $flag (@$flags) { - my $flag_type = $flag->flag_type; + my %type_map = map { $_->{name} => $_ } @{FLAG_TYPES()}; + foreach my $flag (@$flags) { + my $flag_type = $flag->flag_type; - $json->{flags}->{$flag_type}->{$flag->name} = $flag->bug_flag->value; + $json->{flags}->{$flag_type}->{$flag->name} = $flag->bug_flag->value; - if ($type_map{$flag_type}->{collapsed} - && !grep { $_ eq $flag_type } @{ $json->{types} }) - { - push @{ $json->{types} }, $flag_type; - } + if ($type_map{$flag_type}->{collapsed} && !grep { $_ eq $flag_type } + @{$json->{types}}) + { + push @{$json->{types}}, $flag_type; + } - foreach my $value (@{ $flag->values }) { - if (defined($value->comment) && $value->comment ne '') { - $json->{comments}->{$flag->name}->{$value->value} = $value->comment; - } - } + foreach my $value (@{$flag->values}) { + if (defined($value->comment) && $value->comment ne '') { + $json->{comments}->{$flag->name}->{$value->value} = $value->comment; + } } + } - return encode_json($json); + return encode_json($json); } sub _flags_to_components { - my ($flags, $product) = @_; - - # for each component, generate a list of visible tracking flags - my $json = {}; - foreach my $component (@{ $product->components }) { - next unless $component->is_active; - foreach my $flag (@$flags) { - foreach my $visibility (@{ $flag->visibility }) { - if ($visibility->product_id == $product->id - && (!$visibility->component_id || $visibility->component_id == $component->id)) - { - $json->{$component->name} //= []; - push @{ $json->{$component->name} }, $flag->name; - } - } + my ($flags, $product) = @_; + + # for each component, generate a list of visible tracking flags + my $json = {}; + foreach my $component (@{$product->components}) { + next unless $component->is_active; + foreach my $flag (@$flags) { + foreach my $visibility (@{$flag->visibility}) { + if ($visibility->product_id == $product->id + && (!$visibility->component_id || $visibility->component_id == $component->id)) + { + $json->{$component->name} //= []; + push @{$json->{$component->name}}, $flag->name; } + } } - return encode_json($json); + } + return encode_json($json); } sub _get_highest_status_firefox { - my ($flags) = @_; - - my @status_flags = - sort { $b <=> $a } - map { $_->name =~ /(\d+)$/; $1 } - grep { $_->is_active && $_->name =~ /^cf_status_firefox\d/ } - @$flags; - return @status_flags ? $status_flags[0] : undef; + my ($flags) = @_; + + my @status_flags + = sort { $b <=> $a } + map { $_->name =~ /(\d+)$/; $1 } + grep { $_->is_active && $_->name =~ /^cf_status_firefox\d/ } @$flags; + return @status_flags ? $status_flags[0] : undef; } sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'tracking_flags'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - field_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'fielddefs', - COLUMN => 'id', - DELETE => 'CASCADE' - } - }, - name => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - description => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - type => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - sortkey => { - TYPE => 'INT2', - NOTNULL => 1, - DEFAULT => '0', - }, - enter_bug => { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - }, - is_active => { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - }, - ], - INDEXES => [ - tracking_flags_idx => { - FIELDS => ['name'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'tracking_flags_values'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - tracking_flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'tracking_flags', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - setter_group_id => { - TYPE => 'INT3', - NOTNULL => 0, - REFERENCES => { - TABLE => 'groups', - COLUMN => 'id', - DELETE => 'SET NULL', - }, - }, - value => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - sortkey => { - TYPE => 'INT2', - NOTNULL => 1, - DEFAULT => '0', - }, - enter_bug => { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - }, - is_active => { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - }, - comment => { - TYPE => 'TEXT', - NOTNULL => 0, - }, - ], - INDEXES => [ - tracking_flags_values_idx => { - FIELDS => ['tracking_flag_id', 'value'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'tracking_flags_bugs'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - tracking_flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'tracking_flags', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE', - }, - }, - value => { - TYPE => 'varchar(64)', - NOTNULL => 1, - }, - ], - INDEXES => [ - tracking_flags_bugs_idx => { - FIELDS => ['tracking_flag_id', 'bug_id'], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'tracking_flags_visibility'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - tracking_flag_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'tracking_flags', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - product_id => { - TYPE => 'INT2', - NOTNULL => 1, - REFERENCES => { - TABLE => 'products', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - component_id => { - TYPE => 'INT2', - NOTNULL => 0, - REFERENCES => { - TABLE => 'components', - COLUMN => 'id', - DELETE => 'CASCADE', - }, - }, - ], - INDEXES => [ - tracking_flags_visibility_idx => { - FIELDS => ['tracking_flag_id', 'product_id', 'component_id'], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'tracking_flags'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + field_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'fielddefs', COLUMN => 'id', DELETE => 'CASCADE'} + }, + name => {TYPE => 'varchar(64)', NOTNULL => 1,}, + description => {TYPE => 'varchar(64)', NOTNULL => 1,}, + type => {TYPE => 'varchar(64)', NOTNULL => 1,}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0',}, + enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}, + ], + INDEXES => [tracking_flags_idx => {FIELDS => ['name'], TYPE => 'UNIQUE',},], + }; + $args->{'schema'}->{'tracking_flags_values'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + tracking_flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tracking_flags', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + setter_group_id => { + TYPE => 'INT3', + NOTNULL => 0, + REFERENCES => {TABLE => 'groups', COLUMN => 'id', DELETE => 'SET NULL',}, + }, + value => {TYPE => 'varchar(64)', NOTNULL => 1,}, + sortkey => {TYPE => 'INT2', NOTNULL => 1, DEFAULT => '0',}, + enter_bug => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}, + is_active => {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}, + comment => {TYPE => 'TEXT', NOTNULL => 0,}, + ], + INDEXES => [ + tracking_flags_values_idx => + {FIELDS => ['tracking_flag_id', 'value'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'tracking_flags_bugs'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + tracking_flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tracking_flags', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE',}, + }, + value => {TYPE => 'varchar(64)', NOTNULL => 1,}, + ], + INDEXES => [ + tracking_flags_bugs_idx => + {FIELDS => ['tracking_flag_id', 'bug_id'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'tracking_flags_visibility'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + tracking_flag_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'tracking_flags', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + product_id => { + TYPE => 'INT2', + NOTNULL => 1, + REFERENCES => {TABLE => 'products', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + component_id => { + TYPE => 'INT2', + NOTNULL => 0, + REFERENCES => {TABLE => 'components', COLUMN => 'id', DELETE => 'CASCADE',}, + }, + ], + INDEXES => [ + tracking_flags_visibility_idx => { + FIELDS => ['tracking_flag_id', 'product_id', 'component_id'], + TYPE => 'UNIQUE', + }, + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - - my $fk = $dbh->bz_fk_info('tracking_flags', 'field_id'); - if ($fk and !defined $fk->{DELETE}) { - $fk->{DELETE} = 'CASCADE'; - $dbh->bz_alter_fk('tracking_flags', 'field_id', $fk); - } - - $dbh->bz_add_column( - 'tracking_flags', - 'enter_bug', - { - TYPE => 'BOOLEAN', - NOTNULL => 1, - DEFAULT => 'TRUE', - } - ); - $dbh->bz_add_column( - 'tracking_flags_values', - 'comment', - { - TYPE => 'TEXT', - NOTNULL => 0, - }, - ); + my $dbh = Bugzilla->dbh; + + my $fk = $dbh->bz_fk_info('tracking_flags', 'field_id'); + if ($fk and !defined $fk->{DELETE}) { + $fk->{DELETE} = 'CASCADE'; + $dbh->bz_alter_fk('tracking_flags', 'field_id', $fk); + } + + $dbh->bz_add_column('tracking_flags', 'enter_bug', + {TYPE => 'BOOLEAN', NOTNULL => 1, DEFAULT => 'TRUE',}); + $dbh->bz_add_column('tracking_flags_values', 'comment', + {TYPE => 'TEXT', NOTNULL => 0,}, + ); } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{files}; - my $extensions_dir = bz_locations()->{extensionsdir}; - $files->{"$extensions_dir/TrackingFlags/bin/bulk_flag_clear.pl"} = { - perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{files}; + my $extensions_dir = bz_locations()->{extensionsdir}; + $files->{"$extensions_dir/TrackingFlags/bin/bulk_flag_clear.pl"} + = {perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE}; } sub active_custom_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - my $params = $args->{'params'}; - my $product = $params->{'product'}; - my $component = $params->{'component'}; - - return if $params->{skip_extensions}; - # Create a hash of current fields based on field names - my %field_hash = map { $_->name => $_ } @$$fields; - - my @tracking_flags; - if ($product) { - $params->{'product_id'} = $product->id; - $params->{'component_id'} = $component->id if $component; - $params->{'is_active'} = 1; - @tracking_flags = @{ Bugzilla::Extension::TrackingFlags::Flag->match($params) }; - } - else { - @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - } - - # Add tracking flags to fields hash replacing if already exists for our - # flag object instead of the usual Field.pm object - foreach my $flag (@tracking_flags) { - $field_hash{$flag->name} = $flag; - } - - @$$fields = sort { $a->sortkey <=> $b->sortkey } values %field_hash; + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + my $params = $args->{'params'}; + my $product = $params->{'product'}; + my $component = $params->{'component'}; + + return if $params->{skip_extensions}; + + # Create a hash of current fields based on field names + my %field_hash = map { $_->name => $_ } @$$fields; + + my @tracking_flags; + if ($product) { + $params->{'product_id'} = $product->id; + $params->{'component_id'} = $component->id if $component; + $params->{'is_active'} = 1; + @tracking_flags = @{Bugzilla::Extension::TrackingFlags::Flag->match($params)}; + } + else { + @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + } + + # Add tracking flags to fields hash replacing if already exists for our + # flag object instead of the usual Field.pm object + foreach my $flag (@tracking_flags) { + $field_hash{$flag->name} = $flag; + } + + @$$fields = sort { $a->sortkey <=> $b->sortkey } values %field_hash; } sub buglist_columns { - my ($self, $args) = @_; - my $columns = $args->{columns}; - my $dbh = Bugzilla->dbh; - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - $columns->{$flag->name} = { - name => "COALESCE(map_" . $flag->name . ".value, '---')", - title => $flag->description - }; - } + my ($self, $args) = @_; + my $columns = $args->{columns}; + my $dbh = Bugzilla->dbh; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + $columns->{$flag->name} = { + name => "COALESCE(map_" . $flag->name . ".value, '---')", + title => $flag->description + }; + } } sub buglist_column_joins { - my ($self, $args) = @_; - # if there are elements in the tracking_flags array, then they have been - # removed from the query, so we mustn't generate joins - return if scalar @{ $args->{search}->{tracking_flags} }; - - my $column_joins = $args->{'column_joins'}; - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - $column_joins->{$flag->name} = { - as => 'map_' . $flag->name, - table => 'tracking_flags_bugs', - extra => [ 'map_' . $flag->name . '.tracking_flag_id = ' . $flag->flag_id ] - }; - } + my ($self, $args) = @_; + + # if there are elements in the tracking_flags array, then they have been + # removed from the query, so we mustn't generate joins + return if scalar @{$args->{search}->{tracking_flags}}; + + my $column_joins = $args->{'column_joins'}; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + $column_joins->{$flag->name} = { + as => 'map_' . $flag->name, + table => 'tracking_flags_bugs', + extra => ['map_' . $flag->name . '.tracking_flag_id = ' . $flag->flag_id] + }; + } } sub bug_create_cf_accessors { - my ($self, $args) = @_; - # Create the custom accessors for the flag values - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - my $flag_name = $flag->name; - if (!Bugzilla::Bug->can($flag_name)) { - my $accessor = sub { - my $self = shift; - return $self->{$flag_name} if defined $self->{$flag_name}; - if (!exists $self->{'_tf_bug_values_preloaded'}) { - # preload all values currently set for this bug - my $bug_values - = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $self->id }); - foreach my $value (@$bug_values) { - $self->{$value->tracking_flag->name} = $value->value; - } - $self->{'_tf_bug_values_preloaded'} = 1; - } - return $self->{$flag_name} ||= '---'; - }; - no strict 'refs'; - *{"Bugzilla::Bug::$flag_name"} = $accessor; - } - if (!Bugzilla::Bug->can("set_$flag_name")) { - my $setter = sub { - my ($self, $value) = @_; - $value = ref($value) eq 'ARRAY' - ? $value->[0] - : $value; - $self->set($flag_name, $value); - }; - no strict 'refs'; - *{"Bugzilla::Bug::set_$flag_name"} = $setter; + my ($self, $args) = @_; + + # Create the custom accessors for the flag values + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + my $flag_name = $flag->name; + if (!Bugzilla::Bug->can($flag_name)) { + my $accessor = sub { + my $self = shift; + return $self->{$flag_name} if defined $self->{$flag_name}; + if (!exists $self->{'_tf_bug_values_preloaded'}) { + + # preload all values currently set for this bug + my $bug_values + = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({bug_id => $self->id}); + foreach my $value (@$bug_values) { + $self->{$value->tracking_flag->name} = $value->value; + } + $self->{'_tf_bug_values_preloaded'} = 1; } + return $self->{$flag_name} ||= '---'; + }; + no strict 'refs'; + *{"Bugzilla::Bug::$flag_name"} = $accessor; + } + if (!Bugzilla::Bug->can("set_$flag_name")) { + my $setter = sub { + my ($self, $value) = @_; + $value = ref($value) eq 'ARRAY' ? $value->[0] : $value; + $self->set($flag_name, $value); + }; + no strict 'refs'; + *{"Bugzilla::Bug::set_$flag_name"} = $setter; } + } } sub bug_editable_bug_fields { - my ($self, $args) = @_; - my $fields = $args->{'fields'}; - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - push(@$fields, $flag->name); - } + my ($self, $args) = @_; + my $fields = $args->{'fields'}; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + push(@$fields, $flag->name); + } } sub search_operator_field_override { - my ($self, $args) = @_; - my $operators = $args->{'operators'}; - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - foreach my $flag (@tracking_flags) { - $operators->{$flag->name} = { - _non_changed => sub { - _tracking_flags_search_nonchanged($flag->flag_id, @_) - } - }; - } + my ($self, $args) = @_; + my $operators = $args->{'operators'}; + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + foreach my $flag (@tracking_flags) { + $operators->{$flag->name} = { + _non_changed => sub { + _tracking_flags_search_nonchanged($flag->flag_id, @_); + } + }; + } } sub _tracking_flags_search_nonchanged { - my ($flag_id, $search, $args) = @_; - my ($bugs_table, $chart_id, $joins, $value, $operator) = - @$args{qw(bugs_table chart_id joins value operator)}; - my $dbh = Bugzilla->dbh; - - return if ($operator =~ m/^changed/); - - my $bugs_alias = "tracking_flags_bugs_$chart_id"; - my $flags_alias = "tracking_flags_$chart_id"; - - my $bugs_join = { - table => 'tracking_flags_bugs', - as => $bugs_alias, - from => $bugs_table . ".bug_id", - to => "bug_id", - extra => [$bugs_alias . ".tracking_flag_id = $flag_id"] - }; - - push(@$joins, $bugs_join); - - if ($operator eq 'isempty' or $operator eq 'isnotempty') { - $args->{'full_field'} = "$bugs_alias.value"; - } - else { - $args->{'full_field'} = "COALESCE($bugs_alias.value, '---')"; - } + my ($flag_id, $search, $args) = @_; + my ($bugs_table, $chart_id, $joins, $value, $operator) + = @$args{qw(bugs_table chart_id joins value operator)}; + my $dbh = Bugzilla->dbh; + + return if ($operator =~ m/^changed/); + + my $bugs_alias = "tracking_flags_bugs_$chart_id"; + my $flags_alias = "tracking_flags_$chart_id"; + + my $bugs_join = { + table => 'tracking_flags_bugs', + as => $bugs_alias, + from => $bugs_table . ".bug_id", + to => "bug_id", + extra => [$bugs_alias . ".tracking_flag_id = $flag_id"] + }; + + push(@$joins, $bugs_join); + + if ($operator eq 'isempty' or $operator eq 'isnotempty') { + $args->{'full_field'} = "$bugs_alias.value"; + } + else { + $args->{'full_field'} = "COALESCE($bugs_alias.value, '---')"; + } } sub request_cleanup { - foreach my $flag (@FLAG_CACHE) { - my $bug_flag = delete $flag->{bug_flag}; - if ($bug_flag) { - delete $bug_flag->{bug}; - delete $bug_flag->{tracking_flag}; - } - foreach my $value (@{ $flag->{values} }) { - delete $value->{tracking_flag}; - } + foreach my $flag (@FLAG_CACHE) { + my $bug_flag = delete $flag->{bug_flag}; + if ($bug_flag) { + delete $bug_flag->{bug}; + delete $bug_flag->{tracking_flag}; + } + foreach my $value (@{$flag->{values}}) { + delete $value->{tracking_flag}; } - @FLAG_CACHE = (); + } + @FLAG_CACHE = (); } sub bug_end_of_create { - my ($self, $args) = @_; - my $bug = $args->{'bug'}; - my $timestamp = $args->{'timestamp'}; - my $user = Bugzilla->user; + my ($self, $args) = @_; + my $bug = $args->{'bug'}; + my $timestamp = $args->{'timestamp'}; + my $user = Bugzilla->user; - my $params = Bugzilla->request_cache->{tracking_flags_create_params}; - return if !$params; + my $params = Bugzilla->request_cache->{tracking_flags_create_params}; + return if !$params; - my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - product => $bug->product, - component => $bug->component, - is_active => 1, + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ + product => $bug->product, component => $bug->component, is_active => 1, }); - foreach my $flag (@$tracking_flags) { - next if !$params->{$flag->name}; - foreach my $value (@{$flag->values}) { - next if $value->value ne $params->{$flag->name}; - next if $value->value eq '---'; # do not insert if value is '---', same as empty - if (!$flag->can_set_value($value->value)) { - ThrowUserError('tracking_flags_change_denied', - { flag => $flag, value => $value }); - } - Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ - tracking_flag_id => $flag->flag_id, - bug_id => $bug->id, - value => $value->value, - }); - # Add the name/value pair to the bug object - $bug->{$flag->name} = $value->value; - } + foreach my $flag (@$tracking_flags) { + next if !$params->{$flag->name}; + foreach my $value (@{$flag->values}) { + next if $value->value ne $params->{$flag->name}; + next if $value->value eq '---'; # do not insert if value is '---', same as empty + if (!$flag->can_set_value($value->value)) { + ThrowUserError('tracking_flags_change_denied', + {flag => $flag, value => $value}); + } + Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ + tracking_flag_id => $flag->flag_id, + bug_id => $bug->id, + value => $value->value, + }); + + # Add the name/value pair to the bug object + $bug->{$flag->name} = $value->value; } + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my $object = $args->{object}; - my $params = $args->{params}; + my ($self, $args) = @_; + my $object = $args->{object}; + my $params = $args->{params}; - return unless $object->isa('Bugzilla::Bug'); + return unless $object->isa('Bugzilla::Bug'); - # Do not filter by product/component as we may be changing those - my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - bug_id => $object->id, - is_active => 1, + # Do not filter by product/component as we may be changing those + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ + bug_id => $object->id, is_active => 1, }); - foreach my $flag (@$tracking_flags) { - my $flag_name = $flag->name; - if (exists $params->{$flag_name}) { - my $value = ref($params->{$flag_name}) eq 'ARRAY' - ? $params->{$flag_name}->[0] - : $params->{$flag_name}; - $object->set($flag_name, $value); - } + foreach my $flag (@$tracking_flags) { + my $flag_name = $flag->name; + if (exists $params->{$flag_name}) { + my $value + = ref($params->{$flag_name}) eq 'ARRAY' + ? $params->{$flag_name}->[0] + : $params->{$flag_name}; + $object->set($flag_name, $value); } + } } sub bug_check_can_change_field { - my ($self, $args) = @_; - my ($bug, $field, $old_value, $new_value, $priv_results) - = @$args{qw(bug field old_value new_value priv_results)}; - - return if $field !~ /^cf_/ or $old_value eq $new_value; - return unless my $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ name => $field }); - - if ($flag->can_set_value($new_value)) { - push @$priv_results, PRIVILEGES_REQUIRED_NONE; - } - else { - push @$priv_results, PRIVILEGES_REQUIRED_EMPOWERED; - } + my ($self, $args) = @_; + my ($bug, $field, $old_value, $new_value, $priv_results) + = @$args{qw(bug field old_value new_value priv_results)}; + + return if $field !~ /^cf_/ or $old_value eq $new_value; + return + unless my $flag + = Bugzilla::Extension::TrackingFlags::Flag->new({name => $field}); + + if ($flag->can_set_value($new_value)) { + push @$priv_results, PRIVILEGES_REQUIRED_NONE; + } + else { + push @$priv_results, PRIVILEGES_REQUIRED_EMPOWERED; + } } sub bug_end_of_update { - my ($self, $args) = @_; - my ($bug, $old_bug, $timestamp, $changes) - = @$args{qw(bug old_bug timestamp changes)}; - my $user = Bugzilla->user; - - # Do not filter by product/component as we may be changing those - my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match({ - bug_id => $bug->id, - is_active => 1, + my ($self, $args) = @_; + my ($bug, $old_bug, $timestamp, $changes) + = @$args{qw(bug old_bug timestamp changes)}; + my $user = Bugzilla->user; + + # Do not filter by product/component as we may be changing those + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({ + bug_id => $bug->id, is_active => 1, }); - my $product_id = $bug->product_id; - my $component_id = $bug->component_id; - my $is_visible = sub { - $_->product_id == $product_id && (!$_->component_id || $_->component_id == $component_id); - }; + my $product_id = $bug->product_id; + my $component_id = $bug->component_id; + my $is_visible = sub { + $_->product_id == $product_id + && (!$_->component_id || $_->component_id == $component_id); + }; + + my (@flag_changes); + foreach my $flag (@$tracking_flags) { + my $flag_name = $flag->name; + my $new_value = $bug->$flag_name; + my $old_value = $old_bug->$flag_name; + + if ($flag->bug_flag->id) { + my $visibility = $flag->visibility; + if (none { $is_visible->() } @$visibility) { + push(@flag_changes, {flag => $flag, added => '---', removed => $new_value}); + next; + } + } - my (@flag_changes); - foreach my $flag (@$tracking_flags) { - my $flag_name = $flag->name; - my $new_value = $bug->$flag_name; - my $old_value = $old_bug->$flag_name; - - if ($flag->bug_flag->id) { - my $visibility = $flag->visibility; - if (none { $is_visible->() } @$visibility) { - push(@flag_changes, { flag => $flag, - added => '---', - removed => $new_value }); - next; - } - } + if ($new_value ne $old_value) { - if ($new_value ne $old_value) { - # Do not allow if the user cannot set the old value or the new value - if (!$flag->can_set_value($new_value)) { - ThrowUserError('tracking_flags_change_denied', - { flag => $flag, value => $new_value }); - } - push(@flag_changes, { flag => $flag, - added => $new_value, - removed => $old_value }); - } + # Do not allow if the user cannot set the old value or the new value + if (!$flag->can_set_value($new_value)) { + ThrowUserError('tracking_flags_change_denied', + {flag => $flag, value => $new_value}); + } + push(@flag_changes, + {flag => $flag, added => $new_value, removed => $old_value}); } + } - foreach my $change (@flag_changes) { - my $flag = $change->{'flag'}; - my $added = $change->{'added'}; - my $removed = $change->{'removed'}; + foreach my $change (@flag_changes) { + my $flag = $change->{'flag'}; + my $added = $change->{'added'}; + my $removed = $change->{'removed'}; - if ($added eq '---') { - $flag->bug_flag->remove_from_db(); - } - elsif ($removed eq '---') { - Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ - tracking_flag_id => $flag->flag_id, - bug_id => $bug->id, - value => $added, - }); - } - else { - $flag->bug_flag->set_value($added); - $flag->bug_flag->update($timestamp); - } + if ($added eq '---') { + $flag->bug_flag->remove_from_db(); + } + elsif ($removed eq '---') { + Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ + tracking_flag_id => $flag->flag_id, bug_id => $bug->id, value => $added, + }); + } + else { + $flag->bug_flag->set_value($added); + $flag->bug_flag->update($timestamp); + } - $changes->{$flag->name} = [ $removed, $added ]; - LogActivityEntry($bug->id, $flag->name, $removed, $added, $user->id, $timestamp); + $changes->{$flag->name} = [$removed, $added]; + LogActivityEntry($bug->id, $flag->name, $removed, $added, $user->id, + $timestamp); - # Update the name/value pair in the bug object - $bug->{$flag->name} = $added; - } + # Update the name/value pair in the bug object + $bug->{$flag->name} = $added; + } } sub bug_end_of_create_validators { - my ($self, $args) = @_; - my $params = $args->{params}; - - # We need to stash away any params that are setting/updating tracking - # flags early on. Otherwise set_all or insert_create_data will complain. - my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; - my $cache = Bugzilla->request_cache->{tracking_flags_create_params} ||= {}; - foreach my $flag (@tracking_flags) { - my $flag_name = $flag->name; - if (defined $params->{$flag_name}) { - $cache->{$flag_name} = delete $params->{$flag_name}; - } + my ($self, $args) = @_; + my $params = $args->{params}; + + # We need to stash away any params that are setting/updating tracking + # flags early on. Otherwise set_all or insert_create_data will complain. + my @tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->get_all; + my $cache = Bugzilla->request_cache->{tracking_flags_create_params} ||= {}; + foreach my $flag (@tracking_flags) { + my $flag_name = $flag->name; + if (defined $params->{$flag_name}) { + $cache->{$flag_name} = delete $params->{$flag_name}; } + } } sub mailer_before_send { - my ($self, $args) = @_; - my $email = $args->{email}; + my ($self, $args) = @_; + my $email = $args->{email}; - # Add X-Bugzilla-Tracking header or add to it - # if already exists - if ($email->header('X-Bugzilla-ID')) { - my $bug_id = $email->header('X-Bugzilla-ID'); + # Add X-Bugzilla-Tracking header or add to it + # if already exists + if ($email->header('X-Bugzilla-ID')) { + my $bug_id = $email->header('X-Bugzilla-ID'); - my $tracking_flags - = Bugzilla::Extension::TrackingFlags::Flag->match({ bug_id => $bug_id }); + my $tracking_flags + = Bugzilla::Extension::TrackingFlags::Flag->match({bug_id => $bug_id}); - my @set_values = (); - foreach my $flag (@$tracking_flags) { - next if $flag->bug_flag->value eq '---'; - push(@set_values, $flag->description . ":" . $flag->bug_flag->value); - } + my @set_values = (); + foreach my $flag (@$tracking_flags) { + next if $flag->bug_flag->value eq '---'; + push(@set_values, $flag->description . ":" . $flag->bug_flag->value); + } - if (@set_values) { - my $set_values_string = join(' ', @set_values); - if ($email->header('X-Bugzilla-Tracking')) { - $set_values_string = $email->header('X-Bugzilla-Tracking') . - " " . $set_values_string; - } - $email->header_set('X-Bugzilla-Tracking' => $set_values_string); - } + if (@set_values) { + my $set_values_string = join(' ', @set_values); + if ($email->header('X-Bugzilla-Tracking')) { + $set_values_string + = $email->header('X-Bugzilla-Tracking') . " " . $set_values_string; + } + $email->header_set('X-Bugzilla-Tracking' => $set_values_string); } + } } # Purpose: generically handle generating pretty blocking/status "flags" from # custom field names. sub quicksearch_map { - my ($self, $args) = @_; - my $map = $args->{'map'}; - - foreach my $name (keys %$map) { - if ($name =~ /^cf_(blocking|tracking|status)_([a-z]+)?(\d+)?$/) { - my $type = $1; - my $product = $2; - my $version = $3; - - if ($version) { - $version = join('.', split(//, $version)); - } - - my $pretty_name = $type; - if ($product) { - $pretty_name .= "-" . $product; - } - if ($version) { - $pretty_name .= $version; - } - - $map->{$pretty_name} = $name; - } + my ($self, $args) = @_; + my $map = $args->{'map'}; + + foreach my $name (keys %$map) { + if ($name =~ /^cf_(blocking|tracking|status)_([a-z]+)?(\d+)?$/) { + my $type = $1; + my $product = $2; + my $version = $3; + + if ($version) { + $version = join('.', split(//, $version)); + } + + my $pretty_name = $type; + if ($product) { + $pretty_name .= "-" . $product; + } + if ($version) { + $pretty_name .= $version; + } + + $map->{$pretty_name} = $name; } + } } sub reorg_move_component { - my ($self, $args) = @_; - my $new_product = $args->{new_product}; - my $component = $args->{component}; - - Bugzilla->dbh->do( - "UPDATE tracking_flags_visibility SET product_id=? WHERE component_id=?", - undef, - $new_product->id, $component->id, - ); + my ($self, $args) = @_; + my $new_product = $args->{new_product}; + my $component = $args->{component}; + + Bugzilla->dbh->do( + "UPDATE tracking_flags_visibility SET product_id=? WHERE component_id=?", + undef, $new_product->id, $component->id,); } sub sanitycheck_check { - my ($self, $args) = @_; - my $status = $args->{status}; + my ($self, $args) = @_; + my $status = $args->{status}; - $status->('tracking_flags_check'); + $status->('tracking_flags_check'); - my ($count) = Bugzilla->dbh->selectrow_array(" + my ($count) = Bugzilla->dbh->selectrow_array(" SELECT COUNT(*) FROM tracking_flags_visibility INNER JOIN components ON components.id = tracking_flags_visibility.component_id WHERE tracking_flags_visibility.product_id <> components.product_id "); - if ($count) { - $status->('tracking_flags_alert', undef, 'alert'); - $status->('tracking_flags_repair'); - } + if ($count) { + $status->('tracking_flags_alert', undef, 'alert'); + $status->('tracking_flags_repair'); + } } sub sanitycheck_repair { - my ($self, $args) = @_; - return unless Bugzilla->cgi->param('tracking_flags_repair'); + my ($self, $args) = @_; + return unless Bugzilla->cgi->param('tracking_flags_repair'); - my $status = $args->{'status'}; - my $dbh = Bugzilla->dbh; - $status->('tracking_flags_repairing'); + my $status = $args->{'status'}; + my $dbh = Bugzilla->dbh; + $status->('tracking_flags_repairing'); - my $rows = $dbh->selectall_arrayref(" + my $rows = $dbh->selectall_arrayref(" SELECT DISTINCT tracking_flags_visibility.product_id AS bad_product_id, components.product_id AS good_product_id, tracking_flags_visibility.component_id FROM tracking_flags_visibility INNER JOIN components ON components.id = tracking_flags_visibility.component_id WHERE tracking_flags_visibility.product_id <> components.product_id - ", - { Slice => {} } - ); - foreach my $row (@$rows) { - $dbh->do(" + ", {Slice => {}}); + foreach my $row (@$rows) { + $dbh->do(" UPDATE tracking_flags_visibility SET product_id=? WHERE product_id=? AND component_id=? - ", undef, - $row->{good_product_id}, - $row->{bad_product_id}, - $row->{component_id}, - ); - } + ", undef, $row->{good_product_id}, $row->{bad_product_id}, + $row->{component_id},); + } } __PACKAGE__->NAME; diff --git a/extensions/TrackingFlags/bin/bug_825946.pl b/extensions/TrackingFlags/bin/bug_825946.pl index 896dc5448..8a340175b 100755 --- a/extensions/TrackingFlags/bin/bug_825946.pl +++ b/extensions/TrackingFlags/bin/bug_825946.pl @@ -13,8 +13,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Constants qw( USAGE_MODE_CMDLINE ); @@ -51,8 +51,8 @@ SQL my %visible; foreach my $row (@$tf_vis) { - my ($tracking_flag_id, $product_id, $component_id) = @$row; - $visible{$tracking_flag_id}{$product_id}{$component_id // 'ALL'} = 1; + my ($tracking_flag_id, $product_id, $component_id) = @$row; + $visible{$tracking_flag_id}{$product_id}{$component_id // 'ALL'} = 1; } my %bugs = map { $_->[0] => 1 } @$tf_bugs; @@ -66,13 +66,16 @@ my $removed = 0; $dbh->bz_start_transaction(); my ($timestamp) = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); foreach my $tf_bug (@$tf_bugs) { - my ($flag_name, $value, $bug_id, $tf_id, $product_id, $component_id) = @$tf_bug; - unless ($visible{$tf_id}{$product_id}{$component_id} || $visible{$tf_id}{$product_id}{ALL}) { - $dbh->do("DELETE FROM tracking_flags_bugs WHERE tracking_flag_id = ? AND bug_id = ?", - undef, $tf_id, $bug_id); - LogActivityEntry($bug_id, $flag_name, $value, '---', $user->id, $timestamp); - $removed++; - } + my ($flag_name, $value, $bug_id, $tf_id, $product_id, $component_id) = @$tf_bug; + unless ($visible{$tf_id}{$product_id}{$component_id} + || $visible{$tf_id}{$product_id}{ALL}) + { + $dbh->do( + "DELETE FROM tracking_flags_bugs WHERE tracking_flag_id = ? AND bug_id = ?", + undef, $tf_id, $bug_id); + LogActivityEntry($bug_id, $flag_name, $value, '---', $user->id, $timestamp); + $removed++; + } } $dbh->bz_commit_transaction(); diff --git a/extensions/TrackingFlags/bin/bulk_flag_clear.pl b/extensions/TrackingFlags/bin/bulk_flag_clear.pl index 305fbf883..470269f13 100755 --- a/extensions/TrackingFlags/bin/bulk_flag_clear.pl +++ b/extensions/TrackingFlags/bin/bulk_flag_clear.pl @@ -13,8 +13,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Constants; @@ -27,21 +27,14 @@ use Getopt::Long; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my $config = {}; -GetOptions( - $config, - "trace=i", - "update_db", - "flag=s", - "modified_before=s", - "modified_after=s", - "value=s" -) or exit; +GetOptions($config, "trace=i", "update_db", "flag=s", "modified_before=s", + "modified_after=s", "value=s") + or exit; unless ($config->{flag} - && ($config->{modified_before} - || $config->{modified_after} - || $config->{value})) + && ($config->{modified_before} || $config->{modified_after} || $config->{value}) + ) { - die <check({ name => $config->{flag} }); -push @where, 'tracking_flags_bugs.tracking_flag_id = ?'; +my $flag + = Bugzilla::Extension::TrackingFlags::Flag->check({name => $config->{flag}}); +push @where, 'tracking_flags_bugs.tracking_flag_id = ?'; push @values, $flag->flag_id; if ($config->{modified_before}) { - push @where, 'bugs.delta_ts < ?'; - push @values, $config->{modified_before}; + push @where, 'bugs.delta_ts < ?'; + push @values, $config->{modified_before}; } if ($config->{modified_after}) { - push @where, 'bugs.delta_ts > ?'; - push @values, $config->{modified_after}; + push @where, 'bugs.delta_ts > ?'; + push @values, $config->{modified_after}; } if ($config->{value}) { - push @where, 'tracking_flags_bugs.value = ?'; - push @values, $config->{value}; + push @where, 'tracking_flags_bugs.value = ?'; + push @values, $config->{value}; } my $sql = " @@ -99,37 +93,35 @@ $dbh->{TraceLevel} = $config->{trace} if $config->{trace}; my $bug_ids = $dbh->selectcol_arrayref($sql, undef, @values); if (!@$bug_ids) { - die "no matching bugs found\n"; + die "no matching bugs found\n"; } if (!$config->{update_db}) { - print "bugs found: ", scalar(@$bug_ids), "\n\n", join(',', @$bug_ids), "\n\n"; - print "--update_db not provided, no changes made to the database\n"; - exit; + print "bugs found: ", scalar(@$bug_ids), "\n\n", join(',', @$bug_ids), "\n\n"; + print "--update_db not provided, no changes made to the database\n"; + exit; } # update bugs -my $nobody = Bugzilla::User->check({ name => Bugzilla->params->{'nobody_user'} }); +my $nobody = Bugzilla::User->check({name => Bugzilla->params->{'nobody_user'}}); + # put our nobody user into all groups to avoid permissions issues $nobody->{groups} = [Bugzilla::Group->get_all]; Bugzilla->set_user($nobody); foreach my $bug_id (@$bug_ids) { - print "updating bug $bug_id\n"; - $dbh->bz_start_transaction; - - # update the bug - # this will deal with history for us but not send bugmail - my $bug = Bugzilla::Bug->check({ id => $bug_id }); - $bug->set_all({ $flag->name => '---' }); - $bug->update; - - # update lastdiffed to skip bugmail for this change - $dbh->do( - "UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?", - undef, - $bug->id - ); - $dbh->bz_commit_transaction; + print "updating bug $bug_id\n"; + $dbh->bz_start_transaction; + + # update the bug + # this will deal with history for us but not send bugmail + my $bug = Bugzilla::Bug->check({id => $bug_id}); + $bug->set_all({$flag->name => '---'}); + $bug->update; + + # update lastdiffed to skip bugmail for this change + $dbh->do("UPDATE bugs SET lastdiffed = delta_ts WHERE bug_id = ?", + undef, $bug->id); + $dbh->bz_commit_transaction; } diff --git a/extensions/TrackingFlags/bin/migrate_tracking_flags.pl b/extensions/TrackingFlags/bin/migrate_tracking_flags.pl index cd55f5f83..97b8eccd5 100755 --- a/extensions/TrackingFlags/bin/migrate_tracking_flags.pl +++ b/extensions/TrackingFlags/bin/migrate_tracking_flags.pl @@ -16,8 +16,8 @@ use 5.10.1; use lib qw(. lib local/lib/perl5); BEGIN { - use Bugzilla; - Bugzilla->extensions; + use Bugzilla; + Bugzilla->extensions; } use Bugzilla::Constants; @@ -39,10 +39,7 @@ use Data::Dumper; Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my ($dry_run, $trace) = (0, 0); -GetOptions( - "dry-run" => \$dry_run, - "trace" => \$trace, -) or exit; +GetOptions("dry-run" => \$dry_run, "trace" => \$trace,) or exit; my $dbh = Bugzilla->dbh; @@ -52,263 +49,271 @@ my %product_cache; my %component_cache; sub migrate_flag_visibility { - my ($new_flag, $products) = @_; + my ($new_flag, $products) = @_; + + # Create product/component visibility + foreach my $prod_name (keys %$products) { + $product_cache{$prod_name} ||= Bugzilla::Product->new({name => $prod_name}); + if (!$product_cache{$prod_name}) { + warn "No such product $prod_name\n"; + next; + } - # Create product/component visibility - foreach my $prod_name (keys %$products) { - $product_cache{$prod_name} ||= Bugzilla::Product->new({ name => $prod_name }); - if (!$product_cache{$prod_name}) { - warn "No such product $prod_name\n"; - next; + # If no components specified then we do Product/__any__ + # otherwise, we enter an entry for each Product/Component + my $components = $products->{$prod_name}; + if (!@$components) { + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $new_flag->flag_id, + product_id => $product_cache{$prod_name}->id, + component_id => undef + }); + } + else { + foreach my $comp_name (@$components) { + my $comp_matches = []; + + # If the component is a regexp, we need to find all components + # matching the regex and insert each individually + if (ref $comp_name eq 'Regexp') { + my $comp_re = $comp_name; + $comp_re =~ s/\?\-xism://; + $comp_re =~ s/\(//; + $comp_re =~ s/\)//; + $comp_matches = $dbh->selectcol_arrayref( + 'SELECT components.name FROM components + WHERE components.product_id = ? + AND ' + . $dbh->sql_regexp('components.name', $dbh->quote($comp_re)) . ' + ORDER BY components.name', undef, $product_cache{$prod_name}->id + ); + } + else { + $comp_matches = [$comp_name]; } - # If no components specified then we do Product/__any__ - # otherwise, we enter an entry for each Product/Component - my $components = $products->{$prod_name}; - if (!@$components) { - Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ - tracking_flag_id => $new_flag->flag_id, - product_id => $product_cache{$prod_name}->id, - component_id => undef + foreach my $comp_match (@$comp_matches) { + $component_cache{"${prod_name}:${comp_match}"} + ||= Bugzilla::Component->new({ + name => $comp_match, product => $product_cache{$prod_name} }); + if (!$component_cache{"${prod_name}:${comp_match}"}) { + warn "No such product $prod_name and component $comp_match\n"; + next; + } + + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $new_flag->flag_id, + product_id => $product_cache{$prod_name}->id, + component_id => $component_cache{"${prod_name}:${comp_match}"}->id, + }); } - else { - foreach my $comp_name (@$components) { - my $comp_matches = []; - # If the component is a regexp, we need to find all components - # matching the regex and insert each individually - if (ref $comp_name eq 'Regexp') { - my $comp_re = $comp_name; - $comp_re =~ s/\?\-xism://; - $comp_re =~ s/\(//; - $comp_re =~ s/\)//; - $comp_matches = $dbh->selectcol_arrayref( - 'SELECT components.name FROM components - WHERE components.product_id = ? - AND ' . $dbh->sql_regexp('components.name', $dbh->quote($comp_re)) . ' - ORDER BY components.name', - undef, - $product_cache{$prod_name}->id); - } - else { - $comp_matches = [ $comp_name ]; - } - - foreach my $comp_match (@$comp_matches) { - $component_cache{"${prod_name}:${comp_match}"} - ||= Bugzilla::Component->new({ name => $comp_match, - product => $product_cache{$prod_name} }); - if (!$component_cache{"${prod_name}:${comp_match}"}) { - warn "No such product $prod_name and component $comp_match\n"; - next; - } - - Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ - tracking_flag_id => $new_flag->flag_id, - product_id => $product_cache{$prod_name}->id, - component_id => $component_cache{"${prod_name}:${comp_match}"}->id, - }); - } - } - } + } } + } } sub migrate_flag_values { - my ($new_flag, $field) = @_; - - print "Migrating flag values..."; - - my %blocking_trusted_requesters - = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_requesters}; - my %blocking_trusted_setters - = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_setters}; - my %status_trusted_wanters - = %{$Bugzilla::Extension::BMO::Data::status_trusted_wanters}; - my %status_trusted_setters - = %{$Bugzilla::Extension::BMO::Data::status_trusted_setters}; - - my %group_cache; - foreach my $value (@{ $field->legal_values }) { - my $group_name = 'everyone'; - - if ($field->name =~ /^cf_(blocking|tracking)_/) { - if ($value->name ne '---' && $value->name !~ '\?$') { - $group_name = get_setter_group($field->name, \%blocking_trusted_setters); - } - if ($value->name eq '?') { - $group_name = get_setter_group($field->name, \%blocking_trusted_requesters); - } - } elsif ($field->name =~ /^cf_status_/) { - if ($value->name eq 'wanted') { - $group_name = get_setter_group($field->name, \%status_trusted_wanters); - } elsif ($value->name ne '---' && $value->name ne '?') { - $group_name = get_setter_group($field->name, \%status_trusted_setters); - } - } + my ($new_flag, $field) = @_; + + print "Migrating flag values..."; + + my %blocking_trusted_requesters + = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_requesters}; + my %blocking_trusted_setters + = %{$Bugzilla::Extension::BMO::Data::blocking_trusted_setters}; + my %status_trusted_wanters + = %{$Bugzilla::Extension::BMO::Data::status_trusted_wanters}; + my %status_trusted_setters + = %{$Bugzilla::Extension::BMO::Data::status_trusted_setters}; + + my %group_cache; + foreach my $value (@{$field->legal_values}) { + my $group_name = 'everyone'; + + if ($field->name =~ /^cf_(blocking|tracking)_/) { + if ($value->name ne '---' && $value->name !~ '\?$') { + $group_name = get_setter_group($field->name, \%blocking_trusted_setters); + } + if ($value->name eq '?') { + $group_name = get_setter_group($field->name, \%blocking_trusted_requesters); + } + } + elsif ($field->name =~ /^cf_status_/) { + if ($value->name eq 'wanted') { + $group_name = get_setter_group($field->name, \%status_trusted_wanters); + } + elsif ($value->name ne '---' && $value->name ne '?') { + $group_name = get_setter_group($field->name, \%status_trusted_setters); + } + } - $group_cache{$group_name} ||= Bugzilla::Group->new({ name => $group_name }); - $group_cache{$group_name} || die "Setter group '$group_name' does not exist"; + $group_cache{$group_name} ||= Bugzilla::Group->new({name => $group_name}); + $group_cache{$group_name} || die "Setter group '$group_name' does not exist"; - Bugzilla::Extension::TrackingFlags::Flag::Value->create({ - tracking_flag_id => $new_flag->flag_id, - value => $value->name, - setter_group_id => $group_cache{$group_name}->id, - sortkey => $value->sortkey, - is_active => $value->is_active - }); - } + Bugzilla::Extension::TrackingFlags::Flag::Value->create({ + tracking_flag_id => $new_flag->flag_id, + value => $value->name, + setter_group_id => $group_cache{$group_name}->id, + sortkey => $value->sortkey, + is_active => $value->is_active + }); + } - print "done.\n"; + print "done.\n"; } sub get_setter_group { - my ($field, $trusted) = @_; - my $setter_group = $trusted->{'_default'} || ""; - foreach my $dfield (keys %$trusted) { - if ($field =~ $dfield) { - $setter_group = $trusted->{$dfield}; - } + my ($field, $trusted) = @_; + my $setter_group = $trusted->{'_default'} || ""; + foreach my $dfield (keys %$trusted) { + if ($field =~ $dfield) { + $setter_group = $trusted->{$dfield}; } - return $setter_group; + } + return $setter_group; } sub migrate_flag_bugs { - my ($new_flag, $field) = @_; + my ($new_flag, $field) = @_; - print "Migrating bug values..."; + print "Migrating bug values..."; - my $bugs = $dbh->selectall_arrayref("SELECT bug_id, " . $field->name . " + my $bugs = $dbh->selectall_arrayref( + "SELECT bug_id, " . $field->name . " FROM bugs WHERE " . $field->name . " != '---' - ORDER BY bug_id"); - local $| = 1; - my $count = 1; - my $total = scalar @$bugs; - foreach my $row (@$bugs) { - my ($id, $value) = @$row; - indicate_progress({ current => $count++, total => $total, every => 25 }); - Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ - tracking_flag_id => $new_flag->flag_id, - bug_id => $id, - value => $value, - - }); - } - - print "done.\n"; + ORDER BY bug_id" + ); + local $| = 1; + my $count = 1; + my $total = scalar @$bugs; + foreach my $row (@$bugs) { + my ($id, $value) = @$row; + indicate_progress({current => $count++, total => $total, every => 25}); + Bugzilla::Extension::TrackingFlags::Flag::Bug->create({ + tracking_flag_id => $new_flag->flag_id, + bug_id => $id, + value => $value, + + }); + } + + print "done.\n"; } sub migrate_flag_activity { - my ($new_flag, $field) = @_; + my ($new_flag, $field) = @_; - print "Migating flag activity..."; + print "Migating flag activity..."; - my $new_field = Bugzilla::Field->new({ name => $new_flag->name }); - $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE fieldid = ?", - undef, $new_field->id, $field->id); + my $new_field = Bugzilla::Field->new({name => $new_flag->name}); + $dbh->do("UPDATE bugs_activity SET fieldid = ? WHERE fieldid = ?", + undef, $new_field->id, $field->id); - print "done.\n"; + print "done.\n"; } sub do_migration { - my $bmo_tracking_flags = $Bugzilla::Extension::BMO::Data::cf_visible_in_products; - my $bmo_project_flags = $Bugzilla::Extension::BMO::Data::cf_project_flags; - my $bmo_disabled_flags = $Bugzilla::Extension::BMO::Data::cf_disabled_flags; + my $bmo_tracking_flags + = $Bugzilla::Extension::BMO::Data::cf_visible_in_products; + my $bmo_project_flags = $Bugzilla::Extension::BMO::Data::cf_project_flags; + my $bmo_disabled_flags = $Bugzilla::Extension::BMO::Data::cf_disabled_flags; - my $fields = Bugzilla::Field->match({ custom => 1, - type => FIELD_TYPE_SINGLE_SELECT }); + my $fields + = Bugzilla::Field->match({custom => 1, type => FIELD_TYPE_SINGLE_SELECT}); - my @drop_columns; - foreach my $field (@$fields) { - next if $field->name !~ /^cf_(blocking|tracking|status)_/; + my @drop_columns; + foreach my $field (@$fields) { + next if $field->name !~ /^cf_(blocking|tracking|status)_/; - foreach my $field_re (keys %$bmo_tracking_flags) { - next if $field->name !~ $field_re; + foreach my $field_re (keys %$bmo_tracking_flags) { + next if $field->name !~ $field_re; - # Create the new tracking flag if not exists - my $new_flag - = Bugzilla::Extension::TrackingFlags::Flag->new({ name => $field->name }); + # Create the new tracking flag if not exists + my $new_flag + = Bugzilla::Extension::TrackingFlags::Flag->new({name => $field->name}); - next if $new_flag; + next if $new_flag; - print "----------------------------------\n" . - "Migrating custom tracking field " . $field->name . "...\n"; + print "----------------------------------\n" + . "Migrating custom tracking field " + . $field->name . "...\n"; - my $new_flag_name = $field->name . "_new"; # Temporary name til we delete the old + my $new_flag_name = $field->name . "_new"; # Temporary name til we delete the old - my $type = grep($field->name =~ $_, @$bmo_project_flags) - ? 'project' - : 'tracking'; + my $type + = grep($field->name =~ $_, @$bmo_project_flags) ? 'project' : 'tracking'; - my $is_active = grep($_ eq $field->name, @$bmo_disabled_flags) ? 0 : 1; + my $is_active = grep($_ eq $field->name, @$bmo_disabled_flags) ? 0 : 1; - $new_flag = Bugzilla::Extension::TrackingFlags::Flag->create({ - name => $new_flag_name, - description => $field->description, - type => $type, - sortkey => $field->sortkey, - is_active => $is_active, - enter_bug => $field->enter_bug, - }); + $new_flag = Bugzilla::Extension::TrackingFlags::Flag->create({ + name => $new_flag_name, + description => $field->description, + type => $type, + sortkey => $field->sortkey, + is_active => $is_active, + enter_bug => $field->enter_bug, + }); - migrate_flag_visibility($new_flag, $bmo_tracking_flags->{$field_re}); + migrate_flag_visibility($new_flag, $bmo_tracking_flags->{$field_re}); - migrate_flag_values($new_flag, $field); + migrate_flag_values($new_flag, $field); - migrate_flag_bugs($new_flag, $field); + migrate_flag_bugs($new_flag, $field); - migrate_flag_activity($new_flag, $field); + migrate_flag_activity($new_flag, $field); - push(@drop_columns, $field->name); + push(@drop_columns, $field->name); - # Remove the old flag entry from fielddefs - $dbh->do("DELETE FROM fielddefs WHERE name = ?", - undef, $field->name); + # Remove the old flag entry from fielddefs + $dbh->do("DELETE FROM fielddefs WHERE name = ?", undef, $field->name); - # Rename the new flag - $dbh->do("UPDATE fielddefs SET name = ? WHERE name = ?", - undef, $field->name, $new_flag_name); + # Rename the new flag + $dbh->do("UPDATE fielddefs SET name = ? WHERE name = ?", + undef, $field->name, $new_flag_name); - $new_flag->set_name($field->name); - $new_flag->update; + $new_flag->set_name($field->name); + $new_flag->update; - # more than one regex could possibly match but we only want the first one - last; - } + # more than one regex could possibly match but we only want the first one + last; } + } - # Drop each custom flag's value table and the column from the bz schema object - if (!$dry_run && @drop_columns) { - print "Dropping value tables and updating bz schema object...\n"; + # Drop each custom flag's value table and the column from the bz schema object + if (!$dry_run && @drop_columns) { + print "Dropping value tables and updating bz schema object...\n"; - foreach my $column (@drop_columns) { - # Drop the values table - $dbh->bz_drop_table($column); + foreach my $column (@drop_columns) { - # Drop the bugs table column from the bz schema object - $dbh->_bz_real_schema->delete_column('bugs', $column); - $dbh->_bz_store_real_schema; - } + # Drop the values table + $dbh->bz_drop_table($column); - # Do the one alter table to drop all columns at once - $dbh->do("ALTER TABLE bugs DROP COLUMN " . join(", DROP COLUMN ", @drop_columns)); + # Drop the bugs table column from the bz schema object + $dbh->_bz_real_schema->delete_column('bugs', $column); + $dbh->_bz_store_real_schema; } + + # Do the one alter table to drop all columns at once + $dbh->do( + "ALTER TABLE bugs DROP COLUMN " . join(", DROP COLUMN ", @drop_columns)); + } } # Start Main eval { - if ($dry_run) { - print "** dry run : no changes to the database will be made **\n"; - $dbh->bz_start_transaction(); - } - print "Starting migration...\n"; - do_migration(); - $dbh->bz_rollback_transaction() if $dry_run; - print "All done!\n"; + if ($dry_run) { + print "** dry run : no changes to the database will be made **\n"; + $dbh->bz_start_transaction(); + } + print "Starting migration...\n"; + do_migration(); + $dbh->bz_rollback_transaction() if $dry_run; + print "All done!\n"; }; if ($@) { - $dbh->bz_rollback_transaction() if $dry_run; - die "$@" if $@; + $dbh->bz_rollback_transaction() if $dry_run; + die "$@" if $@; } diff --git a/extensions/TrackingFlags/lib/Admin.pm b/extensions/TrackingFlags/lib/Admin.pm index 50a0e0a61..e6ff9a31a 100644 --- a/extensions/TrackingFlags/lib/Admin.pm +++ b/extensions/TrackingFlags/lib/Admin.pm @@ -30,8 +30,8 @@ use Scalar::Util qw(blessed); use base qw(Exporter); our @EXPORT = qw( - admin_list - admin_edit + admin_list + admin_edit ); # @@ -39,177 +39,189 @@ our @EXPORT = qw( # sub admin_list { - my ($vars) = @_; - $vars->{show_bug_counts} = Bugzilla->input_params->{show_bug_counts}; - $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; + my ($vars) = @_; + $vars->{show_bug_counts} = Bugzilla->input_params->{show_bug_counts}; + $vars->{flags} = [Bugzilla::Extension::TrackingFlags::Flag->get_all()]; } sub admin_edit { - my ($vars, $page) = @_; - my $input = Bugzilla->input_params; - - $vars->{groups} = _groups_to_json(); - $vars->{mode} = $input->{mode} || 'new'; - $vars->{flag_id} = $input->{flag_id} || 0; - $vars->{tracking_flag_types} = FLAG_TYPES; - - if ($input->{delete}) { - my $token = $input->{token}; - check_hash_token($token, ['tracking_flags_edit']); - delete_token($token); - - my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); - $flag->remove_from_db(); - - $vars->{message} = 'tracking_flag_deleted'; - $vars->{flag} = $flag; - $vars->{flags} = [ Bugzilla::Extension::TrackingFlags::Flag->get_all() ]; - - print Bugzilla->cgi->header; - my $template = Bugzilla->template; - $template->process('pages/tracking_flags_admin_list.html.tmpl', $vars) - || ThrowTemplateError($template->error()); - exit; - - } elsif ($input->{save}) { - my $token = $input->{token}; - check_hash_token($token, ['tracking_flags_edit']); - delete_token($token); - - my ($flag, $values, $visibilities) = _load_from_input($input, $vars); - _validate($flag, $values, $visibilities); - my $flag_obj = _update_db($flag, $values, $visibilities); - - $vars->{flag} = $flag_obj; - $vars->{values} = _flag_values_to_json($values); - $vars->{visibility} = _flag_visibility_to_json($visibilities); - - if ($vars->{mode} eq 'new') { - $vars->{message} = 'tracking_flag_created'; - } else { - $vars->{message} = 'tracking_flag_updated'; - } - $vars->{mode} = 'edit'; - - } else { - # initial load - - if ($vars->{mode} eq 'edit') { - # edit - straight load - my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{flag_id} }); - $vars->{flag} = $flag; - $vars->{values} = _flag_values_to_json($flag->values); - $vars->{visibility} = _flag_visibility_to_json($flag->visibility); - $vars->{can_delete} = !$flag->bug_count; - - } elsif ($vars->{mode} eq 'copy') { - # copy - load the source flag - $vars->{mode} = 'new'; - my $flag = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $vars->{copy_from} }); - - # increment the number at the end of the name and description - if ($flag->name =~ /^(\D+)(\d+)$/) { - $flag->set_name("$1" . ($2 + 1)); - } - if ($flag->description =~ /^(\D+)([\d\.]+)$/) { - my $description = $1; - my $version = $2; - if ($version =~ /\./) { - my ($major, $minor) = split(/\./, $version); - $minor++; - $version = "$major.$minor"; - } - else { - $version++; - } - $flag->set_description($description . $version); - } - $flag->set_sortkey(_next_unique_sortkey($flag->sortkey)); - $flag->set_type($flag->flag_type); - $flag->set_enter_bug($flag->enter_bug); - # always default new flags as active, even when copying an inactive one - $flag->set_is_active(1); - - $vars->{flag} = $flag; - $vars->{values} = _flag_values_to_json($flag->values, 1); - $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1); - $vars->{can_delete} = 0; - - } else { - $vars->{mode} = 'new'; - $vars->{flag} = { - sortkey => 0, - enter_bug => 1, - is_active => 1, - }; - $vars->{values} = _flag_values_to_json([ - { - id => 0, - value => '---', - setter_group_id => '', - is_active => 1, - comment => '', - }, - ]); - $vars->{visibility} = ''; - $vars->{can_delete} = 0; - } + my ($vars, $page) = @_; + my $input = Bugzilla->input_params; + + $vars->{groups} = _groups_to_json(); + $vars->{mode} = $input->{mode} || 'new'; + $vars->{flag_id} = $input->{flag_id} || 0; + $vars->{tracking_flag_types} = FLAG_TYPES; + + if ($input->{delete}) { + my $token = $input->{token}; + check_hash_token($token, ['tracking_flags_edit']); + delete_token($token); + + my $flag + = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $vars->{flag_id}}); + $flag->remove_from_db(); + + $vars->{message} = 'tracking_flag_deleted'; + $vars->{flag} = $flag; + $vars->{flags} = [Bugzilla::Extension::TrackingFlags::Flag->get_all()]; + + print Bugzilla->cgi->header; + my $template = Bugzilla->template; + $template->process('pages/tracking_flags_admin_list.html.tmpl', $vars) + || ThrowTemplateError($template->error()); + exit; + + } + elsif ($input->{save}) { + my $token = $input->{token}; + check_hash_token($token, ['tracking_flags_edit']); + delete_token($token); + + my ($flag, $values, $visibilities) = _load_from_input($input, $vars); + _validate($flag, $values, $visibilities); + my $flag_obj = _update_db($flag, $values, $visibilities); + + $vars->{flag} = $flag_obj; + $vars->{values} = _flag_values_to_json($values); + $vars->{visibility} = _flag_visibility_to_json($visibilities); + + if ($vars->{mode} eq 'new') { + $vars->{message} = 'tracking_flag_created'; } -} + else { + $vars->{message} = 'tracking_flag_updated'; + } + $vars->{mode} = 'edit'; + + } + else { + # initial load + + if ($vars->{mode} eq 'edit') { + + # edit - straight load + my $flag + = Bugzilla::Extension::TrackingFlags::Flag->new($vars->{flag_id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $vars->{flag_id}}); + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility); + $vars->{can_delete} = !$flag->bug_count; -sub _load_from_input { - my ($input, $vars) = @_; - - # flag - - my $flag = { - id => ($input->{mode} eq 'edit' ? $input->{flag_id} : 0), - name => trim($input->{flag_name} || ''), - description => trim($input->{flag_desc} || ''), - sortkey => $input->{flag_sort} || 0, - type => trim($input->{flag_type} || ''), - enter_bug => $input->{flag_enter_bug} ? 1 : 0, - is_active => $input->{flag_active} ? 1 : 0, - }; - detaint_natural($flag->{id}); - detaint_natural($flag->{sortkey}); - detaint_natural($flag->{enter_bug}); - detaint_natural($flag->{is_active}); - - # values - - my $values = decode_json($input->{values} || '[]'); - foreach my $value (@$values) { - $value->{value} = '' unless exists $value->{value} && defined $value->{value}; - $value->{setter_group_id} = '' unless $value->{setter_group_id}; - $value->{is_active} = $value->{is_active} ? 1 : 0; } + elsif ($vars->{mode} eq 'copy') { + + # copy - load the source flag + $vars->{mode} = 'new'; + my $flag + = Bugzilla::Extension::TrackingFlags::Flag->new($input->{copy_from}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $vars->{copy_from}}); + + # increment the number at the end of the name and description + if ($flag->name =~ /^(\D+)(\d+)$/) { + $flag->set_name("$1" . ($2 + 1)); + } + if ($flag->description =~ /^(\D+)([\d\.]+)$/) { + my $description = $1; + my $version = $2; + if ($version =~ /\./) { + my ($major, $minor) = split(/\./, $version); + $minor++; + $version = "$major.$minor"; + } + else { + $version++; + } + $flag->set_description($description . $version); + } + $flag->set_sortkey(_next_unique_sortkey($flag->sortkey)); + $flag->set_type($flag->flag_type); + $flag->set_enter_bug($flag->enter_bug); + + # always default new flags as active, even when copying an inactive one + $flag->set_is_active(1); - # vibility + $vars->{flag} = $flag; + $vars->{values} = _flag_values_to_json($flag->values, 1); + $vars->{visibility} = _flag_visibility_to_json($flag->visibility, 1); + $vars->{can_delete} = 0; - my $visibilities = decode_json($input->{visibility} || '[]'); - foreach my $visibility (@$visibilities) { - $visibility->{product} = '' unless exists $visibility->{product} && defined $visibility->{product}; - $visibility->{component} = '' unless exists $visibility->{component} && defined $visibility->{component}; } + else { + $vars->{mode} = 'new'; + $vars->{flag} = {sortkey => 0, enter_bug => 1, is_active => 1,}; + $vars->{values} = _flag_values_to_json([ + { + id => 0, + value => '---', + setter_group_id => '', + is_active => 1, + comment => '', + }, + ]); + $vars->{visibility} = ''; + $vars->{can_delete} = 0; + } + } +} - return ($flag, $values, $visibilities); +sub _load_from_input { + my ($input, $vars) = @_; + + # flag + + my $flag = { + id => ($input->{mode} eq 'edit' ? $input->{flag_id} : 0), + name => trim($input->{flag_name} || ''), + description => trim($input->{flag_desc} || ''), + sortkey => $input->{flag_sort} || 0, + type => trim($input->{flag_type} || ''), + enter_bug => $input->{flag_enter_bug} ? 1 : 0, + is_active => $input->{flag_active} ? 1 : 0, + }; + detaint_natural($flag->{id}); + detaint_natural($flag->{sortkey}); + detaint_natural($flag->{enter_bug}); + detaint_natural($flag->{is_active}); + + # values + + my $values = decode_json($input->{values} || '[]'); + foreach my $value (@$values) { + $value->{value} = '' unless exists $value->{value} && defined $value->{value}; + $value->{setter_group_id} = '' unless $value->{setter_group_id}; + $value->{is_active} = $value->{is_active} ? 1 : 0; + } + + # vibility + + my $visibilities = decode_json($input->{visibility} || '[]'); + foreach my $visibility (@$visibilities) { + $visibility->{product} = '' + unless exists $visibility->{product} && defined $visibility->{product}; + $visibility->{component} = '' + unless exists $visibility->{component} && defined $visibility->{component}; + } + + return ($flag, $values, $visibilities); } sub _next_unique_sortkey { - my ($sortkey) = @_; + my ($sortkey) = @_; - my %current; - foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) { - $current{$flag->sortkey} = 1; - } + my %current; + foreach my $flag (Bugzilla::Extension::TrackingFlags::Flag->get_all()) { + $current{$flag->sortkey} = 1; + } - $sortkey += 5; - $sortkey += 5 while exists $current{$sortkey}; - return $sortkey; + $sortkey += 5; + $sortkey += 5 while exists $current{$sortkey}; + return $sortkey; } # @@ -217,77 +229,83 @@ sub _next_unique_sortkey { # sub _validate { - my ($flag, $values, $visibilities) = @_; - - # flag - - my @missing; - push @missing, 'Field Name' if $flag->{name} eq ''; - push @missing, 'Field Description' if $flag->{description} eq ''; - push @missing, 'Field Sort Key' if $flag->{sortkey} eq ''; - scalar(@missing) - && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); - - $flag->{name} =~ /^cf_/ - || ThrowUserError('tracking_flags_cf_prefix'); - - if ($flag->{id}) { - my $old_flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); - if ($flag->{name} ne $old_flag->name) { - Bugzilla::Field->new({ name => $flag->{name} }) - && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); - } - } else { - Bugzilla::Field->new({ name => $flag->{name} }) - && ThrowUserError('field_already_exists', { field => { name => $flag->{name} }}); + my ($flag, $values, $visibilities) = @_; + + # flag + + my @missing; + push @missing, 'Field Name' if $flag->{name} eq ''; + push @missing, 'Field Description' if $flag->{description} eq ''; + push @missing, 'Field Sort Key' if $flag->{sortkey} eq ''; + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', {fields => \@missing}); + + $flag->{name} =~ /^cf_/ || ThrowUserError('tracking_flags_cf_prefix'); + + if ($flag->{id}) { + my $old_flag + = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $flag->{id}}); + if ($flag->{name} ne $old_flag->name) { + Bugzilla::Field->new({name => $flag->{name}}) + && ThrowUserError('field_already_exists', {field => {name => $flag->{name}}}); } + } + else { + Bugzilla::Field->new({name => $flag->{name}}) + && ThrowUserError('field_already_exists', {field => {name => $flag->{name}}}); + } - # values + # values - scalar(@$values) - || ThrowUserError('tracking_flags_missing_values'); + scalar(@$values) || ThrowUserError('tracking_flags_missing_values'); - my %seen; - foreach my $value (@$values) { - my $v = $value->{value}; + my %seen; + foreach my $value (@$values) { + my $v = $value->{value}; - $v eq '' - && ThrowUserError('tracking_flags_missing_value'); + $v eq '' && ThrowUserError('tracking_flags_missing_value'); - exists $seen{$v} - && ThrowUserError('tracking_flags_duplicate_value', { value => $v }); - $seen{$v} = 1; + exists $seen{$v} + && ThrowUserError('tracking_flags_duplicate_value', {value => $v}); + $seen{$v} = 1; - push @missing, "Setter for $v" if !$value->{setter_group_id}; - } - scalar(@missing) - && ThrowUserError('tracking_flags_missing_mandatory', { fields => \@missing }); + push @missing, "Setter for $v" if !$value->{setter_group_id}; + } + scalar(@missing) + && ThrowUserError('tracking_flags_missing_mandatory', {fields => \@missing}); - # visibility + # visibility - scalar(@$visibilities) - || ThrowUserError('tracking_flags_missing_visibility'); + scalar(@$visibilities) || ThrowUserError('tracking_flags_missing_visibility'); - %seen = (); - foreach my $visibility (@$visibilities) { - my $name = $visibility->{product} . ':' . $visibility->{component}; + %seen = (); + foreach my $visibility (@$visibilities) { + my $name = $visibility->{product} . ':' . $visibility->{component}; - exists $seen{$name} - && ThrowUserError('tracking_flags_duplicate_visibility', { name => $name }); + exists $seen{$name} + && ThrowUserError('tracking_flags_duplicate_visibility', {name => $name}); - $visibility->{product_obj} = Bugzilla::Product->new({ name => $visibility->{product} }) - || ThrowCodeError('tracking_flags_invalid_product', { product => $visibility->{product} }); + $visibility->{product_obj} + = Bugzilla::Product->new({name => $visibility->{product}}) + || ThrowCodeError('tracking_flags_invalid_product', + {product => $visibility->{product}}); - if ($visibility->{component} ne '') { - $visibility->{component_obj} = Bugzilla::Component->new({ product => $visibility->{product_obj}, - name => $visibility->{component} }) - || ThrowCodeError('tracking_flags_invalid_component', { - product => $visibility->{product}, - component_name => $visibility->{component}, - }); + if ($visibility->{component} ne '') { + $visibility->{component_obj} + = Bugzilla::Component->new({ + product => $visibility->{product_obj}, name => $visibility->{component} + }) + || ThrowCodeError( + 'tracking_flags_invalid_component', + { + product => $visibility->{product}, + component_name => $visibility->{component}, } + ); } + } } @@ -296,106 +314,115 @@ sub _validate { # sub _update_db { - my ($flag, $values, $visibilities) = @_; - my $dbh = Bugzilla->dbh; + my ($flag, $values, $visibilities) = @_; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); - my $flag_obj = _update_db_flag($flag); - _update_db_values($flag_obj, $flag, $values); - _update_db_visibility($flag_obj, $flag, $visibilities); - $dbh->bz_commit_transaction(); + $dbh->bz_start_transaction(); + my $flag_obj = _update_db_flag($flag); + _update_db_values($flag_obj, $flag, $values); + _update_db_visibility($flag_obj, $flag, $visibilities); + $dbh->bz_commit_transaction(); - return $flag_obj; + return $flag_obj; } sub _update_db_flag { - my ($flag) = @_; + my ($flag) = @_; + + my $object_set = { + name => $flag->{name}, + description => $flag->{description}, + sortkey => $flag->{sortkey}, + type => $flag->{type}, + enter_bug => $flag->{enter_bug}, + is_active => $flag->{is_active}, + }; + + my $flag_obj; + if ($flag->{id}) { + + # update existing flag + $flag_obj + = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag', id => $flag->{id}}); + $flag_obj->set_all($object_set); + $flag_obj->update(); + + } + else { + # create new flag + $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set); + } + + return $flag_obj; +} - my $object_set = { - name => $flag->{name}, - description => $flag->{description}, - sortkey => $flag->{sortkey}, - type => $flag->{type}, - enter_bug => $flag->{enter_bug}, - is_active => $flag->{is_active}, - }; +sub _update_db_values { + my ($flag_obj, $flag, $values) = @_; - my $flag_obj; - if ($flag->{id}) { - # update existing flag - $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->new($flag->{id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag', id => $flag->{id} }); - $flag_obj->set_all($object_set); - $flag_obj->update(); - - } else { - # create new flag - $flag_obj = Bugzilla::Extension::TrackingFlags::Flag->create($object_set); + # delete + foreach my $current_value (@{$flag_obj->values}) { + if (!grep { $_->{id} == $current_value->id } @$values) { + $current_value->remove_from_db(); } + } - return $flag_obj; -} + # add/update + my $sortkey = 0; + foreach my $value (@{$values}) { + $sortkey += 10; -sub _update_db_values { - my ($flag_obj, $flag, $values) = @_; + my $object_set = { + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active}, + sortkey => $sortkey, + comment => $value->{comment}, + }; - # delete - foreach my $current_value (@{ $flag_obj->values }) { - if (!grep { $_->{id} == $current_value->id } @$values) { - $current_value->remove_from_db(); - } + if ($value->{id}) { + my $value_obj + = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id}) + || ThrowCodeError('tracking_flags_invalid_item_id', + {item => 'flag value', id => $flag->{id}}); + my $old_value = $value_obj->value; + $value_obj->set_all($object_set); + $value_obj->update(); + Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({ + value_obj => $value_obj, + old_value => $old_value, + new_value => $value_obj->value, + }); } - - # add/update - my $sortkey = 0; - foreach my $value (@{ $values }) { - $sortkey += 10; - - my $object_set = { - value => $value->{value}, - setter_group_id => $value->{setter_group_id}, - is_active => $value->{is_active}, - sortkey => $sortkey, - comment => $value->{comment}, - }; - - if ($value->{id}) { - my $value_obj = Bugzilla::Extension::TrackingFlags::Flag::Value->new($value->{id}) - || ThrowCodeError('tracking_flags_invalid_item_id', { item => 'flag value', id => $flag->{id} }); - my $old_value = $value_obj->value; - $value_obj->set_all($object_set); - $value_obj->update(); - Bugzilla::Extension::TrackingFlags::Flag::Bug->update_all_values({ - value_obj => $value_obj, - old_value => $old_value, - new_value => $value_obj->value, - }); - } else { - $object_set->{tracking_flag_id} = $flag_obj->flag_id; - Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set); - } + else { + $object_set->{tracking_flag_id} = $flag_obj->flag_id; + Bugzilla::Extension::TrackingFlags::Flag::Value->create($object_set); } + } } sub _update_db_visibility { - my ($flag_obj, $flag, $visibilities) = @_; + my ($flag_obj, $flag, $visibilities) = @_; - # delete - foreach my $current_visibility (@{ $flag_obj->visibility }) { - if (!grep { $_->{id} == $current_visibility->id } @$visibilities) { - $current_visibility->remove_from_db(); - } - } - - # add - foreach my $visibility (@{ $visibilities }) { - next if $visibility->{id}; - Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ - tracking_flag_id => $flag_obj->flag_id, - product_id => $visibility->{product_obj}->id, - component_id => $visibility->{component} ? $visibility->{component_obj}->id : undef, - }); + # delete + foreach my $current_visibility (@{$flag_obj->visibility}) { + if (!grep { $_->{id} == $current_visibility->id } @$visibilities) { + $current_visibility->remove_from_db(); } + } + + # add + foreach my $visibility (@{$visibilities}) { + next if $visibility->{id}; + Bugzilla::Extension::TrackingFlags::Flag::Visibility->create({ + tracking_flag_id => $flag_obj->flag_id, + product_id => $visibility->{product_obj}->id, + component_id => $visibility->{component} + ? $visibility->{component_obj}->id + : undef, + }); + } } # @@ -403,62 +430,66 @@ sub _update_db_visibility { # sub _groups_to_json { - my @data; - foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) { - push @data, { - id => $group->id, - name => $group->name, - }; - } - return encode_json(\@data); + my @data; + foreach my $group (sort { $a->name cmp $b->name } Bugzilla::Group->get_all()) { + push @data, {id => $group->id, name => $group->name,}; + } + return encode_json(\@data); } sub _flag_values_to_json { - my ($values, $is_copy) = @_; - # setting is_copy will set the id's to zero, to force new values rather - # than editing existing ones - my @data; - foreach my $value (@$values) { - push @data, { - id => $is_copy ? 0 : $value->{id}, - value => $value->{value}, - setter_group_id => $value->{setter_group_id}, - is_active => $value->{is_active} ? JSON::true : JSON::false, - comment => $value->{comment} // '', - }; - } - return encode_json(\@data); + my ($values, $is_copy) = @_; + + # setting is_copy will set the id's to zero, to force new values rather + # than editing existing ones + my @data; + foreach my $value (@$values) { + push @data, + { + id => $is_copy ? 0 : $value->{id}, + value => $value->{value}, + setter_group_id => $value->{setter_group_id}, + is_active => $value->{is_active} ? JSON::true : JSON::false, + comment => $value->{comment} // '', + }; + } + return encode_json(\@data); } sub _flag_visibility_to_json { - my ($visibilities, $is_copy) = @_; - # setting is_copy will set the id's to zero, to force new visibilites - # rather than editing existing ones - my @data; - - foreach my $visibility (@$visibilities) { - my $product = exists $visibility->{product_id} - ? $visibility->product->name - : $visibility->{product}; - my $component; - if (exists $visibility->{component_id} && $visibility->{component_id}) { - $component = $visibility->component->name; - } elsif (exists $visibility->{component}) { - $component = $visibility->{component}; - } else { - $component = undef; - } - push @data, { - id => $is_copy ? 0 : $visibility->{id}, - product => $product, - component => $component, - }; + my ($visibilities, $is_copy) = @_; + + # setting is_copy will set the id's to zero, to force new visibilites + # rather than editing existing ones + my @data; + + foreach my $visibility (@$visibilities) { + my $product + = exists $visibility->{product_id} + ? $visibility->product->name + : $visibility->{product}; + my $component; + if (exists $visibility->{component_id} && $visibility->{component_id}) { + $component = $visibility->component->name; + } + elsif (exists $visibility->{component}) { + $component = $visibility->{component}; + } + else { + $component = undef; } - @data = sort { - lc($a->{product}) cmp lc($b->{product}) - || lc($a->{component}) cmp lc($b->{component}) - } @data; - return encode_json(\@data); + push @data, + { + id => $is_copy ? 0 : $visibility->{id}, + product => $product, + component => $component, + }; + } + @data = sort { + lc($a->{product}) cmp lc($b->{product}) + || lc($a->{component}) cmp lc($b->{component}) + } @data; + return encode_json(\@data); } 1; diff --git a/extensions/TrackingFlags/lib/Constants.pm b/extensions/TrackingFlags/lib/Constants.pm index 00827aa7a..842e7501d 100644 --- a/extensions/TrackingFlags/lib/Constants.pm +++ b/extensions/TrackingFlags/lib/Constants.pm @@ -14,31 +14,31 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( - FLAG_TYPES + FLAG_TYPES ); sub FLAG_TYPES { - my @flag_types = ( - { - name => 'project', - description => 'Project Flags', - collapsed => 0, - sortkey => 0 - }, - { - name => 'tracking', - description => 'Tracking Flags', - collapsed => 1, - sortkey => 1 - }, - { - name => 'blocking', - description => 'Blocking Flags', - collapsed => 1, - sortkey => 2 - }, - ); - return [ sort { $a->{'sortkey'} <=> $b->{'sortkey'} } @flag_types ]; + my @flag_types = ( + { + name => 'project', + description => 'Project Flags', + collapsed => 0, + sortkey => 0 + }, + { + name => 'tracking', + description => 'Tracking Flags', + collapsed => 1, + sortkey => 1 + }, + { + name => 'blocking', + description => 'Blocking Flags', + collapsed => 1, + sortkey => 2 + }, + ); + return [sort { $a->{'sortkey'} <=> $b->{'sortkey'} } @flag_types]; } 1; diff --git a/extensions/TrackingFlags/lib/Flag.pm b/extensions/TrackingFlags/lib/Flag.pm index 82c0314e3..c1ecbc4ad 100644 --- a/extensions/TrackingFlags/lib/Flag.pm +++ b/extensions/TrackingFlags/lib/Flag.pm @@ -30,43 +30,43 @@ use Bugzilla::Extension::TrackingFlags::Flag::Visibility; use constant DB_TABLE => 'tracking_flags'; use constant DB_COLUMNS => qw( - id - field_id - name - description - type - sortkey - enter_bug - is_active + id + field_id + name + description + type + sortkey + enter_bug + is_active ); use constant LIST_ORDER => 'sortkey'; use constant UPDATE_COLUMNS => qw( - name - description - type - sortkey - enter_bug - is_active + name + description + type + sortkey + enter_bug + is_active ); use constant VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - type => \&_check_type, - sortkey => \&_check_sortkey, - enter_bug => \&Bugzilla::Object::check_boolean, - is_active => \&Bugzilla::Object::check_boolean, + name => \&_check_name, + description => \&_check_description, + type => \&_check_type, + sortkey => \&_check_sortkey, + enter_bug => \&Bugzilla::Object::check_boolean, + is_active => \&Bugzilla::Object::check_boolean, }; use constant UPDATE_VALIDATORS => { - name => \&_check_name, - description => \&_check_description, - type => \&_check_type, - sortkey => \&_check_sortkey, - enter_bug => \&Bugzilla::Object::check_boolean, - is_active => \&Bugzilla::Object::check_boolean, + name => \&_check_name, + description => \&_check_description, + type => \&_check_type, + sortkey => \&_check_sortkey, + enter_bug => \&Bugzilla::Object::check_boolean, + is_active => \&Bugzilla::Object::check_boolean, }; ############################### @@ -74,257 +74,260 @@ use constant UPDATE_VALIDATORS => { ############################### sub new { - my $class = shift; - my $param = shift; - my $cache = Bugzilla->request_cache; - - if (!ref $param - && exists $cache->{'tracking_flags'} - && exists $cache->{'tracking_flags'}->{$param}) - { - return $cache->{'tracking_flags'}->{$param}; - } - - return $class->SUPER::new($param); + my $class = shift; + my $param = shift; + my $cache = Bugzilla->request_cache; + + if (!ref $param + && exists $cache->{'tracking_flags'} + && exists $cache->{'tracking_flags'}->{$param}) + { + return $cache->{'tracking_flags'}->{$param}; + } + + return $class->SUPER::new($param); } sub new_from_hash { - my $class = shift; - my $cache = Bugzilla->request_cache->{'tracking_flags'} //= {}; - my $flag = $class->SUPER::new_from_hash(@_); - if ($flag) { - push @Bugzilla::Extension::TrackingFlags::FLAG_CACHE, $flag; - } - return $flag; + my $class = shift; + my $cache = Bugzilla->request_cache->{'tracking_flags'} //= {}; + my $flag = $class->SUPER::new_from_hash(@_); + if ($flag) { + push @Bugzilla::Extension::TrackingFlags::FLAG_CACHE, $flag; + } + return $flag; } sub create { - my $class = shift; - my $params = shift; - my $dbh = Bugzilla->dbh; - my $flag; - - # Disable bug updates temporarily to avoid conflicts. - SetParam('disable_bug_updates', 1); - write_params(); - - eval { - $dbh->bz_start_transaction(); - - $params = $class->run_create_validators($params); - - # We have to create an entry for this new flag - # in the fielddefs table for use elsewhere. We cannot - # use Bugzilla::Field->create as it will create the - # additional tables needed by custom fields which we - # do not need. Also we do this so as not to add a - # another column to the bugs table. - # We will create the entry as a custom field with a - # type of FIELD_TYPE_EXTENSION so Bugzilla will skip - # these field types in certain parts of the core code. - $dbh->do("INSERT INTO fielddefs + my $class = shift; + my $params = shift; + my $dbh = Bugzilla->dbh; + my $flag; + + # Disable bug updates temporarily to avoid conflicts. + SetParam('disable_bug_updates', 1); + write_params(); + + eval { + $dbh->bz_start_transaction(); + + $params = $class->run_create_validators($params); + + # We have to create an entry for this new flag + # in the fielddefs table for use elsewhere. We cannot + # use Bugzilla::Field->create as it will create the + # additional tables needed by custom fields which we + # do not need. Also we do this so as not to add a + # another column to the bugs table. + # We will create the entry as a custom field with a + # type of FIELD_TYPE_EXTENSION so Bugzilla will skip + # these field types in certain parts of the core code. + $dbh->do( + "INSERT INTO fielddefs (name, description, sortkey, type, custom, obsolete, buglist) VALUES - (?, ?, ?, ?, ?, ?, ?)", - undef, - $params->{'name'}, - $params->{'description'}, - $params->{'sortkey'}, - FIELD_TYPE_EXTENSION, - 1, 0, 1); - $params->{'field_id'} = $dbh->bz_last_key; - - $flag = $class->SUPER::create($params); - - $dbh->bz_commit_transaction(); - }; - my $error = "$@"; - SetParam('disable_bug_updates', 0); - write_params(); - die $error if $error; + (?, ?, ?, ?, ?, ?, ?)", undef, $params->{'name'}, + $params->{'description'}, $params->{'sortkey'}, FIELD_TYPE_EXTENSION, 1, 0, 1 + ); + $params->{'field_id'} = $dbh->bz_last_key; + + $flag = $class->SUPER::create($params); - # fielddefs has been changed so we need to clear global config - Bugzilla->memcached->clear_config(); + $dbh->bz_commit_transaction(); + }; + my $error = "$@"; + SetParam('disable_bug_updates', 0); + write_params(); + die $error if $error; - return $flag; + # fielddefs has been changed so we need to clear global config + Bugzilla->memcached->clear_config(); + + return $flag; } sub update { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - my $old_self = $self->new($self->flag_id); + my $old_self = $self->new($self->flag_id); - # HACK! Bugzilla::Object::update uses hardcoded $self->id - # instead of $self->{ID_FIELD} so we need to reverse field_id - # and the real id temporarily - my $field_id = $self->id; - $self->{'field_id'} = $self->{'id'}; + # HACK! Bugzilla::Object::update uses hardcoded $self->id + # instead of $self->{ID_FIELD} so we need to reverse field_id + # and the real id temporarily + my $field_id = $self->id; + $self->{'field_id'} = $self->{'id'}; - my $changes = $self->SUPER::update(@_); + my $changes = $self->SUPER::update(@_); - $self->{'field_id'} = $field_id; + $self->{'field_id'} = $field_id; - # Update the fielddefs entry - $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?", - undef, - $self->name, $self->description, $old_self->name); + # Update the fielddefs entry + $dbh->do("UPDATE fielddefs SET name = ?, description = ? WHERE name = ?", + undef, $self->name, $self->description, $old_self->name); - # Update request_cache - my $cache = Bugzilla->request_cache; - if (exists $cache->{'tracking_flags'}) { - $cache->{'tracking_flags'}->{$self->flag_id} = $self; - } + # Update request_cache + my $cache = Bugzilla->request_cache; + if (exists $cache->{'tracking_flags'}) { + $cache->{'tracking_flags'}->{$self->flag_id} = $self; + } - # fielddefs has been changed so we need to clear global config - Bugzilla->memcached->clear_config(); + # fielddefs has been changed so we need to clear global config + Bugzilla->memcached->clear_config(); - return $changes; + return $changes; } sub match { - my $class = shift; - my ($params) = @_; - - # Use later for preload - my $bug_id = delete $params->{'bug_id'}; - - # Retrieve all flags relevant for the given product and component - if (!exists $params->{'id'} - && ($params->{'component'} || $params->{'component_id'} - || $params->{'product'} || $params->{'product_id'})) - { - my $visible_flags - = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_); - my @flag_ids = map { $_->tracking_flag_id } @$visible_flags; - - delete $params->{'component'} if exists $params->{'component'}; - delete $params->{'component_id'} if exists $params->{'component_id'}; - delete $params->{'product'} if exists $params->{'product'}; - delete $params->{'product_id'} if exists $params->{'product_id'}; - - $params->{'id'} = \@flag_ids; - } - - # We need to return inactive flags if a value has been set - my $is_active_filter = delete $params->{is_active}; - - my $flags = $class->SUPER::match($params); - preload_all_the_things($flags, { bug_id => $bug_id }); - - if ($is_active_filter) { - $flags = [ grep { $_->is_active || exists $_->{bug_flag} } @$flags ]; - } - return [ sort { $a->sortkey <=> $b->sortkey } @$flags ]; + my $class = shift; + my ($params) = @_; + + # Use later for preload + my $bug_id = delete $params->{'bug_id'}; + + # Retrieve all flags relevant for the given product and component + if ( + !exists $params->{'id'} + && ( $params->{'component'} + || $params->{'component_id'} + || $params->{'product'} + || $params->{'product_id'}) + ) + { + my $visible_flags + = Bugzilla::Extension::TrackingFlags::Flag::Visibility->match(@_); + my @flag_ids = map { $_->tracking_flag_id } @$visible_flags; + + delete $params->{'component'} if exists $params->{'component'}; + delete $params->{'component_id'} if exists $params->{'component_id'}; + delete $params->{'product'} if exists $params->{'product'}; + delete $params->{'product_id'} if exists $params->{'product_id'}; + + $params->{'id'} = \@flag_ids; + } + + # We need to return inactive flags if a value has been set + my $is_active_filter = delete $params->{is_active}; + + my $flags = $class->SUPER::match($params); + preload_all_the_things($flags, {bug_id => $bug_id}); + + if ($is_active_filter) { + $flags = [grep { $_->is_active || exists $_->{bug_flag} } @$flags]; + } + return [sort { $a->sortkey <=> $b->sortkey } @$flags]; } sub get_all { - my $self = shift; - my $cache = Bugzilla->request_cache; - if (!exists $cache->{'tracking_flags'}) { - my @tracking_flags = $self->SUPER::get_all(@_); - preload_all_the_things(\@tracking_flags); - my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags; - $cache->{'tracking_flags'} = \%tracking_flags_hash; - } - return sort { $a->flag_type cmp $b->flag_type || $a->sortkey <=> $b->sortkey } - values %{ $cache->{'tracking_flags'} }; + my $self = shift; + my $cache = Bugzilla->request_cache; + if (!exists $cache->{'tracking_flags'}) { + my @tracking_flags = $self->SUPER::get_all(@_); + preload_all_the_things(\@tracking_flags); + my %tracking_flags_hash = map { $_->flag_id => $_ } @tracking_flags; + $cache->{'tracking_flags'} = \%tracking_flags_hash; + } + return + sort { $a->flag_type cmp $b->flag_type || $a->sortkey <=> $b->sortkey } + values %{$cache->{'tracking_flags'}}; } # avoids the overhead of pre-loading if just the field names are required sub get_all_names { - my $self = shift; - my $cache = Bugzilla->request_cache; - if (!exists $cache->{'tracking_flags_names'}) { - $cache->{'tracking_flags_names'} = - Bugzilla->dbh->selectcol_arrayref("SELECT name FROM tracking_flags ORDER BY name"); - } - return @{ $cache->{'tracking_flags_names'} }; + my $self = shift; + my $cache = Bugzilla->request_cache; + if (!exists $cache->{'tracking_flags_names'}) { + $cache->{'tracking_flags_names'} = Bugzilla->dbh->selectcol_arrayref( + "SELECT name FROM tracking_flags ORDER BY name"); + } + return @{$cache->{'tracking_flags_names'}}; } sub remove_from_db { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - # Check to see if tracking_flags_bugs table has records - if ($self->bug_count) { - ThrowUserError('tracking_flag_has_contents', { flag => $self }); - } + # Check to see if tracking_flags_bugs table has records + if ($self->bug_count) { + ThrowUserError('tracking_flag_has_contents', {flag => $self}); + } - # Disable bug updates temporarily to avoid conflicts. - SetParam('disable_bug_updates', 1); - write_params(); + # Disable bug updates temporarily to avoid conflicts. + SetParam('disable_bug_updates', 1); + write_params(); - eval { - $dbh->bz_start_transaction(); + eval { + $dbh->bz_start_transaction(); - $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ?', undef, $self->id); - $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name); + $dbh->do('DELETE FROM bugs_activity WHERE fieldid = ?', undef, $self->id); + $dbh->do('DELETE FROM fielddefs WHERE name = ?', undef, $self->name); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Remove from request cache - my $cache = Bugzilla->request_cache; - if (exists $cache->{'tracking_flags'}) { - delete $cache->{'tracking_flags'}->{$self->flag_id}; - } - }; - my $error = "$@"; - SetParam('disable_bug_updates', 0); - write_params(); - die $error if $error; + # Remove from request cache + my $cache = Bugzilla->request_cache; + if (exists $cache->{'tracking_flags'}) { + delete $cache->{'tracking_flags'}->{$self->flag_id}; + } + }; + my $error = "$@"; + SetParam('disable_bug_updates', 0); + write_params(); + die $error if $error; } sub preload_all_the_things { - my ($flags, $params) = @_; + my ($flags, $params) = @_; - my %flag_hash = map { $_->flag_id => $_ } @$flags; - my @flag_ids = keys %flag_hash; - return unless @flag_ids; + my %flag_hash = map { $_->flag_id => $_ } @$flags; + my @flag_ids = keys %flag_hash; + return unless @flag_ids; - # Preload values - my $value_objects - = Bugzilla::Extension::TrackingFlags::Flag::Value->match({ tracking_flag_id => \@flag_ids }); + # Preload values + my $value_objects = Bugzilla::Extension::TrackingFlags::Flag::Value->match( + {tracking_flag_id => \@flag_ids}); - # Now populate the tracking flags with this set of value objects. - foreach my $obj (@$value_objects) { - my $flag_id = $obj->tracking_flag_id; + # Now populate the tracking flags with this set of value objects. + foreach my $obj (@$value_objects) { + my $flag_id = $obj->tracking_flag_id; - # Prepopulate the tracking flag object in the value object - $obj->{'tracking_flag'} = $flag_hash{$flag_id}; + # Prepopulate the tracking flag object in the value object + $obj->{'tracking_flag'} = $flag_hash{$flag_id}; - # Prepopulate the current value objects for this tracking flag - $flag_hash{$flag_id}->{'values'} ||= []; - push(@{$flag_hash{$flag_id}->{'values'}}, $obj); - } + # Prepopulate the current value objects for this tracking flag + $flag_hash{$flag_id}->{'values'} ||= []; + push(@{$flag_hash{$flag_id}->{'values'}}, $obj); + } - # Preload bug values if a bug_id is passed - if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) { - # We don't want to use @flag_ids here as we want all flags attached to this bug - # even if they are inactive. - my $bug_objects - = Bugzilla::Extension::TrackingFlags::Flag::Bug->match({ bug_id => $params->{'bug_id'} }); - # Now populate the tracking flags with this set of objects. - # Also we add them to the flag hash since we want them to be visible even if - # they are not longer applicable to this product/component. - foreach my $obj (@$bug_objects) { - my $flag_id = $obj->tracking_flag_id; - - # Load the flag object if it does not yet exist. - # This can happen if the bug value tracking flag - # is no longer visible for the product/component - $flag_hash{$flag_id} - ||= Bugzilla::Extension::TrackingFlags::Flag->new($flag_id); - - # Prepopulate the tracking flag object in the bug flag object - $obj->{'tracking_flag'} = $flag_hash{$flag_id}; - - # Prepopulate the the current bug flag object for the tracking flag - $flag_hash{$flag_id}->{'bug_flag'} = $obj; - } + # Preload bug values if a bug_id is passed + if ($params && exists $params->{'bug_id'} && $params->{'bug_id'}) { + + # We don't want to use @flag_ids here as we want all flags attached to this bug + # even if they are inactive. + my $bug_objects = Bugzilla::Extension::TrackingFlags::Flag::Bug->match( + {bug_id => $params->{'bug_id'}}); + + # Now populate the tracking flags with this set of objects. + # Also we add them to the flag hash since we want them to be visible even if + # they are not longer applicable to this product/component. + foreach my $obj (@$bug_objects) { + my $flag_id = $obj->tracking_flag_id; + + # Load the flag object if it does not yet exist. + # This can happen if the bug value tracking flag + # is no longer visible for the product/component + $flag_hash{$flag_id} + ||= Bugzilla::Extension::TrackingFlags::Flag->new($flag_id); + + # Prepopulate the tracking flag object in the bug flag object + $obj->{'tracking_flag'} = $flag_hash{$flag_id}; + + # Prepopulate the the current bug flag object for the tracking flag + $flag_hash{$flag_id}->{'bug_flag'} = $obj; } + } - @$flags = values %flag_hash; + @$flags = values %flag_hash; } ############################### @@ -332,125 +335,126 @@ sub preload_all_the_things { ############################### sub _check_name { - my ($invocant, $name) = @_; - $name = trim($name); - $name || ThrowCodeError('param_required', { param => 'name' }); - return $name; + my ($invocant, $name) = @_; + $name = trim($name); + $name || ThrowCodeError('param_required', {param => 'name'}); + return $name; } sub _check_description { - my ($invocant, $description) = @_; - $description = trim($description); - $description || ThrowCodeError( 'param_required', { param => 'description' } ); - return $description; + my ($invocant, $description) = @_; + $description = trim($description); + $description || ThrowCodeError('param_required', {param => 'description'}); + return $description; } sub _check_type { - my ($invocant, $type) = @_; - $type = trim($type); - $type || ThrowCodeError( 'param_required', { param => 'type' } ); - grep($_->{name} eq $type, @{FLAG_TYPES()}) - || ThrowUserError('tracking_flags_invalid_flag_type', { type => $type }); - return $type; + my ($invocant, $type) = @_; + $type = trim($type); + $type || ThrowCodeError('param_required', {param => 'type'}); + grep($_->{name} eq $type, @{FLAG_TYPES()}) + || ThrowUserError('tracking_flags_invalid_flag_type', {type => $type}); + return $type; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - detaint_natural($sortkey) - || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); - return $sortkey; + my ($invocant, $sortkey) = @_; + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', {sortkey => $sortkey}); + return $sortkey; } ############################### #### Setters #### ############################### -sub set_name { $_[0]->set('name', $_[1]); } +sub set_name { $_[0]->set('name', $_[1]); } sub set_description { $_[0]->set('description', $_[1]); } -sub set_type { $_[0]->set('type', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } -sub set_is_active { $_[0]->set('is_active', $_[1]); } +sub set_type { $_[0]->set('type', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_enter_bug { $_[0]->set('enter_bug', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } ############################### #### Accessors #### ############################### -sub flag_id { return $_[0]->{'id'}; } -sub name { return $_[0]->{'name'}; } +sub flag_id { return $_[0]->{'id'}; } +sub name { return $_[0]->{'name'}; } sub description { return $_[0]->{'description'}; } -sub flag_type { return $_[0]->{'type'}; } -sub sortkey { return $_[0]->{'sortkey'}; } -sub enter_bug { return $_[0]->{'enter_bug'}; } -sub is_active { return $_[0]->{'is_active'}; } +sub flag_type { return $_[0]->{'type'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub enter_bug { return $_[0]->{'enter_bug'}; } +sub is_active { return $_[0]->{'is_active'}; } sub values { - return $_[0]->{'values'} ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match({ - tracking_flag_id => $_[0]->flag_id - }); + return $_[0]->{'values'} + ||= Bugzilla::Extension::TrackingFlags::Flag::Value->match( + {tracking_flag_id => $_[0]->flag_id}); } sub visibility { - return $_[0]->{'visibility'} ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match({ - tracking_flag_id => $_[0]->flag_id - }); + return $_[0]->{'visibility'} + ||= Bugzilla::Extension::TrackingFlags::Flag::Visibility->match( + {tracking_flag_id => $_[0]->flag_id}); } sub can_set_value { - my ($self, $new_value, $user) = @_; - $user ||= Bugzilla->user; - my $new_value_obj; - foreach my $value (@{$self->values}) { - if ($value->value eq $new_value) { - $new_value_obj = $value; - last; - } + my ($self, $new_value, $user) = @_; + $user ||= Bugzilla->user; + my $new_value_obj; + foreach my $value (@{$self->values}) { + if ($value->value eq $new_value) { + $new_value_obj = $value; + last; } - return $new_value_obj - && $new_value_obj->setter_group - && $user->in_group($new_value_obj->setter_group->name) - ? 1 - : 0; + } + return + $new_value_obj + && $new_value_obj->setter_group + && $user->in_group($new_value_obj->setter_group->name) ? 1 : 0; } sub bug_flag { - my ($self, $bug_id) = @_; - # Return the current bug value object if defined unless the passed bug_id does - # not equal the current bug value objects id. - if (defined $self->{'bug_flag'} - && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id)) - { - return $self->{'bug_flag'}; - } - - # Flag::Bug->new will return a default bug value object if $params undefined - my $params = !$bug_id - ? undef - : { condition => "tracking_flag_id = ? AND bug_id = ?", - values => [ $self->flag_id, $bug_id ] }; - return $self->{'bug_flag'} = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params); + my ($self, $bug_id) = @_; + + # Return the current bug value object if defined unless the passed bug_id does + # not equal the current bug value objects id. + if (defined $self->{'bug_flag'} + && (!$bug_id || $self->{'bug_flag'}->bug->id == $bug_id)) + { + return $self->{'bug_flag'}; + } + + # Flag::Bug->new will return a default bug value object if $params undefined + my $params = !$bug_id + ? undef + : { + condition => "tracking_flag_id = ? AND bug_id = ?", + values => [$self->flag_id, $bug_id] + }; + return $self->{'bug_flag'} + = Bugzilla::Extension::TrackingFlags::Flag::Bug->new($params); } sub bug_count { - my ($self) = @_; - return $self->{'bug_count'} if defined $self->{'bug_count'}; - my $dbh = Bugzilla->dbh; - return $self->{'bug_count'} = scalar $dbh->selectrow_array(" + my ($self) = @_; + return $self->{'bug_count'} if defined $self->{'bug_count'}; + my $dbh = Bugzilla->dbh; + return $self->{'bug_count'} = scalar $dbh->selectrow_array(" SELECT COUNT(bug_id) FROM tracking_flags_bugs - WHERE tracking_flag_id = ?", - undef, $self->flag_id); + WHERE tracking_flag_id = ?", undef, $self->flag_id); } sub activity_count { - my ($self) = @_; - return $self->{'activity_count'} if defined $self->{'activity_count'}; - my $dbh = Bugzilla->dbh; - return $self->{'activity_count'} = scalar $dbh->selectrow_array(" + my ($self) = @_; + return $self->{'activity_count'} if defined $self->{'activity_count'}; + my $dbh = Bugzilla->dbh; + return $self->{'activity_count'} = scalar $dbh->selectrow_array(" SELECT COUNT(bug_id) FROM bugs_activity - WHERE fieldid = ?", - undef, $self->id); + WHERE fieldid = ?", undef, $self->id); } ###################################### @@ -460,25 +464,25 @@ sub activity_count { # Here we return 'field_id' instead of the real # id as we want other Bugzilla code to treat this # as a Bugzilla::Field object in certain places. -sub id { return $_[0]->{'field_id'}; } +sub id { return $_[0]->{'field_id'}; } sub type { return FIELD_TYPE_EXTENSION; } -sub legal_values { return $_[0]->values; } -sub custom { return 1; } -sub in_new_bugmail { return 1; } +sub legal_values { return $_[0]->values; } +sub custom { return 1; } +sub in_new_bugmail { return 1; } sub obsolete { return $_[0]->is_active ? 0 : 1; } -sub buglist { return 1; } -sub is_select { return 1; } -sub is_abnormal { return 1; } -sub is_timetracking { return 0; } +sub buglist { return 1; } +sub is_select { return 1; } +sub is_abnormal { return 1; } +sub is_timetracking { return 0; } sub visibility_field { return undef; } sub visibility_values { return undef; } sub controls_visibility_of { return undef; } sub value_field { return undef; } sub controls_values_of { return undef; } -sub is_visible_on_bug { return 1; } -sub is_relationship { return 0; } -sub reverse_desc { return ''; } -sub is_mandatory { return 0; } -sub is_numeric { return 0; } +sub is_visible_on_bug { return 1; } +sub is_relationship { return 0; } +sub reverse_desc { return ''; } +sub is_mandatory { return 0; } +sub is_numeric { return 0; } 1; diff --git a/extensions/TrackingFlags/lib/Flag/Bug.pm b/extensions/TrackingFlags/lib/Flag/Bug.pm index 7be661720..9d9c5ce8c 100644 --- a/extensions/TrackingFlags/lib/Flag/Bug.pm +++ b/extensions/TrackingFlags/lib/Flag/Bug.pm @@ -24,32 +24,26 @@ use Scalar::Util qw(blessed); #### Initialization #### ############################### -use constant DEFAULT_FLAG_BUG => { - 'id' => 0, - 'tracking_flag_id' => 0, - 'bug_id' => 0, - 'value' => '---', -}; +use constant DEFAULT_FLAG_BUG => + {'id' => 0, 'tracking_flag_id' => 0, 'bug_id' => 0, 'value' => '---',}; use constant DB_TABLE => 'tracking_flags_bugs'; use constant DB_COLUMNS => qw( - id - tracking_flag_id - bug_id - value + id + tracking_flag_id + bug_id + value ); use constant LIST_ORDER => 'id'; use constant UPDATE_COLUMNS => qw( - value + value ); -use constant VALIDATORS => { - tracking_flag_id => \&_check_tracking_flag, - value => \&_check_value, -}; +use constant VALIDATORS => + {tracking_flag_id => \&_check_tracking_flag, value => \&_check_value,}; use constant AUDIT_CREATES => 0; use constant AUDIT_UPDATES => 0; @@ -60,67 +54,67 @@ use constant AUDIT_REMOVES => 0; ############################### sub new { - my $invocant = shift; - my $class = ref($invocant) || $invocant; - my ($param) = @_; - - my $self; - if ($param) { - $self = $class->SUPER::new(@_); - if (!$self) { - $self = DEFAULT_FLAG_BUG; - bless($self, $class); - } - } - else { - $self = DEFAULT_FLAG_BUG; - bless($self, $class); + my $invocant = shift; + my $class = ref($invocant) || $invocant; + my ($param) = @_; + + my $self; + if ($param) { + $self = $class->SUPER::new(@_); + if (!$self) { + $self = DEFAULT_FLAG_BUG; + bless($self, $class); } + } + else { + $self = DEFAULT_FLAG_BUG; + bless($self, $class); + } - return $self + return $self; } sub match { - my $class = shift; - my $bug_flags = $class->SUPER::match(@_); - preload_all_the_things($bug_flags); - return $bug_flags; + my $class = shift; + my $bug_flags = $class->SUPER::match(@_); + preload_all_the_things($bug_flags); + return $bug_flags; } sub remove_from_db { - my ($self) = @_; - $self->SUPER::remove_from_db(); - $self->{'id'} = $self->{'tracking_flag_id'} = $self->{'bug_id'} = 0; - $self->{'value'} = '---'; + my ($self) = @_; + $self->SUPER::remove_from_db(); + $self->{'id'} = $self->{'tracking_flag_id'} = $self->{'bug_id'} = 0; + $self->{'value'} = '---'; } sub preload_all_the_things { - my ($bug_flags) = @_; - my $cache = Bugzilla->request_cache; - - # Preload tracking flag objects - my @tracking_flag_ids; - foreach my $bug_flag (@$bug_flags) { - if (exists $cache->{'tracking_flags'} - && $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}) - { - $bug_flag->{'tracking_flag'} - = $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}; - next; - } - push(@tracking_flag_ids, $bug_flag->tracking_flag_id); + my ($bug_flags) = @_; + my $cache = Bugzilla->request_cache; + + # Preload tracking flag objects + my @tracking_flag_ids; + foreach my $bug_flag (@$bug_flags) { + if (exists $cache->{'tracking_flags'} + && $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}) + { + $bug_flag->{'tracking_flag'} + = $cache->{'tracking_flags'}->{$bug_flag->tracking_flag_id}; + next; } + push(@tracking_flag_ids, $bug_flag->tracking_flag_id); + } - return unless @tracking_flag_ids; + return unless @tracking_flag_ids; - my $tracking_flags - = Bugzilla::Extension::TrackingFlags::Flag->match({ id => \@tracking_flag_ids }); - my %tracking_flag_hash = map { $_->flag_id => $_ } @$tracking_flags; + my $tracking_flags = Bugzilla::Extension::TrackingFlags::Flag->match( + {id => \@tracking_flag_ids}); + my %tracking_flag_hash = map { $_->flag_id => $_ } @$tracking_flags; - foreach my $bug_flag (@$bug_flags) { - next if exists $bug_flag->{'tracking_flag'}; - $bug_flag->{'tracking_flag'} = $tracking_flag_hash{$bug_flag->tracking_flag_id}; - } + foreach my $bug_flag (@$bug_flags) { + next if exists $bug_flag->{'tracking_flag'}; + $bug_flag->{'tracking_flag'} = $tracking_flag_hash{$bug_flag->tracking_flag_id}; + } } ############################## @@ -128,15 +122,15 @@ sub preload_all_the_things { ############################## sub update_all_values { - my ($invocant, $params) = @_; - my $dbh = Bugzilla->dbh; - $dbh->do( - "UPDATE tracking_flags_bugs SET value=? WHERE tracking_flag_id=? AND value=?", - undef, - $params->{new_value}, - $params->{value_obj}->tracking_flag_id, - $params->{old_value}, - ); + my ($invocant, $params) = @_; + my $dbh = Bugzilla->dbh; + $dbh->do( + "UPDATE tracking_flags_bugs SET value=? WHERE tracking_flag_id=? AND value=?", + undef, + $params->{new_value}, + $params->{value_obj}->tracking_flag_id, + $params->{old_value}, + ); } ############################### @@ -144,19 +138,21 @@ sub update_all_values { ############################### sub _check_value { - my ($invocant, $value) = @_; - $value || ThrowCodeError('param_required', { param => 'value' }); - return $value; + my ($invocant, $value) = @_; + $value || ThrowCodeError('param_required', {param => 'value'}); + return $value; } sub _check_tracking_flag { - my ($invocant, $flag) = @_; - if (blessed $flag) { - return $flag->flag_id; - } - $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 }) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + my ($invocant, $flag) = @_; + if (blessed $flag) { return $flag->flag_id; + } + $flag + = Bugzilla::Extension::TrackingFlags::Flag->new({id => $flag, cache => 1}) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'flag_id', value => $flag}); + return $flag->flag_id; } ############################### @@ -170,19 +166,17 @@ sub set_value { $_[0]->set('value', $_[1]); } ############################### sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } -sub bug_id { return $_[0]->{'bug_id'}; } -sub value { return $_[0]->{'value'}; } +sub bug_id { return $_[0]->{'bug_id'}; } +sub value { return $_[0]->{'value'}; } sub bug { - return $_[0]->{'bug'} ||= Bugzilla::Bug->new({ - id => $_[0]->bug_id, cache => 1 - }); + return $_[0]->{'bug'} ||= Bugzilla::Bug->new({id => $_[0]->bug_id, cache => 1}); } sub tracking_flag { - return $_[0]->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new({ - id => $_[0]->tracking_flag_id, cache => 1 - }); + return $_[0]->{'tracking_flag'} + ||= Bugzilla::Extension::TrackingFlags::Flag->new( + {id => $_[0]->tracking_flag_id, cache => 1}); } 1; diff --git a/extensions/TrackingFlags/lib/Flag/Value.pm b/extensions/TrackingFlags/lib/Flag/Value.pm index 4f2aacc3a..52d63970d 100644 --- a/extensions/TrackingFlags/lib/Flag/Value.pm +++ b/extensions/TrackingFlags/lib/Flag/Value.pm @@ -25,32 +25,32 @@ use Scalar::Util qw(blessed weaken); use constant DB_TABLE => 'tracking_flags_values'; use constant DB_COLUMNS => qw( - id - tracking_flag_id - setter_group_id - value - sortkey - is_active - comment + id + tracking_flag_id + setter_group_id + value + sortkey + is_active + comment ); use constant LIST_ORDER => 'sortkey'; use constant UPDATE_COLUMNS => qw( - setter_group_id - value - sortkey - is_active - comment + setter_group_id + value + sortkey + is_active + comment ); use constant VALIDATORS => { - tracking_flag_id => \&_check_tracking_flag, - setter_group_id => \&_check_setter_group, - value => \&_check_value, - sortkey => \&_check_sortkey, - is_active => \&Bugzilla::Object::check_boolean, - comment => \&_check_comment, + tracking_flag_id => \&_check_tracking_flag, + setter_group_id => \&_check_setter_group, + value => \&_check_value, + sortkey => \&_check_sortkey, + is_active => \&Bugzilla::Object::check_boolean, + comment => \&_check_comment, }; ############################### @@ -58,43 +58,47 @@ use constant VALIDATORS => { ############################### sub _check_value { - my ($invocant, $value) = @_; - defined $value || ThrowCodeError('param_required', { param => 'value' }); - return $value; + my ($invocant, $value) = @_; + defined $value || ThrowCodeError('param_required', {param => 'value'}); + return $value; } sub _check_tracking_flag { - my ($invocant, $flag) = @_; - if (blessed $flag) { - return $flag->flag_id; - } - $flag = Bugzilla::Extension::TrackingFlags::Flag->new({ id => $flag, cache => 1 }) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + my ($invocant, $flag) = @_; + if (blessed $flag) { return $flag->flag_id; + } + $flag + = Bugzilla::Extension::TrackingFlags::Flag->new({id => $flag, cache => 1}) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'flag_id', value => $flag}); + return $flag->flag_id; } sub _check_setter_group { - my ($invocant, $group) = @_; - if (blessed $group) { - return $group->id; - } - $group = Bugzilla::Group->new({ id => $group, cache => 1 }) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'setter_group_id', value => $group }); + my ($invocant, $group) = @_; + if (blessed $group) { return $group->id; + } + $group + = Bugzilla::Group->new({id => $group, cache => 1}) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'setter_group_id', value => $group}); + return $group->id; } sub _check_sortkey { - my ($invocant, $sortkey) = @_; - detaint_natural($sortkey) - || ThrowUserError('field_invalid_sortkey', { sortkey => $sortkey }); - return $sortkey; + my ($invocant, $sortkey) = @_; + detaint_natural($sortkey) + || ThrowUserError('field_invalid_sortkey', {sortkey => $sortkey}); + return $sortkey; } sub _check_comment { - my ($invocant, $value) = @_; - return undef unless defined $value; - $value = trim($value); - return $value eq '' ? undef : $value; + my ($invocant, $value) = @_; + return undef unless defined $value; + $value = trim($value); + return $value eq '' ? undef : $value; } ############################### @@ -102,38 +106,37 @@ sub _check_comment { ############################### sub set_setter_group_id { $_[0]->set('setter_group_id', $_[1]); } -sub set_value { $_[0]->set('value', $_[1]); } -sub set_sortkey { $_[0]->set('sortkey', $_[1]); } -sub set_is_active { $_[0]->set('is_active', $_[1]); } -sub set_comment { $_[0]->set('comment', $_[1]); } +sub set_value { $_[0]->set('value', $_[1]); } +sub set_sortkey { $_[0]->set('sortkey', $_[1]); } +sub set_is_active { $_[0]->set('is_active', $_[1]); } +sub set_comment { $_[0]->set('comment', $_[1]); } ############################### #### Accessors #### ############################### sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } -sub setter_group_id { return $_[0]->{'setter_group_id'}; } -sub value { return $_[0]->{'value'}; } -sub sortkey { return $_[0]->{'sortkey'}; } -sub is_active { return $_[0]->{'is_active'}; } -sub comment { return $_[0]->{'comment'}; } +sub setter_group_id { return $_[0]->{'setter_group_id'}; } +sub value { return $_[0]->{'value'}; } +sub sortkey { return $_[0]->{'sortkey'}; } +sub is_active { return $_[0]->{'is_active'}; } +sub comment { return $_[0]->{'comment'}; } sub tracking_flag { - return $_[0]->{'tracking_flag'} if $_[0]->{'tracking_flag'}; - my $tf = $_[0]->{'tracking_flag'} = Bugzilla::Extension::TrackingFlags::Flag->new({ - id => $_[0]->tracking_flag_id, cache => 1 - }); - weaken($_[0]->{'tracking_flag'}); - return $tf; + return $_[0]->{'tracking_flag'} if $_[0]->{'tracking_flag'}; + my $tf = $_[0]->{'tracking_flag'} + = Bugzilla::Extension::TrackingFlags::Flag->new( + {id => $_[0]->tracking_flag_id, cache => 1}); + weaken($_[0]->{'tracking_flag'}); + return $tf; } sub setter_group { - if ($_[0]->setter_group_id) { - $_[0]->{'setter_group'} ||= Bugzilla::Group->new({ - id => $_[0]->setter_group_id, cache => 1 - }); - } - return $_[0]->{'setter_group'}; + if ($_[0]->setter_group_id) { + $_[0]->{'setter_group'} + ||= Bugzilla::Group->new({id => $_[0]->setter_group_id, cache => 1}); + } + return $_[0]->{'setter_group'}; } ######################################## @@ -141,6 +144,6 @@ sub setter_group { ######################################## sub name { return $_[0]->{'value'}; } -sub is_visible_on_bug { return 1; } +sub is_visible_on_bug { return 1; } 1; diff --git a/extensions/TrackingFlags/lib/Flag/Visibility.pm b/extensions/TrackingFlags/lib/Flag/Visibility.pm index 878c16f99..e90c0bf22 100644 --- a/extensions/TrackingFlags/lib/Flag/Visibility.pm +++ b/extensions/TrackingFlags/lib/Flag/Visibility.pm @@ -25,20 +25,20 @@ use Scalar::Util qw(blessed); use constant DB_TABLE => 'tracking_flags_visibility'; use constant DB_COLUMNS => qw( - id - tracking_flag_id - product_id - component_id + id + tracking_flag_id + product_id + component_id ); use constant LIST_ORDER => 'id'; -use constant UPDATE_COLUMNS => (); # imutable +use constant UPDATE_COLUMNS => (); # imutable use constant VALIDATORS => { - tracking_flag_id => \&_check_tracking_flag, - product_id => \&_check_product, - component_id => \&_check_component, + tracking_flag_id => \&_check_tracking_flag, + product_id => \&_check_product, + component_id => \&_check_component, }; ############################### @@ -46,66 +46,72 @@ use constant VALIDATORS => { ############################### sub match { - my $class= shift; - my ($params) = @_; - my $dbh = Bugzilla->dbh; - - # Allow matching component and product by name - # (in addition to matching by ID). - # Borrowed from Bugzilla::Bug::match - my %translate_fields = ( - product => 'Bugzilla::Product', - component => 'Bugzilla::Component', - ); - - 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 - if (exists $params->{$field}) { - my $names = $params->{$field}; - my $type = $translate_fields{$field}; - my $objects = Bugzilla::Object::match($type, { name => $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"}) { - delete $params->{$field}; - $params->{"${field}_id"} = scalar(@ids) == 1 ? [ $ids[0] ] : \@ids; - } + my $class = shift; + my ($params) = @_; + my $dbh = Bugzilla->dbh; + + # Allow matching component and product by name + # (in addition to matching by ID). + # Borrowed from Bugzilla::Bug::match + my %translate_fields + = (product => 'Bugzilla::Product', component => 'Bugzilla::Component',); + + 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 + if (exists $params->{$field}) { + my $names = $params->{$field}; + my $type = $translate_fields{$field}; + my $objects = Bugzilla::Object::match($type, {name => $names}); + push(@ids, map { $_->id } @$objects); } - # If we aren't matching on the product, use the default matching code - if (!exists $params->{product_id}) { - return $class->SUPER::match(@_); + # 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); } - my @criteria = ("1=1"); - - if ($params->{product_id}) { - push(@criteria, $dbh->sql_in('product_id', $params->{'product_id'})); - if ($params->{component_id}) { - my $component_id = $params->{component_id}; - push(@criteria, "(" . $dbh->sql_in('component_id', $params->{'component_id'}) . - " OR component_id IS NULL)"); - } + # 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"}) { + delete $params->{$field}; + $params->{"${field}_id"} = scalar(@ids) == 1 ? [$ids[0]] : \@ids; + } + } + + # If we aren't matching on the product, use the default matching code + if (!exists $params->{product_id}) { + return $class->SUPER::match(@_); + } + + my @criteria = ("1=1"); + + if ($params->{product_id}) { + push(@criteria, $dbh->sql_in('product_id', $params->{'product_id'})); + if ($params->{component_id}) { + my $component_id = $params->{component_id}; + push(@criteria, + "(" + . $dbh->sql_in('component_id', $params->{'component_id'}) + . " OR component_id IS NULL)"); } + } - my $where = join(' AND ', @criteria); - my $flag_ids = $dbh->selectcol_arrayref("SELECT id + my $where = join(' AND ', @criteria); + my $flag_ids = $dbh->selectcol_arrayref( + "SELECT id FROM tracking_flags_visibility - WHERE $where"); + WHERE $where" + ); - return Bugzilla::Extension::TrackingFlags::Flag::Visibility->new_from_list($flag_ids); + return Bugzilla::Extension::TrackingFlags::Flag::Visibility->new_from_list( + $flag_ids); } ############################### @@ -113,34 +119,40 @@ sub match { ############################### sub _check_tracking_flag { - my ($invocant, $flag) = @_; - if (blessed $flag) { - return $flag->flag_id; - } - $flag = Bugzilla::Extension::TrackingFlags::Flag->new($flag) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'flag_id', value => $flag }); + my ($invocant, $flag) = @_; + if (blessed $flag) { return $flag->flag_id; + } + $flag + = Bugzilla::Extension::TrackingFlags::Flag->new($flag) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'flag_id', value => $flag}); + return $flag->flag_id; } sub _check_product { - my ($invocant, $product) = @_; - if (blessed $product) { - return $product->id; - } - $product = Bugzilla::Product->new($product) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'product_id', value => $product }); + my ($invocant, $product) = @_; + if (blessed $product) { return $product->id; + } + $product + = Bugzilla::Product->new($product) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'product_id', value => $product}); + return $product->id; } sub _check_component { - my ($invocant, $component) = @_; - return undef unless defined $component; - if (blessed $component) { - return $component->id; - } - $component = Bugzilla::Component->new($component) - || ThrowCodeError('tracking_flags_invalid_param', { name => 'component_id', value => $component }); + my ($invocant, $component) = @_; + return undef unless defined $component; + if (blessed $component) { return $component->id; + } + $component + = Bugzilla::Component->new($component) + || ThrowCodeError('tracking_flags_invalid_param', + {name => 'component_id', value => $component}); + return $component->id; } ############################### @@ -148,26 +160,27 @@ sub _check_component { ############################### sub tracking_flag_id { return $_[0]->{'tracking_flag_id'}; } -sub product_id { return $_[0]->{'product_id'}; } -sub component_id { return $_[0]->{'component_id'}; } +sub product_id { return $_[0]->{'product_id'}; } +sub component_id { return $_[0]->{'component_id'}; } sub tracking_flag { - my ($self) = @_; - $self->{'tracking_flag'} ||= Bugzilla::Extension::TrackingFlags::Flag->new($self->tracking_flag_id); - return $self->{'tracking_flag'}; + my ($self) = @_; + $self->{'tracking_flag'} + ||= Bugzilla::Extension::TrackingFlags::Flag->new($self->tracking_flag_id); + return $self->{'tracking_flag'}; } sub product { - my ($self) = @_; - $self->{'product'} ||= Bugzilla::Product->new($self->product_id); - return $self->{'product'}; + my ($self) = @_; + $self->{'product'} ||= Bugzilla::Product->new($self->product_id); + return $self->{'product'}; } sub component { - my ($self) = @_; - return undef unless $self->component_id; - $self->{'component'} ||= Bugzilla::Component->new($self->component_id); - return $self->{'component'}; + my ($self) = @_; + return undef unless $self->component_id; + $self->{'component'} ||= Bugzilla::Component->new($self->component_id); + return $self->{'component'}; } 1; diff --git a/extensions/TypeSniffer/Config.pm b/extensions/TypeSniffer/Config.pm index 4545f69f6..336f92c8f 100644 --- a/extensions/TypeSniffer/Config.pm +++ b/extensions/TypeSniffer/Config.pm @@ -28,16 +28,8 @@ use warnings; use constant NAME => 'TypeSniffer'; use constant REQUIRED_MODULES => [ - { - package => 'File-MimeInfo', - module => 'File::MimeInfo::Magic', - version => '0' - }, - { - package => 'IO-stringy', - module => 'IO::Scalar', - version => '0' - }, + {package => 'File-MimeInfo', module => 'File::MimeInfo::Magic', version => '0'}, + {package => 'IO-stringy', module => 'IO::Scalar', version => '0'}, ]; -__PACKAGE__->NAME; \ No newline at end of file +__PACKAGE__->NAME; diff --git a/extensions/TypeSniffer/Extension.pm b/extensions/TypeSniffer/Extension.pm index 6c34cb169..52170f683 100644 --- a/extensions/TypeSniffer/Extension.pm +++ b/extensions/TypeSniffer/Extension.pm @@ -33,9 +33,7 @@ use IO::Scalar; our $VERSION = '1'; # These extensions override/supplement File::MimeInfo::Magic's detection. -our %EXTENSION_OVERRIDES = ( - '.lang' => 'text/plain', -); +our %EXTENSION_OVERRIDES = ('.lang' => 'text/plain',); ################################################################################ # This extension uses magic to guess MIME types for data where the browser has @@ -43,62 +41,63 @@ our %EXTENSION_OVERRIDES = ( # extension, or it's a text type with a non-.txt file extension). ################################################################################ sub attachment_process_data { - my ($self, $args) = @_; - my $attributes = $args->{'attributes'}; - my $params = Bugzilla->input_params; + my ($self, $args) = @_; + my $attributes = $args->{'attributes'}; + my $params = Bugzilla->input_params; - # If we have autodetected application/octet-stream from the Content-Type - # header, let's have a better go using a sniffer. - if ($params->{'contenttypemethod'} && - $params->{'contenttypemethod'} eq 'autodetect' && - $attributes->{'mimetype'} eq 'application/octet-stream') - { - my $filename = $attributes->{'filename'} . ''; + # If we have autodetected application/octet-stream from the Content-Type + # header, let's have a better go using a sniffer. + if ( $params->{'contenttypemethod'} + && $params->{'contenttypemethod'} eq 'autodetect' + && $attributes->{'mimetype'} eq 'application/octet-stream') + { + my $filename = $attributes->{'filename'} . ''; - # Check for an override first - if ($filename =~ /^.+(\..+$)/) { - my $ext = lc($1); - if (exists $EXTENSION_OVERRIDES{$ext}) { - $attributes->{'mimetype'} = $EXTENSION_OVERRIDES{$ext}; - return; - } - } + # Check for an override first + if ($filename =~ /^.+(\..+$)/) { + my $ext = lc($1); + if (exists $EXTENSION_OVERRIDES{$ext}) { + $attributes->{'mimetype'} = $EXTENSION_OVERRIDES{$ext}; + return; + } + } - # Then try file extension detection - my $mimetype = mimetype($filename); - if ($mimetype) { - $attributes->{'mimetype'} = $mimetype; - return; - } + # Then try file extension detection + my $mimetype = mimetype($filename); + if ($mimetype) { + $attributes->{'mimetype'} = $mimetype; + return; + } - # data attribute can be either scalar data or filehandle - # bugzilla.org/docs/3.6/en/html/api/Bugzilla/Attachment.html#create - my $fh = $attributes->{'data'}; - if (!ref($fh)) { - my $data = $attributes->{'data'}; - $fh = new IO::Scalar \$data; - } - else { - # CGI.pm sends us an Fh that isn't actually an IO::Handle, but - # has a method for getting an actual handle out of it. - if (!$fh->isa('IO::Handle')) { - $fh = $fh->handle; - # ->handle returns an literal IO::Handle, even though the - # underlying object is a file. So we rebless it to be a proper - # IO::File object so that we can call ->seek on it and so on. - # Just in case CGI.pm fixes this some day, we check ->isa first. - if (!$fh->isa('IO::File')) { - bless $fh, 'IO::File'; - } - } - } + # data attribute can be either scalar data or filehandle + # bugzilla.org/docs/3.6/en/html/api/Bugzilla/Attachment.html#create + my $fh = $attributes->{'data'}; + if (!ref($fh)) { + my $data = $attributes->{'data'}; + $fh = new IO::Scalar \$data; + } + else { + # CGI.pm sends us an Fh that isn't actually an IO::Handle, but + # has a method for getting an actual handle out of it. + if (!$fh->isa('IO::Handle')) { + $fh = $fh->handle; - $mimetype = mimetype($fh); - $fh->seek(0, 0); - if ($mimetype) { - $attributes->{'mimetype'} = $mimetype; + # ->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'; } + } + } + + $mimetype = mimetype($fh); + $fh->seek(0, 0); + if ($mimetype) { + $attributes->{'mimetype'} = $mimetype; } + } } __PACKAGE__->NAME; diff --git a/extensions/UserProfile/Config.pm b/extensions/UserProfile/Config.pm index 99fae1610..83b562163 100644 --- a/extensions/UserProfile/Config.pm +++ b/extensions/UserProfile/Config.pm @@ -11,8 +11,8 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'UserProfile'; -use constant REQUIRED_MODULES => [ ]; -use constant OPTIONAL_MODULES => [ ]; +use constant NAME => 'UserProfile'; +use constant REQUIRED_MODULES => []; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/UserProfile/Extension.pm b/extensions/UserProfile/Extension.pm index 9171b942d..cc8be3f1f 100644 --- a/extensions/UserProfile/Extension.pm +++ b/extensions/UserProfile/Extension.pm @@ -29,44 +29,43 @@ our $VERSION = '1'; # BEGIN { - *Bugzilla::User::last_activity_ts = \&_user_last_activity_ts; - *Bugzilla::User::set_last_activity_ts = \&_user_set_last_activity_ts; - *Bugzilla::User::last_statistics_ts = \&_user_last_statistics_ts; - *Bugzilla::User::clear_last_statistics_ts = \&_user_clear_last_statistics_ts; - *Bugzilla::User::address = \&_user_address; + *Bugzilla::User::last_activity_ts = \&_user_last_activity_ts; + *Bugzilla::User::set_last_activity_ts = \&_user_set_last_activity_ts; + *Bugzilla::User::last_statistics_ts = \&_user_last_statistics_ts; + *Bugzilla::User::clear_last_statistics_ts = \&_user_clear_last_statistics_ts; + *Bugzilla::User::address = \&_user_address; } -sub _user_last_activity_ts { $_[0]->{last_activity_ts} } -sub _user_last_statistics_ts { $_[0]->{last_statistics_ts} } +sub _user_last_activity_ts { $_[0]->{last_activity_ts} } +sub _user_last_statistics_ts { $_[0]->{last_statistics_ts} } + sub _user_address { - my $mode = Bugzilla->usage_mode; + my $mode = Bugzilla->usage_mode; - Email::Address->disable_cache if any { $mode == $_ } USAGE_MODE_CMDLINE, USAGE_MODE_TEST, USAGE_MODE_EMAIL; - return Email::Address->new(undef, $_[0]->email); + Email::Address->disable_cache + if any { $mode == $_ } USAGE_MODE_CMDLINE, USAGE_MODE_TEST, USAGE_MODE_EMAIL; + return Email::Address->new(undef, $_[0]->email); } -sub _user_set_last_activity_ts { - my ($self, $value) = @_; - $self->set('last_activity_ts', $_[1]); +sub _user_set_last_activity_ts { + my ($self, $value) = @_; + $self->set('last_activity_ts', $_[1]); - # we update the database directly to avoid audit_log entries - Bugzilla->dbh->do( - "UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", - undef, - $value, $self->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); + # we update the database directly to avoid audit_log entries + Bugzilla->dbh->do("UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", + undef, $value, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); } sub _user_clear_last_statistics_ts { - my ($self) = @_; - $self->set('last_statistics_ts', undef); - - # we update the database directly to avoid audit_log entries - Bugzilla->dbh->do( - "UPDATE profiles SET last_statistics_ts = NULL WHERE userid = ?", - undef, - $self->id); - Bugzilla->memcached->clear({ table => 'profiles', id => $self->id }); + my ($self) = @_; + $self->set('last_statistics_ts', undef); + + # we update the database directly to avoid audit_log entries + Bugzilla->dbh->do( + "UPDATE profiles SET last_statistics_ts = NULL WHERE userid = ?", + undef, $self->id); + Bugzilla->memcached->clear({table => 'profiles', id => $self->id}); } # @@ -76,327 +75,331 @@ sub _user_clear_last_statistics_ts { sub request_cleanup { Email::Address->purge_cache } sub bug_after_create { - my ($self, $args) = @_; - $self->_bug_touched($args); + my ($self, $args) = @_; + $self->_bug_touched($args); } sub bug_after_update { - my ($self, $args) = @_; - $self->_bug_touched($args); + my ($self, $args) = @_; + $self->_bug_touched($args); } sub _bug_touched { - my ($self, $args) = @_; - my $bug = $args->{bug}; - - my $user = Bugzilla->user; - my ($assigned_to, $qa_contact); - - # bug update - if (exists $args->{changes}) { - return unless - scalar(keys %{ $args->{changes} }) - || exists $args->{bug}->{added_comments}; - - # if the assignee or qa-contact is changed to someone other than the - # current user, update them - if (exists $args->{changes}->{assigned_to} - && $args->{changes}->{assigned_to}->[1] ne $user->login) - { - $assigned_to = $bug->assigned_to; - } - if (exists $args->{changes}->{qa_contact} - && ($args->{changes}->{qa_contact}->[1] || '') ne $user->login) - { - $qa_contact = $bug->qa_contact; - } - - # if the product is changed, we need to recount everyone involved with - # this bug - if (exists $args->{changes}->{product}) { - tag_for_recount_from_bug($bug->id); - } - + my ($self, $args) = @_; + my $bug = $args->{bug}; + + my $user = Bugzilla->user; + my ($assigned_to, $qa_contact); + + # bug update + if (exists $args->{changes}) { + return + unless scalar(keys %{$args->{changes}}) + || exists $args->{bug}->{added_comments}; + + # if the assignee or qa-contact is changed to someone other than the + # current user, update them + if (exists $args->{changes}->{assigned_to} + && $args->{changes}->{assigned_to}->[1] ne $user->login) + { + $assigned_to = $bug->assigned_to; } - # new bug - else { - # if the assignee or qa-contact is created set to someone other than - # the current user, update them - if ($bug->assigned_to->id != $user->id) { - $assigned_to = $bug->assigned_to; - } - if ($bug->qa_contact && $bug->qa_contact->id != $user->id) { - $qa_contact = $bug->qa_contact; - } + if (exists $args->{changes}->{qa_contact} + && ($args->{changes}->{qa_contact}->[1] || '') ne $user->login) + { + $qa_contact = $bug->qa_contact; } - my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction(); + # if the product is changed, we need to recount everyone involved with + # this bug + if (exists $args->{changes}->{product}) { + tag_for_recount_from_bug($bug->id); + } - # update user's last_activity_ts + } + + # new bug + else { + # if the assignee or qa-contact is created set to someone other than + # the current user, update them + if ($bug->assigned_to->id != $user->id) { + $assigned_to = $bug->assigned_to; + } + if ($bug->qa_contact && $bug->qa_contact->id != $user->id) { + $qa_contact = $bug->qa_contact; + } + } + + my $dbh = Bugzilla->dbh; + $dbh->bz_start_transaction(); + + # update user's last_activity_ts + eval { + $user->set_last_activity_ts($args->{timestamp}); + $self->_recalc_remove($user); + }; + if ($@) { + warn $@; + $self->_recalc_insert($user); + } + + # clear the last_statistics_ts for assignee/qa-contact to force a recount + # at the next poll + if ($assigned_to) { eval { - $user->set_last_activity_ts($args->{timestamp}); - $self->_recalc_remove($user); + $assigned_to->clear_last_statistics_ts(); + $self->_recalc_remove($assigned_to); }; if ($@) { - warn $@; - $self->_recalc_insert($user); + warn $@; + $self->_recalc_insert($assigned_to); } - - # clear the last_statistics_ts for assignee/qa-contact to force a recount - # at the next poll - if ($assigned_to) { - eval { - $assigned_to->clear_last_statistics_ts(); - $self->_recalc_remove($assigned_to); - }; - if ($@) { - warn $@; - $self->_recalc_insert($assigned_to); - } - } - if ($qa_contact) { - eval { - $qa_contact->clear_last_statistics_ts(); - $self->_recalc_remove($qa_contact); - }; - if ($@) { - warn $@; - $self->_recalc_insert($qa_contact); - } + } + if ($qa_contact) { + eval { + $qa_contact->clear_last_statistics_ts(); + $self->_recalc_remove($qa_contact); + }; + if ($@) { + warn $@; + $self->_recalc_insert($qa_contact); } + } - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub _recalc_insert { - my ($self, $user) = @_; - Bugzilla->dbh->do( - "INSERT IGNORE INTO profiles_statistics_recalc SET user_id=?", - undef, $user->id - ); + my ($self, $user) = @_; + Bugzilla->dbh->do("INSERT IGNORE INTO profiles_statistics_recalc SET user_id=?", + undef, $user->id); } sub _recalc_remove { - my ($self, $user) = @_; - Bugzilla->dbh->do( - "DELETE FROM profiles_statistics_recalc WHERE user_id=?", - undef, $user->id - ); + my ($self, $user) = @_; + Bugzilla->dbh->do("DELETE FROM profiles_statistics_recalc WHERE user_id=?", + undef, $user->id); } sub object_end_of_create { - my ($self, $args) = @_; - $self->_object_touched($args); + my ($self, $args) = @_; + $self->_object_touched($args); } sub object_end_of_update { - my ($self, $args) = @_; - $self->_object_touched($args); + my ($self, $args) = @_; + $self->_object_touched($args); } sub _object_touched { - my ($self, $args) = @_; - my $object = $args->{object} - or return; - return if exists $args->{changes} && !scalar(keys %{ $args->{changes} }); - - if ($object->isa('Bugzilla::Attachment')) { - # if an attachment is created or updated, that counts as user activity - my $user = Bugzilla->user; - my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - eval { - $user->set_last_activity_ts($timestamp); - $self->_recalc_remove($user); - }; - if ($@) { - warn $@; - $self->_recalc_insert($user); - } - } - elsif ($object->isa('Bugzilla::Product') && exists $args->{changes}->{name}) { - # if a product is renamed by an admin, rename in the - # profiles_statistics_products table - Bugzilla->dbh->do( - "UPDATE profiles_statistics_products SET product=? where product=?", - undef, - $args->{changes}->{name}->[1], $args->{changes}->{name}->[0], - ); + my ($self, $args) = @_; + my $object = $args->{object} or return; + return if exists $args->{changes} && !scalar(keys %{$args->{changes}}); + + if ($object->isa('Bugzilla::Attachment')) { + + # if an attachment is created or updated, that counts as user activity + my $user = Bugzilla->user; + my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + eval { + $user->set_last_activity_ts($timestamp); + $self->_recalc_remove($user); + }; + if ($@) { + warn $@; + $self->_recalc_insert($user); } + } + elsif ($object->isa('Bugzilla::Product') && exists $args->{changes}->{name}) { + + # if a product is renamed by an admin, rename in the + # profiles_statistics_products table + Bugzilla->dbh->do( + "UPDATE profiles_statistics_products SET product=? where product=?", + undef, + $args->{changes}->{name}->[1], + $args->{changes}->{name}->[0], + ); + } } sub reorg_move_bugs { - my ($self, $args) = @_; - my $bug_ids = $args->{bug_ids}; - printf "Touching user profile data for %s bugs.\n", scalar(@$bug_ids); - my $count = 0; - foreach my $bug_id (@$bug_ids) { - $count += tag_for_recount_from_bug($bug_id); - } - print "Updated $count users.\n"; + my ($self, $args) = @_; + my $bug_ids = $args->{bug_ids}; + printf "Touching user profile data for %s bugs.\n", scalar(@$bug_ids); + my $count = 0; + foreach my $bug_id (@$bug_ids) { + $count += tag_for_recount_from_bug($bug_id); + } + print "Updated $count users.\n"; } sub merge_users_before { - my ($self, $args) = @_; - my ($old_id, $new_id) = @$args{qw(old_id new_id)}; - # when users are merged, we have to delete all the statistics for both users - # we'll recalcuate the stats after the merge - print "deleting user profile statistics for $old_id and $new_id\n"; - my $dbh = Bugzilla->dbh; - foreach my $table (qw( profiles_statistics profiles_statistics_status profiles_statistics_products )) { - $dbh->do("DELETE FROM $table WHERE " . $dbh->sql_in('user_id', [ $old_id, $new_id ])); - } + my ($self, $args) = @_; + my ($old_id, $new_id) = @$args{qw(old_id new_id)}; + + # when users are merged, we have to delete all the statistics for both users + # we'll recalcuate the stats after the merge + print "deleting user profile statistics for $old_id and $new_id\n"; + my $dbh = Bugzilla->dbh; + foreach my $table ( + qw( profiles_statistics profiles_statistics_status profiles_statistics_products ) + ) + { + $dbh->do( + "DELETE FROM $table WHERE " . $dbh->sql_in('user_id', [$old_id, $new_id])); + } } sub merge_users_after { - my ($self, $args) = @_; - my $new_id = $args->{new_id}; - print "generating user profile statistics $new_id\n"; - update_statistics_by_user($new_id); + my ($self, $args) = @_; + my $new_id = $args->{new_id}; + print "generating user profile statistics $new_id\n"; + update_statistics_by_user($new_id); } sub webservice_user_get { - my ($self, $args) = @_; - my ($service, $users) = @$args{qw(webservice users)}; - - my $dbh = Bugzilla->dbh; - my $ids = [ - map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } - grep { exists $_->{id} } - @$users - ]; - return unless @$ids; - my $timestamps = $dbh->selectall_hashref( - "SELECT userid,last_activity_ts FROM profiles WHERE " . $dbh->sql_in('userid', $ids), - 'userid', - ); - foreach my $user (@$users) { - my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; - $user->{last_activity} = $service->type('dateTime', $timestamps->{$id}->{last_activity_ts}); - } + my ($self, $args) = @_; + my ($service, $users) = @$args{qw(webservice users)}; + + my $dbh = Bugzilla->dbh; + my $ids = [map { blessed($_->{id}) ? $_->{id}->value : $_->{id} } + grep { exists $_->{id} } @$users]; + return unless @$ids; + my $timestamps = $dbh->selectall_hashref( + "SELECT userid,last_activity_ts FROM profiles WHERE " + . $dbh->sql_in('userid', $ids), + 'userid', + ); + foreach my $user (@$users) { + my $id = blessed($user->{id}) ? $user->{id}->value : $user->{id}; + $user->{last_activity} + = $service->type('dateTime', $timestamps->{$id}->{last_activity_ts}); + } } sub template_before_create { - my ($self, $args) = @_; - $args->{config}->{FILTERS}->{timeago} = sub { - my ($time_str) = @_; - return time_ago(datetime_from($time_str, 'UTC')); - }; + my ($self, $args) = @_; + $args->{config}->{FILTERS}->{timeago} = sub { + my ($time_str) = @_; + return time_ago(datetime_from($time_str, 'UTC')); + }; } sub page_before_template { - my ($self, $args) = @_; - my ($vars, $page) = @$args{qw(vars page_id)}; - return unless $page eq 'user_profile.html'; - my $user = Bugzilla->user; - - # determine user to display - my ($target, $login); - my $input = Bugzilla->input_params; - if (my $user_id = $input->{user_id}) { - # load from user_id - $user_id = 0 if $user_id =~ /\D/; - $target = Bugzilla::User->check({ id => $user_id }); - } else { - # loading from login name requires authentication - Bugzilla->login(LOGIN_REQUIRED); - $login = $input->{login}; - if (!$login) { - # show current user's profile by default - $target = $user; - } else { - my $limit = Bugzilla->params->{'maxusermatches'} + 1; - my $users = Bugzilla::User::match($login, $limit, 1); - if (scalar(@$users) == 1) { - # always allow singular matches without confirmation - $target = $users->[0]; - } else { - Bugzilla::User::match_field({ 'login' => {'type' => 'single'} }); - $target = Bugzilla::User->check($login); - } - } + my ($self, $args) = @_; + my ($vars, $page) = @$args{qw(vars page_id)}; + return unless $page eq 'user_profile.html'; + my $user = Bugzilla->user; + + # determine user to display + my ($target, $login); + my $input = Bugzilla->input_params; + if (my $user_id = $input->{user_id}) { + + # load from user_id + $user_id = 0 if $user_id =~ /\D/; + $target = Bugzilla::User->check({id => $user_id}); + } + else { + # loading from login name requires authentication + Bugzilla->login(LOGIN_REQUIRED); + $login = $input->{login}; + if (!$login) { + + # show current user's profile by default + $target = $user; + } + else { + my $limit = Bugzilla->params->{'maxusermatches'} + 1; + my $users = Bugzilla::User::match($login, $limit, 1); + if (scalar(@$users) == 1) { + + # always allow singular matches without confirmation + $target = $users->[0]; + } + else { + Bugzilla::User::match_field({'login' => {'type' => 'single'}}); + $target = Bugzilla::User->check($login); + } } - $login ||= $target->login; + } + $login ||= $target->login; - # load statistics into $vars - my $dbh = Bugzilla->switch_to_shadow_db; + # load statistics into $vars + my $dbh = Bugzilla->switch_to_shadow_db; - my $stats = $dbh->selectall_hashref( - "SELECT name, count + my $stats = $dbh->selectall_hashref( + "SELECT name, count FROM profiles_statistics - WHERE user_id = ?", - "name", - undef, - $target->id, - ); - map { $stats->{$_} = $stats->{$_}->{count} } keys %$stats; + WHERE user_id = ?", "name", undef, $target->id, + ); + map { $stats->{$_} = $stats->{$_}->{count} } keys %$stats; - my $statuses = $dbh->selectall_hashref( - "SELECT status, count + my $statuses = $dbh->selectall_hashref( + "SELECT status, count FROM profiles_statistics_status - WHERE user_id = ?", - "status", - undef, - $target->id, - ); - map { $statuses->{$_} = $statuses->{$_}->{count} } keys %$statuses; + WHERE user_id = ?", "status", undef, $target->id, + ); + map { $statuses->{$_} = $statuses->{$_}->{count} } keys %$statuses; - my $products = $dbh->selectall_arrayref( - "SELECT product, count + my $products = $dbh->selectall_arrayref( + "SELECT product, count FROM profiles_statistics_products WHERE user_id = ? - ORDER BY product = '', count DESC", - { Slice => {} }, - $target->id, - ); - - # ensure there's always an "other" product entry - my ($other_product) = grep { $_->{product} eq '' } @$products; - if (!$other_product) { - $other_product = { product => '', count => 0 }; - push @$products, $other_product; + ORDER BY product = '', count DESC", {Slice => {}}, $target->id, + ); + + # ensure there's always an "other" product entry + my ($other_product) = grep { $_->{product} eq '' } @$products; + if (!$other_product) { + $other_product = {product => '', count => 0}; + push @$products, $other_product; + } + + # load product objects and validate product visibility + foreach my $product (@$products) { + next if $product->{product} eq ''; + my $product_obj = Bugzilla::Product->new({name => $product->{product}}); + if (!$product_obj || !$user->can_see_product($product_obj->name)) { + + # products not accessible to current user are moved into "other" + $other_product->{count} += $product->{count}; + $product->{count} = 0; } - - # load product objects and validate product visibility - foreach my $product (@$products) { - next if $product->{product} eq ''; - my $product_obj = Bugzilla::Product->new({ name => $product->{product} }); - if (!$product_obj || !$user->can_see_product($product_obj->name)) { - # products not accessible to current user are moved into "other" - $other_product->{count} += $product->{count}; - $product->{count} = 0; - } else { - $product->{product} = $product_obj; - } + else { + $product->{product} = $product_obj; } + } - # set other's name, and remove empty products - $other_product->{product} = { name => 'Other' }; - $products = [ grep { $_->{count} } @$products ]; + # set other's name, and remove empty products + $other_product->{product} = {name => 'Other'}; + $products = [grep { $_->{count} } @$products]; - $vars->{stats} = $stats; - $vars->{statuses} = $statuses; - $vars->{products} = $products; - $vars->{login} = $login; - $vars->{target} = $target; + $vars->{stats} = $stats; + $vars->{statuses} = $statuses; + $vars->{products} = $products; + $vars->{login} = $login; + $vars->{target} = $target; } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::User')) { - my $dbh = Bugzilla->dbh; - my @new_columns = qw(last_activity_ts last_statistics_ts); - push @$columns, grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::User')) { + my $dbh = Bugzilla->dbh; + my @new_columns = qw(last_activity_ts last_statistics_ts); + push @$columns, + grep { $dbh->bz_column_info($class->DB_TABLE, $_) } @new_columns; + } } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::User')) { - push(@$columns, qw(last_activity_ts last_statistics_ts)); - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::User')) { + push(@$columns, qw(last_activity_ts last_statistics_ts)); + } } # @@ -404,161 +407,96 @@ sub object_update_columns { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'profiles_statistics'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - name => { - TYPE => 'VARCHAR(30)', - NOTNULL => 1, - }, - count => { - TYPE => 'INT', - NOTNULL => 1, - }, - ], - INDEXES => [ - profiles_statistics_name_idx => { - FIELDS => [ 'user_id', 'name' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'profiles_statistics_status'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - status => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - count => { - TYPE => 'INT', - NOTNULL => 1, - }, - ], - INDEXES => [ - profiles_statistics_status_idx => { - FIELDS => [ 'user_id', 'status' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'profiles_statistics_products'} = { - FIELDS => [ - id => { - TYPE => 'MEDIUMSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - product => { - TYPE => 'VARCHAR(64)', - NOTNULL => 1, - }, - count => { - TYPE => 'INT', - NOTNULL => 1, - }, - ], - INDEXES => [ - profiles_statistics_products_idx => { - FIELDS => [ 'user_id', 'product' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'profiles_statistics_recalc'} = { - FIELDS => [ - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - profiles_statistics_recalc_idx => { - FIELDS => [ 'user_id' ], - TYPE => 'UNIQUE', - }, - ], - }; - $args->{'schema'}->{'profiles_statistics_recalc'} = { - FIELDS => [ - user_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'profiles', - COLUMN => 'userid', - DELETE => 'CASCADE', - } - }, - ], - INDEXES => [ - profiles_statistics_recalc_idx => { - FIELDS => [ 'user_id' ], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'profiles_statistics'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + name => {TYPE => 'VARCHAR(30)', NOTNULL => 1,}, + count => {TYPE => 'INT', NOTNULL => 1,}, + ], + INDEXES => [ + profiles_statistics_name_idx => + {FIELDS => ['user_id', 'name'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'profiles_statistics_status'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + status => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + count => {TYPE => 'INT', NOTNULL => 1,}, + ], + INDEXES => [ + profiles_statistics_status_idx => + {FIELDS => ['user_id', 'status'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'profiles_statistics_products'} = { + FIELDS => [ + id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + product => {TYPE => 'VARCHAR(64)', NOTNULL => 1,}, + count => {TYPE => 'INT', NOTNULL => 1,}, + ], + INDEXES => [ + profiles_statistics_products_idx => + {FIELDS => ['user_id', 'product'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'profiles_statistics_recalc'} = { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + profiles_statistics_recalc_idx => {FIELDS => ['user_id'], TYPE => 'UNIQUE',}, + ], + }; + $args->{'schema'}->{'profiles_statistics_recalc'} = { + FIELDS => [ + user_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'profiles', COLUMN => 'userid', DELETE => 'CASCADE',} + }, + ], + INDEXES => [ + profiles_statistics_recalc_idx => {FIELDS => ['user_id'], TYPE => 'UNIQUE',}, + ], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - $dbh->bz_add_column('profiles', 'last_activity_ts', { TYPE => 'DATETIME' }); - $dbh->bz_add_column('profiles', 'last_statistics_ts', { TYPE => 'DATETIME' }); + my $dbh = Bugzilla->dbh; + $dbh->bz_add_column('profiles', 'last_activity_ts', {TYPE => 'DATETIME'}); + $dbh->bz_add_column('profiles', 'last_statistics_ts', {TYPE => 'DATETIME'}); } sub install_filesystem { - my ($self, $args) = @_; - my $files = $args->{'files'}; - my $extensions_dir = bz_locations()->{'extensionsdir'}; - my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/update.pl"; - $files->{$script_name} = { - perms => Bugzilla::Install::Filesystem::WS_EXECUTE - }; - $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/migrate.pl"; - $files->{$script_name} = { - perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE - }; + my ($self, $args) = @_; + my $files = $args->{'files'}; + my $extensions_dir = bz_locations()->{'extensionsdir'}; + my $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/update.pl"; + $files->{$script_name} = {perms => Bugzilla::Install::Filesystem::WS_EXECUTE}; + $script_name = $extensions_dir . "/" . __PACKAGE__->NAME . "/bin/migrate.pl"; + $files->{$script_name} + = {perms => Bugzilla::Install::Filesystem::OWNER_EXECUTE}; } __PACKAGE__->NAME; diff --git a/extensions/UserProfile/bin/migrate.pl b/extensions/UserProfile/bin/migrate.pl index dd257f6bd..08c9f54f4 100755 --- a/extensions/UserProfile/bin/migrate.pl +++ b/extensions/UserProfile/bin/migrate.pl @@ -26,7 +26,7 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE); my $dbh = Bugzilla->dbh; my $user_ids = $dbh->selectcol_arrayref( - "SELECT userid + "SELECT userid FROM profiles WHERE last_activity_ts IS NULL ORDER BY userid" @@ -34,11 +34,9 @@ my $user_ids = $dbh->selectcol_arrayref( my ($current, $total) = (1, scalar(@$user_ids)); foreach my $user_id (@$user_ids) { - indicate_progress({ current => $current++, total => $total, every => 25 }); - my $ts = last_user_activity($user_id); - next unless $ts; - $dbh->do( - "UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", - undef, - $ts, $user_id); + indicate_progress({current => $current++, total => $total, every => 25}); + my $ts = last_user_activity($user_id); + next unless $ts; + $dbh->do("UPDATE profiles SET last_activity_ts = ? WHERE userid = ?", + undef, $ts, $user_id); } diff --git a/extensions/UserProfile/bin/update.pl b/extensions/UserProfile/bin/update.pl index 4d704a1f0..bdbcef329 100755 --- a/extensions/UserProfile/bin/update.pl +++ b/extensions/UserProfile/bin/update.pl @@ -26,56 +26,50 @@ my $user_ids; my $verbose = grep { $_ eq '-v' } @ARGV; $user_ids = $dbh->selectcol_arrayref( - "SELECT user_id + "SELECT user_id FROM profiles_statistics_recalc - ORDER BY user_id", - { Slice => {} } + ORDER BY user_id", {Slice => {}} ); if (@$user_ids) { - print "recalculating last_user_activity\n"; - my ($count, $total) = (0, scalar(@$user_ids)); - foreach my $user_id (@$user_ids) { - if ($verbose) { - $count++; - my $login = user_id_to_login($user_id); - print "$count/$total $login ($user_id)\n"; - } - $dbh->do( - "UPDATE profiles - SET last_activity_ts = ?, - last_statistics_ts = NULL - WHERE userid = ?", - undef, - last_user_activity($user_id), - $user_id - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $user_id }); + print "recalculating last_user_activity\n"; + my ($count, $total) = (0, scalar(@$user_ids)); + foreach my $user_id (@$user_ids) { + if ($verbose) { + $count++; + my $login = user_id_to_login($user_id); + print "$count/$total $login ($user_id)\n"; } $dbh->do( - "DELETE FROM profiles_statistics_recalc WHERE " . $dbh->sql_in('user_id', $user_ids) + "UPDATE profiles + SET last_activity_ts = ?, + last_statistics_ts = NULL + WHERE userid = ?", undef, last_user_activity($user_id), $user_id ); + Bugzilla->memcached->clear({table => 'profiles', id => $user_id}); + } + $dbh->do("DELETE FROM profiles_statistics_recalc WHERE " + . $dbh->sql_in('user_id', $user_ids)); } $user_ids = $dbh->selectcol_arrayref( - "SELECT userid + "SELECT userid FROM profiles WHERE last_activity_ts IS NOT NULL AND (last_statistics_ts IS NULL OR last_activity_ts > last_statistics_ts) - ORDER BY userid", - { Slice => {} } + ORDER BY userid", {Slice => {}} ); if (@$user_ids) { - $verbose && print "updating statistics\n"; - my ($count, $total) = (0, scalar(@$user_ids)); - foreach my $user_id (@$user_ids) { - if ($verbose) { - $count++; - my $login = user_id_to_login($user_id); - print "$count/$total $login ($user_id)\n"; - } - update_statistics_by_user($user_id); + $verbose && print "updating statistics\n"; + my ($count, $total) = (0, scalar(@$user_ids)); + foreach my $user_id (@$user_ids) { + if ($verbose) { + $count++; + my $login = user_id_to_login($user_id); + print "$count/$total $login ($user_id)\n"; } + update_statistics_by_user($user_id); + } } diff --git a/extensions/UserProfile/lib/Util.pm b/extensions/UserProfile/lib/Util.pm index 6b2eff098..54a9c2eea 100644 --- a/extensions/UserProfile/lib/Util.pm +++ b/extensions/UserProfile/lib/Util.pm @@ -13,44 +13,45 @@ use warnings; use base qw(Exporter); our @EXPORT = qw( update_statistics_by_user - tag_for_recount_from_bug - last_user_activity ); + tag_for_recount_from_bug + last_user_activity ); use Bugzilla; sub update_statistics_by_user { - my ($user_id) = @_; + my ($user_id) = @_; - # run all our queries on the slaves + # run all our queries on the slaves - my $dbh = Bugzilla->switch_to_shadow_db(); + my $dbh = Bugzilla->switch_to_shadow_db(); - my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + my $now = $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - # grab the current values + # grab the current values - my $last_statistics_ts = _get_last_statistics_ts($user_id); + my $last_statistics_ts = _get_last_statistics_ts($user_id); - my $statistics = _get_stats($user_id, 'profiles_statistics', 'name'); - my $by_status = _get_stats($user_id, 'profiles_statistics_status', 'status'); - my $by_product = _get_stats($user_id, 'profiles_statistics_products', 'product'); + my $statistics = _get_stats($user_id, 'profiles_statistics', 'name'); + my $by_status = _get_stats($user_id, 'profiles_statistics_status', 'status'); + my $by_product + = _get_stats($user_id, 'profiles_statistics_products', 'product'); - # bugs filed - _update_statistics($statistics, 'bugs_filed', [ $user_id ], <switch_to_main_db(); - $dbh->bz_start_transaction(); + $dbh = Bugzilla->switch_to_main_db(); + $dbh->bz_start_transaction(); - # commit updated statistics + # commit updated statistics - _set_stats($statistics, $user_id, 'profiles_statistics', 'name') - if _has_dirty($statistics); - _set_stats($by_status, $user_id, 'profiles_statistics_status', 'status') - if _has_dirty($by_status); - _set_stats($by_product, $user_id, 'profiles_statistics_products', 'product') - if _has_dirty($by_product); + _set_stats($statistics, $user_id, 'profiles_statistics', 'name') + if _has_dirty($statistics); + _set_stats($by_status, $user_id, 'profiles_statistics_status', 'status') + if _has_dirty($by_status); + _set_stats($by_product, $user_id, 'profiles_statistics_products', 'product') + if _has_dirty($by_product); - # update the user's last_statistics_ts - _set_last_statistics_ts($user_id, $now); + # update the user's last_statistics_ts + _set_last_statistics_ts($user_id, $now); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); } sub tag_for_recount_from_bug { - my ($bug_id) = @_; - my $dbh = Bugzilla->dbh; - # get a list of all users associated with this bug - my $user_ids = $dbh->selectcol_arrayref(<dbh; + + # get a list of all users associated with this bug + my $user_ids + = $dbh->selectcol_arrayref(<do( - "UPDATE profiles SET last_statistics_ts=NULL WHERE " . $dbh->sql_in('userid', $user_ids) - ); - foreach my $id (@$user_ids) { - Bugzilla->memcached->clear({ table => 'profiles', id => $id }); - } - return scalar(@$user_ids); + + # clear last_statistics_ts + $dbh->do("UPDATE profiles SET last_statistics_ts=NULL WHERE " + . $dbh->sql_in('userid', $user_ids)); + foreach my $id (@$user_ids) { + Bugzilla->memcached->clear({table => 'profiles', id => $id}); + } + return scalar(@$user_ids); } sub last_user_activity { - # last comment, or change to a bug (excluding CC changes) - my ($user_id) = @_; - return Bugzilla->dbh->selectrow_array(<dbh->selectrow_array( + <dbh->selectrow_array( - "SELECT last_statistics_ts FROM profiles WHERE userid = ?", - undef, $user_id - ); + my ($user_id) = @_; + return Bugzilla->dbh->selectrow_array( + "SELECT last_statistics_ts FROM profiles WHERE userid = ?", + undef, $user_id); } sub _set_last_statistics_ts { - my ($user_id, $timestamp) = @_; - Bugzilla->dbh->do( - "UPDATE profiles SET last_statistics_ts = ? WHERE userid = ?", - undef, - $timestamp, $user_id, - ); - Bugzilla->memcached->clear({ table => 'profiles', id => $user_id }); + my ($user_id, $timestamp) = @_; + Bugzilla->dbh->do("UPDATE profiles SET last_statistics_ts = ? WHERE userid = ?", + undef, $timestamp, $user_id,); + Bugzilla->memcached->clear({table => 'profiles', id => $user_id}); } sub _update_statistics { - my ($statistics, $name, $values, $sql) = @_; - my ($count) = Bugzilla->dbh->selectrow_array($sql, undef, @$values); - if (!exists $statistics->{$name}) { - $statistics->{$name} = { - id => 0, - count => $count, - dirty => 1, - }; - } elsif ($statistics->{$name}->{count} != $count) { - $statistics->{$name}->{count} = $count; - $statistics->{$name}->{dirty} = 1; - }; + my ($statistics, $name, $values, $sql) = @_; + my ($count) = Bugzilla->dbh->selectrow_array($sql, undef, @$values); + if (!exists $statistics->{$name}) { + $statistics->{$name} = {id => 0, count => $count, dirty => 1,}; + } + elsif ($statistics->{$name}->{count} != $count) { + $statistics->{$name}->{count} = $count; + $statistics->{$name}->{dirty} = 1; + } } sub _activity_by_status { - my ($by_status, $user_id) = @_; - my $dbh = Bugzilla->dbh; + my ($by_status, $user_id) = @_; + my $dbh = Bugzilla->dbh; - # we actually track both status and resolution changes as statuses - my @values = ($user_id, _field_id('bug_status'), $user_id, _field_id('resolution')); - my $rows = $dbh->selectall_arrayref(< {} }, @values); + # we actually track both status and resolution changes as statuses + my @values + = ($user_id, _field_id('bug_status'), $user_id, _field_id('resolution')); + my $rows = $dbh->selectall_arrayref(< {}}, @values); SELECT added AS status, COUNT(*) AS count FROM bugs_activity WHERE who = ? @@ -254,29 +255,26 @@ sub _activity_by_status { GROUP BY added EOF - foreach my $row (@$rows) { - my $status = $row->{status}; - if (!exists $by_status->{$status}) { - $by_status->{$status} = { - id => 0, - count => $row->{count}, - dirty => 1, - }; - } elsif ($by_status->{$status}->{count} != $row->{count}) { - $by_status->{$status}->{count} = $row->{count}; - $by_status->{$status}->{dirty} = 1; - } + foreach my $row (@$rows) { + my $status = $row->{status}; + if (!exists $by_status->{$status}) { + $by_status->{$status} = {id => 0, count => $row->{count}, dirty => 1,}; + } + elsif ($by_status->{$status}->{count} != $row->{count}) { + $by_status->{$status}->{count} = $row->{count}; + $by_status->{$status}->{dirty} = 1; } + } } sub _activity_by_product { - my ($by_product, $user_id) = @_; - my $dbh = Bugzilla->dbh; + my ($by_product, $user_id) = @_; + my $dbh = Bugzilla->dbh; - my %products; + my %products; - # changes - my $rows = $dbh->selectall_arrayref(< {} }, $user_id); + # changes + my $rows = $dbh->selectall_arrayref(< {}}, $user_id); SELECT products.name AS product, count(*) AS count FROM bugs_activity INNER JOIN bugs ON bugs.bug_id = bugs_activity.bug_id @@ -284,10 +282,10 @@ sub _activity_by_product { WHERE who = ? GROUP BY bugs.product_id EOF - map { $products{$_->{product}} += $_->{count} } @$rows; + map { $products{$_->{product}} += $_->{count} } @$rows; - # comments - $rows = $dbh->selectall_arrayref(< {} }, $user_id); + # comments + $rows = $dbh->selectall_arrayref(< {}}, $user_id); SELECT products.name AS product, count(*) AS count FROM longdescs INNER JOIN bugs ON bugs.bug_id = longdescs.bug_id @@ -295,96 +293,89 @@ EOF WHERE who = ? GROUP BY bugs.product_id EOF - map { $products{$_->{product}} += $_->{count} } @$rows; - - # store only the top 10 and 'other' (which is an empty string) - my @sorted = sort { $products{$b} <=> $products{$a} } keys %products; - my @other; - @other = splice(@sorted, 10) if scalar(@sorted) > 10; - map { $products{''} += $products{$_} } @other; - push @sorted, '' if $products{''}; - - # update by_product - foreach my $product (@sorted) { - if (!exists $by_product->{$product}) { - $by_product->{$product} = { - id => 0, - count => $products{$product}, - dirty => 1, - }; - } elsif ($by_product->{$product}->{count} != $products{$product}) { - $by_product->{$product}->{count} = $products{$product}; - $by_product->{$product}->{dirty} = 1; - } + map { $products{$_->{product}} += $_->{count} } @$rows; + + # store only the top 10 and 'other' (which is an empty string) + my @sorted = sort { $products{$b} <=> $products{$a} } keys %products; + my @other; + @other = splice(@sorted, 10) if scalar(@sorted) > 10; + map { $products{''} += $products{$_} } @other; + push @sorted, '' if $products{''}; + + # update by_product + foreach my $product (@sorted) { + if (!exists $by_product->{$product}) { + $by_product->{$product} = {id => 0, count => $products{$product}, dirty => 1,}; } - foreach my $product (keys %$by_product) { - if (!grep { $_ eq $product } @sorted) { - delete $by_product->{$product}; - } + elsif ($by_product->{$product}->{count} != $products{$product}) { + $by_product->{$product}->{count} = $products{$product}; + $by_product->{$product}->{dirty} = 1; } + } + foreach my $product (keys %$by_product) { + if (!grep { $_ eq $product } @sorted) { + delete $by_product->{$product}; + } + } } our $_field_id_cache; + sub _field_id { - my ($name) = @_; - if (!$_field_id_cache) { - my $rows = Bugzilla->dbh->selectall_arrayref("SELECT id, name FROM fielddefs"); - foreach my $row (@$rows) { - $_field_id_cache->{$row->[1]} = $row->[0]; - } + my ($name) = @_; + if (!$_field_id_cache) { + my $rows = Bugzilla->dbh->selectall_arrayref("SELECT id, name FROM fielddefs"); + foreach my $row (@$rows) { + $_field_id_cache->{$row->[1]} = $row->[0]; } - return $_field_id_cache->{$name}; + } + return $_field_id_cache->{$name}; } sub _get_stats { - my ($user_id, $table, $name_field) = @_; - my $result = {}; - my $rows = Bugzilla->dbh->selectall_arrayref( - "SELECT * FROM $table WHERE user_id = ?", - { Slice => {} }, - $user_id, - ); - foreach my $row (@$rows) { - unless (defined $row->{$name_field}) { - print "$user_id $table $name_field\n"; - die; - } - $result->{$row->{$name_field}} = { - id => $row->{id}, - count => $row->{count}, - dirty => 0, - } + my ($user_id, $table, $name_field) = @_; + my $result = {}; + my $rows + = Bugzilla->dbh->selectall_arrayref("SELECT * FROM $table WHERE user_id = ?", + {Slice => {}}, $user_id,); + foreach my $row (@$rows) { + unless (defined $row->{$name_field}) { + print "$user_id $table $name_field\n"; + die; } - return $result; + $result->{$row->{$name_field}} + = {id => $row->{id}, count => $row->{count}, dirty => 0,}; + } + return $result; } sub _set_stats { - my ($statistics, $user_id, $table, $name_field) = @_; - my $dbh = Bugzilla->dbh; - foreach my $name (keys %$statistics) { - next unless $statistics->{$name}->{dirty}; - if ($statistics->{$name}->{id}) { - $dbh->do( - "UPDATE $table SET count = ? WHERE user_id = ? AND $name_field = ?", - undef, - $statistics->{$name}->{count}, $user_id, $name, - ); - } else { - $dbh->do( - "INSERT INTO $table(user_id, $name_field, count) VALUES (?, ?, ?)", - undef, - $user_id, $name, $statistics->{$name}->{count}, - ); - } + my ($statistics, $user_id, $table, $name_field) = @_; + my $dbh = Bugzilla->dbh; + foreach my $name (keys %$statistics) { + next unless $statistics->{$name}->{dirty}; + if ($statistics->{$name}->{id}) { + $dbh->do( + "UPDATE $table SET count = ? WHERE user_id = ? AND $name_field = ?", + undef, $statistics->{$name}->{count}, + $user_id, $name, + ); + } + else { + $dbh->do( + "INSERT INTO $table(user_id, $name_field, count) VALUES (?, ?, ?)", + undef, $user_id, $name, $statistics->{$name}->{count}, + ); } + } } sub _has_dirty { - my ($statistics) = @_; - foreach my $name (keys %$statistics) { - return 1 if $statistics->{$name}->{dirty}; - } - return 0; + my ($statistics) = @_; + foreach my $name (keys %$statistics) { + return 1 if $statistics->{$name}->{dirty}; + } + return 0; } 1; diff --git a/extensions/UserStory/Config.pm b/extensions/UserStory/Config.pm index 8668deaa7..baf1776d3 100644 --- a/extensions/UserStory/Config.pm +++ b/extensions/UserStory/Config.pm @@ -12,13 +12,8 @@ use strict; use warnings; use constant NAME => 'UserStory'; -use constant REQUIRED_MODULES => [ - { - package => 'Text-Diff', - module => 'Text::Diff', - version => 0, - }, -]; +use constant REQUIRED_MODULES => + [{package => 'Text-Diff', module => 'Text::Diff', version => 0,},]; use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/UserStory/Extension.pm b/extensions/UserStory/Extension.pm index f9fdb3f29..0be43173b 100644 --- a/extensions/UserStory/Extension.pm +++ b/extensions/UserStory/Extension.pm @@ -23,82 +23,82 @@ use Bugzilla::Extension::BMO::FakeBug; use Text::Diff; BEGIN { - *Bugzilla::Bug::user_story_visible = \&_bug_user_story_visible; - *Bugzilla::Extension::BMO::FakeBug::user_story_visible = \&_bug_user_story_visible; + *Bugzilla::Bug::user_story_visible = \&_bug_user_story_visible; + *Bugzilla::Extension::BMO::FakeBug::user_story_visible + = \&_bug_user_story_visible; } sub _bug_user_story_visible { - my ($self) = @_; - if (!exists $self->{user_story_visible}) { - # Visible by default - $self->{user_story_visible} = 1; - my ($product, $component) = ($self->product, $self->component); - my $exclude_components = []; - if (exists USER_STORY_EXCLUDE->{$product}) { - $exclude_components = USER_STORY_EXCLUDE->{$product}; - if (scalar(@$exclude_components) == 0 - || ($component && grep { $_ eq $component } @$exclude_components)) - { - $self->{user_story_visible} = 0; - } - } - $self->{user_story_exclude_components} = $exclude_components; + my ($self) = @_; + if (!exists $self->{user_story_visible}) { + + # Visible by default + $self->{user_story_visible} = 1; + my ($product, $component) = ($self->product, $self->component); + my $exclude_components = []; + if (exists USER_STORY_EXCLUDE->{$product}) { + $exclude_components = USER_STORY_EXCLUDE->{$product}; + if (scalar(@$exclude_components) == 0 + || ($component && grep { $_ eq $component } @$exclude_components)) + { + $self->{user_story_visible} = 0; + } } - return ($self->{user_story_visible}, $self->{user_story_exclude_components}); + $self->{user_story_exclude_components} = $exclude_components; + } + return ($self->{user_story_visible}, $self->{user_story_exclude_components}); } # ensure user is allowed to edit the story sub bug_check_can_change_field { - my ($self, $args) = @_; - my ($bug, $field, $priv_results) = @$args{qw(bug field priv_results)}; - return unless $field eq 'cf_user_story'; - if (!Bugzilla->user->in_group(USER_STORY_GROUP)) { - push (@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); - } + my ($self, $args) = @_; + my ($bug, $field, $priv_results) = @$args{qw(bug field priv_results)}; + return unless $field eq 'cf_user_story'; + if (!Bugzilla->user->in_group(USER_STORY_GROUP)) { + push(@$priv_results, PRIVILEGES_REQUIRED_EMPOWERED); + } } # store just a diff of the changes in the bugs_activity table sub bug_update_before_logging { - my ($self, $args) = @_; - my $changes = $args->{changes}; - return unless exists $changes->{cf_user_story}; - my $diff = diff( - \$changes->{cf_user_story}->[0], - \$changes->{cf_user_story}->[1], - { - CONTEXT => 0, - }, - ); - $changes->{cf_user_story} = [ '', $diff ]; + my ($self, $args) = @_; + my $changes = $args->{changes}; + return unless exists $changes->{cf_user_story}; + my $diff = diff( + \$changes->{cf_user_story}->[0], + \$changes->{cf_user_story}->[1], + {CONTEXT => 0,}, + ); + $changes->{cf_user_story} = ['', $diff]; } # stop inline-history from displaying changes to the user story sub inline_history_activtiy { - my ($self, $args) = @_; - foreach my $activity (@{ $args->{activity} }) { - foreach my $change (@{ $activity->{changes} }) { - if ($change->{fieldname} eq 'cf_user_story') { - $change->{removed} = ''; - $change->{added} = '(updated)'; - } - } + my ($self, $args) = @_; + foreach my $activity (@{$args->{activity}}) { + foreach my $change (@{$activity->{changes}}) { + if ($change->{fieldname} eq 'cf_user_story') { + $change->{removed} = ''; + $change->{added} = '(updated)'; + } } + } } # create cf_user_story field sub install_update_db { - my ($self, $args) = @_; - return if Bugzilla::Field->new({ name => 'cf_user_story'}); - Bugzilla::Field->create({ - name => 'cf_user_story', - description => 'User Story', - type => FIELD_TYPE_TEXTAREA, - mailhead => 0, - enter_bug => 0, - obsolete => 0, - custom => 1, - buglist => 0, - }); + my ($self, $args) = @_; + return if Bugzilla::Field->new({name => 'cf_user_story'}); + Bugzilla::Field->create({ + name => 'cf_user_story', + description => 'User Story', + type => FIELD_TYPE_TEXTAREA, + mailhead => 0, + enter_bug => 0, + obsolete => 0, + custom => 1, + buglist => 0, + }); } __PACKAGE__->NAME; diff --git a/extensions/UserStory/lib/Constants.pm b/extensions/UserStory/lib/Constants.pm index 6a1c0c449..49f6b0d22 100644 --- a/extensions/UserStory/lib/Constants.pm +++ b/extensions/UserStory/lib/Constants.pm @@ -25,6 +25,6 @@ use constant USER_STORY_GROUP => 'editbugs'; # Don't show User Story on Developer Tools component, visible on all other # Firefox components # 'Firefox' => ['Developer Tools'], -use constant USER_STORY_EXCLUDE => { }; +use constant USER_STORY_EXCLUDE => {}; 1; diff --git a/extensions/Voting/Config.pm b/extensions/Voting/Config.pm index 97c44933e..d72caa3cc 100644 --- a/extensions/Voting/Config.pm +++ b/extensions/Voting/Config.pm @@ -13,10 +13,8 @@ use warnings; use constant NAME => 'Voting'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/extensions/Voting/Extension.pm b/extensions/Voting/Extension.pm index 198b065fa..523ee653d 100644 --- a/extensions/Voting/Extension.pm +++ b/extensions/Voting/Extension.pm @@ -23,15 +23,16 @@ use Bugzilla::Token; use List::Util qw(min sum); -use constant VERSION => BUGZILLA_VERSION; +use constant VERSION => BUGZILLA_VERSION; use constant DEFAULT_VOTES_PER_BUG => 1; + # These came from Bugzilla itself, so they maintain the old numbers # they had before. use constant CMT_POPULAR_VOTES => 3; -use constant REL_VOTER => 4; +use constant REL_VOTER => 4; BEGIN { - *Bugzilla::Bug::user_votes = \&_bug_user_votes; + *Bugzilla::Bug::user_votes = \&_bug_user_votes; } ################ @@ -39,70 +40,71 @@ BEGIN { ################ BEGIN { - *Bugzilla::Bug::votes = \&votes; + *Bugzilla::Bug::votes = \&votes; } sub votes { - my $self = shift; - my $dbh = Bugzilla->dbh; + my $self = shift; + my $dbh = Bugzilla->dbh; - return $self->{votes} if exists $self->{votes}; + return $self->{votes} if exists $self->{votes}; - $self->{votes} = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?', - undef, $self->id); - return $self->{votes}; + $self->{votes} + = $dbh->selectrow_array('SELECT votes FROM bugs WHERE bug_id = ?', + undef, $self->id); + return $self->{votes}; } sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'votes'} = { - FIELDS => [ - who => {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'}}, - vote_count => {TYPE => 'INT2', NOTNULL => 1}, - ], - INDEXES => [ - votes_who_idx => ['who'], - votes_bug_id_idx => ['bug_id'], - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'votes'} = { + FIELDS => [ + who => { + 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'} + }, + vote_count => {TYPE => 'INT2', NOTNULL => 1}, + ], + INDEXES => [votes_who_idx => ['who'], votes_bug_id_idx => ['bug_id'],], + }; } sub install_update_db { - my $dbh = Bugzilla->dbh; - # Note that before Bugzilla 4.0, voting was a built-in part of Bugzilla, - # so updates to the columns for old versions of Bugzilla happen in - # Bugzilla::Install::DB, and can't safely be moved to this extension. - - my $field = new Bugzilla::Field({ name => 'votes' }); - if (!$field) { - Bugzilla::Field->create( - { name => 'votes', description => 'Votes', buglist => 1 }); - } - - $dbh->bz_add_column('products', 'votesperuser', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_column('products', 'maxvotesperbug', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); - $dbh->bz_add_column('products', 'votestoconfirm', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); - - $dbh->bz_add_column('bugs', 'votes', - {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); - $dbh->bz_add_index('bugs', 'bugs_votes_idx', ['votes']); - - # maxvotesperbug used to default to 10,000, which isn't very sensible. - my $per_bug = $dbh->bz_column_info('products', 'maxvotesperbug'); - if ($per_bug->{DEFAULT} != DEFAULT_VOTES_PER_BUG) { - $dbh->bz_alter_column('products', 'maxvotesperbug', - {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); - } + my $dbh = Bugzilla->dbh; + + # Note that before Bugzilla 4.0, voting was a built-in part of Bugzilla, + # so updates to the columns for old versions of Bugzilla happen in + # Bugzilla::Install::DB, and can't safely be moved to this extension. + + my $field = new Bugzilla::Field({name => 'votes'}); + if (!$field) { + Bugzilla::Field->create( + {name => 'votes', description => 'Votes', buglist => 1}); + } + + $dbh->bz_add_column('products', 'votesperuser', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_column('products', 'maxvotesperbug', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); + $dbh->bz_add_column('products', 'votestoconfirm', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => 0}); + + $dbh->bz_add_column('bugs', 'votes', + {TYPE => 'INT3', NOTNULL => 1, DEFAULT => 0}); + $dbh->bz_add_index('bugs', 'bugs_votes_idx', ['votes']); + + # maxvotesperbug used to default to 10,000, which isn't very sensible. + my $per_bug = $dbh->bz_column_info('products', 'maxvotesperbug'); + if ($per_bug->{DEFAULT} != DEFAULT_VOTES_PER_BUG) { + $dbh->bz_alter_column('products', 'maxvotesperbug', + {TYPE => 'INT2', NOTNULL => 1, DEFAULT => DEFAULT_VOTES_PER_BUG}); + } } ########### @@ -110,102 +112,108 @@ sub install_update_db { ########### sub _bug_user_votes { - my ($self) = @_; - return $self->{'user_votes'} if exists $self->{'user_votes'}; - $self->{'user_votes'} = Bugzilla->dbh->selectrow_array( - "SELECT vote_count FROM votes WHERE bug_id = ? AND who = ?", - undef, $self->id, Bugzilla->user->id); - return $self->{'user_votes'}; + my ($self) = @_; + return $self->{'user_votes'} if exists $self->{'user_votes'}; + $self->{'user_votes'} + = Bugzilla->dbh->selectrow_array( + "SELECT vote_count FROM votes WHERE bug_id = ? AND who = ?", + undef, $self->id, Bugzilla->user->id); + return $self->{'user_votes'}; } sub object_columns { - my ($self, $args) = @_; - my ($class, $columns) = @$args{qw(class columns)}; - if ($class->isa('Bugzilla::Bug')) { - push(@$columns, 'votes'); - } - elsif ($class->isa('Bugzilla::Product')) { - push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); - } + my ($self, $args) = @_; + my ($class, $columns) = @$args{qw(class columns)}; + if ($class->isa('Bugzilla::Bug')) { + push(@$columns, 'votes'); + } + elsif ($class->isa('Bugzilla::Product')) { + push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); + } } sub bug_fields { - my ($self, $args) = @_; - my $fields = $args->{fields}; - push(@$fields, 'votes'); + my ($self, $args) = @_; + my $fields = $args->{fields}; + push(@$fields, 'votes'); } sub object_update_columns { - my ($self, $args) = @_; - my ($object, $columns) = @$args{qw(object columns)}; - if ($object->isa('Bugzilla::Product')) { - push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); - } + my ($self, $args) = @_; + my ($object, $columns) = @$args{qw(object columns)}; + if ($object->isa('Bugzilla::Product')) { + push(@$columns, qw(votesperuser maxvotesperbug votestoconfirm)); + } } sub object_validators { - my ($self, $args) = @_; - my ($class, $validators) = @$args{qw(class validators)}; - if ($class->isa('Bugzilla::Product')) { - $validators->{'votesperuser'} = \&_check_votesperuser; - $validators->{'maxvotesperbug'} = \&_check_maxvotesperbug; - $validators->{'votestoconfirm'} = \&_check_votestoconfirm; - } + my ($self, $args) = @_; + my ($class, $validators) = @$args{qw(class validators)}; + if ($class->isa('Bugzilla::Product')) { + $validators->{'votesperuser'} = \&_check_votesperuser; + $validators->{'maxvotesperbug'} = \&_check_maxvotesperbug; + $validators->{'votestoconfirm'} = \&_check_votestoconfirm; + } } sub object_before_create { - my ($self, $args) = @_; - my ($class, $params) = @$args{qw(class params)}; - if ($class->isa('Bugzilla::Bug')) { - # Don't ever allow people to directly specify "votes" into the bugs - # table. - delete $params->{votes}; - } - elsif ($class->isa('Bugzilla::Product')) { - my $input = Bugzilla->input_params; - $params->{votesperuser} = $input->{'votesperuser'}; - $params->{maxvotesperbug} = $input->{'maxvotesperbug'}; - $params->{votestoconfirm} = $input->{'votestoconfirm'}; - } + my ($self, $args) = @_; + my ($class, $params) = @$args{qw(class params)}; + if ($class->isa('Bugzilla::Bug')) { + + # Don't ever allow people to directly specify "votes" into the bugs + # table. + delete $params->{votes}; + } + elsif ($class->isa('Bugzilla::Product')) { + my $input = Bugzilla->input_params; + $params->{votesperuser} = $input->{'votesperuser'}; + $params->{maxvotesperbug} = $input->{'maxvotesperbug'}; + $params->{votestoconfirm} = $input->{'votestoconfirm'}; + } } sub object_end_of_set_all { - my ($self, $args) = @_; - my ($object) = $args->{object}; - if ($object->isa('Bugzilla::Product')) { - my $input = Bugzilla->input_params; - $object->set('votesperuser', $input->{'votesperuser'}); - $object->set('maxvotesperbug', $input->{'maxvotesperbug'}); - $object->set('votestoconfirm', $input->{'votestoconfirm'}); - } + my ($self, $args) = @_; + my ($object) = $args->{object}; + if ($object->isa('Bugzilla::Product')) { + my $input = Bugzilla->input_params; + $object->set('votesperuser', $input->{'votesperuser'}); + $object->set('maxvotesperbug', $input->{'maxvotesperbug'}); + $object->set('votestoconfirm', $input->{'votestoconfirm'}); + } } sub object_end_of_update { - my ($self, $args) = @_; - my ($object, $changes) = @$args{qw(object changes)}; - if ( $object->isa('Bugzilla::Product') - and ($changes->{maxvotesperbug} or $changes->{votesperuser} - or $changes->{votestoconfirm}) ) - { - _modify_bug_votes($object, $changes); - } + my ($self, $args) = @_; + my ($object, $changes) = @$args{qw(object changes)}; + if ( + $object->isa('Bugzilla::Product') + and ($changes->{maxvotesperbug} + or $changes->{votesperuser} + or $changes->{votestoconfirm}) + ) + { + _modify_bug_votes($object, $changes); + } } sub bug_end_of_update { - my ($self, $args) = @_; - my ($bug, $changes) = @$args{qw(bug changes)}; - - if ($changes->{'product'}) { - my @msgs; - # If some votes have been removed, RemoveVotes() returns - # a list of messages to send to voters. - @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved'); - _confirm_if_vote_confirmed($bug); - - foreach my $msg (@msgs) { - MessageToMTA($msg); - } + my ($self, $args) = @_; + my ($bug, $changes) = @$args{qw(bug changes)}; + + if ($changes->{'product'}) { + my @msgs; + + # If some votes have been removed, RemoveVotes() returns + # a list of messages to send to voters. + @msgs = _remove_votes($bug->id, 0, 'votes_bug_moved'); + _confirm_if_vote_confirmed($bug); + + foreach my $msg (@msgs) { + MessageToMTA($msg); } + } } ############# @@ -213,27 +221,28 @@ sub bug_end_of_update { ############# sub template_before_create { - my ($self, $args) = @_; - my $config = $args->{config}; - my $constants = $config->{VARIABLES}{constants}; - $constants->{REL_VOTER} = REL_VOTER; - $constants->{CMT_POPULAR_VOTES} = CMT_POPULAR_VOTES; - $constants->{DEFAULT_VOTES_PER_BUG} = DEFAULT_VOTES_PER_BUG; + my ($self, $args) = @_; + my $config = $args->{config}; + my $constants = $config->{VARIABLES}{constants}; + $constants->{REL_VOTER} = REL_VOTER; + $constants->{CMT_POPULAR_VOTES} = CMT_POPULAR_VOTES; + $constants->{DEFAULT_VOTES_PER_BUG} = DEFAULT_VOTES_PER_BUG; } sub template_before_process { - my ($self, $args) = @_; - my ($vars, $file) = @$args{qw(vars file)}; - if ($file eq 'admin/users/confirm-delete.html.tmpl') { - my $who = $vars->{otheruser}; - my $votes = Bugzilla->dbh->selectrow_array( - 'SELECT COUNT(*) FROM votes WHERE who = ?', undef, $who->id); - if ($votes) { - $vars->{other_safe} = 1; - $vars->{votes} = $votes; - } - } + my ($self, $args) = @_; + my ($vars, $file) = @$args{qw(vars file)}; + if ($file eq 'admin/users/confirm-delete.html.tmpl') { + my $who = $vars->{otheruser}; + my $votes + = Bugzilla->dbh->selectrow_array('SELECT COUNT(*) FROM votes WHERE who = ?', + undef, $who->id); + if ($votes) { + $vars->{other_safe} = 1; + $vars->{votes} = $votes; + } + } } ########### @@ -241,19 +250,19 @@ sub template_before_process { ########### sub bugmail_recipients { - my ($self, $args) = @_; - my ($bug, $recipients) = @$args{qw(bug recipients)}; - my $dbh = Bugzilla->dbh; + my ($self, $args) = @_; + my ($bug, $recipients) = @$args{qw(bug recipients)}; + my $dbh = Bugzilla->dbh; - my $voters = $dbh->selectcol_arrayref( - "SELECT who FROM votes WHERE bug_id = ?", undef, $bug->id); - $recipients->{$_}->{+REL_VOTER} = 1 foreach (@$voters); + my $voters = $dbh->selectcol_arrayref("SELECT who FROM votes WHERE bug_id = ?", + undef, $bug->id); + $recipients->{$_}->{+REL_VOTER} = 1 foreach (@$voters); } sub bugmail_relationships { - my ($self, $args) = @_; - my $relationships = $args->{relationships}; - $relationships->{+REL_VOTER} = 'Voter'; + my ($self, $args) = @_; + my $relationships = $args->{relationships}; + $relationships->{+REL_VOTER} = 'Voter'; } ############### @@ -261,59 +270,59 @@ sub bugmail_relationships { ############### sub sanitycheck_check { - my ($self, $args) = @_; - my $status = $args->{status}; - - # Vote Cache - $status->('voting_count_start'); - my $dbh = Bugzilla->dbh; - my %cached_counts = @{ $dbh->selectcol_arrayref( - 'SELECT bug_id, votes FROM bugs', {Columns=>[1,2]}) }; - - my %real_counts = @{ $dbh->selectcol_arrayref( - 'SELECT bug_id, SUM(vote_count) FROM votes ' - . $dbh->sql_group_by('bug_id'), {Columns=>[1,2]}) }; - - my $needs_rebuild; - foreach my $id (keys %cached_counts) { - my $cached_count = $cached_counts{$id}; - my $real_count = $real_counts{$id} || 0; - if ($cached_count < 0) { - $status->('voting_count_alert', { id => $id }, 'alert'); - } - elsif ($cached_count != $real_count) { - $status->('voting_cache_alert', { id => $id }, 'alert'); - $needs_rebuild = 1; - } - } - - $status->('voting_cache_rebuild_fix') if $needs_rebuild; + my ($self, $args) = @_; + my $status = $args->{status}; + + # Vote Cache + $status->('voting_count_start'); + my $dbh = Bugzilla->dbh; + my %cached_counts = @{$dbh->selectcol_arrayref('SELECT bug_id, votes FROM bugs', + {Columns => [1, 2]})}; + + my %real_counts = @{ + $dbh->selectcol_arrayref( + 'SELECT bug_id, SUM(vote_count) FROM votes ' . $dbh->sql_group_by('bug_id'), + {Columns => [1, 2]}) + }; + + my $needs_rebuild; + foreach my $id (keys %cached_counts) { + my $cached_count = $cached_counts{$id}; + my $real_count = $real_counts{$id} || 0; + if ($cached_count < 0) { + $status->('voting_count_alert', {id => $id}, 'alert'); + } + elsif ($cached_count != $real_count) { + $status->('voting_cache_alert', {id => $id}, 'alert'); + $needs_rebuild = 1; + } + } + + $status->('voting_cache_rebuild_fix') if $needs_rebuild; } sub sanitycheck_repair { - my ($self, $args) = @_; - my $status = $args->{status}; - my $input = Bugzilla->input_params; - my $dbh = Bugzilla->dbh; - - return if !$input->{rebuild_vote_cache}; - - $status->('voting_cache_rebuild_start'); - $dbh->bz_start_transaction(); - $dbh->do('UPDATE bugs SET votes = 0'); - - my $sth = $dbh->prepare( - 'SELECT bug_id, SUM(vote_count) FROM votes ' - . $dbh->sql_group_by('bug_id')); - $sth->execute(); - - my $sth_update = $dbh->prepare( - 'UPDATE bugs SET votes = ? WHERE bug_id = ?'); - while (my ($id, $count) = $sth->fetchrow_array) { - $sth_update->execute($count, $id); - } - $dbh->bz_commit_transaction(); - $status->('voting_cache_rebuild_end'); + my ($self, $args) = @_; + my $status = $args->{status}; + my $input = Bugzilla->input_params; + my $dbh = Bugzilla->dbh; + + return if !$input->{rebuild_vote_cache}; + + $status->('voting_cache_rebuild_start'); + $dbh->bz_start_transaction(); + $dbh->do('UPDATE bugs SET votes = 0'); + + my $sth = $dbh->prepare( + 'SELECT bug_id, SUM(vote_count) FROM votes ' . $dbh->sql_group_by('bug_id')); + $sth->execute(); + + my $sth_update = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?'); + while (my ($id, $count) = $sth->fetchrow_array) { + $sth_update->execute($count, $id); + } + $dbh->bz_commit_transaction(); + $status->('voting_cache_rebuild_end'); } @@ -322,35 +331,36 @@ sub sanitycheck_repair { ############## sub _check_votesperuser { - return _check_votes(0, @_); + return _check_votes(0, @_); } sub _check_maxvotesperbug { - return _check_votes(DEFAULT_VOTES_PER_BUG, @_); + return _check_votes(DEFAULT_VOTES_PER_BUG, @_); } sub _check_votestoconfirm { - return _check_votes(0, @_); + return _check_votes(0, @_); } # This subroutine is only used internally by other _check_votes_* validators. sub _check_votes { - my ($default, $invocant, $votes, $field) = @_; - - detaint_natural($votes) if defined $votes; - # On product creation, if the number of votes is not a valid integer, - # we silently fall back to the given default value. - # If the product already exists and the change is illegal, we complain. - if (!defined $votes) { - if (ref $invocant) { - ThrowUserError('voting_product_illegal_votes', - { field => $field, votes => $_[2] }); - } - else { - $votes = $default; - } + my ($default, $invocant, $votes, $field) = @_; + + detaint_natural($votes) if defined $votes; + + # On product creation, if the number of votes is not a valid integer, + # we silently fall back to the given default value. + # If the product already exists and the change is illegal, we complain. + if (!defined $votes) { + if (ref $invocant) { + ThrowUserError('voting_product_illegal_votes', + {field => $field, votes => $_[2]}); } - return $votes; + else { + $votes = $default; + } + } + return $votes; } ######### @@ -358,291 +368,322 @@ sub _check_votes { ######### sub page_before_template { - my ($self, $args) = @_; - my $page = $args->{page_id}; - my $vars = $args->{vars}; - - if ($page =~ m{^voting/bug\.}) { - _page_bug($vars); - } - elsif ($page =~ m{^voting/user\.}) { - _page_user($vars); - } + my ($self, $args) = @_; + my $page = $args->{page_id}; + my $vars = $args->{vars}; + + if ($page =~ m{^voting/bug\.}) { + _page_bug($vars); + } + elsif ($page =~ m{^voting/user\.}) { + _page_user($vars); + } } sub _page_bug { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $input = Bugzilla->input_params; + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $input = Bugzilla->input_params; - my $bug_id = $input->{bug_id}; - my $bug = Bugzilla::Bug->check($bug_id); + my $bug_id = $input->{bug_id}; + my $bug = Bugzilla::Bug->check($bug_id); - $vars->{'bug'} = $bug; - $vars->{'users'} = - $dbh->selectall_arrayref('SELECT profiles.login_name, + $vars->{'bug'} = $bug; + $vars->{'users'} = $dbh->selectall_arrayref( + 'SELECT profiles.login_name, profiles.userid AS id, votes.vote_count FROM votes INNER JOIN profiles ON profiles.userid = votes.who - WHERE votes.bug_id = ?', - {Slice=>{}}, $bug->id); + WHERE votes.bug_id = ?', {Slice => {}}, $bug->id + ); } sub _page_user { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my $input = Bugzilla->input_params; + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my $input = Bugzilla->input_params; - my $action = $input->{action}; - if ($action and $action eq 'vote') { - _update_votes($vars); - } + my $action = $input->{action}; + if ($action and $action eq 'vote') { + _update_votes($vars); + } - # If a bug_id is given, and we're editing, we'll add it to the votes list. + # If a bug_id is given, and we're editing, we'll add it to the votes list. - my $bug_id = $input->{bug_id}; - $bug_id = $bug_id->[0] if ref($bug_id) eq 'ARRAY'; - my $bug = Bugzilla::Bug->check({ id => $bug_id, cache => 1 }) if $bug_id; - my $who_id = $input->{user_id} || $user->id; + my $bug_id = $input->{bug_id}; + $bug_id = $bug_id->[0] if ref($bug_id) eq 'ARRAY'; + my $bug = Bugzilla::Bug->check({id => $bug_id, cache => 1}) if $bug_id; + my $who_id = $input->{user_id} || $user->id; - # Logged-out users must specify a user_id. - Bugzilla->login(LOGIN_REQUIRED) if !$who_id; + # Logged-out users must specify a user_id. + Bugzilla->login(LOGIN_REQUIRED) if !$who_id; - my $who = Bugzilla::User->check({ id => $who_id }); + my $who = Bugzilla::User->check({id => $who_id}); - my $canedit = $user->id == $who->id; + my $canedit = $user->id == $who->id; - $dbh->bz_start_transaction(); + $dbh->bz_start_transaction(); - if ($canedit && $bug) { - # Make sure there is an entry for this bug - # in the vote table, just so that things display right. - my $has_votes = $dbh->selectrow_array('SELECT vote_count FROM votes - WHERE bug_id = ? AND who = ?', - undef, ($bug->id, $who->id)); - if (!$has_votes) { - $dbh->do('INSERT INTO votes (who, bug_id, vote_count) - VALUES (?, ?, 0)', undef, ($who->id, $bug->id)); - } + if ($canedit && $bug) { + + # Make sure there is an entry for this bug + # in the vote table, just so that things display right. + my $has_votes = $dbh->selectrow_array( + 'SELECT vote_count FROM votes + WHERE bug_id = ? AND who = ?', undef, + ($bug->id, $who->id) + ); + if (!$has_votes) { + $dbh->do( + 'INSERT INTO votes (who, bug_id, vote_count) + VALUES (?, ?, 0)', undef, ($who->id, $bug->id) + ); } + } - my (@products, @all_bug_ids); - # Read the votes data for this user for each product. - foreach my $product (@{ $user->get_selectable_products }) { - next unless ($product->{votesperuser} > 0); + my (@products, @all_bug_ids); - my $vote_list = - $dbh->selectall_arrayref('SELECT votes.bug_id, votes.vote_count + # Read the votes data for this user for each product. + foreach my $product (@{$user->get_selectable_products}) { + next unless ($product->{votesperuser} > 0); + + my $vote_list = $dbh->selectall_arrayref( + 'SELECT votes.bug_id, votes.vote_count FROM votes INNER JOIN bugs ON votes.bug_id = bugs.bug_id WHERE votes.who = ? - AND bugs.product_id = ?', - undef, ($who->id, $product->id)); - - my %votes = map { $_->[0] => $_->[1] } @$vote_list; - my @bug_ids = sort keys %votes; - # Exclude bugs that the user can no longer see. - @bug_ids = @{ $user->visible_bugs(\@bug_ids) }; - next unless scalar @bug_ids; - - push(@all_bug_ids, @bug_ids); - my @bugs = @{ Bugzilla::Bug->new_from_list(\@bug_ids) }; - $_->{count} = $votes{$_->id} foreach @bugs; - # We include votes from bugs that the user can no longer see. - my $total = sum(values %votes) || 0; - - my $onevoteonly = 0; - $onevoteonly = 1 if (min($product->{votesperuser}, - $product->{maxvotesperbug}) == 1); - - push(@products, { name => $product->name, - bugs => \@bugs, - bug_ids => \@bug_ids, - onevoteonly => $onevoteonly, - total => $total, - maxvotes => $product->{votesperuser}, - maxperbug => $product->{maxvotesperbug} }); - } - - if ($canedit && $bug) { - $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?', - undef, $who->id); - } - $dbh->bz_commit_transaction(); - - $vars->{'canedit'} = $canedit; - $vars->{'voting_user'} = { "login" => $who->name }; - $vars->{'products'} = \@products; - $vars->{'this_bug'} = $bug; - $vars->{'all_bug_ids'} = \@all_bug_ids; + AND bugs.product_id = ?', undef, + ($who->id, $product->id) + ); + + my %votes = map { $_->[0] => $_->[1] } @$vote_list; + my @bug_ids = sort keys %votes; + + # Exclude bugs that the user can no longer see. + @bug_ids = @{$user->visible_bugs(\@bug_ids)}; + next unless scalar @bug_ids; + + push(@all_bug_ids, @bug_ids); + my @bugs = @{Bugzilla::Bug->new_from_list(\@bug_ids)}; + $_->{count} = $votes{$_->id} foreach @bugs; + + # We include votes from bugs that the user can no longer see. + my $total = sum(values %votes) || 0; + + my $onevoteonly = 0; + $onevoteonly = 1 + if (min($product->{votesperuser}, $product->{maxvotesperbug}) == 1); + + push( + @products, + { + name => $product->name, + bugs => \@bugs, + bug_ids => \@bug_ids, + onevoteonly => $onevoteonly, + total => $total, + maxvotes => $product->{votesperuser}, + maxperbug => $product->{maxvotesperbug} + } + ); + } + + if ($canedit && $bug) { + $dbh->do('DELETE FROM votes WHERE vote_count = 0 AND who = ?', undef, $who->id); + } + $dbh->bz_commit_transaction(); + + $vars->{'canedit'} = $canedit; + $vars->{'voting_user'} = {"login" => $who->name}; + $vars->{'products'} = \@products; + $vars->{'this_bug'} = $bug; + $vars->{'all_bug_ids'} = \@all_bug_ids; } sub _update_votes { - my ($vars) = @_; - - ############################################################################ - # Begin Data/Security Validation - ############################################################################ - - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my $template = Bugzilla->template; - my $user = Bugzilla->login(LOGIN_REQUIRED); - my $input = Bugzilla->input_params; - - # Build a list of bug IDs for which votes have been submitted. Votes - # are submitted in form fields in which the field names are the bug - # IDs and the field values are the number of votes. - - my @buglist = grep {/^\d+$/} keys %$input; - my (%bugs, %votes); - - # If no bugs are in the buglist, let's make sure the user gets notified - # that their votes will get nuked if they continue. - if (scalar(@buglist) == 0) { - if (!defined $cgi->param('delete_all_votes')) { - print $cgi->header(); - $template->process("voting/delete-all.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; - } - elsif ($cgi->param('delete_all_votes') == 0) { - print $cgi->redirect("page.cgi?id=voting/user.html"); - exit; + my ($vars) = @_; + + ############################################################################ + # Begin Data/Security Validation + ############################################################################ + + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my $template = Bugzilla->template; + my $user = Bugzilla->login(LOGIN_REQUIRED); + my $input = Bugzilla->input_params; + + # Build a list of bug IDs for which votes have been submitted. Votes + # are submitted in form fields in which the field names are the bug + # IDs and the field values are the number of votes. + + my @buglist = grep {/^\d+$/} keys %$input; + my (%bugs, %votes); + + # If no bugs are in the buglist, let's make sure the user gets notified + # that their votes will get nuked if they continue. + if (scalar(@buglist) == 0) { + if (!defined $cgi->param('delete_all_votes')) { + print $cgi->header(); + $template->process("voting/delete-all.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + elsif ($cgi->param('delete_all_votes') == 0) { + print $cgi->redirect("page.cgi?id=voting/user.html"); + exit; + } + } + else { + $user->visible_bugs(\@buglist); + my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist); + $bugs{$_->id} = $_ foreach @$bugs_obj; + } + + # Call check_is_visible() on each bug to make sure it is an existing bug + # that the user is authorized to access, and make sure the number of votes + # submitted is also an integer. + foreach my $id (@buglist) { + my $bug = $bugs{$id} + or ThrowUserError('bug_id_does_not_exist', {bug_id => $id}); + $bug->check_is_visible; + $id = $bug->id; + $votes{$id} = $input->{$id}; + detaint_natural($votes{$id}) || ThrowUserError("voting_must_be_nonnegative"); + } + + my $token = $cgi->param('token'); + check_hash_token($token, ['vote']); + + ############################################################################ + # End Data/Security Validation + ############################################################################ + my $who = $user->id; + + # If the user is voting for bugs, make sure they aren't overstuffing + # the ballot box. + if (scalar @buglist) { + my (%prodcount, %products); + foreach my $bug_id (keys %bugs) { + my $bug = $bugs{$bug_id}; + my $prod = $bug->product; + $products{$prod} ||= $bug->product_obj; + $prodcount{$prod} ||= 0; + $prodcount{$prod} += $votes{$bug_id}; + + # Make sure we haven't broken the votes-per-bug limit + ($votes{$bug_id} <= $products{$prod}->{maxvotesperbug}) || ThrowUserError( + "voting_too_many_votes_for_bug", + { + max => $products{$prod}->{maxvotesperbug}, + product => $prod, + votes => $votes{$bug_id} } - } - else { - $user->visible_bugs(\@buglist); - my $bugs_obj = Bugzilla::Bug->new_from_list(\@buglist); - $bugs{$_->id} = $_ foreach @$bugs_obj; + ); } - # Call check_is_visible() on each bug to make sure it is an existing bug - # that the user is authorized to access, and make sure the number of votes - # submitted is also an integer. - foreach my $id (@buglist) { - my $bug = $bugs{$id} - or ThrowUserError('bug_id_does_not_exist', { bug_id => $id }); - $bug->check_is_visible; - $id = $bug->id; - $votes{$id} = $input->{$id}; - detaint_natural($votes{$id}) - || ThrowUserError("voting_must_be_nonnegative"); - } - - my $token = $cgi->param('token'); - check_hash_token($token, ['vote']); - - ############################################################################ - # End Data/Security Validation - ############################################################################ - my $who = $user->id; - - # If the user is voting for bugs, make sure they aren't overstuffing - # the ballot box. - if (scalar @buglist) { - my (%prodcount, %products); - foreach my $bug_id (keys %bugs) { - my $bug = $bugs{$bug_id}; - my $prod = $bug->product; - $products{$prod} ||= $bug->product_obj; - $prodcount{$prod} ||= 0; - $prodcount{$prod} += $votes{$bug_id}; - - # Make sure we haven't broken the votes-per-bug limit - ($votes{$bug_id} <= $products{$prod}->{maxvotesperbug}) - || ThrowUserError("voting_too_many_votes_for_bug", - {max => $products{$prod}->{maxvotesperbug}, - product => $prod, - votes => $votes{$bug_id}}); - } - - # Make sure we haven't broken the votes-per-product limit - foreach my $prod (keys(%prodcount)) { - ($prodcount{$prod} <= $products{$prod}->{votesperuser}) - || ThrowUserError("voting_too_many_votes_for_product", - {max => $products{$prod}->{votesperuser}, - product => $prod, - votes => $prodcount{$prod}}); + # Make sure we haven't broken the votes-per-product limit + foreach my $prod (keys(%prodcount)) { + ($prodcount{$prod} <= $products{$prod}->{votesperuser}) || ThrowUserError( + "voting_too_many_votes_for_product", + { + max => $products{$prod}->{votesperuser}, + product => $prod, + votes => $prodcount{$prod} } + ); } + } - # Update the user's votes in the database. - $dbh->bz_start_transaction(); + # Update the user's votes in the database. + $dbh->bz_start_transaction(); - my $old_list = $dbh->selectall_arrayref('SELECT bug_id, vote_count FROM votes - WHERE who = ?', undef, $who); + my $old_list = $dbh->selectall_arrayref( + 'SELECT bug_id, vote_count FROM votes + WHERE who = ?', undef, $who + ); - my %old_votes = map { $_->[0] => $_->[1] } @$old_list; + my %old_votes = map { $_->[0] => $_->[1] } @$old_list; - my $sth_insertVotes = $dbh->prepare('INSERT INTO votes (who, bug_id, vote_count) - VALUES (?, ?, ?)'); - my $sth_updateVotes = $dbh->prepare('UPDATE votes SET vote_count = ? - WHERE bug_id = ? AND who = ?'); + my $sth_insertVotes = $dbh->prepare( + 'INSERT INTO votes (who, bug_id, vote_count) + VALUES (?, ?, ?)' + ); + my $sth_updateVotes = $dbh->prepare( + 'UPDATE votes SET vote_count = ? + WHERE bug_id = ? AND who = ?' + ); - my %affected = map { $_ => 1 } (@buglist, keys %old_votes); - my @deleted_votes; - - foreach my $id (keys %affected) { - if (!$votes{$id}) { - push(@deleted_votes, $id); - next; - } - if ($votes{$id} == ($old_votes{$id} || 0)) { - delete $affected{$id}; - next; - } - # We use 'defined' in case 0 was accidentally stored in the DB. - if (defined $old_votes{$id}) { - $sth_updateVotes->execute($votes{$id}, $id, $who); - } - else { - $sth_insertVotes->execute($who, $id, $votes{$id}); - } - } + my %affected = map { $_ => 1 } (@buglist, keys %old_votes); + my @deleted_votes; - if (@deleted_votes) { - $dbh->do('DELETE FROM votes WHERE who = ? AND ' . - $dbh->sql_in('bug_id', \@deleted_votes), undef, $who); + foreach my $id (keys %affected) { + if (!$votes{$id}) { + push(@deleted_votes, $id); + next; } - - # Update the cached values in the bugs table - my @updated_bugs = (); - - my $sth_getVotes = $dbh->prepare("SELECT SUM(vote_count) FROM votes - WHERE bug_id = ?"); - - $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?'); - - foreach my $id (keys %affected) { - $sth_getVotes->execute($id); - my $v = $sth_getVotes->fetchrow_array || 0; - $sth_updateVotes->execute($v, $id); - $bugs{$id}->{votes} = $v if $bugs{$id}; - my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id); - push (@updated_bugs, $id) if $confirmed; + if ($votes{$id} == ($old_votes{$id} || 0)) { + delete $affected{$id}; + next; } - $dbh->bz_commit_transaction(); - - print $cgi->header() if scalar @updated_bugs; - $vars->{'type'} = "votes"; - $vars->{'title_tag'} = 'change_votes'; - foreach my $bug_id (@updated_bugs) { - $vars->{'id'} = $bug_id; - $vars->{'sent_bugmail'} = - Bugzilla::BugMail::Send($bug_id, { 'changer' => $user }); - - $template->process("bug/process/results.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - # Set header_done to 1 only after the first bug. - $vars->{'header_done'} = 1; + # We use 'defined' in case 0 was accidentally stored in the DB. + if (defined $old_votes{$id}) { + $sth_updateVotes->execute($votes{$id}, $id, $who); } - $vars->{'votes_recorded'} = 1; + else { + $sth_insertVotes->execute($who, $id, $votes{$id}); + } + } + + if (@deleted_votes) { + $dbh->do( + 'DELETE FROM votes WHERE who = ? AND ' + . $dbh->sql_in('bug_id', \@deleted_votes), + undef, $who + ); + } + + # Update the cached values in the bugs table + my @updated_bugs = (); + + my $sth_getVotes = $dbh->prepare( + "SELECT SUM(vote_count) FROM votes + WHERE bug_id = ?" + ); + + $sth_updateVotes = $dbh->prepare('UPDATE bugs SET votes = ? WHERE bug_id = ?'); + + foreach my $id (keys %affected) { + $sth_getVotes->execute($id); + my $v = $sth_getVotes->fetchrow_array || 0; + $sth_updateVotes->execute($v, $id); + $bugs{$id}->{votes} = $v if $bugs{$id}; + my $confirmed = _confirm_if_vote_confirmed($bugs{$id} || $id); + push(@updated_bugs, $id) if $confirmed; + } + + $dbh->bz_commit_transaction(); + + print $cgi->header() if scalar @updated_bugs; + $vars->{'type'} = "votes"; + $vars->{'title_tag'} = 'change_votes'; + foreach my $bug_id (@updated_bugs) { + $vars->{'id'} = $bug_id; + $vars->{'sent_bugmail'} + = Bugzilla::BugMail::Send($bug_id, {'changer' => $user}); + + $template->process("bug/process/results.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + + # Set header_done to 1 only after the first bug. + $vars->{'header_done'} = 1; + } + $vars->{'votes_recorded'} = 1; } ###################### @@ -650,234 +691,246 @@ sub _update_votes { ###################### sub _modify_bug_votes { - my ($product, $changes) = @_; - my $dbh = Bugzilla->dbh; - my @msgs; + my ($product, $changes) = @_; + my $dbh = Bugzilla->dbh; + my @msgs; - # 1. too many votes for a single user on a single bug. - my @toomanyvotes_list; - if ($product->{maxvotesperbug} < $product->{votesperuser}) { - my $votes = $dbh->selectall_arrayref( - 'SELECT votes.who, votes.bug_id + # 1. too many votes for a single user on a single bug. + my @toomanyvotes_list; + if ($product->{maxvotesperbug} < $product->{votesperuser}) { + my $votes = $dbh->selectall_arrayref( + 'SELECT votes.who, votes.bug_id FROM votes INNER JOIN bugs ON bugs.bug_id = votes.bug_id WHERE bugs.product_id = ? - AND votes.vote_count > ?', - undef, ($product->id, $product->{maxvotesperbug})); + AND votes.vote_count > ?', undef, + ($product->id, $product->{maxvotesperbug}) + ); + + foreach my $vote (@$votes) { + my ($who, $id) = (@$vote); - foreach my $vote (@$votes) { - my ($who, $id) = (@$vote); - # If some votes are removed, _remove_votes() returns a list - # of messages to send to voters. - push(@msgs, _remove_votes($id, $who, 'votes_too_many_per_bug')); - my $name = user_id_to_login($who); + # If some votes are removed, _remove_votes() returns a list + # of messages to send to voters. + push(@msgs, _remove_votes($id, $who, 'votes_too_many_per_bug')); + my $name = user_id_to_login($who); - push(@toomanyvotes_list, {id => $id, name => $name}); - } + push(@toomanyvotes_list, {id => $id, name => $name}); } + } - $changes->{'_too_many_votes'} = \@toomanyvotes_list; + $changes->{'_too_many_votes'} = \@toomanyvotes_list; - # 2. too many total votes for a single user. - # This part doesn't work in the general case because _remove_votes - # doesn't enforce votesperuser (except per-bug when it's less - # than maxvotesperbug). See _remove_votes(). + # 2. too many total votes for a single user. + # This part doesn't work in the general case because _remove_votes + # doesn't enforce votesperuser (except per-bug when it's less + # than maxvotesperbug). See _remove_votes(). - my $votes = $dbh->selectall_arrayref( - 'SELECT votes.who, votes.vote_count + my $votes = $dbh->selectall_arrayref( + 'SELECT votes.who, votes.vote_count FROM votes INNER JOIN bugs ON bugs.bug_id = votes.bug_id - WHERE bugs.product_id = ?', - undef, $product->id); + WHERE bugs.product_id = ?', undef, $product->id + ); - my %counts; - foreach my $vote (@$votes) { - my ($who, $count) = @$vote; - if (!defined $counts{$who}) { - $counts{$who} = $count; - } else { - $counts{$who} += $count; - } + my %counts; + foreach my $vote (@$votes) { + my ($who, $count) = @$vote; + if (!defined $counts{$who}) { + $counts{$who} = $count; + } + else { + $counts{$who} += $count; } + } - my @toomanytotalvotes_list; - foreach my $who (keys(%counts)) { - if ($counts{$who} > $product->{votesperuser}) { - my $bug_ids = $dbh->selectcol_arrayref( - 'SELECT votes.bug_id + my @toomanytotalvotes_list; + foreach my $who (keys(%counts)) { + if ($counts{$who} > $product->{votesperuser}) { + my $bug_ids = $dbh->selectcol_arrayref( + 'SELECT votes.bug_id FROM votes INNER JOIN bugs ON bugs.bug_id = votes.bug_id WHERE bugs.product_id = ? - AND votes.who = ?', - undef, $product->id, $who); - - foreach my $bug_id (@$bug_ids) { - # _remove_votes returns a list of messages to send - # in case some voters had too many votes. - push(@msgs, _remove_votes($bug_id, $who, - 'votes_too_many_per_user')); - my $name = user_id_to_login($who); - - push(@toomanytotalvotes_list, {id => $bug_id, name => $name}); - } - } - } - - $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list; - - # 3. enough votes to confirm - my $bug_list = $dbh->selectcol_arrayref( - 'SELECT bug_id FROM bugs - WHERE product_id = ? AND bug_status = ? AND votes >= ?', - undef, ($product->id, 'UNCONFIRMED', $product->{votestoconfirm})); - - my @updated_bugs; - foreach my $bug_id (@$bug_list) { - my $confirmed = _confirm_if_vote_confirmed($bug_id, $product); - push (@updated_bugs, $bug_id) if $confirmed; - } - $changes->{'_confirmed_bugs'} = \@updated_bugs; - - # Now that changes are done, we can send emails to voters. - foreach my $msg (@msgs) { - MessageToMTA($msg); - } - # And send out emails about changed bugs - foreach my $bug_id (@updated_bugs) { - my $sent_bugmail = Bugzilla::BugMail::Send( - $bug_id, { changer => Bugzilla->user }); - $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail; - } + AND votes.who = ?', undef, $product->id, $who + ); + + foreach my $bug_id (@$bug_ids) { + + # _remove_votes returns a list of messages to send + # in case some voters had too many votes. + push(@msgs, _remove_votes($bug_id, $who, 'votes_too_many_per_user')); + my $name = user_id_to_login($who); + + push(@toomanytotalvotes_list, {id => $bug_id, name => $name}); + } + } + } + + $changes->{'_too_many_total_votes'} = \@toomanytotalvotes_list; + + # 3. enough votes to confirm + my $bug_list = $dbh->selectcol_arrayref( + 'SELECT bug_id FROM bugs + WHERE product_id = ? AND bug_status = ? AND votes >= ?', undef, + ($product->id, 'UNCONFIRMED', $product->{votestoconfirm}) + ); + + my @updated_bugs; + foreach my $bug_id (@$bug_list) { + my $confirmed = _confirm_if_vote_confirmed($bug_id, $product); + push(@updated_bugs, $bug_id) if $confirmed; + } + $changes->{'_confirmed_bugs'} = \@updated_bugs; + + # Now that changes are done, we can send emails to voters. + foreach my $msg (@msgs) { + MessageToMTA($msg); + } + + # And send out emails about changed bugs + foreach my $bug_id (@updated_bugs) { + my $sent_bugmail + = Bugzilla::BugMail::Send($bug_id, {changer => Bugzilla->user}); + $changes->{'_confirmed_bugs_sent_bugmail'}->{$bug_id} = $sent_bugmail; + } } # If a bug is moved to a product which allows less votes per bug # compared to the previous product, extra votes need to be removed. sub _remove_votes { - my ($id, $who, $reason) = (@_); - my $dbh = Bugzilla->dbh; - - my $whopart = ($who) ? " AND votes.who = $who" : ""; - - my $sth = $dbh->prepare("SELECT profiles.login_name, " . - "profiles.userid, votes.vote_count, " . - "products.votesperuser, products.maxvotesperbug " . - "FROM profiles " . - "LEFT JOIN votes ON profiles.userid = votes.who " . - "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " . - "LEFT JOIN products ON products.id = bugs.product_id " . - "WHERE votes.bug_id = ? " . $whopart); - $sth->execute($id); - my @list; - while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = $sth->fetchrow_array()) { - push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]); - } - - # @messages stores all emails which have to be sent, if any. - # This array is passed to the caller which will send these emails itself. - my @messages = (); - - if (scalar(@list)) { - foreach my $ref (@list) { - my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref); - - $maxvotesperbug = min($votesperuser, $maxvotesperbug); - - # If this product allows voting and the user's votes are in - # the acceptable range, then don't do anything. - next if $votesperuser && $oldvotes <= $maxvotesperbug; - - # If the user has more votes on this bug than this product - # allows, then reduce the number of votes so it fits - my $newvotes = $maxvotesperbug; - - my $removedvotes = $oldvotes - $newvotes; - - if ($newvotes) { - $dbh->do("UPDATE votes SET vote_count = ? " . - "WHERE bug_id = ? AND who = ?", - undef, ($newvotes, $id, $userid)); - } else { - $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?", - undef, ($id, $userid)); - } - - # Notice that we did not make sure that the user fit within the $votesperuser - # range. This is considered to be an acceptable alternative to losing votes - # during product moves. Then next time the user attempts to change their votes, - # they will be forced to fit within the $votesperuser limit. - - # Now lets send the e-mail to alert the user to the fact that their votes have - # been reduced or removed. - my $vars = { - 'to' => $name . Bugzilla->params->{'emailsuffix'}, - 'bugid' => $id, - 'reason' => $reason, - - 'votesremoved' => $removedvotes, - 'votesold' => $oldvotes, - 'votesnew' => $newvotes, - }; - - my $voter = new Bugzilla::User($userid); - my $template = Bugzilla->template_inner($voter->setting('lang')); - - my $msg; - $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg); - push(@messages, $msg); - } - - my $votes = $dbh->selectrow_array("SELECT SUM(vote_count) " . - "FROM votes WHERE bug_id = ?", - undef, $id) || 0; - $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?", - undef, ($votes, $id)); - } - # Now return the array containing emails to be sent. - return @messages; + my ($id, $who, $reason) = (@_); + my $dbh = Bugzilla->dbh; + + my $whopart = ($who) ? " AND votes.who = $who" : ""; + + my $sth + = $dbh->prepare("SELECT profiles.login_name, " + . "profiles.userid, votes.vote_count, " + . "products.votesperuser, products.maxvotesperbug " + . "FROM profiles " + . "LEFT JOIN votes ON profiles.userid = votes.who " + . "LEFT JOIN bugs ON votes.bug_id = bugs.bug_id " + . "LEFT JOIN products ON products.id = bugs.product_id " + . "WHERE votes.bug_id = ? " + . $whopart); + $sth->execute($id); + my @list; + while (my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) + = $sth->fetchrow_array()) + { + push(@list, [$name, $userid, $oldvotes, $votesperuser, $maxvotesperbug]); + } + + # @messages stores all emails which have to be sent, if any. + # This array is passed to the caller which will send these emails itself. + my @messages = (); + + if (scalar(@list)) { + foreach my $ref (@list) { + my ($name, $userid, $oldvotes, $votesperuser, $maxvotesperbug) = (@$ref); + + $maxvotesperbug = min($votesperuser, $maxvotesperbug); + + # If this product allows voting and the user's votes are in + # the acceptable range, then don't do anything. + next if $votesperuser && $oldvotes <= $maxvotesperbug; + + # If the user has more votes on this bug than this product + # allows, then reduce the number of votes so it fits + my $newvotes = $maxvotesperbug; + + my $removedvotes = $oldvotes - $newvotes; + + if ($newvotes) { + $dbh->do("UPDATE votes SET vote_count = ? " . "WHERE bug_id = ? AND who = ?", + undef, ($newvotes, $id, $userid)); + } + else { + $dbh->do("DELETE FROM votes WHERE bug_id = ? AND who = ?", + undef, ($id, $userid)); + } + + # Notice that we did not make sure that the user fit within the $votesperuser + # range. This is considered to be an acceptable alternative to losing votes + # during product moves. Then next time the user attempts to change their votes, + # they will be forced to fit within the $votesperuser limit. + + # Now lets send the e-mail to alert the user to the fact that their votes have + # been reduced or removed. + my $vars = { + 'to' => $name . Bugzilla->params->{'emailsuffix'}, + 'bugid' => $id, + 'reason' => $reason, + + 'votesremoved' => $removedvotes, + 'votesold' => $oldvotes, + 'votesnew' => $newvotes, + }; + + my $voter = new Bugzilla::User($userid); + my $template = Bugzilla->template_inner($voter->setting('lang')); + + my $msg; + $template->process("voting/votes-removed.txt.tmpl", $vars, \$msg); + push(@messages, $msg); + } + + my $votes + = $dbh->selectrow_array( + "SELECT SUM(vote_count) " . "FROM votes WHERE bug_id = ?", + undef, $id) + || 0; + $dbh->do("UPDATE bugs SET votes = ? WHERE bug_id = ?", undef, ($votes, $id)); + } + + # Now return the array containing emails to be sent. + return @messages; } # If a user votes for a bug, or the number of votes required to # confirm a bug has been reduced, check if the bug is now confirmed. sub _confirm_if_vote_confirmed { - my ($id, $product) = @_; - my $bug = ref $id ? $id : new Bugzilla::Bug({ id => $id, cache => 1 }); - $product ||= $bug->product_obj; - - my $ret = 0; - if (!$bug->everconfirmed - and $product->{votestoconfirm} - and $bug->votes >= $product->{votestoconfirm}) - { - $bug->add_comment('', { type => CMT_POPULAR_VOTES }); - - if ($bug->bug_status eq 'UNCONFIRMED') { - # Get a valid open state. - my $new_status; - foreach my $state (@{$bug->status->can_change_to}) { - if ($state->is_open && $state->name ne 'UNCONFIRMED') { - $new_status = $state->name; - last; - } - } - ThrowCodeError('voting_no_open_bug_status') unless $new_status; - - # We cannot call $bug->set_bug_status() here, because a user without - # canconfirm privs should still be able to confirm a bug by - # popular vote. We already know the new status is valid, so it's safe. - $bug->{bug_status} = $new_status; - $bug->{everconfirmed} = 1; - delete $bug->{'status'}; # Contains the status object. - } - else { - # If the bug is in a closed state, only set everconfirmed to 1. - # Do not call $bug->_set_everconfirmed(), for the same reason as above. - $bug->{everconfirmed} = 1; + my ($id, $product) = @_; + my $bug = ref $id ? $id : new Bugzilla::Bug({id => $id, cache => 1}); + $product ||= $bug->product_obj; + + my $ret = 0; + if ( !$bug->everconfirmed + and $product->{votestoconfirm} + and $bug->votes >= $product->{votestoconfirm}) + { + $bug->add_comment('', {type => CMT_POPULAR_VOTES}); + + if ($bug->bug_status eq 'UNCONFIRMED') { + + # Get a valid open state. + my $new_status; + foreach my $state (@{$bug->status->can_change_to}) { + if ($state->is_open && $state->name ne 'UNCONFIRMED') { + $new_status = $state->name; + last; } - $bug->update(); + } + ThrowCodeError('voting_no_open_bug_status') unless $new_status; - $ret = 1; + # We cannot call $bug->set_bug_status() here, because a user without + # canconfirm privs should still be able to confirm a bug by + # popular vote. We already know the new status is valid, so it's safe. + $bug->{bug_status} = $new_status; + $bug->{everconfirmed} = 1; + delete $bug->{'status'}; # Contains the status object. } - return $ret; + else { + # If the bug is in a closed state, only set everconfirmed to 1. + # Do not call $bug->_set_everconfirmed(), for the same reason as above. + $bug->{everconfirmed} = 1; + } + $bug->update(); + + $ret = 1; + } + return $ret; } diff --git a/extensions/ZPushNotify/Config.pm b/extensions/ZPushNotify/Config.pm index 1a31285e2..04c607974 100644 --- a/extensions/ZPushNotify/Config.pm +++ b/extensions/ZPushNotify/Config.pm @@ -11,7 +11,7 @@ use 5.10.1; use strict; use warnings; -use constant NAME => 'ZPushNotify'; +use constant NAME => 'ZPushNotify'; use constant REQUIRED_MODULES => []; use constant OPTIONAL_MODULES => []; diff --git a/extensions/ZPushNotify/Extension.pm b/extensions/ZPushNotify/Extension.pm index df3ab8f46..7c3b6cbf0 100644 --- a/extensions/ZPushNotify/Extension.pm +++ b/extensions/ZPushNotify/Extension.pm @@ -21,46 +21,38 @@ use Bugzilla; # sub _notify { - my ($bug_id, $delta_ts) = @_; - # beacuse the push_notify table is hot, we defer updating it until the - # request has completed. this ensures we are outside the scope of any - # transaction blocks. + my ($bug_id, $delta_ts) = @_; - my $stash = Bugzilla->request_cache->{ZPushNotify_stash} ||= []; - push @$stash, { bug_id => $bug_id, delta_ts => $delta_ts }; + # beacuse the push_notify table is hot, we defer updating it until the + # request has completed. this ensures we are outside the scope of any + # transaction blocks. + + my $stash = Bugzilla->request_cache->{ZPushNotify_stash} ||= []; + push @$stash, {bug_id => $bug_id, delta_ts => $delta_ts}; } sub request_cleanup { - my $stash = Bugzilla->request_cache->{ZPushNotify_stash} - || return; - - my $dbh = Bugzilla->dbh; - foreach my $rh (@$stash) { - # using REPLACE INTO or INSERT .. ON DUPLICATE KEY UPDATE results in a - # lock on the bugs table due to the FK. this way is more verbose but - # only locks the push_notify table. - $dbh->bz_start_transaction(); - my ($id) = $dbh->selectrow_array( - "SELECT id FROM push_notify WHERE bug_id=?", - undef, - $rh->{bug_id} - ); - if ($id) { - $dbh->do( - "UPDATE push_notify SET delta_ts=? WHERE id=?", - undef, - $rh->{delta_ts}, $id - ); - } - else { - $dbh->do( - "INSERT INTO push_notify (bug_id, delta_ts) VALUES (?, ?)", - undef, - $rh->{bug_id}, $rh->{delta_ts} - ); - } - $dbh->bz_commit_transaction(); + my $stash = Bugzilla->request_cache->{ZPushNotify_stash} || return; + + my $dbh = Bugzilla->dbh; + foreach my $rh (@$stash) { + + # using REPLACE INTO or INSERT .. ON DUPLICATE KEY UPDATE results in a + # lock on the bugs table due to the FK. this way is more verbose but + # only locks the push_notify table. + $dbh->bz_start_transaction(); + my ($id) = $dbh->selectrow_array("SELECT id FROM push_notify WHERE bug_id=?", + undef, $rh->{bug_id}); + if ($id) { + $dbh->do("UPDATE push_notify SET delta_ts=? WHERE id=?", + undef, $rh->{delta_ts}, $id); + } + else { + $dbh->do("INSERT INTO push_notify (bug_id, delta_ts) VALUES (?, ?)", + undef, $rh->{bug_id}, $rh->{delta_ts}); } + $dbh->bz_commit_transaction(); + } } # @@ -68,59 +60,59 @@ sub request_cleanup { # sub object_end_of_create { - my ($self, $args) = @_; - my $object = $args->{object}; - return unless Bugzilla->params->{enable_simple_push}; - return unless $object->isa('Bugzilla::Flag'); - _notify($object->bug->id, $object->creation_date); + my ($self, $args) = @_; + my $object = $args->{object}; + return unless Bugzilla->params->{enable_simple_push}; + return unless $object->isa('Bugzilla::Flag'); + _notify($object->bug->id, $object->creation_date); } sub flag_updated { - my ($self, $args) = @_; - my $flag = $args->{flag}; - my $timestamp = $args->{timestamp}; - my $changes = $args->{changes}; - return unless Bugzilla->params->{enable_simple_push}; - return unless scalar(keys %$changes); - _notify($flag->bug->id, $timestamp); + my ($self, $args) = @_; + my $flag = $args->{flag}; + my $timestamp = $args->{timestamp}; + my $changes = $args->{changes}; + return unless Bugzilla->params->{enable_simple_push}; + return unless scalar(keys %$changes); + _notify($flag->bug->id, $timestamp); } sub flag_deleted { - my ($self, $args) = @_; - my $flag = $args->{flag}; - my $timestamp = $args->{timestamp}; - return unless Bugzilla->params->{enable_simple_push}; - _notify($flag->bug->id, $timestamp); + my ($self, $args) = @_; + my $flag = $args->{flag}; + my $timestamp = $args->{timestamp}; + return unless Bugzilla->params->{enable_simple_push}; + _notify($flag->bug->id, $timestamp); } sub attachment_end_of_update { - my ($self, $args) = @_; - return unless Bugzilla->params->{enable_simple_push}; - return unless scalar keys %{ $args->{changes} }; - return unless my $object = $args->{object}; - _notify($object->bug->id, $object->modification_time); + my ($self, $args) = @_; + return unless Bugzilla->params->{enable_simple_push}; + return unless scalar keys %{$args->{changes}}; + return unless my $object = $args->{object}; + _notify($object->bug->id, $object->modification_time); } sub object_before_delete { - my ($self, $args) = @_; - return unless Bugzilla->params->{enable_simple_push}; - return unless my $object = $args->{object}; - if ($object->isa('Bugzilla::Attachment')) { - my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); - _notify($object->bug->id, $timestamp); - } + my ($self, $args) = @_; + return unless Bugzilla->params->{enable_simple_push}; + return unless my $object = $args->{object}; + if ($object->isa('Bugzilla::Attachment')) { + my $timestamp = Bugzilla->dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)'); + _notify($object->bug->id, $timestamp); + } } sub bug_end_of_update_delta_ts { - my ($self, $args) = @_; - return unless Bugzilla->params->{enable_simple_push}; - _notify($args->{bug_id}, $args->{timestamp}); + my ($self, $args) = @_; + return unless Bugzilla->params->{enable_simple_push}; + _notify($args->{bug_id}, $args->{timestamp}); } sub bug_end_of_create { - my ($self, $args) = @_; - return unless Bugzilla->params->{enable_simple_push}; - _notify($args->{bug}->id, $args->{timestamp}); + my ($self, $args) = @_; + return unless Bugzilla->params->{enable_simple_push}; + _notify($args->{bug}->id, $args->{timestamp}); } # @@ -128,44 +120,25 @@ sub bug_end_of_create { # sub db_schema_abstract_schema { - my ($self, $args) = @_; - $args->{'schema'}->{'push_notify'} = { - FIELDS => [ - id => { - TYPE => 'INTSERIAL', - NOTNULL => 1, - PRIMARYKEY => 1, - }, - bug_id => { - TYPE => 'INT3', - NOTNULL => 1, - REFERENCES => { - TABLE => 'bugs', - COLUMN => 'bug_id', - DELETE => 'CASCADE' - }, - }, - delta_ts => { - TYPE => 'DATETIME', - NOTNULL => 1, - }, - ], - INDEXES => [ - push_notify_idx => { - FIELDS => [ 'bug_id' ], - TYPE => 'UNIQUE', - }, - ], - }; + my ($self, $args) = @_; + $args->{'schema'}->{'push_notify'} = { + FIELDS => [ + id => {TYPE => 'INTSERIAL', NOTNULL => 1, PRIMARYKEY => 1,}, + bug_id => { + TYPE => 'INT3', + NOTNULL => 1, + REFERENCES => {TABLE => 'bugs', COLUMN => 'bug_id', DELETE => 'CASCADE'}, + }, + delta_ts => {TYPE => 'DATETIME', NOTNULL => 1,}, + ], + INDEXES => [push_notify_idx => {FIELDS => ['bug_id'], TYPE => 'UNIQUE',},], + }; } sub config_modify_panels { - my ($self, $args) = @_; - push @{ $args->{panels}->{advanced}->{params} }, { - name => 'enable_simple_push', - type => 'b', - default => 0, - }; + my ($self, $args) = @_; + push @{$args->{panels}->{advanced}->{params}}, + {name => 'enable_simple_push', type => 'b', default => 0,}; } __PACKAGE__->NAME; diff --git a/extensions/create.pl b/extensions/create.pl index 8927c172c..3d3bfaa93 100755 --- a/extensions/create.pl +++ b/extensions/create.pl @@ -27,7 +27,7 @@ my $base_dir = bz_locations()->{'extensionsdir'}; my $name = $ARGV[0] or ThrowUserError('extension_create_no_name'); if ($name !~ /^[A-Z]/) { - ThrowUserError('extension_first_letter_caps', { name => $name }); + ThrowUserError('extension_first_letter_caps', {name => $name}); } my $extension_dir = "$base_dir/$name"; @@ -35,33 +35,33 @@ mkpath($extension_dir) || die "$extension_dir already exists or cannot be created.\n"; my $lcname = lc($name); -foreach my $path (qw(lib web template/en/default/hook), - "template/en/default/$lcname") +foreach + my $path (qw(lib web template/en/default/hook), "template/en/default/$lcname") { - mkpath("$extension_dir/$path") || die "$extension_dir/$path: $!"; + mkpath("$extension_dir/$path") || die "$extension_dir/$path: $!"; } my $year = DateTime->now()->year; -my $template = Bugzilla->template; -my $vars = { year => $year, name => $name, path => $extension_dir }; +my $template = Bugzilla->template; +my $vars = {year => $year, name => $name, path => $extension_dir}; my %create_files = ( - 'config.pm.tmpl' => 'Config.pm', - 'extension.pm.tmpl' => 'Extension.pm', - 'util.pm.tmpl' => 'lib/Util.pm', - 'web-readme.txt.tmpl' => 'web/README', - 'hook-readme.txt.tmpl' => 'template/en/default/hook/README', - 'name-readme.txt.tmpl' => "template/en/default/$lcname/README", + 'config.pm.tmpl' => 'Config.pm', + 'extension.pm.tmpl' => 'Extension.pm', + 'util.pm.tmpl' => 'lib/Util.pm', + 'web-readme.txt.tmpl' => 'web/README', + 'hook-readme.txt.tmpl' => 'template/en/default/hook/README', + 'name-readme.txt.tmpl' => "template/en/default/$lcname/README", ); foreach my $template_file (keys %create_files) { - my $target = $create_files{$template_file}; - my $output; - $template->process("extensions/$template_file", $vars, \$output) - or ThrowTemplateError($template->error()); - open(my $fh, '>', "$extension_dir/$target"); - print $fh $output; - close($fh); + my $target = $create_files{$template_file}; + my $output; + $template->process("extensions/$template_file", $vars, \$output) + or ThrowTemplateError($template->error()); + open(my $fh, '>', "$extension_dir/$target"); + print $fh $output; + close($fh); } print get_text('extension_created', $vars), "\n"; diff --git a/gen-cpanfile.pl b/gen-cpanfile.pl index be07259d1..f773ddc99 100755 --- a/gen-cpanfile.pl +++ b/gen-cpanfile.pl @@ -21,48 +21,48 @@ use lib qw(. lib local/lib/perl5); use Getopt::Long qw(:config gnu_getopt); if (-f "MYMETA.json") { - eval { - require CPAN::Meta; - require Module::CPANfile; + eval { + require CPAN::Meta; + require Module::CPANfile; - my (@with_feature, @without_feature); - my $with_all_features = 0; - GetOptions( - 'with-all-features|A!' => \$with_all_features, - 'with-feature|D=s@' => \@with_feature, - 'without-feature|U=s@' => \@without_feature - ); + my (@with_feature, @without_feature); + my $with_all_features = 0; + GetOptions( + 'with-all-features|A!' => \$with_all_features, + 'with-feature|D=s@' => \@with_feature, + 'without-feature|U=s@' => \@without_feature + ); - my $meta = CPAN::Meta->load_file("MYMETA.json"); + my $meta = CPAN::Meta->load_file("MYMETA.json"); - my @phases = qw(configure build test develop runtime); - my @types = qw(requires recommends suggests conflicts); + my @phases = qw(configure build test develop runtime); + my @types = qw(requires recommends suggests conflicts); - my %features; - if ($with_all_features) { - $features{$_->identifier} = 1 foreach ($meta->features); - } - $features{$_} = 1 foreach @with_feature; - $features{$_} = 0 foreach @without_feature; - my @features = grep { $features{$_} } keys %features; + my %features; + if ($with_all_features) { + $features{$_->identifier} = 1 foreach ($meta->features); + } + $features{$_} = 1 foreach @with_feature; + $features{$_} = 0 foreach @without_feature; + my @features = grep { $features{$_} } keys %features; - my $prereqs = $meta->effective_prereqs(\@features)->as_string_hash; - my $filtered = {}; + my $prereqs = $meta->effective_prereqs(\@features)->as_string_hash; + my $filtered = {}; - while (my($phase, $types) = each %$prereqs) { - while (my($type, $reqs) = each %$types) { - $filtered->{$phase}{$type} = $reqs; - } - } + while (my ($phase, $types) = each %$prereqs) { + while (my ($type, $reqs) = each %$types) { + $filtered->{$phase}{$type} = $reqs; + } + } - my $cpanfile = Module::CPANfile->from_prereqs($filtered); - open my $cpanfile_fh, '>', 'cpanfile' or die "cannot write to cpanfile: $!"; - print $cpanfile_fh $cpanfile->to_string(); - close $cpanfile_fh; - }; - die "Unable generate cpanfile: $@\n" if $@; + my $cpanfile = Module::CPANfile->from_prereqs($filtered); + open my $cpanfile_fh, '>', 'cpanfile' or die "cannot write to cpanfile: $!"; + print $cpanfile_fh $cpanfile->to_string(); + close $cpanfile_fh; + }; + die "Unable generate cpanfile: $@\n" if $@; } else { - die "MYMETA.yml is missing, cannot generate cpanfile\n"; + die "MYMETA.yml is missing, cannot generate cpanfile\n"; } diff --git a/github.cgi b/github.cgi index f280f6ac9..7ad400c3d 100755 --- a/github.cgi +++ b/github.cgi @@ -17,9 +17,9 @@ use Bugzilla::Util qw(remote_ip); use Bugzilla::Error; use Bugzilla::Constants; use Bugzilla::Token qw( issue_short_lived_session_token - set_token_extra_data - get_token_extra_data - delete_token ); + set_token_extra_data + get_token_extra_data + delete_token ); use URI; use URI::QueryParam; BEGIN { Bugzilla->extensions } @@ -29,91 +29,104 @@ my $cgi = Bugzilla->cgi; my $urlbase = Bugzilla->localconfig->{urlbase}; if (lc($cgi->request_method) eq 'post') { - # POST requests come from Bugzilla itself and begin the GitHub login process - # by redirecting the user to GitHub's authentication endpoint. - my $user = Bugzilla->login(LOGIN_OPTIONAL); - my $target_uri = $cgi->param('target_uri') or ThrowCodeError("github_invalid_target"); - my $github_secret = $cgi->param('github_secret') or ThrowCodeError("github_invalid_request", { reason => 'invalid secret' }); - my $github_secret2 = Bugzilla->github_secret or ThrowCodeError("github_invalid_request", { reason => 'invalid secret' }); - - if ($github_secret ne $github_secret2) { - Bugzilla->check_rate_limit('github', remote_ip()); - ThrowCodeError("github_invalid_request", { reason => 'invalid secret' }); - } - - ThrowCodeError("github_invalid_target", { target_uri => $target_uri }) - unless $target_uri =~ /^\Q$urlbase\E/; - - ThrowCodeError("github_insecure_referer", { target_uri => $target_uri }) - if $cgi->referer && $cgi->referer =~ /(?:reset_password\.cgi|token\.cgi|\bt=|token=|api_key=)/; - - if ($user->id) { - print $cgi->redirect($target_uri); - exit; - } - - my $state = issue_short_lived_session_token("github_state"); - set_token_extra_data($state, { type => 'github_login', target_uri => $target_uri }); - - $cgi->send_cookie(-name => 'github_state', - -value => $state, - -httponly => 1); - print $cgi->redirect(Bugzilla::Extension::GitHubAuth::Client->authorize_uri($state)); + # POST requests come from Bugzilla itself and begin the GitHub login process + # by redirecting the user to GitHub's authentication endpoint. + + my $user = Bugzilla->login(LOGIN_OPTIONAL); + my $target_uri = $cgi->param('target_uri') + or ThrowCodeError("github_invalid_target"); + my $github_secret = $cgi->param('github_secret') + or ThrowCodeError("github_invalid_request", {reason => 'invalid secret'}); + my $github_secret2 = Bugzilla->github_secret + or ThrowCodeError("github_invalid_request", {reason => 'invalid secret'}); + + if ($github_secret ne $github_secret2) { + Bugzilla->check_rate_limit('github', remote_ip()); + ThrowCodeError("github_invalid_request", {reason => 'invalid secret'}); + } + + ThrowCodeError("github_invalid_target", {target_uri => $target_uri}) + unless $target_uri =~ /^\Q$urlbase\E/; + + ThrowCodeError("github_insecure_referer", {target_uri => $target_uri}) + if $cgi->referer + && $cgi->referer =~ /(?:reset_password\.cgi|token\.cgi|\bt=|token=|api_key=)/; + + if ($user->id) { + print $cgi->redirect($target_uri); + exit; + } + + my $state = issue_short_lived_session_token("github_state"); + set_token_extra_data($state, + {type => 'github_login', target_uri => $target_uri}); + + $cgi->send_cookie(-name => 'github_state', -value => $state, -httponly => 1); + print $cgi->redirect( + Bugzilla::Extension::GitHubAuth::Client->authorize_uri($state)); } elsif (lc($cgi->request_method) eq 'get') { - # GET requests come from GitHub, with this script acting as the OAuth2 callback. - my $state_param = $cgi->param('state'); - my $state_cookie = $cgi->cookie('github_state'); - - # If the state or params are missing, or the github_state cookie is missing - # we just redirect to index.cgi. - unless ($state_param && $state_cookie && ($cgi->param('code') || $cgi->param('email'))) { - print $cgi->redirect($urlbase . "index.cgi"); - exit; - } - my $invalid_request = $state_param ne $state_cookie; - - my $state_data; - unless ($invalid_request) { - $state_data = get_token_extra_data($state_param); - $invalid_request = !( $state_data && $state_data->{type} && $state_data->{type} =~ /^github_(?:login|email)$/ ); - } - - if ($invalid_request) { - Bugzilla->check_rate_limit('github', remote_ip()); - ThrowCodeError("github_invalid_request", { reason => 'invalid state param' } ) - } - - $cgi->remove_cookie('github_state'); - delete_token($state_param); - - if ($state_data->{type} eq 'github_login') { - Bugzilla->request_cache->{github_action} = 'login'; - Bugzilla->request_cache->{github_target_uri} = $state_data->{target_uri}; - } - elsif ($state_data->{type} eq 'github_email') { - Bugzilla->request_cache->{github_action} = 'email'; - Bugzilla->request_cache->{github_emails} = $state_data->{emails}; - } - my $user = Bugzilla->login(LOGIN_REQUIRED); - - my $target_uri = URI->new($state_data->{target_uri}); - - # It makes very little sense to login to a page with the logout parameter. - # doing so would be a no-op, so we ignore the logout param here. - $target_uri->query_param_delete('logout'); - - if ($target_uri->path =~ /attachment\.cgi/) { - my $attachment_uri = URI->new($urlbase . "attachment.cgi"); - $attachment_uri->query_param(id => scalar $target_uri->query_param('id')); - if ($target_uri->query_param('action')) { - $attachment_uri->query_param(action => scalar $target_uri->query_param('action')); - } - print $cgi->redirect($attachment_uri); - } - else { - print $cgi->redirect($target_uri); + # GET requests come from GitHub, with this script acting as the OAuth2 callback. + my $state_param = $cgi->param('state'); + my $state_cookie = $cgi->cookie('github_state'); + + # If the state or params are missing, or the github_state cookie is missing + # we just redirect to index.cgi. + unless ($state_param + && $state_cookie + && ($cgi->param('code') || $cgi->param('email'))) + { + print $cgi->redirect($urlbase . "index.cgi"); + exit; + } + + my $invalid_request = $state_param ne $state_cookie; + + my $state_data; + unless ($invalid_request) { + $state_data = get_token_extra_data($state_param); + $invalid_request + = !($state_data + && $state_data->{type} + && $state_data->{type} =~ /^github_(?:login|email)$/); + } + + if ($invalid_request) { + Bugzilla->check_rate_limit('github', remote_ip()); + ThrowCodeError("github_invalid_request", {reason => 'invalid state param'}); + } + + $cgi->remove_cookie('github_state'); + delete_token($state_param); + + if ($state_data->{type} eq 'github_login') { + Bugzilla->request_cache->{github_action} = 'login'; + Bugzilla->request_cache->{github_target_uri} = $state_data->{target_uri}; + } + elsif ($state_data->{type} eq 'github_email') { + Bugzilla->request_cache->{github_action} = 'email'; + Bugzilla->request_cache->{github_emails} = $state_data->{emails}; + } + my $user = Bugzilla->login(LOGIN_REQUIRED); + + my $target_uri = URI->new($state_data->{target_uri}); + + # It makes very little sense to login to a page with the logout parameter. + # doing so would be a no-op, so we ignore the logout param here. + $target_uri->query_param_delete('logout'); + + if ($target_uri->path =~ /attachment\.cgi/) { + my $attachment_uri = URI->new($urlbase . "attachment.cgi"); + $attachment_uri->query_param(id => scalar $target_uri->query_param('id')); + if ($target_uri->query_param('action')) { + $attachment_uri->query_param( + action => scalar $target_uri->query_param('action')); } + print $cgi->redirect($attachment_uri); + } + else { + print $cgi->redirect($target_uri); + } } diff --git a/heartbeat.cgi b/heartbeat.cgi index 493674c16..c36c734ae 100755 --- a/heartbeat.cgi +++ b/heartbeat.cgi @@ -19,29 +19,36 @@ use Bugzilla::Error; use Bugzilla::Update; my $ok = eval { - # Ensure that any Throw*Error calls just use die, rather than trying to return html... - Bugzilla->error_mode(ERROR_MODE_DIE); - my $memcached = Bugzilla->memcached; - my $dbh = Bugzilla->dbh; - my $database_ok = $dbh->ping; - my $versions = $memcached->{memcached}->server_versions; - my $memcached_ok = keys %$versions; - die "database not available" unless $database_ok; - die "memcached server(s) not available" unless $memcached_ok; - if ($dbh->isa('Bugzilla::DB::Mysql') && Bugzilla->params->{utf8} eq 'utf8mb4') { - my $mysql_var = $dbh->selectall_hashref(q{SHOW VARIABLES LIKE 'character_set%'}, 'Variable_name'); - foreach my $name (qw( character_set_client character_set_connection character_set_database )) { - my $value = $mysql_var->{$name}{Value}; - if ($value ne 'utf8mb4') { - die "Expected MySQL variable '$name' to be 'utf8mb4', found '$value'"; - } - } +# Ensure that any Throw*Error calls just use die, rather than trying to return html... + Bugzilla->error_mode(ERROR_MODE_DIE); + my $memcached = Bugzilla->memcached; + my $dbh = Bugzilla->dbh; + my $database_ok = $dbh->ping; + my $versions = $memcached->{memcached}->server_versions; + my $memcached_ok = keys %$versions; + + die "database not available" unless $database_ok; + die "memcached server(s) not available" unless $memcached_ok; + if ($dbh->isa('Bugzilla::DB::Mysql') && Bugzilla->params->{utf8} eq 'utf8mb4') { + my $mysql_var = $dbh->selectall_hashref(q{SHOW VARIABLES LIKE 'character_set%'}, + 'Variable_name'); + foreach my $name ( + qw( character_set_client character_set_connection character_set_database )) + { + my $value = $mysql_var->{$name}{Value}; + if ($value ne 'utf8mb4') { + die "Expected MySQL variable '$name' to be 'utf8mb4', found '$value'"; + } } - 1; + } + 1; }; FATAL("heartbeat error: $@") if !$ok && $@; my $cgi = Bugzilla->cgi; -print $cgi->header(-type => 'text/plain', -status => $ok ? '200 OK' : '500 Internal Server Error'); -print $ok ? "Bugzilla OK\n" : "Bugzilla NOT OK\n"; \ No newline at end of file +print $cgi->header( + -type => 'text/plain', + -status => $ok ? '200 OK' : '500 Internal Server Error' +); +print $ok ? "Bugzilla OK\n" : "Bugzilla NOT OK\n"; diff --git a/importxml.pl b/importxml.pl index 23609f2b3..e815b638a 100755 --- a/importxml.pl +++ b/importxml.pl @@ -38,15 +38,19 @@ use warnings; ##################################################################### use File::Basename qw(dirname); + # MTAs may call this script from any directory, but it should always # run from this one so that it can find its modules. BEGIN { - require File::Basename; - my $dir = $0; $dir =~ /(.*)/; $dir = $1; # trick taint - chdir(File::Basename::dirname($dir)); + require File::Basename; + my $dir = $0; + $dir =~ /(.*)/; + $dir = $1; # trick taint + chdir(File::Basename::dirname($dir)); } use lib qw(. lib local/lib/perl5); + # Data dumber is used for debugging, I got tired of copying it back in # and then removing it. #use Data::Dumper; @@ -75,19 +79,19 @@ use Getopt::Long; use Pod::Usage; use XML::Twig; -my $debug = 0; -my $mail = ''; +my $debug = 0; +my $mail = ''; my $attach_path = ''; -my $help = 0; +my $help = 0; my ($default_product_name, $default_component_name); my $result = GetOptions( - "verbose|debug+" => \$debug, - "mail|sendmail!" => \$mail, - "attach_path=s" => \$attach_path, - "help|?" => \$help, - "product=s" => \$default_product_name, - "component=s" => \$default_component_name, + "verbose|debug+" => \$debug, + "mail|sendmail!" => \$mail, + "attach_path=s" => \$attach_path, + "help|?" => \$help, + "product=s" => \$default_product_name, + "component=s" => \$default_component_name, ); pod2usage(0) if $help; @@ -100,11 +104,11 @@ our @logs; our @attachments; our $bugtotal; my $xml; -my $dbh = Bugzilla->dbh; -my $params = Bugzilla->params; +my $dbh = Bugzilla->dbh; +my $params = Bugzilla->params; my ($timestamp) = $dbh->selectrow_array("SELECT NOW()"); -$default_product_name = '' if !defined $default_product_name; +$default_product_name = '' if !defined $default_product_name; $default_component_name = '' if !defined $default_component_name; ############################################################################### @@ -112,166 +116,168 @@ $default_component_name = '' if !defined $default_component_name; ############################################################################### sub MailMessage { - return unless ($mail); - my $subject = shift; - my $message = shift; - my @recipients = @_; - my $from = $params->{"mailfrom"}; - $from =~ s/@/\@/g; - - foreach my $to (@recipients){ - my $header = "To: $to\n"; - $header .= "From: Bugzilla <$from>\n"; - $header .= "Subject: $subject\n\n"; - my $sendmessage = $header . $message . "\n"; - MessageToMTA($sendmessage); - } + return unless ($mail); + my $subject = shift; + my $message = shift; + my @recipients = @_; + my $from = $params->{"mailfrom"}; + $from =~ s/@/\@/g; + + foreach my $to (@recipients) { + my $header = "To: $to\n"; + $header .= "From: Bugzilla <$from>\n"; + $header .= "Subject: $subject\n\n"; + my $sendmessage = $header . $message . "\n"; + MessageToMTA($sendmessage); + } } sub Debug { - return unless ($debug); - my ( $message, $level ) = (@_); - print STDERR "OK: $message \n" if ( $level == OK_LEVEL ); - print STDERR "ERR: $message \n" if ( $level == ERR_LEVEL ); - print STDERR "$message\n" - if ( ( $debug == $level ) && ( $level == DEBUG_LEVEL ) ); + return unless ($debug); + my ($message, $level) = (@_); + print STDERR "OK: $message \n" if ($level == OK_LEVEL); + print STDERR "ERR: $message \n" if ($level == ERR_LEVEL); + print STDERR "$message\n" if (($debug == $level) && ($level == DEBUG_LEVEL)); } sub Error { - my ( $reason, $errtype, $exporter ) = @_; - my $subject = "Bug import error: $reason"; - my $message = "Cannot import these bugs because $reason "; - $message .= "\n\nPlease re-open the original bug.\n" if ($errtype); - $message .= "For more info, contact " . $params->{"maintainer"} . ".\n"; - my @to = ( $params->{"maintainer"}, $exporter); - Debug( $message, ERR_LEVEL ); - MailMessage( $subject, $message, @to ); - exit; + my ($reason, $errtype, $exporter) = @_; + my $subject = "Bug import error: $reason"; + my $message = "Cannot import these bugs because $reason "; + $message .= "\n\nPlease re-open the original bug.\n" if ($errtype); + $message .= "For more info, contact " . $params->{"maintainer"} . ".\n"; + my @to = ($params->{"maintainer"}, $exporter); + Debug($message, ERR_LEVEL); + MailMessage($subject, $message, @to); + exit; } # This subroutine handles flags for process_bug. It is generic in that # it can handle both attachment flags and bug flags. sub flag_handler { - my ( - $name, $status, $setter_login, - $requestee_login, $exporterid, $bugid, - $componentid, $productid, $attachid - ) - = @_; - - my $type = ($attachid) ? "attachment" : "bug"; - my $err = ''; - my $setter = new Bugzilla::User({ name => $setter_login }); - my $requestee; - my $requestee_id; - - unless ($setter) { - $err = "Invalid setter $setter_login on $type flag $name\n"; - $err .= " Dropping flag $name\n"; - return $err; + my ( + $name, $status, $setter_login, + $requestee_login, $exporterid, $bugid, + $componentid, $productid, $attachid + ) = @_; + + my $type = ($attachid) ? "attachment" : "bug"; + my $err = ''; + my $setter = new Bugzilla::User({name => $setter_login}); + my $requestee; + my $requestee_id; + + unless ($setter) { + $err = "Invalid setter $setter_login on $type flag $name\n"; + $err .= " Dropping flag $name\n"; + return $err; + } + if (!$setter->can_see_bug($bugid)) { + $err .= "Setter is not a member of bug group\n"; + $err .= " Dropping flag $name\n"; + return $err; + } + my $setter_id = $setter->id; + if (defined($requestee_login)) { + $requestee = new Bugzilla::User({name => $requestee_login}); + if ($requestee) { + if (!$requestee->can_see_bug($bugid)) { + $err .= "Requestee is not a member of bug group\n"; + $err .= " Requesting from the wind\n"; + } + else { + $requestee_id = $requestee->id; + } } - if ( !$setter->can_see_bug($bugid) ) { - $err .= "Setter is not a member of bug group\n"; - $err .= " Dropping flag $name\n"; - return $err; + else { + $err = "Invalid requestee $requestee_login on $type flag $name\n"; + $err .= " Requesting from the wind.\n"; } - my $setter_id = $setter->id; - if ( defined($requestee_login) ) { - $requestee = new Bugzilla::User({ name => $requestee_login }); - if ( $requestee ) { - if ( !$requestee->can_see_bug($bugid) ) { - $err .= "Requestee is not a member of bug group\n"; - $err .= " Requesting from the wind\n"; - } - else{ - $requestee_id = $requestee->id; - } - } - else { - $err = "Invalid requestee $requestee_login on $type flag $name\n"; - $err .= " Requesting from the wind.\n"; - } + } + my $flag_types; + + # If this is an attachment flag we need to do some dirty work to look + # up the flagtype ID + if ($attachid) { + $flag_types = Bugzilla::FlagType::match({ + 'target_type' => 'attachment', + 'product_id' => $productid, + 'component_id' => $componentid + }); + } + else { + my $bug = new Bugzilla::Bug($bugid); + $flag_types = $bug->flag_types; + } + unless ($flag_types) { + $err = "No flag types defined for this bug\n"; + $err .= " Dropping flag $name\n"; + return $err; + } + + # We need to see if the imported flag is in the list of known flags + # It is possible for two flags on the same bug have the same name + # If this is the case, we will only match the first one. + my $ftype; + foreach my $f (@{$flag_types}) { + if ($f->name eq $name) { + $ftype = $f; + last; } - my $flag_types; - - # If this is an attachment flag we need to do some dirty work to look - # up the flagtype ID - if ($attachid) { - $flag_types = Bugzilla::FlagType::match( - { - 'target_type' => 'attachment', - 'product_id' => $productid, - 'component_id' => $componentid - } ); - } - else { - my $bug = new Bugzilla::Bug($bugid); - $flag_types = $bug->flag_types; + } + + if ($ftype) { # We found the flag in the list + my $grant_group = $ftype->grant_group; + if ( ($status eq '+' || $status eq '-') + && $grant_group + && !$setter->in_group_id($grant_group->id)) + { + $err = "Setter $setter_login on $type flag $name "; + $err .= "is not in the Grant Group\n"; + $err .= " Dropping flag $name\n"; + return $err; } - unless ($flag_types){ - $err = "No flag types defined for this bug\n"; - $err .= " Dropping flag $name\n"; - return $err; + my $request_group = $ftype->request_group; + if ( $request_group + && $status eq '?' + && !$setter->in_group_id($request_group->id)) + { + $err = "Setter $setter_login on $type flag $name "; + $err .= "is not in the Request Group\n"; + $err .= " Dropping flag $name\n"; + return $err; } - # We need to see if the imported flag is in the list of known flags - # It is possible for two flags on the same bug have the same name - # If this is the case, we will only match the first one. - my $ftype; - foreach my $f ( @{$flag_types} ) { - if ( $f->name eq $name) { - $ftype = $f; - last; - } + # Take the first flag_type that matches + unless ($ftype->is_active) { + $err = "Flag $name is not active in this database\n"; + $err .= " Dropping flag $name\n"; + return $err; } - if ($ftype) { # We found the flag in the list - my $grant_group = $ftype->grant_group; - if (( $status eq '+' || $status eq '-' ) - && $grant_group && !$setter->in_group_id($grant_group->id)) { - $err = "Setter $setter_login on $type flag $name "; - $err .= "is not in the Grant Group\n"; - $err .= " Dropping flag $name\n"; - return $err; - } - my $request_group = $ftype->request_group; - if ($request_group - && $status eq '?' && !$setter->in_group_id($request_group->id)) { - $err = "Setter $setter_login on $type flag $name "; - $err .= "is not in the Request Group\n"; - $err .= " Dropping flag $name\n"; - return $err; - } - - # Take the first flag_type that matches - unless ($ftype->is_active) { - $err = "Flag $name is not active in this database\n"; - $err .= " Dropping flag $name\n"; - return $err; - } - - $dbh->do("INSERT INTO flags + $dbh->do( + "INSERT INTO flags (type_id, status, bug_id, attach_id, creation_date, setter_id, requestee_id) VALUES (?, ?, ?, ?, ?, ?, ?)", undef, - ($ftype->id, $status, $bugid, $attachid, $timestamp, - $setter_id, $requestee_id)); - } - else { - $err = "Dropping unknown $type flag: $name\n"; - return $err; - } + ($ftype->id, $status, $bugid, $attachid, $timestamp, $setter_id, $requestee_id) + ); + } + else { + $err = "Dropping unknown $type flag: $name\n"; return $err; + } + return $err; } # Converts and returns the input data as an array. sub _to_array { - my $value = shift; + my $value = shift; - $value = [$value] if !ref($value); - return @$value; + $value = [$value] if !ref($value); + return @$value; } ############################################################################### @@ -289,25 +295,26 @@ sub _to_array { # bugs are being moved from # sub init() { - my ( $twig, $bugzilla ) = @_; - my $root = $twig->root; - my $maintainer = $root->{'att'}->{'maintainer'}; - my $exporter = $root->{'att'}->{'exporter'}; - my $urlbase = $root->{'att'}->{'urlbase'}; - my $xmlversion = $root->{'att'}->{'version'}; - - if ($xmlversion ne BUGZILLA_VERSION) { - my $log = "Possible version conflict!\n"; - $log .= " XML was exported from Bugzilla version $xmlversion\n"; - $log .= " But this installation uses "; - $log .= BUGZILLA_VERSION . "\n"; - Debug($log, OK_LEVEL); - push(@logs, $log); - } - Error( "no maintainer", "REOPEN", $exporter ) unless ($maintainer); - Error( "no exporter", "REOPEN", $exporter ) unless ($exporter); - Error( "invalid exporter: $exporter", "REOPEN", $exporter ) if ( !login_to_id($exporter) ); - Error( "no urlbase set", "REOPEN", $exporter ) unless ($urlbase); + my ($twig, $bugzilla) = @_; + my $root = $twig->root; + my $maintainer = $root->{'att'}->{'maintainer'}; + my $exporter = $root->{'att'}->{'exporter'}; + my $urlbase = $root->{'att'}->{'urlbase'}; + my $xmlversion = $root->{'att'}->{'version'}; + + if ($xmlversion ne BUGZILLA_VERSION) { + my $log = "Possible version conflict!\n"; + $log .= " XML was exported from Bugzilla version $xmlversion\n"; + $log .= " But this installation uses "; + $log .= BUGZILLA_VERSION . "\n"; + Debug($log, OK_LEVEL); + push(@logs, $log); + } + Error("no maintainer", "REOPEN", $exporter) unless ($maintainer); + Error("no exporter", "REOPEN", $exporter) unless ($exporter); + Error("invalid exporter: $exporter", "REOPEN", $exporter) + if (!login_to_id($exporter)); + Error("no urlbase set", "REOPEN", $exporter) unless ($urlbase); } @@ -326,69 +333,74 @@ sub init() { # The submitter_id gets filled in with $exporterid. sub process_attachment() { - my ( $twig, $attach ) = @_; - Debug( "Parsing attachments", DEBUG_LEVEL ); - my %attachment; - - $attachment{'date'} = - format_time( $attach->field('date'), "%Y-%m-%d %R" ) || $timestamp; - $attachment{'desc'} = $attach->field('desc'); - $attachment{'ctype'} = $attach->field('type') || "unknown/unknown"; - $attachment{'attachid'} = $attach->field('attachid'); - $attachment{'ispatch'} = $attach->{'att'}->{'ispatch'} || 0; - $attachment{'isobsolete'} = $attach->{'att'}->{'isobsolete'} || 0; - $attachment{'isprivate'} = $attach->{'att'}->{'isprivate'} || 0; - $attachment{'filename'} = $attach->field('filename') || "file"; - $attachment{'attacher'} = $attach->field('attacher'); - # Attachment data is not exported in versions 2.20 and older. - if (defined $attach->first_child('data') && - defined $attach->first_child('data')->{'att'}->{'encoding'}) { - my $encoding = $attach->first_child('data')->{'att'}->{'encoding'}; - if ($encoding =~ /base64/) { - # decode the base64 - my $data = $attach->field('data'); - my $output = decode_base64($data); - $attachment{'data'} = $output; - } - elsif ($encoding =~ /filename/) { - # read the attachment file - Error("attach_path is required", undef) unless ($attach_path); - - my $filename = $attach->field('data'); - # Remove any leading path data from the filename - $filename =~ s/(.*\/|.*\\)//gs; - - my $attach_filename = $attach_path . "/" . $filename; - open(ATTACH_FH, "<", $attach_filename) or - Error("cannot open $attach_filename", undef); - $attachment{'data'} = do { local $/; }; - close ATTACH_FH; - } - } - else { - $attachment{'data'} = $attach->field('data'); + my ($twig, $attach) = @_; + Debug("Parsing attachments", DEBUG_LEVEL); + my %attachment; + + $attachment{'date'} + = format_time($attach->field('date'), "%Y-%m-%d %R") || $timestamp; + $attachment{'desc'} = $attach->field('desc'); + $attachment{'ctype'} = $attach->field('type') || "unknown/unknown"; + $attachment{'attachid'} = $attach->field('attachid'); + $attachment{'ispatch'} = $attach->{'att'}->{'ispatch'} || 0; + $attachment{'isobsolete'} = $attach->{'att'}->{'isobsolete'} || 0; + $attachment{'isprivate'} = $attach->{'att'}->{'isprivate'} || 0; + $attachment{'filename'} = $attach->field('filename') || "file"; + $attachment{'attacher'} = $attach->field('attacher'); + + # Attachment data is not exported in versions 2.20 and older. + if ( defined $attach->first_child('data') + && defined $attach->first_child('data')->{'att'}->{'encoding'}) + { + my $encoding = $attach->first_child('data')->{'att'}->{'encoding'}; + if ($encoding =~ /base64/) { + + # decode the base64 + my $data = $attach->field('data'); + my $output = decode_base64($data); + $attachment{'data'} = $output; } + elsif ($encoding =~ /filename/) { - # attachment flags - my @aflags; - foreach my $aflag ( $attach->children('flag') ) { - my %aflag; - $aflag{'name'} = $aflag->{'att'}->{'name'}; - $aflag{'status'} = $aflag->{'att'}->{'status'}; - $aflag{'setter'} = $aflag->{'att'}->{'setter'}; - $aflag{'requestee'} = $aflag->{'att'}->{'requestee'}; - push @aflags, \%aflag; - } - $attachment{'flags'} = \@aflags if (@aflags); + # read the attachment file + Error("attach_path is required", undef) unless ($attach_path); - # free up the memory for use by the rest of the script - $attach->delete; - if ($attachment{'attachid'}) { - push @attachments, \%attachment; - } - else { - push @attachments, "err"; + my $filename = $attach->field('data'); + + # Remove any leading path data from the filename + $filename =~ s/(.*\/|.*\\)//gs; + + my $attach_filename = $attach_path . "/" . $filename; + open(ATTACH_FH, "<", $attach_filename) + or Error("cannot open $attach_filename", undef); + $attachment{'data'} = do { local $/; }; + close ATTACH_FH; } + } + else { + $attachment{'data'} = $attach->field('data'); + } + + # attachment flags + my @aflags; + foreach my $aflag ($attach->children('flag')) { + my %aflag; + $aflag{'name'} = $aflag->{'att'}->{'name'}; + $aflag{'status'} = $aflag->{'att'}->{'status'}; + $aflag{'setter'} = $aflag->{'att'}->{'setter'}; + $aflag{'requestee'} = $aflag->{'att'}->{'requestee'}; + push @aflags, \%aflag; + } + $attachment{'flags'} = \@aflags if (@aflags); + + # free up the memory for use by the rest of the script + $attach->delete; + if ($attachment{'attachid'}) { + push @attachments, \%attachment; + } + else { + push @attachments, "err"; + } } # This subroutine will be called once for each in the xml file. @@ -400,836 +412,862 @@ sub process_attachment() { # purged from memory to free it up for later bugs. sub process_bug { - my ( $twig, $bug ) = @_; - my $root = $twig->root; - my $maintainer = $root->{'att'}->{'maintainer'}; - my $exporter_login = $root->{'att'}->{'exporter'}; - my $exporter = new Bugzilla::User({ name => $exporter_login }); - my $urlbase = $root->{'att'}->{'urlbase'}; - - # We will store output information in this variable. - my $log = ""; - if ( defined $bug->{'att'}->{'error'} ) { - $log .= "\nError in bug " . $bug->field('bug_id') . "\@$urlbase: "; - $log .= $bug->{'att'}->{'error'} . "\n"; - if ( $bug->{'att'}->{'error'} =~ /NotFound/ ) { - $log .= "$exporter_login tried to move bug " . $bug->field('bug_id'); - $log .= " here, but $urlbase reports that this bug"; - $log .= " does not exist.\n"; - } - elsif ( $bug->{'att'}->{'error'} =~ /NotPermitted/ ) { - $log .= "$exporter_login tried to move bug " . $bug->field('bug_id'); - $log .= " here, but $urlbase reports that $exporter_login does "; - $log .= " not have access to that bug.\n"; - } - return; + my ($twig, $bug) = @_; + my $root = $twig->root; + my $maintainer = $root->{'att'}->{'maintainer'}; + my $exporter_login = $root->{'att'}->{'exporter'}; + my $exporter = new Bugzilla::User({name => $exporter_login}); + my $urlbase = $root->{'att'}->{'urlbase'}; + + # We will store output information in this variable. + my $log = ""; + if (defined $bug->{'att'}->{'error'}) { + $log .= "\nError in bug " . $bug->field('bug_id') . "\@$urlbase: "; + $log .= $bug->{'att'}->{'error'} . "\n"; + if ($bug->{'att'}->{'error'} =~ /NotFound/) { + $log .= "$exporter_login tried to move bug " . $bug->field('bug_id'); + $log .= " here, but $urlbase reports that this bug"; + $log .= " does not exist.\n"; } - $bugtotal++; - - # This list contains all other bug fields that we want to process. - # If it is not in this list it will not be included. - my %all_fields; - foreach my $field ( - qw(long_desc attachment flag group), Bugzilla::Bug::fields() ) - { - $all_fields{$field} = 1; + elsif ($bug->{'att'}->{'error'} =~ /NotPermitted/) { + $log .= "$exporter_login tried to move bug " . $bug->field('bug_id'); + $log .= " here, but $urlbase reports that $exporter_login does "; + $log .= " not have access to that bug.\n"; } - - my %bug_fields; - my $err = ""; - - # Loop through all the xml tags inside a and compare them to the - # lists of fields. If they match throw them into the hash. Otherwise - # append it to the log, which will go into the comments when we are done. - foreach my $bugchild ( $bug->children() ) { - Debug( "Parsing field: " . $bugchild->name, DEBUG_LEVEL ); - - # Skip the token if one is included. We don't want it included in - # the comments, and it is not used by the importer. - next if $bugchild->name eq 'token'; - - if ( defined $all_fields{ $bugchild->name } ) { - my @values = $bug->children_text($bugchild->name); - if (scalar @values > 1) { - $bug_fields{$bugchild->name} = \@values; - } - else { - $bug_fields{$bugchild->name} = $values[0]; - } - } - else { - $err .= "Unknown bug field \"" . $bugchild->name . "\""; - $err .= " encountered while moving bug\n"; - $err .= " <" . $bugchild->name . ">"; - if ( $bugchild->children_count > 1 ) { - $err .= "\n"; - foreach my $subchild ( $bugchild->children() ) { - $err .= " <" . $subchild->name . ">"; - $err .= $subchild->field; - $err .= "name . ">\n"; - } - } - else { - $err .= $bugchild->field; - } - $err .= "name . ">\n"; - } + return; + } + $bugtotal++; + + # This list contains all other bug fields that we want to process. + # If it is not in this list it will not be included. + my %all_fields; + foreach my $field (qw(long_desc attachment flag group), Bugzilla::Bug::fields()) + { + $all_fields{$field} = 1; + } + + my %bug_fields; + my $err = ""; + + # Loop through all the xml tags inside a and compare them to the + # lists of fields. If they match throw them into the hash. Otherwise + # append it to the log, which will go into the comments when we are done. + foreach my $bugchild ($bug->children()) { + Debug("Parsing field: " . $bugchild->name, DEBUG_LEVEL); + + # Skip the token if one is included. We don't want it included in + # the comments, and it is not used by the importer. + next if $bugchild->name eq 'token'; + + if (defined $all_fields{$bugchild->name}) { + my @values = $bug->children_text($bugchild->name); + if (scalar @values > 1) { + $bug_fields{$bugchild->name} = \@values; + } + else { + $bug_fields{$bugchild->name} = $values[0]; + } } - - # Parse long descriptions - my @long_descs; - foreach my $comment ( $bug->children('long_desc') ) { - Debug( "Parsing Long Description", DEBUG_LEVEL ); - my %long_desc = ( who => $comment->field('who'), - bug_when => format_time($comment->field('bug_when'), '%Y-%m-%d %T'), - isprivate => $comment->{'att'}->{'isprivate'} || 0 ); - - # If the exporter is not in the insidergroup, keep the comment public. - $long_desc{isprivate} = 0 unless $exporter->is_insider; - - my $data = $comment->field('thetext'); - if ( defined $comment->first_child('thetext')->{'att'}->{'encoding'} - && $comment->first_child('thetext')->{'att'}->{'encoding'} =~ - /base64/ ) - { - $data = decode_base64($data); - } - - # For backwards-compatibility with Bugzillas before 3.6: - # - # If we leave the attachment ID in the comment it will be made a link - # to the wrong attachment. Since the new attachment ID is unknown yet - # let's strip it out for now. We will make a comment with the right ID - # later - $data =~ s/Created an attachment \(id=\d+\)/Created an attachment/g; - - # Same goes for bug #'s Since we don't know if the referenced bug - # is also being moved, lets make sure they know it means a different - # bugzilla. - my $url = $urlbase . "show_bug.cgi?id="; - $data =~ s/([Bb]ugs?\s*\#?\s*(\d+))/$url$2/g; - - # Keep the original commenter if possible, else we will fall back - # to the exporter account. - $long_desc{whoid} = login_to_id($long_desc{who}); - - if (!$long_desc{whoid}) { - $data = "The original author of this comment is $long_desc{who}.\n\n" . $data; + else { + $err .= "Unknown bug field \"" . $bugchild->name . "\""; + $err .= " encountered while moving bug\n"; + $err .= " <" . $bugchild->name . ">"; + if ($bugchild->children_count > 1) { + $err .= "\n"; + foreach my $subchild ($bugchild->children()) { + $err .= " <" . $subchild->name . ">"; + $err .= $subchild->field; + $err .= "name . ">\n"; } - - $long_desc{'thetext'} = $data; - push @long_descs, \%long_desc; + } + else { + $err .= $bugchild->field; + } + $err .= "name . ">\n"; } - - my @sorted_descs = sort { $a->{'bug_when'} cmp $b->{'bug_when'} } @long_descs; - - my $comments = "\n\n--- Bug imported by $exporter_login "; - $comments .= format_time(scalar localtime(time()), '%Y-%m-%d %R %Z') . " "; - $comments .= " ---\n\n"; - $comments .= "This bug was previously known as _bug_ $bug_fields{'bug_id'} at "; - $comments .= $urlbase . "show_bug.cgi?id=" . $bug_fields{'bug_id'} . "\n"; - if ( defined $bug_fields{'dependson'} ) { - $comments .= "This bug depended on bug(s) " . - join(' ', _to_array($bug_fields{'dependson'})) . ".\n"; + } + + # Parse long descriptions + my @long_descs; + foreach my $comment ($bug->children('long_desc')) { + Debug("Parsing Long Description", DEBUG_LEVEL); + my %long_desc = ( + who => $comment->field('who'), + bug_when => format_time($comment->field('bug_when'), '%Y-%m-%d %T'), + isprivate => $comment->{'att'}->{'isprivate'} || 0 + ); + + # If the exporter is not in the insidergroup, keep the comment public. + $long_desc{isprivate} = 0 unless $exporter->is_insider; + + my $data = $comment->field('thetext'); + if (defined $comment->first_child('thetext')->{'att'}->{'encoding'} + && $comment->first_child('thetext')->{'att'}->{'encoding'} =~ /base64/) + { + $data = decode_base64($data); } - if ( defined $bug_fields{'blocked'} ) { - $comments .= "This bug blocked bug(s) " . - join(' ', _to_array($bug_fields{'blocked'})) . ".\n"; + + # For backwards-compatibility with Bugzillas before 3.6: + # + # If we leave the attachment ID in the comment it will be made a link + # to the wrong attachment. Since the new attachment ID is unknown yet + # let's strip it out for now. We will make a comment with the right ID + # later + $data =~ s/Created an attachment \(id=\d+\)/Created an attachment/g; + + # Same goes for bug #'s Since we don't know if the referenced bug + # is also being moved, lets make sure they know it means a different + # bugzilla. + my $url = $urlbase . "show_bug.cgi?id="; + $data =~ s/([Bb]ugs?\s*\#?\s*(\d+))/$url$2/g; + + # Keep the original commenter if possible, else we will fall back + # to the exporter account. + $long_desc{whoid} = login_to_id($long_desc{who}); + + if (!$long_desc{whoid}) { + $data = "The original author of this comment is $long_desc{who}.\n\n" . $data; } - # Now we process each of the fields in turn and make sure they contain - # valid data. We will create two parallel arrays, one for the query - # and one for the values. For every field we need to push an entry onto - # each array. - my @query = (); - my @values = (); - - # Each of these fields we will check for newlines and shove onto the array - foreach my $field (qw(status_whiteboard bug_file_loc short_desc)) { - if ($bug_fields{$field}) { - $bug_fields{$field} = clean_text( $bug_fields{$field} ); - push( @query, $field ); - push( @values, $bug_fields{$field} ); - } + $long_desc{'thetext'} = $data; + push @long_descs, \%long_desc; + } + + my @sorted_descs = sort { $a->{'bug_when'} cmp $b->{'bug_when'} } @long_descs; + + my $comments = "\n\n--- Bug imported by $exporter_login "; + $comments .= format_time(scalar localtime(time()), '%Y-%m-%d %R %Z') . " "; + $comments .= " ---\n\n"; + $comments .= "This bug was previously known as _bug_ $bug_fields{'bug_id'} at "; + $comments .= $urlbase . "show_bug.cgi?id=" . $bug_fields{'bug_id'} . "\n"; + if (defined $bug_fields{'dependson'}) { + $comments .= "This bug depended on bug(s) " + . join(' ', _to_array($bug_fields{'dependson'})) . ".\n"; + } + if (defined $bug_fields{'blocked'}) { + $comments .= "This bug blocked bug(s) " + . join(' ', _to_array($bug_fields{'blocked'})) . ".\n"; + } + + # Now we process each of the fields in turn and make sure they contain + # valid data. We will create two parallel arrays, one for the query + # and one for the values. For every field we need to push an entry onto + # each array. + my @query = (); + my @values = (); + + # Each of these fields we will check for newlines and shove onto the array + foreach my $field (qw(status_whiteboard bug_file_loc short_desc)) { + if ($bug_fields{$field}) { + $bug_fields{$field} = clean_text($bug_fields{$field}); + push(@query, $field); + push(@values, $bug_fields{$field}); } + } - # Alias - if ( $bug_fields{'alias'} ) { - my ($alias) = $dbh->selectrow_array("SELECT COUNT(*) FROM bugs + # Alias + if ($bug_fields{'alias'}) { + my ($alias) = $dbh->selectrow_array( + "SELECT COUNT(*) FROM bugs WHERE alias = ?", undef, - $bug_fields{'alias'} ); - if ($alias) { - $err .= "Dropping conflicting bug alias "; - $err .= $bug_fields{'alias'} . "\n"; - } - else { - $alias = $bug_fields{'alias'}; - push @query, 'alias'; - push @values, $alias; - } + $bug_fields{'alias'} + ); + if ($alias) { + $err .= "Dropping conflicting bug alias "; + $err .= $bug_fields{'alias'} . "\n"; } - - # Timestamps - push( @query, "creation_ts" ); - push( @values, - format_time( $bug_fields{'creation_ts'}, "%Y-%m-%d %T" ) - || $timestamp ); - - push( @query, "delta_ts" ); - push( @values, - format_time( $bug_fields{'delta_ts'}, "%Y-%m-%d %T" ) - || $timestamp ); - - # Bug Access - push( @query, "cclist_accessible" ); - push( @values, $bug_fields{'cclist_accessible'} ? 1 : 0 ); - - push( @query, "reporter_accessible" ); - push( @values, $bug_fields{'reporter_accessible'} ? 1 : 0 ); - - my $product = new Bugzilla::Product( - { name => $bug_fields{'product'} || '' }); - if (!$product) { - $err .= "Unknown Product " . $bug_fields{'product'} . "\n"; - $err .= " Using default product set at the command line.\n"; - $product = new Bugzilla::Product({ name => $default_product_name }) - or Error("an invalid default product was defined for the target" - . " DB. " . $params->{"maintainer"} . " needs to specify " - . "--product when calling importxml.pl", "REOPEN", - $exporter); + else { + $alias = $bug_fields{'alias'}; + push @query, 'alias'; + push @values, $alias; } - my $component = new Bugzilla::Component({ - product => $product, name => $bug_fields{'component'} || '' }); + } + + # Timestamps + push(@query, "creation_ts"); + push(@values, + format_time($bug_fields{'creation_ts'}, "%Y-%m-%d %T") || $timestamp); + + push(@query, "delta_ts"); + push(@values, + format_time($bug_fields{'delta_ts'}, "%Y-%m-%d %T") || $timestamp); + + # Bug Access + push(@query, "cclist_accessible"); + push(@values, $bug_fields{'cclist_accessible'} ? 1 : 0); + + push(@query, "reporter_accessible"); + push(@values, $bug_fields{'reporter_accessible'} ? 1 : 0); + + my $product = new Bugzilla::Product({name => $bug_fields{'product'} || ''}); + if (!$product) { + $err .= "Unknown Product " . $bug_fields{'product'} . "\n"; + $err .= " Using default product set at the command line.\n"; + $product = new Bugzilla::Product({name => $default_product_name}) + or Error( + "an invalid default product was defined for the target" . " DB. " + . $params->{"maintainer"} + . " needs to specify " + . "--product when calling importxml.pl", + "REOPEN", $exporter + ); + } + my $component + = new Bugzilla::Component({ + product => $product, name => $bug_fields{'component'} || '' + }); + if (!$component) { + $err .= "Unknown Component " . $bug_fields{'component'} . "\n"; + $err .= " Using default product and component set "; + $err .= "at the command line.\n"; + + $product = new Bugzilla::Product({name => $default_product_name}); + $component = new Bugzilla::Component( + {name => $default_component_name, product => $product}); if (!$component) { - $err .= "Unknown Component " . $bug_fields{'component'} . "\n"; - $err .= " Using default product and component set "; - $err .= "at the command line.\n"; - - $product = new Bugzilla::Product({ name => $default_product_name }); - $component = new Bugzilla::Component({ - name => $default_component_name, product => $product }); - if (!$component) { - Error("an invalid default component was defined for the target" - . " DB. ". $params->{"maintainer"} . " needs to specify " - . "--component when calling importxml.pl", "REOPEN", - $exporter); - } + Error( + "an invalid default component was defined for the target" . " DB. " + . $params->{"maintainer"} + . " needs to specify " + . "--component when calling importxml.pl", + "REOPEN", $exporter + ); } + } + + my $prod_id = $product->id; + my $comp_id = $component->id; + + push(@query, "product_id"); + push(@values, $prod_id); + push(@query, "component_id"); + push(@values, $comp_id); + + # Since there is no default version for a product, we check that the one + # coming over is valid. If not we will use the first one in @versions + # and warn them. + my $version = new Bugzilla::Version( + {product => $product, name => $bug_fields{'version'}}); + + push(@query, "version"); + if ($version) { + push(@values, $version->name); + } + else { + my @versions = @{$product->versions}; + my $v = $versions[0]; + push(@values, $v->name); + $err .= "Unknown version \""; + $err .= (defined $bug_fields{'version'}) ? $bug_fields{'version'} : "unknown"; + $err .= " in product " . $product->name . ". \n"; + $err .= " Setting version to \"" . $v->name . "\".\n"; + } + + # Milestone + if ($params->{"usetargetmilestone"}) { + my $milestone; + if (defined $bug_fields{'target_milestone'} + && $bug_fields{'target_milestone'} ne "") + { - my $prod_id = $product->id; - my $comp_id = $component->id; - - push( @query, "product_id" ); - push( @values, $prod_id ); - push( @query, "component_id" ); - push( @values, $comp_id ); - - # Since there is no default version for a product, we check that the one - # coming over is valid. If not we will use the first one in @versions - # and warn them. - my $version = new Bugzilla::Version( - { product => $product, name => $bug_fields{'version'} }); - - push( @query, "version" ); - if ($version) { - push( @values, $version->name ); + $milestone = new Bugzilla::Milestone( + {product => $product, name => $bug_fields{'target_milestone'}}); + } + if ($milestone) { + push(@values, $milestone->name); } else { - my @versions = @{ $product->versions }; - my $v = $versions[0]; - push( @values, $v->name ); - $err .= "Unknown version \""; - $err .= ( defined $bug_fields{'version'} ) - ? $bug_fields{'version'} - : "unknown"; - $err .= " in product " . $product->name . ". \n"; - $err .= " Setting version to \"" . $v->name . "\".\n"; + push(@values, $product->default_milestone); + $err .= "Unknown milestone \""; + $err + .= (defined $bug_fields{'target_milestone'}) + ? $bug_fields{'target_milestone'} + : "unknown"; + $err .= " in product " . $product->name . ". \n"; + $err .= " Setting to default milestone for this product, "; + $err .= "\"" . $product->default_milestone . "\".\n"; } - - # Milestone - if ( $params->{"usetargetmilestone"} ) { - my $milestone; - if (defined $bug_fields{'target_milestone'} - && $bug_fields{'target_milestone'} ne "") { - - $milestone = new Bugzilla::Milestone( - { product => $product, name => $bug_fields{'target_milestone'} }); - } - if ($milestone) { - push( @values, $milestone->name ); - } - else { - push( @values, $product->default_milestone ); - $err .= "Unknown milestone \""; - $err .= ( defined $bug_fields{'target_milestone'} ) - ? $bug_fields{'target_milestone'} - : "unknown"; - $err .= " in product " . $product->name . ". \n"; - $err .= " Setting to default milestone for this product, "; - $err .= "\"" . $product->default_milestone . "\".\n"; - } - push( @query, "target_milestone" ); + push(@query, "target_milestone"); + } + + # For priority, severity, opsys and platform we check that the one being + # imported is valid. If it is not we use the defaults set in the parameters. + if ( + defined($bug_fields{'bug_severity'}) + && check_field( + 'bug_severity', scalar $bug_fields{'bug_severity'}, + undef, ERR_LEVEL + ) + ) + { + push(@values, $bug_fields{'bug_severity'}); + } + else { + push(@values, $params->{'defaultseverity'}); + $err .= "Unknown severity "; + $err + .= (defined $bug_fields{'bug_severity'}) + ? $bug_fields{'bug_severity'} + : "unknown"; + $err .= ". Setting to default severity \""; + $err .= $params->{'defaultseverity'} . "\".\n"; + } + push(@query, "bug_severity"); + + if (defined($bug_fields{'priority'}) + && check_field('priority', scalar $bug_fields{'priority'}, undef, ERR_LEVEL)) + { + push(@values, $bug_fields{'priority'}); + } + else { + push(@values, $params->{'defaultpriority'}); + $err .= "Unknown priority "; + $err .= (defined $bug_fields{'priority'}) ? $bug_fields{'priority'} : "unknown"; + $err .= ". Setting to default priority \""; + $err .= $params->{'defaultpriority'} . "\".\n"; + } + push(@query, "priority"); + + if ( + defined($bug_fields{'rep_platform'}) + && check_field( + 'rep_platform', scalar $bug_fields{'rep_platform'}, + undef, ERR_LEVEL + ) + ) + { + push(@values, $bug_fields{'rep_platform'}); + } + else { + push(@values, $params->{'defaultplatform'}); + $err .= "Unknown platform "; + $err + .= (defined $bug_fields{'rep_platform'}) + ? $bug_fields{'rep_platform'} + : "unknown"; + $err .= ". Setting to default platform \""; + $err .= $params->{'defaultplatform'} . "\".\n"; + } + push(@query, "rep_platform"); + + if (defined($bug_fields{'op_sys'}) + && check_field('op_sys', scalar $bug_fields{'op_sys'}, undef, ERR_LEVEL)) + { + push(@values, $bug_fields{'op_sys'}); + } + else { + push(@values, $params->{'defaultopsys'}); + $err .= "Unknown operating system "; + $err .= (defined $bug_fields{'op_sys'}) ? $bug_fields{'op_sys'} : "unknown"; + $err .= ". Setting to default OS \"" . $params->{'defaultopsys'} . "\".\n"; + } + push(@query, "op_sys"); + + # Process time fields + if ($params->{"timetrackinggroup"}) { + my $date + = validate_date($bug_fields{'deadline'}) ? $bug_fields{'deadline'} : undef; + push(@values, $date); + push(@query, "deadline"); + if (defined $bug_fields{'estimated_time'}) { + eval { Bugzilla::Object::_validate_time($bug_fields{'estimated_time'}, "e"); }; + if (!$@) { + push(@values, $bug_fields{'estimated_time'}); + push(@query, "estimated_time"); + } } - - # For priority, severity, opsys and platform we check that the one being - # imported is valid. If it is not we use the defaults set in the parameters. - if (defined( $bug_fields{'bug_severity'} ) - && check_field('bug_severity', scalar $bug_fields{'bug_severity'}, - undef, ERR_LEVEL) ) - { - push( @values, $bug_fields{'bug_severity'} ); + if (defined $bug_fields{'remaining_time'}) { + eval { Bugzilla::Object::_validate_time($bug_fields{'remaining_time'}, "r"); }; + if (!$@) { + push(@values, $bug_fields{'remaining_time'}); + push(@query, "remaining_time"); + } + } + if (defined $bug_fields{'actual_time'}) { + eval { Bugzilla::Object::_validate_time($bug_fields{'actual_time'}, "a"); }; + if ($@) { + $bug_fields{'actual_time'} = 0.0; + $err .= "Invalid Actual Time. Setting to 0.0\n"; + } } else { - push( @values, $params->{'defaultseverity'} ); - $err .= "Unknown severity "; - $err .= ( defined $bug_fields{'bug_severity'} ) - ? $bug_fields{'bug_severity'} - : "unknown"; - $err .= ". Setting to default severity \""; - $err .= $params->{'defaultseverity'} . "\".\n"; + $bug_fields{'actual_time'} = 0.0; + $err .= "Actual time not defined. Setting to 0.0\n"; } - push( @query, "bug_severity" ); - - if (defined( $bug_fields{'priority'} ) - && check_field('priority', scalar $bug_fields{'priority'}, - undef, ERR_LEVEL ) ) - { - push( @values, $bug_fields{'priority'} ); + } + + # Reporter Assignee QA Contact + my $exporterid = $exporter->id; + my $reporterid = login_to_id($bug_fields{'reporter'}) + if $bug_fields{'reporter'}; + push(@query, "reporter"); + if (($bug_fields{'reporter'}) && ($reporterid)) { + push(@values, $reporterid); + } + else { + push(@values, $exporterid); + $err .= "The original reporter of this bug does not have\n"; + $err .= " an account here. Reassigning to the person who moved\n"; + $err .= " it here: $exporter_login.\n"; + if ($bug_fields{'reporter'}) { + $err .= " Previous reporter was $bug_fields{'reporter'}.\n"; } else { - push( @values, $params->{'defaultpriority'} ); - $err .= "Unknown priority "; - $err .= ( defined $bug_fields{'priority'} ) - ? $bug_fields{'priority'} - : "unknown"; - $err .= ". Setting to default priority \""; - $err .= $params->{'defaultpriority'} . "\".\n"; + $err .= " Previous reporter is unknown.\n"; } - push( @query, "priority" ); - - if (defined( $bug_fields{'rep_platform'} ) - && check_field('rep_platform', scalar $bug_fields{'rep_platform'}, - undef, ERR_LEVEL ) ) - { - push( @values, $bug_fields{'rep_platform'} ); + } + + my $changed_owner = 0; + my $owner; + push(@query, "assigned_to"); + if ( ($bug_fields{'assigned_to'}) + && ($owner = login_to_id($bug_fields{'assigned_to'}))) + { + push(@values, $owner); + } + else { + push(@values, $component->default_assignee->id); + $changed_owner = 1; + $err .= "The original assignee of this bug does not have\n"; + $err .= " an account here. Reassigning to the default assignee\n"; + $err .= " for the component, " . $component->default_assignee->login . ".\n"; + if ($bug_fields{'assigned_to'}) { + $err .= " Previous assignee was $bug_fields{'assigned_to'}.\n"; } else { - push( @values, $params->{'defaultplatform'} ); - $err .= "Unknown platform "; - $err .= ( defined $bug_fields{'rep_platform'} ) - ? $bug_fields{'rep_platform'} - : "unknown"; - $err .=". Setting to default platform \""; - $err .= $params->{'defaultplatform'} . "\".\n"; + $err .= " Previous assignee is unknown.\n"; } - push( @query, "rep_platform" ); + } - if (defined( $bug_fields{'op_sys'} ) - && check_field('op_sys', scalar $bug_fields{'op_sys'}, - undef, ERR_LEVEL ) ) + if ($params->{"useqacontact"}) { + my $qa_contact; + push(@query, "qa_contact"); + if ( (defined $bug_fields{'qa_contact'}) + && ($qa_contact = login_to_id($bug_fields{'qa_contact'}))) { - push( @values, $bug_fields{'op_sys'} ); + push(@values, $qa_contact); } else { - push( @values, $params->{'defaultopsys'} ); - $err .= "Unknown operating system "; - $err .= ( defined $bug_fields{'op_sys'} ) - ? $bug_fields{'op_sys'} - : "unknown"; - $err .= ". Setting to default OS \"" . $params->{'defaultopsys'} . "\".\n"; - } - push( @query, "op_sys" ); - - # Process time fields - if ( $params->{"timetrackinggroup"} ) { - my $date = validate_date( $bug_fields{'deadline'} ) ? $bug_fields{'deadline'} : undef; - push( @values, $date ); - push( @query, "deadline" ); - if ( defined $bug_fields{'estimated_time'} ) { - eval { - Bugzilla::Object::_validate_time($bug_fields{'estimated_time'}, "e"); - }; - if (!$@){ - push( @values, $bug_fields{'estimated_time'} ); - push( @query, "estimated_time" ); - } - } - if ( defined $bug_fields{'remaining_time'} ) { - eval { - Bugzilla::Object::_validate_time($bug_fields{'remaining_time'}, "r"); - }; - if (!$@){ - push( @values, $bug_fields{'remaining_time'} ); - push( @query, "remaining_time" ); - } - } - if ( defined $bug_fields{'actual_time'} ) { - eval { - Bugzilla::Object::_validate_time($bug_fields{'actual_time'}, "a"); - }; - if ($@){ - $bug_fields{'actual_time'} = 0.0; - $err .= "Invalid Actual Time. Setting to 0.0\n"; - } - } - else { - $bug_fields{'actual_time'} = 0.0; - $err .= "Actual time not defined. Setting to 0.0\n"; - } + push(@values, $component->default_qa_contact->id || undef); + if ($component->default_qa_contact->id) { + $err .= "Setting qa contact to the default for this product.\n"; + $err .= " This bug either had no qa contact or an invalid one.\n"; + } } - - # Reporter Assignee QA Contact - my $exporterid = $exporter->id; - my $reporterid = login_to_id( $bug_fields{'reporter'} ) - if $bug_fields{'reporter'}; - push( @query, "reporter" ); - if ( ( $bug_fields{'reporter'} ) && ($reporterid) ) { - push( @values, $reporterid ); + } + + # Status & Resolution + my $valid_res + = check_field('resolution', scalar $bug_fields{'resolution'}, undef, + ERR_LEVEL); + my $valid_status + = check_field('bug_status', scalar $bug_fields{'bug_status'}, undef, + ERR_LEVEL); + my $is_open = is_open_state($bug_fields{'bug_status'}); + my $status = $bug_fields{'bug_status'} || undef; + my $resolution = $bug_fields{'resolution'} || undef; + + # Check everconfirmed + my $everconfirmed; + if ($product->allows_unconfirmed) { + $everconfirmed = $bug_fields{'everconfirmed'} || 0; + } + else { + $everconfirmed = 1; + } + push(@query, "everconfirmed"); + push(@values, $everconfirmed); + + # Sanity check will complain about having bugs marked duplicate but no + # entry in the dup table. Since we can't tell the bug ID of bugs + # that might not yet be in the database we have no way of populating + # this table. Change the resolution instead. + if ($valid_res && ($bug_fields{'resolution'} eq "DUPLICATE")) { + $resolution = "INVALID"; + $err .= "This bug was marked DUPLICATE in the database "; + $err .= "it was moved from.\n Changing resolution to \"INVALID\"\n"; + } + + # If there is at least 1 initial bug status different from UNCO, use it, + # else use the open bug status with the lowest sortkey (different from UNCO). + my @bug_statuses = @{Bugzilla::Status->can_change_to()}; + @bug_statuses = grep { $_->name ne 'UNCONFIRMED' } @bug_statuses; + + my $initial_status; + if (scalar(@bug_statuses)) { + $initial_status = $bug_statuses[0]->name; + } + else { + @bug_statuses = Bugzilla::Status->get_all(); + + # Exclude UNCO and inactive bug statuses. + @bug_statuses + = grep { $_->is_active && $_->name ne 'UNCONFIRMED' } @bug_statuses; + my @open_statuses = grep { $_->is_open } @bug_statuses; + if (scalar(@open_statuses)) { + $initial_status = $open_statuses[0]->name; } else { - push( @values, $exporterid ); - $err .= "The original reporter of this bug does not have\n"; - $err .= " an account here. Reassigning to the person who moved\n"; - $err .= " it here: $exporter_login.\n"; - if ( $bug_fields{'reporter'} ) { - $err .= " Previous reporter was $bug_fields{'reporter'}.\n"; + # There is NO other open bug statuses outside UNCO??? + Error("no open bug statuses available."); + } + } + + if ($status) { + if ($valid_status) { + if ($is_open) { + if ($resolution) { + $err .= "Resolution set on an open status.\n"; + $err .= " Dropping resolution $resolution\n"; + $resolution = undef; } - else { - $err .= " Previous reporter is unknown.\n"; + if ($changed_owner) { + if ($everconfirmed) { + $status = $initial_status; + } + else { + $status = "UNCONFIRMED"; + } + if ($status ne $bug_fields{'bug_status'}) { + $err .= "Bug reassigned, setting status to \"$status\".\n"; + $err .= " Previous status was \""; + $err .= $bug_fields{'bug_status'} . "\".\n"; + } } - } - - my $changed_owner = 0; - my $owner; - push( @query, "assigned_to" ); - if ( ( $bug_fields{'assigned_to'} ) - && ( $owner = login_to_id( $bug_fields{'assigned_to'} )) ) { - push( @values, $owner ); - } - else { - push( @values, $component->default_assignee->id ); - $changed_owner = 1; - $err .= "The original assignee of this bug does not have\n"; - $err .= " an account here. Reassigning to the default assignee\n"; - $err .= " for the component, ". $component->default_assignee->login .".\n"; - if ( $bug_fields{'assigned_to'} ) { - $err .= " Previous assignee was $bug_fields{'assigned_to'}.\n"; + if ($everconfirmed) { + if ($status eq "UNCONFIRMED") { + $err .= "Bug Status was UNCONFIRMED but everconfirmed was true\n"; + $err .= " Setting status to $initial_status\n"; + $status = $initial_status; + } } - else { - $err .= " Previous assignee is unknown.\n"; + else { # $everconfirmed is false + if ($status ne "UNCONFIRMED") { + $err .= "Bug Status was $status but everconfirmed was false\n"; + $err .= " Setting status to UNCONFIRMED\n"; + $status = "UNCONFIRMED"; + } } - } - - if ( $params->{"useqacontact"} ) { - my $qa_contact; - push( @query, "qa_contact" ); - if ( ( defined $bug_fields{'qa_contact'}) - && ( $qa_contact = login_to_id( $bug_fields{'qa_contact'} ) ) ) { - push( @values, $qa_contact ); + } + else { # $is_open is false + if (!$resolution) { + $err .= "Missing Resolution. Setting status to "; + if ($everconfirmed) { + $status = $initial_status; + $err .= "$initial_status\n"; + } + else { + $status = "UNCONFIRMED"; + $err .= "UNCONFIRMED\n"; + } } - else { - push( @values, $component->default_qa_contact->id || undef ); - if ($component->default_qa_contact->id){ - $err .= "Setting qa contact to the default for this product.\n"; - $err .= " This bug either had no qa contact or an invalid one.\n"; - } + elsif (!$valid_res) { + $err .= "Unknown resolution \"$resolution\".\n"; + $err .= " Setting resolution to INVALID\n"; + $resolution = "INVALID"; } + } } - - # Status & Resolution - my $valid_res = check_field('resolution', - scalar $bug_fields{'resolution'}, - undef, ERR_LEVEL ); - my $valid_status = check_field('bug_status', - scalar $bug_fields{'bug_status'}, - undef, ERR_LEVEL ); - my $is_open = is_open_state($bug_fields{'bug_status'}); - my $status = $bug_fields{'bug_status'} || undef; - my $resolution = $bug_fields{'resolution'} || undef; - - # Check everconfirmed - my $everconfirmed; - if ($product->allows_unconfirmed) { - $everconfirmed = $bug_fields{'everconfirmed'} || 0; + else { # $valid_status is false + if ($everconfirmed) { + $status = $initial_status; + } + else { + $status = "UNCONFIRMED"; + } + $err .= "Bug has invalid status, setting status to \"$status\".\n"; + $err .= " Previous status was \""; + $err .= $bug_fields{'bug_status'} . "\".\n"; + $resolution = undef; + } + } + else { + if ($everconfirmed) { + $status = $initial_status; } else { - $everconfirmed = 1; + $status = "UNCONFIRMED"; } - push (@query, "everconfirmed"); - push (@values, $everconfirmed); - - # Sanity check will complain about having bugs marked duplicate but no - # entry in the dup table. Since we can't tell the bug ID of bugs - # that might not yet be in the database we have no way of populating - # this table. Change the resolution instead. - if ( $valid_res && ( $bug_fields{'resolution'} eq "DUPLICATE" ) ) { - $resolution = "INVALID"; - $err .= "This bug was marked DUPLICATE in the database "; - $err .= "it was moved from.\n Changing resolution to \"INVALID\"\n"; + $err .= "Bug has no status, setting status to \"$status\".\n"; + $err .= " Previous status was unknown\n"; + $resolution = undef; + } + + if ($resolution) { + push(@query, "resolution"); + push(@values, $resolution); + } + + # Bug status + push(@query, "bug_status"); + push(@values, $status); + + # Custom fields - Multi-select fields have their own table. + my %multi_select_fields; + foreach my $field (Bugzilla->active_custom_fields) { + my $custom_field = $field->name; + my $value = $bug_fields{$custom_field}; + next unless defined $value; + if ($field->type == FIELD_TYPE_FREETEXT) { + push(@query, $custom_field); + push(@values, clean_text($value)); } - - # If there is at least 1 initial bug status different from UNCO, use it, - # else use the open bug status with the lowest sortkey (different from UNCO). - my @bug_statuses = @{Bugzilla::Status->can_change_to()}; - @bug_statuses = grep { $_->name ne 'UNCONFIRMED' } @bug_statuses; - - my $initial_status; - if (scalar(@bug_statuses)) { - $initial_status = $bug_statuses[0]->name; - } - else { - @bug_statuses = Bugzilla::Status->get_all(); - # Exclude UNCO and inactive bug statuses. - @bug_statuses = grep { $_->is_active && $_->name ne 'UNCONFIRMED'} @bug_statuses; - my @open_statuses = grep { $_->is_open } @bug_statuses; - if (scalar(@open_statuses)) { - $initial_status = $open_statuses[0]->name; - } - else { - # There is NO other open bug statuses outside UNCO??? - Error("no open bug statuses available."); - } + elsif ($field->type == FIELD_TYPE_TEXTAREA) { + push(@query, $custom_field); + push(@values, $value); } - - if ($status) { - if($valid_status){ - if($is_open){ - if ($resolution) { - $err .= "Resolution set on an open status.\n"; - $err .= " Dropping resolution $resolution\n"; - $resolution = undef; - } - if($changed_owner){ - if($everconfirmed){ - $status = $initial_status; - } - else{ - $status = "UNCONFIRMED"; - } - if ($status ne $bug_fields{'bug_status'}){ - $err .= "Bug reassigned, setting status to \"$status\".\n"; - $err .= " Previous status was \""; - $err .= $bug_fields{'bug_status'} . "\".\n"; - } - } - if($everconfirmed){ - if($status eq "UNCONFIRMED"){ - $err .= "Bug Status was UNCONFIRMED but everconfirmed was true\n"; - $err .= " Setting status to $initial_status\n"; - $status = $initial_status; - } - } - else{ # $everconfirmed is false - if($status ne "UNCONFIRMED"){ - $err .= "Bug Status was $status but everconfirmed was false\n"; - $err .= " Setting status to UNCONFIRMED\n"; - $status = "UNCONFIRMED"; - } - } - } - else{ # $is_open is false - if (!$resolution) { - $err .= "Missing Resolution. Setting status to "; - if($everconfirmed){ - $status = $initial_status; - $err .= "$initial_status\n"; - } - else{ - $status = "UNCONFIRMED"; - $err .= "UNCONFIRMED\n"; - } - } - elsif (!$valid_res) { - $err .= "Unknown resolution \"$resolution\".\n"; - $err .= " Setting resolution to INVALID\n"; - $resolution = "INVALID"; - } - } - } - else{ # $valid_status is false - if($everconfirmed){ - $status = $initial_status; - } - else{ - $status = "UNCONFIRMED"; - } - $err .= "Bug has invalid status, setting status to \"$status\".\n"; - $err .= " Previous status was \""; - $err .= $bug_fields{'bug_status'} . "\".\n"; - $resolution = undef; - } + elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) { + my $is_well_formed = check_field($custom_field, $value, undef, ERR_LEVEL); + if ($is_well_formed) { + push(@query, $custom_field); + push(@values, $value); + } + else { + $err .= "Skipping illegal value \"$value\" in $custom_field.\n"; + } } - else { - if($everconfirmed){ - $status = $initial_status; + elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { + my @legal_values; + foreach my $item (_to_array($value)) { + my $is_well_formed = check_field($custom_field, $item, undef, ERR_LEVEL); + if ($is_well_formed) { + push(@legal_values, $item); } - else{ - $status = "UNCONFIRMED"; + else { + $err .= "Skipping illegal value \"$item\" in $custom_field.\n"; } - $err .= "Bug has no status, setting status to \"$status\".\n"; - $err .= " Previous status was unknown\n"; - $resolution = undef; + } + if (scalar @legal_values) { + $multi_select_fields{$custom_field} = \@legal_values; + } } - - if ($resolution) { - push( @query, "resolution" ); - push( @values, $resolution ); + elsif ($field->type == FIELD_TYPE_DATETIME) { + eval { $value = Bugzilla::Bug->_check_datetime_field($value); }; + if ($@) { + $err .= "Skipping illegal value \"$value\" in $custom_field.\n"; + } + else { + push(@query, $custom_field); + push(@values, $value); + } } - - # Bug status - push( @query, "bug_status" ); - push( @values, $status ); - - # Custom fields - Multi-select fields have their own table. - my %multi_select_fields; - foreach my $field (Bugzilla->active_custom_fields) { - my $custom_field = $field->name; - my $value = $bug_fields{$custom_field}; - next unless defined $value; - if ($field->type == FIELD_TYPE_FREETEXT) { - push(@query, $custom_field); - push(@values, clean_text($value)); - } elsif ($field->type == FIELD_TYPE_TEXTAREA) { - push(@query, $custom_field); - push(@values, $value); - } elsif ($field->type == FIELD_TYPE_SINGLE_SELECT) { - my $is_well_formed = check_field($custom_field, $value, undef, ERR_LEVEL); - if ($is_well_formed) { - push(@query, $custom_field); - push(@values, $value); - } else { - $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ; - } - } elsif ($field->type == FIELD_TYPE_MULTI_SELECT) { - my @legal_values; - foreach my $item (_to_array($value)) { - my $is_well_formed = check_field($custom_field, $item, undef, ERR_LEVEL); - if ($is_well_formed) { - push(@legal_values, $item); - } else { - $err .= "Skipping illegal value \"$item\" in $custom_field.\n" ; - } - } - if (scalar @legal_values) { - $multi_select_fields{$custom_field} = \@legal_values; - } - } elsif ($field->type == FIELD_TYPE_DATETIME) { - eval { $value = Bugzilla::Bug->_check_datetime_field($value); }; - if ($@) { - $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ; - } - else { - push(@query, $custom_field); - push(@values, $value); - } - } elsif ($field->type == FIELD_TYPE_DATE) { - eval { $value = Bugzilla::Bug->_check_date_field($value); }; - if ($@) { - $err .= "Skipping illegal value \"$value\" in $custom_field.\n" ; - } - else { - push(@query, $custom_field); - push(@values, $value); - } - } else { - $err .= "Type of custom field $custom_field is an unhandled FIELD_TYPE: " . - $field->type . "\n"; - } + elsif ($field->type == FIELD_TYPE_DATE) { + eval { $value = Bugzilla::Bug->_check_date_field($value); }; + if ($@) { + $err .= "Skipping illegal value \"$value\" in $custom_field.\n"; + } + else { + push(@query, $custom_field); + push(@values, $value); + } } - - # For the sake of sanitycheck.cgi we do this. - # Update lastdiffed if you do not want to have mail sent - unless ($mail) { - push @query, "lastdiffed"; - push @values, $timestamp; + else { + $err .= "Type of custom field $custom_field is an unhandled FIELD_TYPE: " + . $field->type . "\n"; } - - # INSERT the bug - my $query = "INSERT INTO bugs (" . join( ", ", @query ) . ") VALUES ("; - $query .= '?,' foreach (@values); - chop($query); # Remove the last comma. - $query .= ")"; - - $dbh->do( $query, undef, @values ); - my $id = $dbh->bz_last_key( 'bugs', 'bug_id' ); - - # We are almost certain to get some uninitialized warnings - # Since this is just for debugging the query, let's shut them up - eval { - no warnings 'uninitialized'; - Debug( - "Bug Query: INSERT INTO bugs (\n" - . join( ",\n", @query ) - . "\n) VALUES (\n" - . join( ",\n", @values ), - DEBUG_LEVEL - ); - }; - - # Handle CC's - if ( defined $bug_fields{'cc'} ) { - my %ccseen; - my $sth_cc = $dbh->prepare("INSERT INTO cc (bug_id, who) VALUES (?,?)"); - foreach my $person (_to_array($bug_fields{'cc'})) { - next unless $person; - my $uid; - if ($uid = login_to_id($person)) { - if ( !$ccseen{$uid} ) { - $sth_cc->execute( $id, $uid ); - $ccseen{$uid} = 1; - } - } - else { - $err .= "CC member $person does not have an account here\n"; - } + } + + # For the sake of sanitycheck.cgi we do this. + # Update lastdiffed if you do not want to have mail sent + unless ($mail) { + push @query, "lastdiffed"; + push @values, $timestamp; + } + + # INSERT the bug + my $query = "INSERT INTO bugs (" . join(", ", @query) . ") VALUES ("; + $query .= '?,' foreach (@values); + chop($query); # Remove the last comma. + $query .= ")"; + + $dbh->do($query, undef, @values); + my $id = $dbh->bz_last_key('bugs', 'bug_id'); + + # We are almost certain to get some uninitialized warnings + # Since this is just for debugging the query, let's shut them up + eval { + no warnings 'uninitialized'; + Debug( + "Bug Query: INSERT INTO bugs (\n" + . join(",\n", @query) + . "\n) VALUES (\n" + . join(",\n", @values), + DEBUG_LEVEL + ); + }; + + # Handle CC's + if (defined $bug_fields{'cc'}) { + my %ccseen; + my $sth_cc = $dbh->prepare("INSERT INTO cc (bug_id, who) VALUES (?,?)"); + foreach my $person (_to_array($bug_fields{'cc'})) { + next unless $person; + my $uid; + if ($uid = login_to_id($person)) { + if (!$ccseen{$uid}) { + $sth_cc->execute($id, $uid); + $ccseen{$uid} = 1; } + } + else { + $err .= "CC member $person does not have an account here\n"; + } } + } - # Handle keywords - if ( defined( $bug_fields{'keywords'} ) ) { - my %keywordseen; - my $key_sth = $dbh->prepare( - "INSERT INTO keywords + # Handle keywords + if (defined($bug_fields{'keywords'})) { + my %keywordseen; + my $key_sth = $dbh->prepare( + "INSERT INTO keywords (bug_id, keywordid) VALUES (?,?)" - ); - foreach my $keyword ( split( /[\s,]+/, $bug_fields{'keywords'} )) { - next unless $keyword; - my $keyword_obj = new Bugzilla::Keyword({name => $keyword}); - if (!$keyword_obj) { - $err .= "Skipping unknown keyword: $keyword.\n"; - next; - } - if (!$keywordseen{$keyword_obj->id}) { - $key_sth->execute($id, $keyword_obj->id); - $keywordseen{$keyword_obj->id} = 1; - } - } + ); + foreach my $keyword (split(/[\s,]+/, $bug_fields{'keywords'})) { + next unless $keyword; + my $keyword_obj = new Bugzilla::Keyword({name => $keyword}); + if (!$keyword_obj) { + $err .= "Skipping unknown keyword: $keyword.\n"; + next; + } + if (!$keywordseen{$keyword_obj->id}) { + $key_sth->execute($id, $keyword_obj->id); + $keywordseen{$keyword_obj->id} = 1; + } } - - # Insert values of custom multi-select fields. They have already - # been validated. - foreach my $custom_field (keys %multi_select_fields) { - my $sth = $dbh->prepare("INSERT INTO bug_$custom_field - (bug_id, value) VALUES (?, ?)"); - foreach my $value (@{$multi_select_fields{$custom_field}}) { - $sth->execute($id, $value); - } + } + + # Insert values of custom multi-select fields. They have already + # been validated. + foreach my $custom_field (keys %multi_select_fields) { + my $sth = $dbh->prepare( + "INSERT INTO bug_$custom_field + (bug_id, value) VALUES (?, ?)" + ); + foreach my $value (@{$multi_select_fields{$custom_field}}) { + $sth->execute($id, $value); } - - # Parse bug flags - foreach my $bflag ( $bug->children('flag')) { - next unless ( defined($bflag) ); - $err .= flag_handler( - $bflag->{'att'}->{'name'}, $bflag->{'att'}->{'status'}, - $bflag->{'att'}->{'setter'}, $bflag->{'att'}->{'requestee'}, - $exporterid, $id, - $comp_id, $prod_id, - undef - ); + } + + # Parse bug flags + foreach my $bflag ($bug->children('flag')) { + next unless (defined($bflag)); + $err .= flag_handler( + $bflag->{'att'}->{'name'}, $bflag->{'att'}->{'status'}, + $bflag->{'att'}->{'setter'}, $bflag->{'att'}->{'requestee'}, + $exporterid, $id, + $comp_id, $prod_id, + undef + ); + } + + # Insert Attachments for the bug + foreach my $att (@attachments) { + if ($att eq "err") { + $err .= "No attachment ID specified, dropping attachment\n"; + next; + } + if (!$exporter->is_insider && $att->{'isprivate'}) { + $err .= "Exporter not in insidergroup and attachment marked private.\n"; + $err .= " Marking attachment public\n"; + $att->{'isprivate'} = 0; } - # Insert Attachments for the bug - foreach my $att (@attachments) { - if ($att eq "err"){ - $err .= "No attachment ID specified, dropping attachment\n"; - next; - } - if (!$exporter->is_insider && $att->{'isprivate'}) { - $err .= "Exporter not in insidergroup and attachment marked private.\n"; - $err .= " Marking attachment public\n"; - $att->{'isprivate'} = 0; - } - - my $attacher_id = $att->{'attacher'} ? login_to_id($att->{'attacher'}) : undef; + my $attacher_id = $att->{'attacher'} ? login_to_id($att->{'attacher'}) : undef; - $dbh->do("INSERT INTO attachments + $dbh->do( + "INSERT INTO attachments (bug_id, creation_ts, modification_time, filename, description, mimetype, ispatch, isprivate, isobsolete, submitter_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - undef, $id, $att->{'date'}, $att->{'date'}, $att->{'filename'}, - $att->{'desc'}, $att->{'ctype'}, $att->{'ispatch'}, - $att->{'isprivate'}, $att->{'isobsolete'}, $attacher_id || $exporterid); - my $att_id = $dbh->bz_last_key( 'attachments', 'attach_id' ); - my $att_data = $att->{'data'}; - my $sth = $dbh->prepare("INSERT INTO attach_data (id, thedata) - VALUES ($att_id, ?)" ); - trick_taint($att_data); - $sth->bind_param( 1, $att_data, $dbh->BLOB_TYPE ); - $sth->execute(); - - $comments .= "Imported an attachment (id=$att_id)\n"; - if (!$attacher_id) { - if ($att->{'attacher'}) { - $err .= "The original submitter of attachment $att_id was\n "; - $err .= $att->{'attacher'} . ", but he doesn't have an account here.\n"; - } - else { - $err .= "The original submitter of attachment $att_id is unknown.\n"; - } - $err .= " Reassigning to the person who moved it here: $exporter_login.\n"; - } + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", undef, $id, $att->{'date'}, + $att->{'date'}, $att->{'filename'}, $att->{'desc'}, $att->{'ctype'}, + $att->{'ispatch'}, $att->{'isprivate'}, $att->{'isobsolete'}, + $attacher_id || $exporterid + ); + my $att_id = $dbh->bz_last_key('attachments', 'attach_id'); + my $att_data = $att->{'data'}; + my $sth = $dbh->prepare( + "INSERT INTO attach_data (id, thedata) + VALUES ($att_id, ?)" + ); + trick_taint($att_data); + $sth->bind_param(1, $att_data, $dbh->BLOB_TYPE); + $sth->execute(); + + $comments .= "Imported an attachment (id=$att_id)\n"; + if (!$attacher_id) { + if ($att->{'attacher'}) { + $err .= "The original submitter of attachment $att_id was\n "; + $err .= $att->{'attacher'} . ", but he doesn't have an account here.\n"; + } + else { + $err .= "The original submitter of attachment $att_id is unknown.\n"; + } + $err .= " Reassigning to the person who moved it here: $exporter_login.\n"; + } - # Process attachment flags - foreach my $aflag (@{ $att->{'flags'} }) { - next unless defined($aflag) ; - $err .= flag_handler( - $aflag->{'name'}, $aflag->{'status'}, - $aflag->{'setter'}, $aflag->{'requestee'}, - $exporterid, $id, - $comp_id, $prod_id, - $att_id - ); - } + # Process attachment flags + foreach my $aflag (@{$att->{'flags'}}) { + next unless defined($aflag); + $err .= flag_handler( + $aflag->{'name'}, $aflag->{'status'}, $aflag->{'setter'}, + $aflag->{'requestee'}, $exporterid, $id, + $comp_id, $prod_id, $att_id + ); } + } - # Clear the attachments array for the next bug - @attachments = (); + # Clear the attachments array for the next bug + @attachments = (); - # Insert comments and append any errors - my $worktime = $bug_fields{'actual_time'} || 0.0; - $worktime = 0.0 if (!$exporter->is_timetracker); - $comments .= "\n$err\n" if $err; + # Insert comments and append any errors + my $worktime = $bug_fields{'actual_time'} || 0.0; + $worktime = 0.0 if (!$exporter->is_timetracker); + $comments .= "\n$err\n" if $err; - my $sth_comment = - $dbh->prepare('INSERT INTO longdescs (bug_id, who, bug_when, isprivate, + my $sth_comment = $dbh->prepare( + 'INSERT INTO longdescs (bug_id, who, bug_when, isprivate, thetext, work_time) - VALUES (?, ?, ?, ?, ?, ?)'); - - foreach my $c (@sorted_descs) { - $sth_comment->execute($id, $c->{whoid} || $exporterid, $c->{bug_when}, - $c->{isprivate}, $c->{thetext}, 0); - } - $sth_comment->execute($id, $exporterid, $timestamp, 0, $comments, $worktime); - Bugzilla::Bug->new($id)->_sync_fulltext( new_bug => 1); - - # Add this bug to each group of which its product is a member. - my $sth_group = $dbh->prepare("INSERT INTO bug_group_map (bug_id, group_id) - VALUES (?, ?)"); - foreach my $group_id ( keys %{ $product->group_controls } ) { - if ($product->group_controls->{$group_id}->{'membercontrol'} != CONTROLMAPNA - && $product->group_controls->{$group_id}->{'othercontrol'} != CONTROLMAPNA){ - $sth_group->execute( $id, $group_id ); - } - } - - $log .= "Bug ${urlbase}show_bug.cgi?id=$bug_fields{'bug_id'} "; - $log .= "imported as bug $id.\n"; - $log .= Bugziilla->localconfig->{"urlbase"} . "show_bug.cgi?id=$id\n\n"; - if ($err) { - $log .= "The following problems were encountered while creating bug $id.\n"; - $log .= $err; - $log .= "You may have to set certain fields in the new bug by hand.\n\n"; + VALUES (?, ?, ?, ?, ?, ?)' + ); + + foreach my $c (@sorted_descs) { + $sth_comment->execute($id, $c->{whoid} || $exporterid, + $c->{bug_when}, $c->{isprivate}, $c->{thetext}, 0); + } + $sth_comment->execute($id, $exporterid, $timestamp, 0, $comments, $worktime); + Bugzilla::Bug->new($id)->_sync_fulltext(new_bug => 1); + + # Add this bug to each group of which its product is a member. + my $sth_group = $dbh->prepare( + "INSERT INTO bug_group_map (bug_id, group_id) + VALUES (?, ?)" + ); + foreach my $group_id (keys %{$product->group_controls}) { + if ( $product->group_controls->{$group_id}->{'membercontrol'} != CONTROLMAPNA + && $product->group_controls->{$group_id}->{'othercontrol'} != CONTROLMAPNA) + { + $sth_group->execute($id, $group_id); } - Debug( $log, OK_LEVEL ); - push(@logs, $log); - Bugzilla::BugMail::Send( $id, { 'changer' => $exporter } ) if ($mail); - - # done with the xml data. Lets clear it from memory - $twig->purge; + } + + $log .= "Bug ${urlbase}show_bug.cgi?id=$bug_fields{'bug_id'} "; + $log .= "imported as bug $id.\n"; + $log .= Bugziilla->localconfig->{"urlbase"} . "show_bug.cgi?id=$id\n\n"; + if ($err) { + $log .= "The following problems were encountered while creating bug $id.\n"; + $log .= $err; + $log .= "You may have to set certain fields in the new bug by hand.\n\n"; + } + Debug($log, OK_LEVEL); + push(@logs, $log); + Bugzilla::BugMail::Send($id, {'changer' => $exporter}) if ($mail); + + # done with the xml data. Lets clear it from memory + $twig->purge; } -Debug( "Reading xml", DEBUG_LEVEL ); +Debug("Reading xml", DEBUG_LEVEL); # Read STDIN in slurp mode. VERY dangerous, but we live on the wild side ;-) local ($/); @@ -1237,30 +1275,28 @@ $xml = <>; # If there's anything except whitespace before new; - $parser->output_to_core(1); - $parser->tmp_to_core(1); - my $entity = $parser->parse_data($xml); - my $bodyhandle = $entity->bodyhandle; - $xml = $bodyhandle->as_string; + # If the email was encoded (Mailer::MessageToMTA() does it when using UTF-8), + # we have to decode it first, else the XML parsing will fail. + my $parser = MIME::Parser->new; + $parser->output_to_core(1); + $parser->tmp_to_core(1); + my $entity = $parser->parse_data($xml); + my $bodyhandle = $entity->bodyhandle; + $xml = $bodyhandle->as_string; } # remove everything in file before xml header $xml =~ s/^.+(<\?xml version.+)$/$1/s; -Debug( "Parsing tree", DEBUG_LEVEL ); +Debug("Parsing tree", DEBUG_LEVEL); my $twig = XML::Twig->new( - twig_handlers => { - bug => \&process_bug, - attachment => \&process_attachment - }, - start_tag_handlers => { bugzilla => \&init } + twig_handlers => {bug => \&process_bug, attachment => \&process_attachment}, + start_tag_handlers => {bugzilla => \&init} ); + # Prevent DoS using the billion laughs attack. $twig->{NoExpand} = 1; @@ -1272,11 +1308,11 @@ my $urlbase = $root->{'att'}->{'urlbase'}; # It is time to email the result of the import. my $log = join("\n\n", @logs); -$log .= "\n\nImported $bugtotal bug(s) from $urlbase,\n sent by $exporter.\n"; -my $subject = "$bugtotal Bug(s) successfully moved from $urlbase to " - . Bugzilla->localconfig->{"urlbase"}; +$log .= "\n\nImported $bugtotal bug(s) from $urlbase,\n sent by $exporter.\n"; +my $subject = "$bugtotal Bug(s) successfully moved from $urlbase to " + . Bugzilla->localconfig->{"urlbase"}; my @to = ($exporter, $maintainer); -MailMessage( $subject, $log, @to ); +MailMessage($subject, $log, @to); __END__ diff --git a/index.cgi b/index.cgi index f21edfc92..4452714e0 100755 --- a/index.cgi +++ b/index.cgi @@ -21,7 +21,7 @@ use List::MoreUtils qw(any); # Check whether or not the user is logged in my $user = Bugzilla->login(LOGIN_OPTIONAL); -my $cgi = Bugzilla->cgi; +my $cgi = Bugzilla->cgi; my $vars = {}; # Yes, I really want to avoid two calls to the id method. @@ -34,59 +34,65 @@ my $can_cache = 0; # And log out the user if requested. We do this first so that nothing # else accidentally relies on the current login. if ($cgi->param('logout')) { - Bugzilla->logout(); - $user = Bugzilla->user; - $user_id = 0; - $can_cache = 0; - $vars->{'message'} = "logged_out"; - # Make sure that templates or other code doesn't get confused about this. - $cgi->delete('logout'); + Bugzilla->logout(); + $user = Bugzilla->user; + $user_id = 0; + $can_cache = 0; + $vars->{'message'} = "logged_out"; + + # Make sure that templates or other code doesn't get confused about this. + $cgi->delete('logout'); } # our weak etag is based on the bugzilla version parameter (BMO customization) and the announcehtml # if either change, the cache will be considered invalid. my @etag_parts = ( - Bugzilla->VERSION, - Bugzilla->params->{announcehtml}, - Bugzilla->params->{createemailregexp}, + Bugzilla->VERSION, + Bugzilla->params->{announcehtml}, + Bugzilla->params->{createemailregexp}, ); -my $weak_etag = q{W/"} . md5_hex(@etag_parts) . q{"}; +my $weak_etag = q{W/"} . md5_hex(@etag_parts) . q{"}; my $if_none_match = $cgi->http('If-None-Match'); # load balancer (or client) will check back with us after max-age seconds # If the etag in If-None-Match is unchanged, we quickly respond without doing much work. -my @cache_control = ( - $can_cache ? 'public' : 'no-cache', - sprintf('max-age=%d', 60 * 5), -); +my @cache_control + = ($can_cache ? 'public' : 'no-cache', sprintf('max-age=%d', 60 * 5),); -if ($can_cache && $if_none_match && any { $_ eq $weak_etag } split(/,\s*/, $if_none_match)) { - print $cgi->header(-status => '304 Not Modified', -ETag => $weak_etag); +if ( + $can_cache && $if_none_match && any { $_ eq $weak_etag } + split(/,\s*/, $if_none_match) + ) +{ + print $cgi->header(-status => '304 Not Modified', -ETag => $weak_etag); } else { - my $template = Bugzilla->template; - $cgi->content_security_policy(script_src => ['self', 'https://www.google-analytics.com']); - - # Return the appropriate HTTP response headers. - print $cgi->header( - -Cache_Control => join(', ', @cache_control), - $can_cache ? (-ETag => $weak_etag) : (), - ); - - if ($user_id && $user->in_group('admin')) { - # If 'urlbase' is not set, display the Welcome page. - unless (Bugzilla->localconfig->{'urlbase'}) { - $template->process('welcome-admin.html.tmpl') - or ThrowTemplateError($template->error()); - exit; - } - # Inform the administrator about new releases, if any. - $vars->{'release'} = Bugzilla::Update::get_notifications(); + my $template = Bugzilla->template; + $cgi->content_security_policy( + script_src => ['self', 'https://www.google-analytics.com']); + + # Return the appropriate HTTP response headers. + print $cgi->header( + -Cache_Control => join(', ', @cache_control), + $can_cache ? (-ETag => $weak_etag) : (), + ); + + if ($user_id && $user->in_group('admin')) { + + # If 'urlbase' is not set, display the Welcome page. + unless (Bugzilla->localconfig->{'urlbase'}) { + $template->process('welcome-admin.html.tmpl') + or ThrowTemplateError($template->error()); + exit; } - $vars->{use_login_page} = 1; + # Inform the administrator about new releases, if any. + $vars->{'release'} = Bugzilla::Update::get_notifications(); + } + + $vars->{use_login_page} = 1; - # Generate and return the UI (HTML page) from the appropriate template. - $template->process("index.html.tmpl", $vars) - or ThrowTemplateError( $template->error() ); + # Generate and return the UI (HTML page) from the appropriate template. + $template->process("index.html.tmpl", $vars) + or ThrowTemplateError($template->error()); } diff --git a/jobqueue-worker.pl b/jobqueue-worker.pl index b26aacdba..566cfdedd 100755 --- a/jobqueue-worker.pl +++ b/jobqueue-worker.pl @@ -14,10 +14,10 @@ use File::Basename qw(basename dirname); use File::Spec::Functions qw(catdir rel2abs); BEGIN { - require lib; - my $dir = rel2abs( dirname(__FILE__) ); - lib->import( $dir, catdir( $dir, 'lib' ), catdir( $dir, qw(local lib perl5) ) ); - chdir $dir or die "chdir $dir failed: $!"; + require lib; + my $dir = rel2abs(dirname(__FILE__)); + lib->import($dir, catdir($dir, 'lib'), catdir($dir, qw(local lib perl5))); + chdir $dir or die "chdir $dir failed: $!"; } @@ -31,17 +31,18 @@ use if $OSNAME eq 'linux', 'Linux::Pdeathsig', 'set_pdeathsig'; BEGIN { Bugzilla->extensions } my $name = basename(__FILE__); -GetOptions( 'name=s' => \$name ); +GetOptions('name=s' => \$name); if ($name) { - ## no critic (Variables::RequireLocalizedPunctuationVars) - $PROGRAM_NAME = $name; - ## use critic + ## no critic (Variables::RequireLocalizedPunctuationVars) + $PROGRAM_NAME = $name; + ## use critic } if ($OSNAME eq 'linux') { - # get SIGTEMR (15) when parent exits. - set_pdeathsig(15); + + # get SIGTEMR (15) when parent exits. + set_pdeathsig(15); } Bugzilla::JobQueue::Worker->run('work'); diff --git a/jobqueue.pl b/jobqueue.pl index 7a884b811..13acfd9bf 100755 --- a/jobqueue.pl +++ b/jobqueue.pl @@ -14,10 +14,10 @@ use File::Basename qw(dirname); use File::Spec::Functions qw(catdir rel2abs); BEGIN { - require lib; - my $dir = rel2abs( dirname(__FILE__) ); - lib->import( $dir, catdir( $dir, 'lib' ), catdir( $dir, qw(local lib perl5) ) ); - chdir $dir or die "chdir $dir failed: $!"; + require lib; + my $dir = rel2abs(dirname(__FILE__)); + lib->import($dir, catdir($dir, 'lib'), catdir($dir, qw(local lib perl5))); + chdir $dir or die "chdir $dir failed: $!"; } use Bugzilla; diff --git a/jsonrpc.cgi b/jsonrpc.cgi index 95ea5c208..9485c1d87 100755 --- a/jsonrpc.cgi +++ b/jsonrpc.cgi @@ -16,10 +16,11 @@ use Bugzilla; use Bugzilla::Constants; use Bugzilla::Error; use Bugzilla::WebService::Constants; + BEGIN { - if (!Bugzilla->feature('jsonrpc')) { - ThrowCodeError('feature_disabled', { feature => 'jsonrpc' }); - } + if (!Bugzilla->feature('jsonrpc')) { + ThrowCodeError('feature_disabled', {feature => 'jsonrpc'}); + } } use Bugzilla::WebService::Server::JSONRPC; diff --git a/long_list.cgi b/long_list.cgi index 91f3208b3..8c166ae87 100755 --- a/long_list.cgi +++ b/long_list.cgi @@ -32,8 +32,9 @@ use Bugzilla; my $cgi = Bugzilla->cgi; # Convert comma/space separated elements into separate params -my $buglist = $cgi->param('buglist') || $cgi->param('bug_id') || $cgi->param('id') || ''; -my @ids = split (/[\s,]+/, $buglist); +my $buglist + = $cgi->param('buglist') || $cgi->param('bug_id') || $cgi->param('id') || ''; +my @ids = split(/[\s,]+/, $buglist); my $ids = join('', map { $_ = "&id=" . $_ } @ids); diff --git a/migrate.pl b/migrate.pl index 8980248c7..907e0cab5 100755 --- a/migrate.pl +++ b/migrate.pl @@ -25,7 +25,7 @@ GetOptions(\%switch, 'help|h|?', 'from=s', 'verbose|v+', 'dry-run'); # Print the help message if that switch was selected or if --from # wasn't specified. if (!$switch{'from'} or $switch{'help'}) { - pod2usage({-exitval => 1}); + pod2usage({-exitval => 1}); } my $migrator = Bugzilla::Migrate->load($switch{'from'}); @@ -37,13 +37,13 @@ $migrator->do_migration(); # Even if there's an error, we want to be sure that the serial values # get reset properly. END { - if ($migrator and $migrator->dry_run) { - my $dbh = Bugzilla->dbh; - if ($dbh->bz_in_transaction) { - $dbh->bz_rollback_transaction(); - } - $migrator->reset_serial_values(); + if ($migrator and $migrator->dry_run) { + my $dbh = Bugzilla->dbh; + if ($dbh->bz_in_transaction) { + $dbh->bz_rollback_transaction(); } + $migrator->reset_serial_values(); + } } __END__ @@ -96,4 +96,4 @@ the size of all attachments in your current bug-tracker. You may also need to increase the number of file handles a process is allowed to hold open (as the migrator will create a file handle for each attachment in your database). On Linux and simliar systems, you can do this as root -by typing C before running your script. \ No newline at end of file +by typing C before running your script. diff --git a/new_bug.cgi b/new_bug.cgi index 23b55e325..a7f3d74b8 100755 --- a/new_bug.cgi +++ b/new_bug.cgi @@ -48,84 +48,87 @@ my $vars = {}; my $dbh = Bugzilla->dbh; if (lc($cgi->request_method) eq 'post') { - my $token = $cgi->param('token'); - check_hash_token($token, ['new_bug']); - my @keywords = $cgi->param('keywords'); - my @groups = $cgi->param('groups'); - my @cc = split /\s*,\s*/, $cgi->param('cc'); - my @bug_mentor = split /\s*,\s*/, $cgi->param('bug_mentor'); - my $new_bug = Bugzilla::Bug->create({ - short_desc => scalar($cgi->param('short_desc')), - product => scalar($cgi->param('product')), - component => scalar($cgi->param('component')), - bug_severity => 'normal', - groups => \@groups, - op_sys => 'Unspecified', - rep_platform => 'Unspecified', - version => scalar( $cgi->param('version')), - keywords => \@keywords, - cc => \@cc, - comment => scalar($cgi->param('comment')), - dependson => scalar($cgi->param('dependson')), - blocked => scalar($cgi->param('blocked')), - assigned_to => scalar($cgi->param('assigned_to')), - bug_mentors => \@bug_mentor, - }); - delete_token($token); - - my $data_fh = $cgi->upload('data'); - - if ($data_fh) { - my $content_type = Bugzilla::Attachment::get_content_type(); - my $attachment; - - my $error_mode_cache = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - my $timestamp = $dbh->selectrow_array( - 'SELECT creation_ts FROM bugs WHERE bug_id = ?', undef, $new_bug->bug_id); - eval { - $attachment = Bugzilla::Attachment->create( - {bug => $new_bug, - creation_ts => $timestamp, - data => $data_fh, - description => scalar $cgi->param('description'), - filename => $data_fh, - ispatch => 0, - isprivate => 0, - mimetype => $content_type, - }); - }; - Bugzilla->error_mode($error_mode_cache); - unless ($attachment) { - $vars->{'message'} = 'attachment_creation_failed'; - } - } - - my $recipients = { changer => $user }; - my $bug_sent = Bugzilla::BugMail::Send($new_bug->bug_id, $recipients); - $bug_sent->{type} = 'created'; - $bug_sent->{id} = $new_bug->bug_id; - my @all_mail_results = ($bug_sent); - - foreach my $dep (@{$new_bug->dependson || []}, @{$new_bug->blocked || []}) { - my $dep_sent = Bugzilla::BugMail::Send($dep, $recipients); - $dep_sent->{type} = 'dep'; - $dep_sent->{id} = $dep; - push(@all_mail_results, $dep_sent); - } - - # Sending emails for any referenced bugs. - foreach my $ref_bug_id (uniq @{ $new_bug->{see_also_changes} || [] }) { - my $ref_sent = Bugzilla::BugMail::Send($ref_bug_id, $recipients); - $ref_sent->{id} = $ref_bug_id; - push(@all_mail_results, $ref_sent); - } - - print $cgi->redirect(Bugzilla->localconfig->{urlbase} . 'show_bug.cgi?id='.$new_bug->bug_id); -} else { - print $cgi->header(); -$template->process("bug/new_bug.html.tmpl", - $vars) - or ThrowTemplateError($template->error()); + my $token = $cgi->param('token'); + check_hash_token($token, ['new_bug']); + my @keywords = $cgi->param('keywords'); + my @groups = $cgi->param('groups'); + my @cc = split /\s*,\s*/, $cgi->param('cc'); + my @bug_mentor = split /\s*,\s*/, $cgi->param('bug_mentor'); + my $new_bug = Bugzilla::Bug->create({ + short_desc => scalar($cgi->param('short_desc')), + product => scalar($cgi->param('product')), + component => scalar($cgi->param('component')), + bug_severity => 'normal', + groups => \@groups, + op_sys => 'Unspecified', + rep_platform => 'Unspecified', + version => scalar($cgi->param('version')), + keywords => \@keywords, + cc => \@cc, + comment => scalar($cgi->param('comment')), + dependson => scalar($cgi->param('dependson')), + blocked => scalar($cgi->param('blocked')), + assigned_to => scalar($cgi->param('assigned_to')), + bug_mentors => \@bug_mentor, + }); + delete_token($token); + + my $data_fh = $cgi->upload('data'); + + if ($data_fh) { + my $content_type = Bugzilla::Attachment::get_content_type(); + my $attachment; + + my $error_mode_cache = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + my $timestamp + = $dbh->selectrow_array('SELECT creation_ts FROM bugs WHERE bug_id = ?', + undef, $new_bug->bug_id); + eval { + $attachment = Bugzilla::Attachment->create({ + bug => $new_bug, + creation_ts => $timestamp, + data => $data_fh, + description => scalar $cgi->param('description'), + filename => $data_fh, + ispatch => 0, + isprivate => 0, + mimetype => $content_type, + }); + }; + Bugzilla->error_mode($error_mode_cache); + + unless ($attachment) { + $vars->{'message'} = 'attachment_creation_failed'; + } + } + + my $recipients = {changer => $user}; + my $bug_sent = Bugzilla::BugMail::Send($new_bug->bug_id, $recipients); + $bug_sent->{type} = 'created'; + $bug_sent->{id} = $new_bug->bug_id; + my @all_mail_results = ($bug_sent); + + foreach my $dep (@{$new_bug->dependson || []}, @{$new_bug->blocked || []}) { + my $dep_sent = Bugzilla::BugMail::Send($dep, $recipients); + $dep_sent->{type} = 'dep'; + $dep_sent->{id} = $dep; + push(@all_mail_results, $dep_sent); + } + + # Sending emails for any referenced bugs. + foreach my $ref_bug_id (uniq @{$new_bug->{see_also_changes} || []}) { + my $ref_sent = Bugzilla::BugMail::Send($ref_bug_id, $recipients); + $ref_sent->{id} = $ref_bug_id; + push(@all_mail_results, $ref_sent); + } + + print $cgi->redirect( + Bugzilla->localconfig->{urlbase} . 'show_bug.cgi?id=' . $new_bug->bug_id); +} +else { + print $cgi->header(); + $template->process("bug/new_bug.html.tmpl", $vars) + or ThrowTemplateError($template->error()); } diff --git a/page.cgi b/page.cgi index 3784b92ed..82d8ce064 100755 --- a/page.cgi +++ b/page.cgi @@ -30,16 +30,17 @@ use Bugzilla::Search::Quicksearch; # For quicksearch.html. sub quicksearch_field_names { - my $fields = Bugzilla::Search::Quicksearch->FIELD_MAP; - my %fields_reverse; - # Put longer names before shorter names. - my @nicknames = sort { length($b) <=> length($a) } (keys %$fields); - foreach my $nickname (@nicknames) { - my $db_field = $fields->{$nickname}; - $fields_reverse{$db_field} ||= []; - push(@{ $fields_reverse{$db_field} }, $nickname); - } - return \%fields_reverse; + my $fields = Bugzilla::Search::Quicksearch->FIELD_MAP; + my %fields_reverse; + + # Put longer names before shorter names. + my @nicknames = sort { length($b) <=> length($a) } (keys %$fields); + foreach my $nickname (@nicknames) { + my $db_field = $fields->{$nickname}; + $fields_reverse{$db_field} ||= []; + push(@{$fields_reverse{$db_field}}, $nickname); + } + return \%fields_reverse; } ############### @@ -48,38 +49,40 @@ sub quicksearch_field_names { Bugzilla->login(); -my $cgi = Bugzilla->cgi; +my $cgi = Bugzilla->cgi; my $template = Bugzilla->template; my $id = $cgi->param('id'); if ($id) { - # Be careful not to allow directory traversal. - if ($id =~ /\.\./) { - # two dots in a row is bad - ThrowCodeError("bad_page_cgi_id", { "page_id" => $id }); - } - # Split into name and ctype. - $id =~ /^([\w\-\/\.]+)\.(\w+)$/; - if (!$2) { - # if this regexp fails to match completely, something bad came in - ThrowCodeError("bad_page_cgi_id", { "page_id" => $id }); - } - - my %vars = ( - quicksearch_field_names => \&quicksearch_field_names, - ); - Bugzilla::Hook::process('page_before_template', - { page_id => $id, vars => \%vars }); - - my $format = $template->get_format("pages/$1", undef, $2); - - $cgi->param('id', $id); - - print $cgi->header($format->{'ctype'}); - - $template->process("$format->{'template'}", \%vars) - || ThrowTemplateError($template->error()); + + # Be careful not to allow directory traversal. + if ($id =~ /\.\./) { + + # two dots in a row is bad + ThrowCodeError("bad_page_cgi_id", {"page_id" => $id}); + } + + # Split into name and ctype. + $id =~ /^([\w\-\/\.]+)\.(\w+)$/; + if (!$2) { + + # if this regexp fails to match completely, something bad came in + ThrowCodeError("bad_page_cgi_id", {"page_id" => $id}); + } + + my %vars = (quicksearch_field_names => \&quicksearch_field_names,); + Bugzilla::Hook::process('page_before_template', + {page_id => $id, vars => \%vars}); + + my $format = $template->get_format("pages/$1", undef, $2); + + $cgi->param('id', $id); + + print $cgi->header($format->{'ctype'}); + + $template->process("$format->{'template'}", \%vars) + || ThrowTemplateError($template->error()); } else { - ThrowUserError("no_page_specified"); + ThrowUserError("no_page_specified"); } diff --git a/post_bug.cgi b/post_bug.cgi index 2fd27ea86..bff0c8d55 100755 --- a/post_bug.cgi +++ b/post_bug.cgi @@ -33,10 +33,10 @@ use MIME::Base64 qw(decode_base64); my $user = Bugzilla->login(LOGIN_REQUIRED); -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; ###################################################################### # Main Script @@ -44,14 +44,16 @@ my $vars = {}; # redirect to enter_bug if no field is passed. unless ($cgi->param()) { - print $cgi->redirect(Bugzilla->localconfig->{urlbase} . 'enter_bug.cgi'); - exit; + print $cgi->redirect(Bugzilla->localconfig->{urlbase} . 'enter_bug.cgi'); + exit; } # BMO: Don't allow updating of bugs if disabled if (Bugzilla->params->{disable_bug_updates}) { - ThrowErrorPage('bug/process/updates-disabled.html.tmpl', - 'Bug updates are currently disabled.'); + ThrowErrorPage( + 'bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.' + ); } # Detect if the user already used the same form to submit a bug @@ -61,21 +63,22 @@ check_token_data($token, 'create_bug', 'index.cgi'); # do a match on the fields if applicable # BMO: allow extensions to define custom user fields my $user_match_fields = { - 'cc' => { 'type' => 'multi' }, - 'assigned_to' => { 'type' => 'single' }, - 'qa_contact' => { 'type' => 'single' }, + 'cc' => {'type' => 'multi'}, + 'assigned_to' => {'type' => 'single'}, + 'qa_contact' => {'type' => 'single'}, }; -Bugzilla::Hook::process('bug_user_match_fields', { fields => $user_match_fields }); +Bugzilla::Hook::process('bug_user_match_fields', + {fields => $user_match_fields}); Bugzilla::User::match_field($user_match_fields); if (defined $cgi->param('maketemplate')) { - $vars->{'url'} = $cgi->canonicalise_query('token'); - $vars->{'short_desc'} = $cgi->param('short_desc'); + $vars->{'url'} = $cgi->canonicalise_query('token'); + $vars->{'short_desc'} = $cgi->param('short_desc'); - print $cgi->header(); - $template->process("bug/create/make-template.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + print $cgi->header(); + $template->process("bug/create/make-template.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; } umask 0; @@ -83,21 +86,24 @@ umask 0; # The format of the initial comment can be structured by adding fields to the # enter_bug template and then referencing them in the comment template. my $comment; -my $format = $template->get_format("bug/create/comment", - scalar($cgi->param('format')), "txt"); +my $format + = $template->get_format("bug/create/comment", scalar($cgi->param('format')), + "txt"); $template->process($format->{'template'}, $vars, \$comment) - || ThrowTemplateError($template->error()); + || ThrowTemplateError($template->error()); # Include custom fields editable on bug creation. -my @custom_bug_fields = grep {$_->type != FIELD_TYPE_MULTI_SELECT && $_->enter_bug} - Bugzilla->active_custom_fields; +my @custom_bug_fields + = grep { $_->type != FIELD_TYPE_MULTI_SELECT && $_->enter_bug } + Bugzilla->active_custom_fields; # Undefined custom fields are ignored to ensure they will get their default # value (e.g. "---" for custom single select fields). my @bug_fields = grep { defined $cgi->param($_->name) } @custom_bug_fields; @bug_fields = map { $_->name } @bug_fields; -push(@bug_fields, qw( +push( + @bug_fields, qw( product component @@ -122,31 +128,33 @@ push(@bug_fields, qw( see_also estimated_time deadline -)); + ) +); my %bug_params; foreach my $field (@bug_fields) { - $bug_params{$field} = $cgi->param($field); + $bug_params{$field} = $cgi->param($field); } foreach my $field (qw(cc groups)) { - next if !$cgi->should_set($field); - $bug_params{$field} = [$cgi->param($field)]; + next if !$cgi->should_set($field); + $bug_params{$field} = [$cgi->param($field)]; } $bug_params{'comment'} = $comment; -my @multi_selects = grep {$_->type == FIELD_TYPE_MULTI_SELECT && $_->enter_bug} - Bugzilla->active_custom_fields; +my @multi_selects + = grep { $_->type == FIELD_TYPE_MULTI_SELECT && $_->enter_bug } + Bugzilla->active_custom_fields; foreach my $field (@multi_selects) { - next if !$cgi->should_set($field->name); - $bug_params{$field->name} = [$cgi->param($field->name)]; + next if !$cgi->should_set($field->name); + $bug_params{$field->name} = [$cgi->param($field->name)]; } # BMO - add user_match_fields. it's important to source from input_params # instead of $cgi->param to ensure we get the correct value. foreach my $field (keys %$user_match_fields) { - next if exists $bug_params{$field}; - next unless $cgi->should_set($field); - $bug_params{$field} = Bugzilla->input_params->{$field} // []; + next if exists $bug_params{$field}; + next unless $cgi->should_set($field); + $bug_params{$field} = Bugzilla->input_params->{$field} // []; } my $bug = Bugzilla::Bug->create(\%bug_params); @@ -157,15 +165,18 @@ delete_token($token); # We do this directly from the DB because $bug->creation_ts has the seconds # formatted out of it (which should be fixed some day). -my $timestamp = $dbh->selectrow_array( - 'SELECT creation_ts FROM bugs WHERE bug_id = ?', undef, $id); +my $timestamp + = $dbh->selectrow_array('SELECT creation_ts FROM bugs WHERE bug_id = ?', + undef, $id); # Set Version cookie, but only if the user actually selected # a version on the page. if (defined $cgi->param('version')) { - $cgi->send_cookie(-name => "VERSION-" . $bug->product, - -value => $bug->version, - -expires => "Fri, 01-Jan-2038 00:00:00 GMT"); + $cgi->send_cookie( + -name => "VERSION-" . $bug->product, + -value => $bug->version, + -expires => "Fri, 01-Jan-2038 00:00:00 GMT" + ); } # We don't have to check if the user can see the bug, because a user filing @@ -173,120 +184,127 @@ if (defined $cgi->param('version')) { # after the bug is filed. # Add an attachment if requested. -my $data_fh = $cgi->upload('data'); +my $data_fh = $cgi->upload('data'); my $attach_text = $cgi->param('attach_text'); my $data_base64 = $cgi->param('data_base64'); if ($data_fh || $attach_text || $data_base64) { - $cgi->param('isprivate', $cgi->param('comment_is_private')); - - # Must be called before create() as it may alter $cgi->param('ispatch'). - my $content_type = Bugzilla::Attachment::get_content_type(); - my $attachment; - my $data; - my $filename; - - if ($attach_text) { - # Convert to unix line-endings if pasting a patch - if (scalar($cgi->param('ispatch'))) { - $attach_text =~ s/[\012\015]{1,2}/\012/g; - } - $data = $attach_text; - $filename = "file_$id.txt"; - } elsif ($data_base64) { - $data = decode_base64($data_base64); - $filename = $cgi->param('filename') || "file_$id"; - } else { - $data = $filename = $data_fh; - } + $cgi->param('isprivate', $cgi->param('comment_is_private')); - # If the attachment cannot be successfully added to the bug, - # we notify the user, but we don't interrupt the bug creation process. - my $error_mode_cache = Bugzilla->error_mode; - Bugzilla->error_mode(ERROR_MODE_DIE); - eval { - $attachment = Bugzilla::Attachment->create( - {bug => $bug, - creation_ts => $timestamp, - data => $data, - description => scalar $cgi->param('description'), - filename => $filename, - ispatch => scalar $cgi->param('ispatch'), - isprivate => scalar $cgi->param('isprivate'), - mimetype => $content_type, - }); - }; - Bugzilla->error_mode($error_mode_cache); - - if ($attachment) { - # Set attachment flags. - Bugzilla::Hook::process('post_bug_attachment_flags', { bug => $bug, attachment => $attachment }); - my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi( - $bug, $attachment, $vars, SKIP_REQUESTEE_ON_ERROR); - $attachment->set_flags($flags, $new_flags); - $attachment->update($timestamp); - my $comment = $bug->comments->[0]; - $comment->set_all({ type => CMT_ATTACHMENT_CREATED, - extra_data => $attachment->id }); - $comment->update(); - } - else { - $vars->{'message'} = 'attachment_creation_failed'; + # Must be called before create() as it may alter $cgi->param('ispatch'). + my $content_type = Bugzilla::Attachment::get_content_type(); + my $attachment; + my $data; + my $filename; + + if ($attach_text) { + + # Convert to unix line-endings if pasting a patch + if (scalar($cgi->param('ispatch'))) { + $attach_text =~ s/[\012\015]{1,2}/\012/g; } + $data = $attach_text; + $filename = "file_$id.txt"; + } + elsif ($data_base64) { + $data = decode_base64($data_base64); + $filename = $cgi->param('filename') || "file_$id"; + } + else { + $data = $filename = $data_fh; + } + + # If the attachment cannot be successfully added to the bug, + # we notify the user, but we don't interrupt the bug creation process. + my $error_mode_cache = Bugzilla->error_mode; + Bugzilla->error_mode(ERROR_MODE_DIE); + eval { + $attachment = Bugzilla::Attachment->create({ + bug => $bug, + creation_ts => $timestamp, + data => $data, + description => scalar $cgi->param('description'), + filename => $filename, + ispatch => scalar $cgi->param('ispatch'), + isprivate => scalar $cgi->param('isprivate'), + mimetype => $content_type, + }); + }; + Bugzilla->error_mode($error_mode_cache); + + if ($attachment) { + + # Set attachment flags. + Bugzilla::Hook::process('post_bug_attachment_flags', + {bug => $bug, attachment => $attachment}); + my ($flags, $new_flags) + = Bugzilla::Flag->extract_flags_from_cgi($bug, $attachment, $vars, + SKIP_REQUESTEE_ON_ERROR); + $attachment->set_flags($flags, $new_flags); + $attachment->update($timestamp); + my $comment = $bug->comments->[0]; + $comment->set_all( + {type => CMT_ATTACHMENT_CREATED, extra_data => $attachment->id}); + $comment->update(); + } + else { + $vars->{'message'} = 'attachment_creation_failed'; + } } # Set bug_ignored from the hidden field if (scalar $cgi->param('bug_ignored')) { - $bug->set_bug_ignored(1); + $bug->set_bug_ignored(1); } # Set bug flags. -my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi($bug, undef, $vars, - SKIP_REQUESTEE_ON_ERROR); +my ($flags, $new_flags) + = Bugzilla::Flag->extract_flags_from_cgi($bug, undef, $vars, + SKIP_REQUESTEE_ON_ERROR); $bug->set_flags($flags, $new_flags); $bug->update($timestamp); -$vars->{'id'} = $id; +$vars->{'id'} = $id; $vars->{'bug'} = $bug; -Bugzilla::Hook::process('post_bug_after_creation', { vars => $vars }); +Bugzilla::Hook::process('post_bug_after_creation', {vars => $vars}); -ThrowCodeError("bug_error", { bug => $bug }) if $bug->error; +ThrowCodeError("bug_error", {bug => $bug}) if $bug->error; -my $recipients = { changer => $user }; +my $recipients = {changer => $user}; my $bug_sent = Bugzilla::BugMail::Send($id, $recipients); $bug_sent->{type} = 'created'; $bug_sent->{id} = $id; my @all_mail_results = ($bug_sent); foreach my $dep (@{$bug->dependson || []}, @{$bug->blocked || []}) { - my $dep_sent = Bugzilla::BugMail::Send($dep, $recipients); - $dep_sent->{type} = 'dep'; - $dep_sent->{id} = $dep; - push(@all_mail_results, $dep_sent); + my $dep_sent = Bugzilla::BugMail::Send($dep, $recipients); + $dep_sent->{type} = 'dep'; + $dep_sent->{id} = $dep; + push(@all_mail_results, $dep_sent); } # Sending emails for any referenced bugs. -foreach my $ref_bug_id (uniq @{ $bug->{see_also_changes} || [] }) { - my $ref_sent = Bugzilla::BugMail::Send($ref_bug_id, $recipients); - $ref_sent->{id} = $ref_bug_id; - push(@all_mail_results, $ref_sent); +foreach my $ref_bug_id (uniq @{$bug->{see_also_changes} || []}) { + my $ref_sent = Bugzilla::BugMail::Send($ref_bug_id, $recipients); + $ref_sent->{id} = $ref_bug_id; + push(@all_mail_results, $ref_sent); } $vars->{sentmail} = \@all_mail_results; $format = $template->get_format("bug/create/created", - scalar($cgi->param('created-format')), - "html"); + scalar($cgi->param('created-format')), "html"); # don't leak the enter_bug format param to show_bug $cgi->delete('format'); if ($user->setting('ui_experiments') eq 'on') { - Bugzilla->cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id)); + Bugzilla->cgi->content_security_policy( + Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug->id)); } print $cgi->header(); $template->process($format->{'template'}, $vars) - || ThrowTemplateError($template->error()); + || ThrowTemplateError($template->error()); 1; diff --git a/process_bug.cgi b/process_bug.cgi index df7dc57d9..c72d72d63 100755 --- a/process_bug.cgi +++ b/process_bug.cgi @@ -33,10 +33,10 @@ use Storable qw(dclone); my $user = Bugzilla->login(LOGIN_REQUIRED); -my $cgi = Bugzilla->cgi; -my $dbh = Bugzilla->dbh; +my $cgi = Bugzilla->cgi; +my $dbh = Bugzilla->dbh; my $template = Bugzilla->template; -my $vars = {}; +my $vars = {}; ###################################################################### # Subroutines @@ -44,18 +44,19 @@ my $vars = {}; # Tells us whether or not a field should be changed by process_bug. sub should_set { - # check_defined is used for fields where there's another field - # whose name starts with "defined_" and then the field name--it's used - # to know when we did things like empty a multi-select or deselect - # a checkbox. - my ($field, $check_defined) = @_; - my $cgi = Bugzilla->cgi; - if ( defined $cgi->param($field) - || ($check_defined && defined $cgi->param("defined_$field")) ) - { - return 1; - } - return 0; + + # check_defined is used for fields where there's another field + # whose name starts with "defined_" and then the field name--it's used + # to know when we did things like empty a multi-select or deselect + # a checkbox. + my ($field, $check_defined) = @_; + my $cgi = Bugzilla->cgi; + if (defined $cgi->param($field) + || ($check_defined && defined $cgi->param("defined_$field"))) + { + return 1; + } + return 0; } ###################################################################### @@ -64,8 +65,10 @@ sub should_set { # BMO: Don't allow updating of bugs if disabled if (Bugzilla->params->{disable_bug_updates}) { - ThrowErrorPage('bug/process/updates-disabled.html.tmpl', - 'Bug updates are currently disabled.'); + ThrowErrorPage( + 'bug/process/updates-disabled.html.tmpl', + 'Bug updates are currently disabled.' + ); } # Create a list of objects for all bugs being modified in this request. @@ -74,42 +77,44 @@ if (defined $cgi->param('id')) { my $bug = Bugzilla::Bug->check(scalar $cgi->param('id')); $cgi->param('id', $bug->id); push(@bug_objects, $bug); -} else { - foreach my $i ($cgi->param()) { - if ($i =~ /^id_([1-9][0-9]*)/) { - my $id = $1; - push(@bug_objects, Bugzilla::Bug->check($id)); - } +} +else { + foreach my $i ($cgi->param()) { + if ($i =~ /^id_([1-9][0-9]*)/) { + my $id = $1; + push(@bug_objects, Bugzilla::Bug->check($id)); } + } } # Make sure there are bugs to process. scalar(@bug_objects) || ThrowUserError("no_bugs_chosen", {action => 'modify'}); -my $first_bug = $bug_objects[0]; # Used when we're only updating a single bug. +my $first_bug = $bug_objects[0]; # Used when we're only updating a single bug. # Delete any parameter set to 'dontchange'. if (defined $cgi->param('dontchange')) { - foreach my $name ($cgi->param) { - next if $name eq 'dontchange'; # But don't delete dontchange itself! - # Skip ones we've already deleted (such as "defined_$name"). - next if !defined $cgi->param($name); - if ($cgi->param($name) eq $cgi->param('dontchange')) { - $cgi->delete($name); - $cgi->delete("defined_$name"); - } + foreach my $name ($cgi->param) { + next if $name eq 'dontchange'; # But don't delete dontchange itself! + # Skip ones we've already deleted (such as "defined_$name"). + next if !defined $cgi->param($name); + if ($cgi->param($name) eq $cgi->param('dontchange')) { + $cgi->delete($name); + $cgi->delete("defined_$name"); } + } } # do a match on the fields if applicable # BMO: allow extensions to define custom user fields my $user_match_fields = { - 'qa_contact' => { 'type' => 'single' }, - 'newcc' => { 'type' => 'multi' }, - 'masscc' => { 'type' => 'multi' }, - 'assigned_to' => { 'type' => 'single' }, + 'qa_contact' => {'type' => 'single'}, + 'newcc' => {'type' => 'multi'}, + 'masscc' => {'type' => 'multi'}, + 'assigned_to' => {'type' => 'single'}, }; -Bugzilla::Hook::process('bug_user_match_fields', { fields => $user_match_fields }); +Bugzilla::Hook::process('bug_user_match_fields', + {fields => $user_match_fields}); Bugzilla::User::match_field($user_match_fields); # Check for a mid-air collision. Currently this only works when updating @@ -117,52 +122,54 @@ Bugzilla::User::match_field($user_match_fields); my $delta_ts = $cgi->param('delta_ts') || ''; if ($delta_ts) { - my $delta_ts_z = datetime_from($delta_ts) - or ThrowCodeError('invalid_timestamp', { timestamp => $delta_ts }); - - my $first_delta_tz_z = datetime_from($first_bug->delta_ts); - - if ($first_delta_tz_z ne $delta_ts_z) { - ($vars->{'operations'}) = Bugzilla::Bug::GetBugActivity($first_bug->id, undef, $delta_ts); - - my $start_at = $cgi->param('longdesclength') - or ThrowCodeError('undefined_field', { field => 'longdesclength' }); - - # Always sort midair collision comments oldest to newest, - # regardless of the user's personal preference. - my $comments = $first_bug->comments({ order => "oldest_to_newest" }); - - # Show midair if previous changes made other than CC - # and/or one or more comments were made - my $do_midair = scalar @$comments > $start_at ? 1 : 0; - - if (!$do_midair) { - foreach my $operation (@{ $vars->{'operations'} }) { - foreach my $change (@{ $operation->{'changes'} }) { - if ($change->{'fieldname'} ne 'cc') { - $do_midair = 1; - last; - } - } - last if $do_midair; - } - } + my $delta_ts_z = datetime_from($delta_ts) + or ThrowCodeError('invalid_timestamp', {timestamp => $delta_ts}); - if ($do_midair) { - $vars->{'title_tag'} = "mid_air"; - $vars->{'start_at'} = $start_at; - $vars->{'comments'} = $comments; - $vars->{'bug'} = $first_bug; - # The token contains the old delta_ts. We need a new one. - $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts])); - - # Warn the user about the mid-air collision and ask them what to do. - print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL; - $template->process("bug/process/midair.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - exit; + my $first_delta_tz_z = datetime_from($first_bug->delta_ts); + + if ($first_delta_tz_z ne $delta_ts_z) { + ($vars->{'operations'}) + = Bugzilla::Bug::GetBugActivity($first_bug->id, undef, $delta_ts); + + my $start_at = $cgi->param('longdesclength') + or ThrowCodeError('undefined_field', {field => 'longdesclength'}); + + # Always sort midair collision comments oldest to newest, + # regardless of the user's personal preference. + my $comments = $first_bug->comments({order => "oldest_to_newest"}); + + # Show midair if previous changes made other than CC + # and/or one or more comments were made + my $do_midair = scalar @$comments > $start_at ? 1 : 0; + + if (!$do_midair) { + foreach my $operation (@{$vars->{'operations'}}) { + foreach my $change (@{$operation->{'changes'}}) { + if ($change->{'fieldname'} ne 'cc') { + $do_midair = 1; + last; + } } + last if $do_midair; + } } + + if ($do_midair) { + $vars->{'title_tag'} = "mid_air"; + $vars->{'start_at'} = $start_at; + $vars->{'comments'} = $comments; + $vars->{'bug'} = $first_bug; + + # The token contains the old delta_ts. We need a new one. + $cgi->param('token', issue_hash_token([$first_bug->id, $first_bug->delta_ts])); + + # Warn the user about the mid-air collision and ask them what to do. + print $cgi->header() unless Bugzilla->usage_mode == USAGE_MODE_EMAIL; + $template->process("bug/process/midair.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + exit; + } + } } # We couldn't do this check earlier as we first had to validate bug IDs @@ -171,10 +178,10 @@ if ($delta_ts) { my $token = $cgi->param('token'); if ($cgi->param('id')) { - check_hash_token($token, [$first_bug->id, $delta_ts || $first_bug->delta_ts]); + check_hash_token($token, [$first_bug->id, $delta_ts || $first_bug->delta_ts]); } else { - check_token_data($token, 'buglist_mass_change', 'query.cgi'); + check_token_data($token, 'buglist_mass_change', 'query.cgi'); } ###################################################################### @@ -185,209 +192,217 @@ $vars->{'title_tag'} = "bug_processed"; my $action; if (defined $cgi->param('id')) { - $action = $user->setting('post_bug_submit_action'); - if ($action ne 'nothing' && $action ne 'same_bug' && $action ne 'next_bug') { - ThrowCodeError("invalid_post_bug_submit_action"); + $action = $user->setting('post_bug_submit_action'); + if ($action ne 'nothing' && $action ne 'same_bug' && $action ne 'next_bug') { + ThrowCodeError("invalid_post_bug_submit_action"); + } + + if ($action eq 'next_bug') { + my $bug_list_obj = $user->recent_search_for($first_bug); + my @bug_list = $bug_list_obj ? @{$bug_list_obj->bug_list} : (); + my $cur = firstidx { $_ eq $cgi->param('id') } @bug_list; + if ($cur >= 0 && $cur < $#bug_list) { + my $next_bug_id = $bug_list[$cur + 1]; + detaint_natural($next_bug_id); + if ($next_bug_id and $user->can_see_bug($next_bug_id)) { + + # We create an object here so that $bug->send_changes can use it + # when displaying the header. + $vars->{'bug'} = new Bugzilla::Bug($next_bug_id); + } } + } - if ($action eq 'next_bug') { - my $bug_list_obj = $user->recent_search_for($first_bug); - my @bug_list = $bug_list_obj ? @{$bug_list_obj->bug_list} : (); - my $cur = firstidx { $_ eq $cgi->param('id') } @bug_list; - if ($cur >= 0 && $cur < $#bug_list) { - my $next_bug_id = $bug_list[$cur + 1]; - detaint_natural($next_bug_id); - if ($next_bug_id and $user->can_see_bug($next_bug_id)) { - # We create an object here so that $bug->send_changes can use it - # when displaying the header. - $vars->{'bug'} = new Bugzilla::Bug($next_bug_id); - } - } - } - # Include both action = 'same_bug' and 'nothing'. - else { - $vars->{'bug'} = $first_bug; - $vars->{'bugids'} = $first_bug->id; - } + # Include both action = 'same_bug' and 'nothing'. + else { + $vars->{'bug'} = $first_bug; + $vars->{'bugids'} = $first_bug->id; + } } else { - # param('id') is not defined when changing multiple bugs at once. - $action = 'nothing'; + # param('id') is not defined when changing multiple bugs at once. + $action = 'nothing'; } # For each bug, we have to check if the user can edit the bug the product # is currently in, before we allow them to change anything. foreach my $bug (@bug_objects) { - if (!Bugzilla->user->can_edit_product($bug->product_obj->id) ) { - ThrowUserError("product_edit_denied", - { product => $bug->product }); - } + if (!Bugzilla->user->can_edit_product($bug->product_obj->id)) { + ThrowUserError("product_edit_denied", {product => $bug->product}); + } } # Component, target_milestone, and version are in here just in case # the 'product' field wasn't defined in the CGI. It doesn't hurt to set # them twice. my @set_fields = qw(op_sys rep_platform priority bug_severity - component target_milestone version - bug_file_loc status_whiteboard short_desc - deadline remaining_time estimated_time - work_time set_default_assignee set_default_qa_contact - cclist_accessible reporter_accessible - product confirm_product_change - bug_status resolution dup_id bug_ignored); + component target_milestone version + bug_file_loc status_whiteboard short_desc + deadline remaining_time estimated_time + work_time set_default_assignee set_default_qa_contact + cclist_accessible reporter_accessible + product confirm_product_change + bug_status resolution dup_id bug_ignored); push(@set_fields, 'assigned_to') if !$cgi->param('set_default_assignee'); push(@set_fields, 'qa_contact') if !$cgi->param('set_default_qa_contact'); my %field_translation = ( - bug_severity => 'severity', - rep_platform => 'platform', - short_desc => 'summary', - bug_file_loc => 'url', - set_default_assignee => 'reset_assigned_to', - set_default_qa_contact => 'reset_qa_contact', - confirm_product_change => 'product_change_confirmed', + bug_severity => 'severity', + rep_platform => 'platform', + short_desc => 'summary', + bug_file_loc => 'url', + set_default_assignee => 'reset_assigned_to', + set_default_qa_contact => 'reset_qa_contact', + confirm_product_change => 'product_change_confirmed', ); -my %set_all_fields = ( other_bugs => \@bug_objects ); +my %set_all_fields = (other_bugs => \@bug_objects); foreach my $field_name (@set_fields) { - if (should_set($field_name, 1)) { - my $param_name = $field_translation{$field_name} || $field_name; - $set_all_fields{$param_name} = $cgi->param($field_name); - } + if (should_set($field_name, 1)) { + my $param_name = $field_translation{$field_name} || $field_name; + $set_all_fields{$param_name} = $cgi->param($field_name); + } } if (should_set('keywords')) { - my $action = $cgi->param('keywordaction') || ''; - # Backward-compatibility for Bugzilla 3.x and older. - $action = 'remove' if $action eq 'delete'; - $action = 'set' if $action eq 'makeexact'; - $set_all_fields{keywords}->{$action} = $cgi->param('keywords'); + my $action = $cgi->param('keywordaction') || ''; + + # Backward-compatibility for Bugzilla 3.x and older. + $action = 'remove' if $action eq 'delete'; + $action = 'set' if $action eq 'makeexact'; + $set_all_fields{keywords}->{$action} = $cgi->param('keywords'); } if (should_set('comment')) { - $set_all_fields{comment} = { - body => scalar $cgi->param('comment'), - is_private => scalar $cgi->param('comment_is_private'), - }; + $set_all_fields{comment} = { + body => scalar $cgi->param('comment'), + is_private => scalar $cgi->param('comment_is_private'), + }; } if (should_set('see_also')) { - $set_all_fields{'see_also'}->{add} = - [split(/[\s,]+/, $cgi->param('see_also'))]; + $set_all_fields{'see_also'}->{add} = [split(/[\s,]+/, $cgi->param('see_also'))]; } if (should_set('remove_see_also')) { - $set_all_fields{'see_also'}->{remove} = [$cgi->param('remove_see_also')]; + $set_all_fields{'see_also'}->{remove} = [$cgi->param('remove_see_also')]; } foreach my $dep_field (qw(dependson blocked)) { - if (should_set($dep_field)) { - if (my $dep_action = $cgi->param("${dep_field}_action")) { - $set_all_fields{$dep_field}->{$dep_action} = - [split(/[\s,]+/, $cgi->param($dep_field))]; - } - else { - $set_all_fields{$dep_field}->{set} = $cgi->param($dep_field); - } + if (should_set($dep_field)) { + if (my $dep_action = $cgi->param("${dep_field}_action")) { + $set_all_fields{$dep_field}->{$dep_action} + = [split(/[\s,]+/, $cgi->param($dep_field))]; } + else { + $set_all_fields{$dep_field}->{set} = $cgi->param($dep_field); + } + } } + # Formulate the CC data into two arrays of users involved in this CC change. -if (defined $cgi->param('newcc') - or defined $cgi->param('addselfcc') - or defined $cgi->param('removecc') - or defined $cgi->param('masscc')) +if ( defined $cgi->param('newcc') + or defined $cgi->param('addselfcc') + or defined $cgi->param('removecc') + or defined $cgi->param('masscc')) { - my (@cc_add, @cc_remove); - # If masscc is defined, then we came from buglist and need to either add or - # remove cc's... otherwise, we came from show_bug and may need to do both. - if (defined $cgi->param('masscc')) { - if ($cgi->param('ccaction') eq 'add') { - @cc_add = $cgi->param('masscc'); - } elsif ($cgi->param('ccaction') eq 'remove') { - @cc_remove = $cgi->param('masscc'); - } - } else { - @cc_add = $cgi->param('newcc'); - push(@cc_add, Bugzilla->user) if $cgi->param('addselfcc'); - - # We came from show_bug which uses a select box to determine what cc's - # need to be removed... - if ($cgi->param('removecc') && $cgi->param('cc')) { - @cc_remove = $cgi->param('cc'); - } + my (@cc_add, @cc_remove); + + # If masscc is defined, then we came from buglist and need to either add or + # remove cc's... otherwise, we came from show_bug and may need to do both. + if (defined $cgi->param('masscc')) { + if ($cgi->param('ccaction') eq 'add') { + @cc_add = $cgi->param('masscc'); + } + elsif ($cgi->param('ccaction') eq 'remove') { + @cc_remove = $cgi->param('masscc'); + } + } + else { + @cc_add = $cgi->param('newcc'); + push(@cc_add, Bugzilla->user) if $cgi->param('addselfcc'); + + # We came from show_bug which uses a select box to determine what cc's + # need to be removed... + if ($cgi->param('removecc') && $cgi->param('cc')) { + @cc_remove = $cgi->param('cc'); } + } - $set_all_fields{cc} = { add => \@cc_add, remove => \@cc_remove }; + $set_all_fields{cc} = {add => \@cc_add, remove => \@cc_remove}; } # Fields that can only be set on one bug at a time. if (defined $cgi->param('id')) { - # Since aliases are unique (like bug numbers), they can only be changed - # for one bug at a time. - if (Bugzilla->params->{"usebugaliases"} && defined $cgi->param('alias')) { - $set_all_fields{alias} = $cgi->param('alias'); - } + + # Since aliases are unique (like bug numbers), they can only be changed + # for one bug at a time. + if (Bugzilla->params->{"usebugaliases"} && defined $cgi->param('alias')) { + $set_all_fields{alias} = $cgi->param('alias'); + } } my %is_private; foreach my $field (grep(/^defined_isprivate/, $cgi->param())) { - $field =~ /(\d+)$/; - my $comment_id = $1; - $is_private{$comment_id} = $cgi->param("isprivate_$comment_id"); + $field =~ /(\d+)$/; + my $comment_id = $1; + $is_private{$comment_id} = $cgi->param("isprivate_$comment_id"); } $set_all_fields{comment_is_private} = \%is_private; my @check_groups = $cgi->param('defined_groups'); -my @set_groups = $cgi->param('groups'); +my @set_groups = $cgi->param('groups'); my ($removed_groups) = diff_arrays(\@check_groups, \@set_groups); -$set_all_fields{groups} = { add => \@set_groups, remove => $removed_groups }; +$set_all_fields{groups} = {add => \@set_groups, remove => $removed_groups}; my @custom_fields = Bugzilla->active_custom_fields; foreach my $field (@custom_fields) { - my $fname = $field->name; - if (should_set($fname, 1)) { - $set_all_fields{$fname} = [$cgi->param($fname)]; - } + my $fname = $field->name; + if (should_set($fname, 1)) { + $set_all_fields{$fname} = [$cgi->param($fname)]; + } } # BMO - add user_match_fields. it's important to source from input_params # instead of $cgi->param to ensure we get the correct value. foreach my $field (keys %$user_match_fields) { - next if exists $set_all_fields{$field}; - next unless should_set($field, 1); - $set_all_fields{$field} = Bugzilla->input_params->{$field} // []; + next if exists $set_all_fields{$field}; + next unless should_set($field, 1); + $set_all_fields{$field} = Bugzilla->input_params->{$field} // []; } # We are going to alter the list of removed groups, so we keep a copy here. my @unchecked_groups = @$removed_groups; foreach my $b (@bug_objects) { - # Don't blindly ask to remove unchecked groups available in the UI. - # A group can be already unchecked, and the user didn't try to remove it. - # In this case, we don't want remove_group() to complain. - my @remove_groups; - foreach my $g (@{$b->groups_in}) { - push(@remove_groups, $g->name) if grep { $_ eq $g->name } @unchecked_groups; + + # Don't blindly ask to remove unchecked groups available in the UI. + # A group can be already unchecked, and the user didn't try to remove it. + # In this case, we don't want remove_group() to complain. + my @remove_groups; + foreach my $g (@{$b->groups_in}) { + push(@remove_groups, $g->name) if grep { $_ eq $g->name } @unchecked_groups; + } + local $set_all_fields{groups}->{remove} = \@remove_groups; + my $ok = eval { + $b->set_all(\%set_all_fields); + 1; + }; + unless ($ok) { + my $error = $@; + if (blessed $error && $error->isa('Bugzilla::Error::Template')) { + print $cgi->header(); + $template->process($error->file, $error->vars); + exit; } - local $set_all_fields{groups}->{remove} = \@remove_groups; - my $ok = eval { - $b->set_all(\%set_all_fields); - 1; - }; - unless ($ok) { - my $error = $@; - if (blessed $error && $error->isa('Bugzilla::Error::Template')) { - print $cgi->header(); - $template->process($error->file, $error->vars); - exit; - } - else { - die $error; - } + else { + die $error; } + } } if (defined $cgi->param('id')) { - # Flags should be set AFTER the bug has been moved into another - # product/component. The structure of flags code doesn't currently - # allow them to be set using set_all. - my ($flags, $new_flags) = Bugzilla::Flag->extract_flags_from_cgi( - $first_bug, undef, $vars); - $first_bug->set_flags($flags, $new_flags); + + # Flags should be set AFTER the bug has been moved into another + # product/component. The structure of flags code doesn't currently + # allow them to be set using set_all. + my ($flags, $new_flags) + = Bugzilla::Flag->extract_flags_from_cgi($first_bug, undef, $vars); + $first_bug->set_flags($flags, $new_flags); } ############################## @@ -395,19 +410,19 @@ if (defined $cgi->param('id')) { ############################## my @all_sent_changes; foreach my $bug (@bug_objects) { - my $changes = $bug->update(); - - if ($changes->{'bug_status'}) { - my $new_status = $changes->{'bug_status'}->[1]; - # We may have zeroed the remaining time, if we moved into a closed - # status, so we should inform the user about that. - if (!is_open_state($new_status) && $changes->{'remaining_time'}) { - $vars->{'message'} = "remaining_time_zeroed" - if Bugzilla->user->is_timetracker; - } + my $changes = $bug->update(); + + if ($changes->{'bug_status'}) { + my $new_status = $changes->{'bug_status'}->[1]; + + # We may have zeroed the remaining time, if we moved into a closed + # status, so we should inform the user about that. + if (!is_open_state($new_status) && $changes->{'remaining_time'}) { + $vars->{'message'} = "remaining_time_zeroed" if Bugzilla->user->is_timetracker; } + } - push @all_sent_changes, $bug->send_changes($changes); + push @all_sent_changes, $bug->send_changes($changes); } # Delete the session token used for the mass-change. @@ -415,59 +430,62 @@ delete_token($token) unless $cgi->param('id'); # BMO: add show_bug_format hook for experimental UI work my $format_params = { - format => scalar $cgi->param('format'), - ctype => scalar $cgi->param('ctype'), + format => scalar $cgi->param('format'), + ctype => scalar $cgi->param('ctype'), }; Bugzilla::Hook::process('show_bug_format', $format_params); if ($format_params->{format} eq 'modal') { - my $bug_id = $vars->{bug} ? $vars->{bug}->id : undef; - $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug_id)); + my $bug_id = $vars->{bug} ? $vars->{bug}->id : undef; + $cgi->content_security_policy(Bugzilla::CGI::SHOW_BUG_MODAL_CSP($bug_id)); } -my $format = $template->get_format("bug/show", - $format_params->{format}, - $format_params->{ctype}); +my $format = $template->get_format( + "bug/show", + $format_params->{format}, + $format_params->{ctype} +); if (Bugzilla->usage_mode != USAGE_MODE_EMAIL) { - print $cgi->header(); - - foreach my $sent_changes (@all_sent_changes) { - foreach my $sent_change (@$sent_changes) { - my $params = $sent_change->{params}; - my $sent_bugmail = $sent_change->{sent_bugmail}; - $vars->{$_} = $params->{$_} foreach keys %$params; - $vars->{'sent_bugmail'} = $sent_bugmail; - $template->process("bug/process/results.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - $vars->{'header_done'} = 1; - } + print $cgi->header(); + + foreach my $sent_changes (@all_sent_changes) { + foreach my $sent_change (@$sent_changes) { + my $params = $sent_change->{params}; + my $sent_bugmail = $sent_change->{sent_bugmail}; + $vars->{$_} = $params->{$_} foreach keys %$params; + $vars->{'sent_bugmail'} = $sent_bugmail; + $template->process("bug/process/results.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + $vars->{'header_done'} = 1; } - - if ($action eq 'next_bug' or $action eq 'same_bug') { - my $bug = $vars->{'bug'}; - if ($bug and $user->can_see_bug($bug)) { - if ($action eq 'same_bug') { - # $bug->update() does not update the internal structure of - # the bug sufficiently to display the bug with the new values. - # (That is, if we just passed in the old Bug object, we'd get - # a lot of old values displayed.) - $bug = Bugzilla::Bug->new($bug->id); - $vars->{'bug'} = $bug; - } - $vars->{'bugs'} = [$bug]; - if ($action eq 'next_bug') { - $vars->{'nextbug'} = $bug->id; - } - - $template->process($format->{template}, $vars) - || ThrowTemplateError($template->error()); - exit; - } + } + + if ($action eq 'next_bug' or $action eq 'same_bug') { + my $bug = $vars->{'bug'}; + if ($bug and $user->can_see_bug($bug)) { + if ($action eq 'same_bug') { + + # $bug->update() does not update the internal structure of + # the bug sufficiently to display the bug with the new values. + # (That is, if we just passed in the old Bug object, we'd get + # a lot of old values displayed.) + $bug = Bugzilla::Bug->new($bug->id); + $vars->{'bug'} = $bug; + } + $vars->{'bugs'} = [$bug]; + if ($action eq 'next_bug') { + $vars->{'nextbug'} = $bug->id; + } + + $template->process($format->{template}, $vars) + || ThrowTemplateError($template->error()); + exit; } + } - # End the response page. - $template->process("bug/navigate.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - $template->process("global/footer.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + # End the response page. + $template->process("bug/navigate.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + $template->process("global/footer.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } 1; diff --git a/qa/config/generate_test_data.pl b/qa/config/generate_test_data.pl index 6bd9dd355..47e583471 100644 --- a/qa/config/generate_test_data.pl +++ b/qa/config/generate_test_data.pl @@ -14,12 +14,16 @@ use warnings; use File::Basename; use File::Spec; + BEGIN { - require lib; - my $dir = File::Spec->rel2abs( - File::Spec->catdir(dirname(__FILE__), "..", "..") - ); - lib->import($dir, File::Spec->catdir($dir, "lib"), File::Spec->catdir($dir, qw(local lib perl5))); + require lib; + my $dir + = File::Spec->rel2abs(File::Spec->catdir(dirname(__FILE__), "..", "..")); + lib->import( + $dir, + File::Spec->catdir($dir, "lib"), + File::Spec->catdir($dir, qw(local lib perl5)) + ); } use Cwd; @@ -29,15 +33,14 @@ my $conf_path; my $config; BEGIN { - print "reading the config file...\n"; - my $conf_file = $ENV{BZ_QA_CONF_FILE} // "selenium_test.conf"; - if (@ARGV) { - $conf_file = shift @ARGV; - } - $config = do "$conf_file" - or die "can't read configuration '$conf_file': $!$@"; - - $conf_path = $config->{bugzilla_path}; + print "reading the config file...\n"; + my $conf_file = $ENV{BZ_QA_CONF_FILE} // "selenium_test.conf"; + if (@ARGV) { + $conf_file = shift @ARGV; + } + $config = do "$conf_file" or die "can't read configuration '$conf_file': $!$@"; + + $conf_path = $config->{bugzilla_path}; } use lib $conf_path; @@ -69,40 +72,34 @@ Bugzilla->usage_mode(USAGE_MODE_CMDLINE); # Create missing priorities # BMO uses P1-P5 which is different from upstream -my $field = Bugzilla::Field->new({ name => 'priority' }); +my $field = Bugzilla::Field->new({name => 'priority'}); foreach my $value (qw(Highest High Normal Low Lowest)) { - Bugzilla::Field::Choice->type($field)->create({ - value => $value, - sortkey => 0 - }); + Bugzilla::Field::Choice->type($field)->create({value => $value, sortkey => 0}); } # Add missing platforms -$field = Bugzilla::Field->new({ name => 'rep_platform' }); +$field = Bugzilla::Field->new({name => 'rep_platform'}); foreach my $value (qw(PC)) { - Bugzilla::Field::Choice->type($field)->create({ - value => $value, - sortkey => 0 - }); + Bugzilla::Field::Choice->type($field)->create({value => $value, sortkey => 0}); } my %set_params = ( - usebugaliases => 1, - useqacontact => 1, - mail_delivery_method => 'Test', - maxattachmentsize => 256, - defaultpriority => 'Highest', # BMO CHANGE - timetrackinggroup => 'editbugs', # BMO CHANGE - letsubmitterchoosepriority => 1, # BMO CHANGE - createemailregexp => '.*', # BMO CHANGE + usebugaliases => 1, + useqacontact => 1, + mail_delivery_method => 'Test', + maxattachmentsize => 256, + defaultpriority => 'Highest', # BMO CHANGE + timetrackinggroup => 'editbugs', # BMO CHANGE + letsubmitterchoosepriority => 1, # BMO CHANGE + createemailregexp => '.*', # BMO CHANGE ); my $params_modified; foreach my $param (keys %set_params) { - my $value = $set_params{$param}; - next unless defined $value && Bugzilla->params->{$param} ne $value; - SetParam($param, $value); - $params_modified = 1; + my $value = $set_params{$param}; + next unless defined $value && Bugzilla->params->{$param} ne $value; + SetParam($param, $value); + $params_modified = 1; } write_params() if $params_modified; @@ -117,71 +114,68 @@ write_params() if $params_modified; my %user_prefs = (post_bug_submit_action => 'nothing'); foreach my $pref (keys %user_prefs) { - my $value = $user_prefs{$pref}; - Bugzilla::User::Setting::set_default($pref, $value, 1); + my $value = $user_prefs{$pref}; + Bugzilla::User::Setting::set_default($pref, $value, 1); } ########################################################################## # Create Users ########################################################################## # First of all, remove the default .* regexp for the editbugs group. -my $group = new Bugzilla::Group({ name => 'editbugs' }); +my $group = new Bugzilla::Group({name => 'editbugs'}); $group->set_user_regexp(''); $group->update(); my @usernames = ( - 'admin', 'no-privs', - 'QA-Selenium-TEST', 'canconfirm', - 'tweakparams', 'permanent_user', - 'editbugs', 'disabled', + 'admin', 'no-privs', 'QA-Selenium-TEST', 'canconfirm', + 'tweakparams', 'permanent_user', 'editbugs', 'disabled', ); print "creating user accounts...\n"; foreach my $username (@usernames) { - my $password; - my $login; - my $realname = exists $config->{"$username" . "_user_username"} - ? $config->{"$username" . "_user_username"} - : $username; - - if ($username eq 'permanent_user') { - $password = $config->{admin_user_passwd}; - $login = $config->{$username}; - } - elsif ($username eq 'no-privs') { - $password = $config->{unprivileged_user_passwd}; - $login = $config->{unprivileged_user_login}; - } - elsif ($username eq 'QA-Selenium-TEST') { - $password = $config->{QA_Selenium_TEST_user_passwd}; - $login = $config->{QA_Selenium_TEST_user_login}; - } - else { - $password = $config->{"$username" . "_user_passwd"}; - $login = $config->{"$username" . "_user_login"}; + my $password; + my $login; + my $realname + = exists $config->{"$username" . "_user_username"} + ? $config->{"$username" . "_user_username"} + : $username; + + if ($username eq 'permanent_user') { + $password = $config->{admin_user_passwd}; + $login = $config->{$username}; + } + elsif ($username eq 'no-privs') { + $password = $config->{unprivileged_user_passwd}; + $login = $config->{unprivileged_user_login}; + } + elsif ($username eq 'QA-Selenium-TEST') { + $password = $config->{QA_Selenium_TEST_user_passwd}; + $login = $config->{QA_Selenium_TEST_user_login}; + } + else { + $password = $config->{"$username" . "_user_passwd"}; + $login = $config->{"$username" . "_user_login"}; + } + + if (is_available_username($login)) { + my %extra_args; + if ($username eq 'disabled') { + $extra_args{disabledtext} = '!!This is the text!!'; } - if ( is_available_username($login) ) { - my %extra_args; - if ($username eq 'disabled') { - $extra_args{disabledtext} = '!!This is the text!!'; - } - - Bugzilla::User->create( - { - login_name => $login, - realname => $realname, - cryptpassword => $password, - %extra_args, - } - ); - - if ( $username eq 'admin' or $username eq 'permanent_user' ) { - - Bugzilla::Install::make_admin($login); - } + Bugzilla::User->create({ + login_name => $login, + realname => $realname, + cryptpassword => $password, + %extra_args, + }); + + if ($username eq 'admin' or $username eq 'permanent_user') { + + Bugzilla::Install::make_admin($login); } + } } ########################################################################## @@ -191,115 +185,159 @@ foreach my $username (@usernames) { # We need to add in the upstream statuses in addition to the BMO ones. my @statuses = ( - { - value => undef, - transitions => [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['NEW', 0], - ['ASSIGNED', 0], ['IN_PROGRESS', 0]], - }, - { - value => 'UNCONFIRMED', - sortkey => 100, - isactive => 1, - isopen => 1, - transitions => [['CONFIRMED', 0], ['NEW', 0], ['ASSIGNED', 0], - ['IN_PROGRESS', 0], ['RESOLVED', 0]], - }, - { - value => 'CONFIRMED', - sortkey => 200, - isactive => 1, - isopen => 1, - transitions => [['UNCONFIRMED', 0], ['NEW', 0], ['ASSIGNED', 0], - ['IN_PROGRESS', 0], ['RESOLVED', 0]], - }, - { - value => 'NEW', - sortkey => 300, - isactive => 1, - isopen => 1, - transitions => [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['ASSIGNED', 0], - ['IN_PROGRESS', 0], ['RESOLVED', 0]], - }, - { - value => 'ASSIGNED', - sortkey => 400, - isactive => 1, - isopen => 1, - transitions => [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['NEW', 0], - ['IN_PROGRESS', 0], ['RESOLVED', 0]], - }, - { - value => 'IN_PROGRESS', - sortkey => 500, - isactive => 1, - isopen => 1, - transitions => [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['NEW', 0], - ['ASSIGNED', 0], ['RESOLVED', 0]], - }, - { - value => 'REOPENED', - sortkey => 600, - isactive => 1, - isopen => 1, - transitions => [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['NEW', 0], - ['ASSIGNED', 0], ['IN_PROGRESS', 0], ['RESOLVED', 0]], - }, - { - value => 'RESOLVED', - sortkey => 700, - isactive => 1, - isopen => 0, - transitions => [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['REOPENED', 0], - ['VERIFIED', 0]], - }, - { - value => 'VERIFIED', - sortkey => 800, - isactive => 1, - isopen => 0, - transitions => [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['REOPENED', 0], - ['RESOLVED', 0]], - }, - { - value => 'CLOSED', - sortkey => 900, - isactive => 1, - isopen => 0, - transitions => [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['REOPENED', 0], - ['RESOLVED', 0]], - }, + { + value => undef, + transitions => [ + ['UNCONFIRMED', 0], + ['CONFIRMED', 0], + ['NEW', 0], + ['ASSIGNED', 0], + ['IN_PROGRESS', 0] + ], + }, + { + value => 'UNCONFIRMED', + sortkey => 100, + isactive => 1, + isopen => 1, + transitions => [ + ['CONFIRMED', 0], + ['NEW', 0], + ['ASSIGNED', 0], + ['IN_PROGRESS', 0], + ['RESOLVED', 0] + ], + }, + { + value => 'CONFIRMED', + sortkey => 200, + isactive => 1, + isopen => 1, + transitions => [ + ['UNCONFIRMED', 0], + ['NEW', 0], + ['ASSIGNED', 0], + ['IN_PROGRESS', 0], + ['RESOLVED', 0] + ], + }, + { + value => 'NEW', + sortkey => 300, + isactive => 1, + isopen => 1, + transitions => [ + ['UNCONFIRMED', 0], + ['CONFIRMED', 0], + ['ASSIGNED', 0], + ['IN_PROGRESS', 0], + ['RESOLVED', 0] + ], + }, + { + value => 'ASSIGNED', + sortkey => 400, + isactive => 1, + isopen => 1, + transitions => [ + ['UNCONFIRMED', 0], + ['CONFIRMED', 0], + ['NEW', 0], + ['IN_PROGRESS', 0], + ['RESOLVED', 0] + ], + }, + { + value => 'IN_PROGRESS', + sortkey => 500, + isactive => 1, + isopen => 1, + transitions => [ + ['UNCONFIRMED', 0], + ['CONFIRMED', 0], + ['NEW', 0], + ['ASSIGNED', 0], + ['RESOLVED', 0] + ], + }, + { + value => 'REOPENED', + sortkey => 600, + isactive => 1, + isopen => 1, + transitions => [ + ['UNCONFIRMED', 0], + ['CONFIRMED', 0], + ['NEW', 0], + ['ASSIGNED', 0], + ['IN_PROGRESS', 0], + ['RESOLVED', 0] + ], + }, + { + value => 'RESOLVED', + sortkey => 700, + isactive => 1, + isopen => 0, + transitions => + [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['REOPENED', 0], ['VERIFIED', 0]], + }, + { + value => 'VERIFIED', + sortkey => 800, + isactive => 1, + isopen => 0, + transitions => + [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['REOPENED', 0], ['RESOLVED', 0]], + }, + { + value => 'CLOSED', + sortkey => 900, + isactive => 1, + isopen => 0, + transitions => + [['UNCONFIRMED', 0], ['CONFIRMED', 0], ['REOPENED', 0], ['RESOLVED', 0]], + }, ); -if ($dbh->selectrow_array("SELECT 1 FROM bug_status WHERE value = 'ASSIGNED'")) { - $dbh->do('DELETE FROM bug_status'); - $dbh->do('DELETE FROM status_workflow'); +if ($dbh->selectrow_array("SELECT 1 FROM bug_status WHERE value = 'ASSIGNED'")) +{ + $dbh->do('DELETE FROM bug_status'); + $dbh->do('DELETE FROM status_workflow'); - print "creating status workflow...\n"; + print "creating status workflow...\n"; - # One pass to add the status entries. - foreach my $status (@statuses) { - next if !$status->{value}; - $dbh->do('INSERT INTO bug_status (value, sortkey, isactive, is_open) VALUES (?, ?, ?, ?)', - undef, ( $status->{value}, $status->{sortkey}, $status->{isactive}, $status->{isopen} )); + # One pass to add the status entries. + foreach my $status (@statuses) { + next if !$status->{value}; + $dbh->do( + 'INSERT INTO bug_status (value, sortkey, isactive, is_open) VALUES (?, ?, ?, ?)', + undef, + ($status->{value}, $status->{sortkey}, $status->{isactive}, $status->{isopen}) + ); + } + + # Another pass to add the transitions. + foreach my $status (@statuses) { + my $old_id; + if ($status->{value}) { + my $from_status = new Bugzilla::Status({name => $status->{value}}); + $old_id = $from_status->{id}; + } + else { + $old_id = undef; } - # Another pass to add the transitions. - foreach my $status (@statuses) { - my $old_id; - if ($status->{value}) { - my $from_status = new Bugzilla::Status({ name => $status->{value} }); - $old_id = $from_status->{id}; - } else { - $old_id = undef; - } - - foreach my $transition (@{$status->{transitions}}) { - my $to_status = new Bugzilla::Status({ name => $transition->[0] }); - - $dbh->do('INSERT INTO status_workflow (old_status, new_status, require_comment) VALUES (?, ?, ?)', - undef, ( $old_id, $to_status->{id}, $transition->[1] )); - } + foreach my $transition (@{$status->{transitions}}) { + my $to_status = new Bugzilla::Status({name => $transition->[0]}); + + $dbh->do( + 'INSERT INTO status_workflow (old_status, new_status, require_comment) VALUES (?, ?, ?)', + undef, + ($old_id, $to_status->{id}, $transition->[1]) + ); } + } } ########################################################################## @@ -311,408 +349,456 @@ my $admin_user = Bugzilla::User->check($config->{admin_user_login}); Bugzilla->set_user($admin_user); my %field_values = ( - 'priority' => 'Highest', - 'bug_status' => 'CONFIRMED', - 'version' => 'unspecified', - 'bug_file_loc' => '', - 'comment' => 'please ignore this bug', - 'component' => 'TestComponent', - 'rep_platform' => 'All', - 'short_desc' => 'This is a testing bug only', - 'product' => 'TestProduct', - 'op_sys' => 'Linux', - 'bug_severity' => 'normal', - 'groups' => [], + 'priority' => 'Highest', + 'bug_status' => 'CONFIRMED', + 'version' => 'unspecified', + 'bug_file_loc' => '', + 'comment' => 'please ignore this bug', + 'component' => 'TestComponent', + 'rep_platform' => 'All', + 'short_desc' => 'This is a testing bug only', + 'product' => 'TestProduct', + 'op_sys' => 'Linux', + 'bug_severity' => 'normal', + 'groups' => [], ); print "creating bugs...\n"; -Bugzilla::Bug->create( \%field_values ); +Bugzilla::Bug->create(\%field_values); if (Bugzilla::Bug->new('public_bug')->{error}) { - # The deadline must be set so that this bug can be used to test - # timetracking fields using WebServices. - Bugzilla::Bug->create({ %field_values, alias => 'public_bug', deadline => '2010-01-01' }); + + # The deadline must be set so that this bug can be used to test + # timetracking fields using WebServices. + Bugzilla::Bug->create( + {%field_values, alias => 'public_bug', deadline => '2010-01-01'}); } ########################################################################## # Create Classifications ########################################################################## -my @classifications = ({ name => "Class2_QA", - description => "required by Selenium... DON'T DELETE" }, +my @classifications = ( + {name => "Class2_QA", description => "required by Selenium... DON'T DELETE"}, ); print "creating classifications...\n"; foreach my $class (@classifications) { - my $new_class = Bugzilla::Classification->new({ name => $class->{name} }); - if (!$new_class) { - $dbh->do('INSERT INTO classifications (name, description) VALUES (?, ?)', - undef, ( $class->{name}, $class->{description} )); - } + my $new_class = Bugzilla::Classification->new({name => $class->{name}}); + if (!$new_class) { + $dbh->do('INSERT INTO classifications (name, description) VALUES (?, ?)', + undef, ($class->{name}, $class->{description})); + } } ########################################################################## # Create Products ########################################################################## -my $default_platform_id = $dbh->selectcol_arrayref("SELECT id FROM rep_platform WHERE value = 'Unspecified'"); -my $default_op_sys_id = $dbh->selectcol_arrayref("SELECT id FROM op_sys WHERE value = 'Unspecified'"); +my $default_platform_id = $dbh->selectcol_arrayref( + "SELECT id FROM rep_platform WHERE value = 'Unspecified'"); +my $default_op_sys_id = $dbh->selectcol_arrayref( + "SELECT id FROM op_sys WHERE value = 'Unspecified'"); my @products = ( - { product_name => 'QA-Selenium-TEST', - description => "used by Selenium test.. DON'T DELETE", - versions => ['unspecified', 'QAVersion'], - milestones => ['QAMilestone'], - defaultmilestone => '---', - components => [ - { name => "QA-Selenium-TEST", - description => "used by Selenium test.. DON'T DELETE", - initialowner => $config->{QA_Selenium_TEST_user_login}, - initialqacontact => $config->{QA_Selenium_TEST_user_login}, - initial_cc => [$config->{QA_Selenium_TEST_user_login}], - - } - ], - default_platform_id => $default_platform_id, - default_op_sys_id => $default_op_sys_id, - }, - - { product_name => 'Another Product', - description => - "Alternate product used by Selenium. Do not edit!", - versions => ['unspecified', 'Another1', 'Another2'], - milestones => ['AnotherMS1', 'AnotherMS2', 'Milestone'], - defaultmilestone => '---', - - components => [ - { name => "c1", - description => "c1", - initialowner => $config->{permanent_user}, - initialqacontact => '', - initial_cc => [], - - }, - { name => "c2", - description => "c2", - initialowner => $config->{permanent_user}, - initialqacontact => '', - initial_cc => [], - - }, - ], - default_platform_id => $default_platform_id, - default_op_sys_id => $default_op_sys_id, - }, - - { product_name => 'C2 Forever', - description => 'I must remain in the Class2_QA classification ' . - 'in all cases! Do not edit!', - classification => 'Class2_QA', - versions => ['unspecified', 'C2Ver'], - milestones => ['C2Mil'], - defaultmilestone => '---', - components => [ - { name => "Helium", - description => "Feel free to add bugs to me", - initialowner => $config->{permanent_user}, - initialqacontact => '', - initial_cc => [], - - } - ], - default_platform_id => $default_platform_id, - default_op_sys_id => $default_op_sys_id, - }, - - { product_name => 'QA Entry Only', - description => 'Only the QA group may enter bugs here.', - versions => ['unspecified'], - milestones => [], - defaultmilestone => '---', - components => [ - { name => "c1", - description => "Same name as Another Product's component", - initialowner => $config->{QA_Selenium_TEST_user_login}, - initialqacontact => '', - initial_cc => [], - } - ], - default_platform_id => $default_platform_id, - default_op_sys_id => $default_op_sys_id, - }, - - { product_name => 'QA Search Only', - description => 'Only the QA group may search for bugs here.', - versions => ['unspecified'], - milestones => [], - defaultmilestone => '---', - components => [ - { name => "c1", - description => "Still same name as the Another component", - initialowner => $config->{QA_Selenium_TEST_user_login}, - initialqacontact => '', - initial_cc => [], - } - ], - default_platform_id => $default_platform_id, - default_op_sys_id => $default_op_sys_id, - }, + { + product_name => 'QA-Selenium-TEST', + description => "used by Selenium test.. DON'T DELETE", + versions => ['unspecified', 'QAVersion'], + milestones => ['QAMilestone'], + defaultmilestone => '---', + components => [{ + name => "QA-Selenium-TEST", + description => "used by Selenium test.. DON'T DELETE", + initialowner => $config->{QA_Selenium_TEST_user_login}, + initialqacontact => $config->{QA_Selenium_TEST_user_login}, + initial_cc => [$config->{QA_Selenium_TEST_user_login}], + + }], + default_platform_id => $default_platform_id, + default_op_sys_id => $default_op_sys_id, + }, + + { + product_name => 'Another Product', + description => "Alternate product used by Selenium. Do not edit!", + versions => ['unspecified', 'Another1', 'Another2'], + milestones => ['AnotherMS1', 'AnotherMS2', 'Milestone'], + defaultmilestone => '---', + + components => [ + { + name => "c1", + description => "c1", + initialowner => $config->{permanent_user}, + initialqacontact => '', + initial_cc => [], + + }, + { + name => "c2", + description => "c2", + initialowner => $config->{permanent_user}, + initialqacontact => '', + initial_cc => [], + + }, + ], + default_platform_id => $default_platform_id, + default_op_sys_id => $default_op_sys_id, + }, + + { + product_name => 'C2 Forever', + description => 'I must remain in the Class2_QA classification ' + . 'in all cases! Do not edit!', + classification => 'Class2_QA', + versions => ['unspecified', 'C2Ver'], + milestones => ['C2Mil'], + defaultmilestone => '---', + components => [{ + name => "Helium", + description => "Feel free to add bugs to me", + initialowner => $config->{permanent_user}, + initialqacontact => '', + initial_cc => [], + + }], + default_platform_id => $default_platform_id, + default_op_sys_id => $default_op_sys_id, + }, + + { + product_name => 'QA Entry Only', + description => 'Only the QA group may enter bugs here.', + versions => ['unspecified'], + milestones => [], + defaultmilestone => '---', + components => [{ + name => "c1", + description => "Same name as Another Product's component", + initialowner => $config->{QA_Selenium_TEST_user_login}, + initialqacontact => '', + initial_cc => [], + }], + default_platform_id => $default_platform_id, + default_op_sys_id => $default_op_sys_id, + }, + + { + product_name => 'QA Search Only', + description => 'Only the QA group may search for bugs here.', + versions => ['unspecified'], + milestones => [], + defaultmilestone => '---', + components => [{ + name => "c1", + description => "Still same name as the Another component", + initialowner => $config->{QA_Selenium_TEST_user_login}, + initialqacontact => '', + initial_cc => [], + }], + default_platform_id => $default_platform_id, + default_op_sys_id => $default_op_sys_id, + }, ); print "creating products...\n"; foreach my $product (@products) { - my $new_product = - Bugzilla::Product->new({ name => $product->{product_name} }); - if (!$new_product) { - my $class_id = 1; - if ($product->{classification}) { - $class_id = Bugzilla::Classification->new({ name => $product->{classification} })->id; - } - $dbh->do('INSERT INTO products (name, description, classification_id, default_platform_id, default_op_sys_id) + my $new_product = Bugzilla::Product->new({name => $product->{product_name}}); + if (!$new_product) { + my $class_id = 1; + if ($product->{classification}) { + $class_id + = Bugzilla::Classification->new({name => $product->{classification}})->id; + } + $dbh->do( + 'INSERT INTO products (name, description, classification_id, default_platform_id, default_op_sys_id) VALUES (?, ?, ?, ?, ?)', - undef, ( $product->{product_name}, $product->{description}, $class_id, - $new_product->{default_platform_id}, $new_product->{default_op_sys_id} )); - - $new_product - = new Bugzilla::Product( { name => $product->{product_name} } ); - - $dbh->do( 'INSERT INTO milestones (product_id, value) VALUES (?, ?)', - undef, ( $new_product->id, $product->{defaultmilestone} ) ); - - # Now clear the internal list of accessible products. - delete Bugzilla->user->{selectable_products}; - - foreach my $component ( @{ $product->{components} } ) { - # BMO Change for ComponentWatching extension - my $watch_user = lc($component->{name}) . '@' . lc($new_product->name) . '.bugs'; - $watch_user =~ s/\s+/\-/g; - - Bugzilla::User->create({ - login_name => $watch_user, - cryptpassword => Bugzilla->passwdqc->generate_password(), - disable_mail => 1, - }); - - my %params = %{ Bugzilla->input_params }; - $params{watch_user} = $watch_user; - Bugzilla->input_params(\%params); - - Bugzilla::Component->create( - { name => $component->{name}, - product => $new_product, - description => $component->{description}, - initialowner => $component->{initialowner}, - initialqacontact => $component->{initialqacontact}, - initial_cc => $component->{initial_cc}, - } - ); - } + undef, + ( + $product->{product_name}, $product->{description}, + $class_id, $new_product->{default_platform_id}, + $new_product->{default_op_sys_id} + ) + ); + + $new_product = new Bugzilla::Product({name => $product->{product_name}}); + + $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?, ?)', + undef, ($new_product->id, $product->{defaultmilestone})); + + # Now clear the internal list of accessible products. + delete Bugzilla->user->{selectable_products}; + + foreach my $component (@{$product->{components}}) { + + # BMO Change for ComponentWatching extension + my $watch_user + = lc($component->{name}) . '@' . lc($new_product->name) . '.bugs'; + $watch_user =~ s/\s+/\-/g; + + Bugzilla::User->create({ + login_name => $watch_user, + cryptpassword => Bugzilla->passwdqc->generate_password(), + disable_mail => 1, + }); + + my %params = %{Bugzilla->input_params}; + $params{watch_user} = $watch_user; + Bugzilla->input_params(\%params); + + Bugzilla::Component->create({ + name => $component->{name}, + product => $new_product, + description => $component->{description}, + initialowner => $component->{initialowner}, + initialqacontact => $component->{initialqacontact}, + initial_cc => $component->{initial_cc}, + }); } + } - foreach my $version (@{ $product->{versions} }) { - if (!new Bugzilla::Version({ name => $version, - product => $new_product })) - { - Bugzilla::Version->create({value => $version, product => $new_product}); - } + foreach my $version (@{$product->{versions}}) { + if (!new Bugzilla::Version({name => $version, product => $new_product})) { + Bugzilla::Version->create({value => $version, product => $new_product}); } + } - foreach my $milestone (@{ $product->{milestones} }) { - if (!new Bugzilla::Milestone({ name => $milestone, - product => $new_product })) - { - # We don't use Bugzilla::Milestone->create because we want to - # bypass security checks. - $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?,?)', - undef, $new_product->id, $milestone); - } + foreach my $milestone (@{$product->{milestones}}) { + if (!new Bugzilla::Milestone({name => $milestone, product => $new_product})) { + + # We don't use Bugzilla::Milestone->create because we want to + # bypass security checks. + $dbh->do('INSERT INTO milestones (product_id, value) VALUES (?,?)', + undef, $new_product->id, $milestone); } + } } ########################################################################## # Create Groups ########################################################################## # create Master group -my ( $group_name, $group_desc ) - = ( "Master", "Master Selenium Group DO NOT EDIT!" ); +my ($group_name, $group_desc) + = ("Master", "Master Selenium Group DO NOT EDIT!"); print "creating groups...\n"; -if ( !Bugzilla::Group->new( { name => $group_name } ) ) { - my $group = Bugzilla::Group->create({ name => $group_name, - description => $group_desc, - isbuggroup => 1}); +if (!Bugzilla::Group->new({name => $group_name})) { + my $group + = Bugzilla::Group->create({ + name => $group_name, description => $group_desc, isbuggroup => 1 + }); - $dbh->do('INSERT INTO group_control_map + $dbh->do( + 'INSERT INTO group_control_map (group_id, product_id, entry, membercontrol, othercontrol, canedit) - SELECT ?, products.id, 0, ?, ?, 0 FROM products', - undef, ( $group->id, CONTROLMAPSHOWN, CONTROLMAPSHOWN ) ); + SELECT ?, products.id, 0, ?, ?, 0 FROM products', undef, + ($group->id, CONTROLMAPSHOWN, CONTROLMAPSHOWN) + ); } # create QA-Selenium-TEST group. Do not use Group->create() so that # the admin group doesn't inherit membership (yes, that's what we want!). -( $group_name, $group_desc ) - = ( "QA-Selenium-TEST", "used by Selenium test.. DON'T DELETE" ); - -if ( !Bugzilla::Group->new( { name => $group_name } ) ) { - $dbh->do('INSERT INTO groups (name, description, isbuggroup, isactive) - VALUES (?, ?, 1, 1)', undef, ( $group_name, $group_desc ) ); +($group_name, $group_desc) + = ("QA-Selenium-TEST", "used by Selenium test.. DON'T DELETE"); + +if (!Bugzilla::Group->new({name => $group_name})) { + $dbh->do( + 'INSERT INTO groups (name, description, isbuggroup, isactive) + VALUES (?, ?, 1, 1)', undef, ($group_name, $group_desc) + ); } # BMO 'editbugs' is also a member of 'canconfirm' -my $editbugs = Bugzilla::Group->new({ name => 'editbugs' }); -my $canconfirm = Bugzilla::Group->new({ name => 'canconfirm' }); +my $editbugs = Bugzilla::Group->new({name => 'editbugs'}); +my $canconfirm = Bugzilla::Group->new({name => 'canconfirm'}); $dbh->do('INSERT INTO group_group_map VALUES (?, ?, 0)', - undef, $editbugs->id, $canconfirm->id); + undef, $editbugs->id, $canconfirm->id); # BMO: Update default security group settings for new products -my $default_security_group = Bugzilla::Group->new({ name => 'core-security-release' }); -$default_security_group ||= Bugzilla::Group->new({ name => 'Master' }); +my $default_security_group + = Bugzilla::Group->new({name => 'core-security-release'}); +$default_security_group ||= Bugzilla::Group->new({name => 'Master'}); if ($default_security_group) { - $dbh->do('UPDATE products SET security_group_id = ? WHERE security_group_id IS NULL', - undef, $default_security_group->id); + $dbh->do( + 'UPDATE products SET security_group_id = ? WHERE security_group_id IS NULL', + undef, $default_security_group->id); } ########################################################################## # Add Users to Groups ########################################################################## my @users_groups = ( - { user => $config->{QA_Selenium_TEST_user_login}, group => 'QA-Selenium-TEST' }, - { user => $config->{tweakparams_user_login}, group => 'tweakparams' }, - { user => $config->{canconfirm_user_login}, group => 'canconfirm' }, - { user => $config->{editbugs_user_login}, group => 'editbugs' }, + {user => $config->{QA_Selenium_TEST_user_login}, group => 'QA-Selenium-TEST'}, + {user => $config->{tweakparams_user_login}, group => 'tweakparams'}, + {user => $config->{canconfirm_user_login}, group => 'canconfirm'}, + {user => $config->{editbugs_user_login}, group => 'editbugs'}, ); print "adding users to groups...\n"; foreach my $user_group (@users_groups) { - my $group = new Bugzilla::Group( { name => $user_group->{group} } ); - my $user = new Bugzilla::User( { name => $user_group->{user} } ); + my $group = new Bugzilla::Group({name => $user_group->{group}}); + my $user = new Bugzilla::User({name => $user_group->{user}}); - my $sth_add_mapping = $dbh->prepare( - qq{INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, ?, ?)}); - # Don't crash if the entry already exists. - eval { - $sth_add_mapping->execute( $user->id, $group->id, 0, GRANT_DIRECT ); - }; + my $sth_add_mapping = $dbh->prepare( + qq{INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)} + ); + + # Don't crash if the entry already exists. + eval { $sth_add_mapping->execute($user->id, $group->id, 0, GRANT_DIRECT); }; } ########################################################################## # Associate Products with groups ########################################################################## # Associate the QA-Selenium-TEST group with the QA-Selenium-TEST. -my $created_group = new Bugzilla::Group( { name => 'QA-Selenium-TEST' } ); -my $secret_product = new Bugzilla::Product( { name => 'QA-Selenium-TEST' } ); -my $no_entry = new Bugzilla::Product({ name => 'QA Entry Only' }); -my $no_search = new Bugzilla::Product({ name => 'QA Search Only' }); +my $created_group = new Bugzilla::Group({name => 'QA-Selenium-TEST'}); +my $secret_product = new Bugzilla::Product({name => 'QA-Selenium-TEST'}); +my $no_entry = new Bugzilla::Product({name => 'QA Entry Only'}); +my $no_search = new Bugzilla::Product({name => 'QA Search Only'}); print "restricting products to groups...\n"; + # Don't crash if the entries already exist. -my $sth = $dbh->prepare('INSERT INTO group_control_map +my $sth = $dbh->prepare( + 'INSERT INTO group_control_map (group_id, product_id, entry, membercontrol, othercontrol, canedit) - VALUES (?, ?, ?, ?, ?, ?)'); + VALUES (?, ?, ?, ?, ?, ?)' +); eval { - $sth->execute($created_group->id, $secret_product->id, 1, CONTROLMAPMANDATORY, - CONTROLMAPMANDATORY, 0); + $sth->execute($created_group->id, $secret_product->id, 1, CONTROLMAPMANDATORY, + CONTROLMAPMANDATORY, 0); }; eval { - $sth->execute($created_group->id, $no_entry->id, 1, CONTROLMAPNA, CONTROLMAPNA, 0); + $sth->execute($created_group->id, $no_entry->id, 1, CONTROLMAPNA, CONTROLMAPNA, + 0); }; eval { - $sth->execute($created_group->id, $no_search->id, 0, CONTROLMAPMANDATORY, - CONTROLMAPMANDATORY, 0); + $sth->execute($created_group->id, $no_search->id, 0, CONTROLMAPMANDATORY, + CONTROLMAPMANDATORY, 0); }; ########################################################################## # Create flag types ########################################################################## my @flagtypes = ( - {name => 'spec_multi_flag', desc => 'Specifically requestable and multiplicable bug flag', - is_requestable => 1, is_requesteeble => 1, is_multiplicable => 1, grant_group => 'editbugs', - target_type => 'b', cc_list => '', inclusions => ['Another Product:c1']}, + { + name => 'spec_multi_flag', + desc => 'Specifically requestable and multiplicable bug flag', + is_requestable => 1, + is_requesteeble => 1, + is_multiplicable => 1, + grant_group => 'editbugs', + target_type => 'b', + cc_list => '', + inclusions => ['Another Product:c1'] + }, ); print "creating flag types...\n"; foreach my $flag (@flagtypes) { - # The name is not unique, even within a single product/component, so there is NO WAY - # to know if the existing flag type is the one we want or not. - # As our Selenium scripts would be confused anyway if there is already such a flag name, - # we simply skip it and assume the existing flag type is the one we want. - next if new Bugzilla::FlagType({ name => $flag->{name} }); - - my $grant_group_id = $flag->{grant_group} ? Bugzilla::Group->new({ name => $flag->{grant_group} })->id : undef; - my $request_group_id = $flag->{request_group} ? Bugzilla::Group->new({ name => $flag->{request_group} })->id : undef; - $dbh->do('INSERT INTO flagtypes (name, description, cc_list, target_type, is_requestable, +# The name is not unique, even within a single product/component, so there is NO WAY +# to know if the existing flag type is the one we want or not. +# As our Selenium scripts would be confused anyway if there is already such a flag name, +# we simply skip it and assume the existing flag type is the one we want. + next if new Bugzilla::FlagType({name => $flag->{name}}); + + my $grant_group_id + = $flag->{grant_group} + ? Bugzilla::Group->new({name => $flag->{grant_group}})->id + : undef; + my $request_group_id + = $flag->{request_group} + ? Bugzilla::Group->new({name => $flag->{request_group}})->id + : undef; + + $dbh->do( + 'INSERT INTO flagtypes (name, description, cc_list, target_type, is_requestable, is_requesteeble, is_multiplicable, grant_group_id, request_group_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)', - undef, ($flag->{name}, $flag->{desc}, $flag->{cc_list}, $flag->{target_type}, - $flag->{is_requestable}, $flag->{is_requesteeble}, $flag->{is_multiplicable}, - $grant_group_id, $request_group_id)); - - my $type_id = $dbh->bz_last_key('flagtypes', 'id'); - - foreach my $inclusion (@{$flag->{inclusions}}) { - my ($product, $component) = split(':', $inclusion); - my ($prod_id, $comp_id); - if ($product) { - my $prod_obj = Bugzilla::Product->new({ name => $product }); - $prod_id = $prod_obj->id; - if ($component) { - $comp_id = Bugzilla::Component->new({ name => $component, product => $prod_obj})->id; - } - } - $dbh->do('INSERT INTO flaginclusions (type_id, product_id, component_id) - VALUES (?, ?, ?)', - undef, ($type_id, $prod_id, $comp_id)); + undef, + ( + $flag->{name}, $flag->{desc}, + $flag->{cc_list}, $flag->{target_type}, + $flag->{is_requestable}, $flag->{is_requesteeble}, + $flag->{is_multiplicable}, $grant_group_id, + $request_group_id + ) + ); + + my $type_id = $dbh->bz_last_key('flagtypes', 'id'); + + foreach my $inclusion (@{$flag->{inclusions}}) { + my ($product, $component) = split(':', $inclusion); + my ($prod_id, $comp_id); + if ($product) { + my $prod_obj = Bugzilla::Product->new({name => $product}); + $prod_id = $prod_obj->id; + if ($component) { + $comp_id + = Bugzilla::Component->new({name => $component, product => $prod_obj})->id; + } } + $dbh->do( + 'INSERT INTO flaginclusions (type_id, product_id, component_id) + VALUES (?, ?, ?)', undef, ($type_id, $prod_id, $comp_id) + ); + } } ########################################################################## # Create custom fields ########################################################################## my @fields = ( - { name => 'cf_QA_status', - description => 'QA Status', - type => FIELD_TYPE_MULTI_SELECT, - sortkey => 100, - mailhead => 0, - enter_bug => 1, - obsolete => 0, - custom => 1, - values => ['verified', 'in progress', 'untested'] - }, - { name => 'cf_single_select', - description => 'SingSel', - type => FIELD_TYPE_SINGLE_SELECT, - sortkey => 200, - mailhead => 0, - enter_bug => 1, - custom => 1, - obsolete => 0, - values => [qw(one two three)], - }, + { + name => 'cf_QA_status', + description => 'QA Status', + type => FIELD_TYPE_MULTI_SELECT, + sortkey => 100, + mailhead => 0, + enter_bug => 1, + obsolete => 0, + custom => 1, + values => ['verified', 'in progress', 'untested'] + }, + { + name => 'cf_single_select', + description => 'SingSel', + type => FIELD_TYPE_SINGLE_SELECT, + sortkey => 200, + mailhead => 0, + enter_bug => 1, + custom => 1, + obsolete => 0, + values => [qw(one two three)], + }, ); print "creating custom fields...\n"; foreach my $f (@fields) { - # Skip existing custom fields. - next if Bugzilla::Field->new({ name => $f->{name} }); - - my @values; - if (exists $f->{values}) { - @values = @{$f->{values}}; - # We have to delete this key, else create() will complain - # that 'values' is not an existing column name. - delete $f->{values}; - } - Bugzilla::Field->create($f); - my $field = Bugzilla::Field->new({ name => $f->{name} }); - # Now populate the table with valid values, if necessary. - next unless scalar @values; + # Skip existing custom fields. + next if Bugzilla::Field->new({name => $f->{name}}); - my $sth = $dbh->prepare('INSERT INTO ' . $field->name . ' (value) VALUES (?)'); - foreach my $value (@values) { - $sth->execute($value); - } + my @values; + if (exists $f->{values}) { + @values = @{$f->{values}}; + + # We have to delete this key, else create() will complain + # that 'values' is not an existing column name. + delete $f->{values}; + } + Bugzilla::Field->create($f); + my $field = Bugzilla::Field->new({name => $f->{name}}); + + # Now populate the table with valid values, if necessary. + next unless scalar @values; + + my $sth = $dbh->prepare('INSERT INTO ' . $field->name . ' (value) VALUES (?)'); + foreach my $value (@values) { + $sth->execute($value); + } } #################################################################### @@ -720,13 +806,13 @@ foreach my $f (@fields) { #################################################################### if (Bugzilla->params->{insidergroup} ne 'QA-Selenium-TEST') { - SetParam('insidergroup', 'QA-Selenium-TEST'); - $params_modified = 1; + SetParam('insidergroup', 'QA-Selenium-TEST'); + $params_modified = 1; } if ($params_modified) { - write_params(); - print <set_user($test_user); print "Creating private bug(s)...\n"; if (Bugzilla::Bug->new('private_bug')->{error}) { - my %priv_values = %field_values; - $priv_values{alias} = 'private_bug'; - $priv_values{product} = 'QA-Selenium-TEST'; - $priv_values{component} = 'QA-Selenium-TEST'; - my $bug = Bugzilla::Bug->create(\%priv_values); + my %priv_values = %field_values; + $priv_values{alias} = 'private_bug'; + $priv_values{product} = 'QA-Selenium-TEST'; + $priv_values{component} = 'QA-Selenium-TEST'; + my $bug = Bugzilla::Bug->create(\%priv_values); } ###################### @@ -755,13 +841,15 @@ if (Bugzilla::Bug->new('private_bug')->{error}) { # BMO FIXME: Users must be in 'editbugs' to set their own # content type other than text/plain or application/octet-stream -$group = new Bugzilla::Group( { name => 'editbugs' } ); +$group = new Bugzilla::Group({name => 'editbugs'}); my $sth_add_mapping = $dbh->prepare( - qq{INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) - VALUES (?, ?, ?, ?)}); + qq{INSERT INTO user_group_map (user_id, group_id, isbless, grant_type) + VALUES (?, ?, ?, ?)} +); + # Don't crash if the entry already exists. eval { - $sth_add_mapping->execute( Bugzilla->user->id, $group->id, 0, GRANT_DIRECT ); + $sth_add_mapping->execute(Bugzilla->user->id, $group->id, 0, GRANT_DIRECT); }; print "creating attachments...\n"; @@ -775,26 +863,28 @@ my $attachment_contents; close($attachment_fh); foreach my $alias (qw(public_bug private_bug)) { - my $bug = Bugzilla::Bug->new($alias); - foreach my $is_private (0, 1) { - Bugzilla::Attachment->create({ - bug => $bug, - data => $attachment_contents, - description => "${alias}_${is_private}", - filename => "${alias}_${is_private}.pl", - mimetype => 'application/x-perl', - isprivate => $is_private, - }); - } + my $bug = Bugzilla::Bug->new($alias); + foreach my $is_private (0, 1) { + Bugzilla::Attachment->create({ + bug => $bug, + data => $attachment_contents, + description => "${alias}_${is_private}", + filename => "${alias}_${is_private}.pl", + mimetype => 'application/x-perl', + isprivate => $is_private, + }); + } } # BMO FIXME: Remove test user from 'editbugs' group my $sth_remove_mapping = $dbh->prepare( - qq{DELETE FROM user_group_map WHERE user_id = ? - AND group_id = ? AND isbless = 0 AND grant_type = ?}); + qq{DELETE FROM user_group_map WHERE user_id = ? + AND group_id = ? AND isbless = 0 AND grant_type = ?} +); + # Don't crash if the entry already exists. eval { - $sth_remove_mapping->execute( Bugzilla->user->id, $group->id, GRANT_DIRECT ); + $sth_remove_mapping->execute(Bugzilla->user->id, $group->id, GRANT_DIRECT); }; ################### @@ -802,16 +892,20 @@ eval { ################### my @keywords = ( - { name => 'test-keyword-1', - description => 'Created for Bugzilla QA Tests, Keyword 1' }, - { name => 'test-keyword-2', - description => 'Created for Bugzilla QA Tests, Keyword 2' }, + { + name => 'test-keyword-1', + description => 'Created for Bugzilla QA Tests, Keyword 1' + }, + { + name => 'test-keyword-2', + description => 'Created for Bugzilla QA Tests, Keyword 2' + }, ); print "creating keywords...\n"; foreach my $kw (@keywords) { - next if new Bugzilla::Keyword({ name => $kw->{name} }); - Bugzilla::Keyword->create($kw); + next if new Bugzilla::Keyword({name => $kw->{name}}); + Bugzilla::Keyword->create($kw); } ############################ diff --git a/qa/extensions/QA/Config.pm b/qa/extensions/QA/Config.pm index 59799ec6b..627753855 100644 --- a/qa/extensions/QA/Config.pm +++ b/qa/extensions/QA/Config.pm @@ -11,10 +11,8 @@ use strict; use constant NAME => 'QA'; -use constant REQUIRED_MODULES => [ -]; +use constant REQUIRED_MODULES => []; -use constant OPTIONAL_MODULES => [ -]; +use constant OPTIONAL_MODULES => []; __PACKAGE__->NAME; diff --git a/qa/extensions/QA/Extension.pm b/qa/extensions/QA/Extension.pm index b5f404d74..764bbb03b 100644 --- a/qa/extensions/QA/Extension.pm +++ b/qa/extensions/QA/Extension.pm @@ -20,52 +20,52 @@ use Bugzilla::User; our $VERSION = '1.0'; sub page_before_template { - my ($self, $args) = @_; - return if $args->{page_id} ne 'qa/email_in.html'; + my ($self, $args) = @_; + return if $args->{page_id} ne 'qa/email_in.html'; - my $template = Bugzilla->template; - my $cgi = Bugzilla->cgi; - print $cgi->header; + my $template = Bugzilla->template; + my $cgi = Bugzilla->cgi; + print $cgi->header; - # Needed to make sure he can access and edit bugs. - my $user = Bugzilla::User->check($cgi->param('sender')); - Bugzilla->set_user($user); + # Needed to make sure he can access and edit bugs. + my $user = Bugzilla::User->check($cgi->param('sender')); + Bugzilla->set_user($user); - my ($output, $tmpl_file); - my $action = $cgi->param('action') || ''; - my $vars = { sender => $user, action => $action, pid => $$ }; + my ($output, $tmpl_file); + my $action = $cgi->param('action') || ''; + my $vars = {sender => $user, action => $action, pid => $$}; - if ($action eq 'create') { - $tmpl_file = 'qa/create_bug.txt.tmpl'; - } - elsif ($action eq 'create_with_headers') { - $tmpl_file = 'qa/create_bug_with_headers.txt.tmpl'; - } - elsif ($action =~ /^update(_with_headers)?$/) { - my $f = $1 || ''; - $tmpl_file = "qa/update_bug$f.txt.tmpl"; - my $bug = Bugzilla::Bug->check($cgi->param('bug_id')); - $vars->{bug_id} = $bug->id; - } - else { - ThrowUserError('unknown_action', { action => $action }); - } + if ($action eq 'create') { + $tmpl_file = 'qa/create_bug.txt.tmpl'; + } + elsif ($action eq 'create_with_headers') { + $tmpl_file = 'qa/create_bug_with_headers.txt.tmpl'; + } + elsif ($action =~ /^update(_with_headers)?$/) { + my $f = $1 || ''; + $tmpl_file = "qa/update_bug$f.txt.tmpl"; + my $bug = Bugzilla::Bug->check($cgi->param('bug_id')); + $vars->{bug_id} = $bug->id; + } + else { + ThrowUserError('unknown_action', {action => $action}); + } - $template->process($tmpl_file, $vars, \$output) - or ThrowTemplateError($template->error()); + $template->process($tmpl_file, $vars, \$output) + or ThrowTemplateError($template->error()); - my $file = "/tmp/email_in_$$.txt"; - open(FH, '>', $file); - print FH $output; - close FH; + my $file = "/tmp/email_in_$$.txt"; + open(FH, '>', $file); + print FH $output; + close FH; - $output = `email_in.pl -v < $file 2>&1`; - unlink $file; + $output = `email_in.pl -v < $file 2>&1`; + unlink $file; - parse_output($output, $vars); + parse_output($output, $vars); - $template->process('qa/results.html.tmpl', $vars) - or ThrowTemplateError($template->error()); + $template->process('qa/results.html.tmpl', $vars) + or ThrowTemplateError($template->error()); } __PACKAGE__->NAME; diff --git a/qa/extensions/QA/lib/Util.pm b/qa/extensions/QA/lib/Util.pm index 9bc2d8dbb..9b9026343 100644 --- a/qa/extensions/QA/lib/Util.pm +++ b/qa/extensions/QA/lib/Util.pm @@ -11,15 +11,15 @@ use strict; use base qw(Exporter); our @EXPORT = qw( - parse_output + parse_output ); sub parse_output { - my ($output, $vars) = @_; + my ($output, $vars) = @_; - $vars->{error} = ($output =~ /software error/i) ? 1 : 0; - $vars->{output} = $output; - $vars->{bug_id} ||= ($output =~ /Created bug (\d+)/i) ? $1 : undef; + $vars->{error} = ($output =~ /software error/i) ? 1 : 0; + $vars->{output} = $output; + $vars->{bug_id} ||= ($output =~ /Created bug (\d+)/i) ? $1 : undef; } 1; diff --git a/qa/t/archived/test_email_preferences.t b/qa/t/archived/test_email_preferences.t index cc314ea2c..fe51ee140 100644 --- a/qa/t/archived/test_email_preferences.t +++ b/qa/t/archived/test_email_preferences.t @@ -59,76 +59,86 @@ $sel->click_ok("link=Email Preferences"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Email Preferences"); $sel->click_ok("//input[\@value='Disable All Bugmail']"); -$sel->click_ok("email-0-1", undef, 'Set "I\'m added to or removed from this capacity" for Assignee role'); -$sel->click_ok("email-0-5", undef, 'Set "The priority, status, severity, or milestone changes" for Assignee role'); -$sel->click_ok("email-0-2", undef, 'Set "New comments are added" for Assignee role'); -$sel->click_ok("email-0-0", undef, 'Set "Any field not mentioned above changes" for Assignee role'); +$sel->click_ok("email-0-1", undef, + 'Set "I\'m added to or removed from this capacity" for Assignee role'); +$sel->click_ok("email-0-5", undef, + 'Set "The priority, status, severity, or milestone changes" for Assignee role'); +$sel->click_ok("email-0-2", undef, + 'Set "New comments are added" for Assignee role'); +$sel->click_ok("email-0-0", undef, + 'Set "Any field not mentioned above changes" for Assignee role'); $sel->click_ok("email-3-8", undef, 'Set "The CC field changes" for CCed role'); -$sel->click_ok("email-1-10", undef, 'Set "A new bug is created" for QA Contact role'); -$sel->click_ok("email-100-101", undef, 'Set "Email me when someone sets a flag I asked for" global option'); +$sel->click_ok("email-1-10", undef, + 'Set "A new bug is created" for QA Contact role'); +$sel->click_ok("email-100-101", undef, + 'Set "Email me when someone sets a flag I asked for" global option'); + # Restore the old 4.2 behavior for 'Disable All Mail'. -foreach my $col (0..3) { - foreach my $row (50..51) { - $sel->click_ok("neg-email-$col-$row"); - } +foreach my $col (0 .. 3) { + foreach my $row (50 .. 51) { + $sel->click_ok("neg-email-$col-$row"); + } } -$sel->value_is("email-0-1", "on"); -$sel->value_is("email-0-10", "off"); -$sel->value_is("email-0-6", "off"); -$sel->value_is("email-0-5", "on"); -$sel->value_is("email-0-2", "on"); -$sel->value_is("email-0-3", "off"); -$sel->value_is("email-0-4", "off"); -$sel->value_is("email-0-7", "off"); -$sel->value_is("email-0-8", "off"); -$sel->value_is("email-0-9", "off"); -$sel->value_is("email-0-0", "on"); +$sel->value_is("email-0-1", "on"); +$sel->value_is("email-0-10", "off"); +$sel->value_is("email-0-6", "off"); +$sel->value_is("email-0-5", "on"); +$sel->value_is("email-0-2", "on"); +$sel->value_is("email-0-3", "off"); +$sel->value_is("email-0-4", "off"); +$sel->value_is("email-0-7", "off"); +$sel->value_is("email-0-8", "off"); +$sel->value_is("email-0-9", "off"); +$sel->value_is("email-0-0", "on"); $sel->value_is("neg-email-0-50", "off"); $sel->value_is("neg-email-0-51", "off"); -$sel->value_is("email-1-1", "off"); -$sel->value_is("email-1-10", "on"); -$sel->value_is("email-1-6", "off"); -$sel->value_is("email-1-5", "off"); -$sel->value_is("email-1-2", "off"); -$sel->value_is("email-1-3", "off"); -$sel->value_is("email-1-4", "off"); -$sel->value_is("email-1-7", "off"); -$sel->value_is("email-1-8", "off"); -$sel->value_is("email-1-9", "off"); -$sel->value_is("email-1-0", "off"); +$sel->value_is("email-1-1", "off"); +$sel->value_is("email-1-10", "on"); +$sel->value_is("email-1-6", "off"); +$sel->value_is("email-1-5", "off"); +$sel->value_is("email-1-2", "off"); +$sel->value_is("email-1-3", "off"); +$sel->value_is("email-1-4", "off"); +$sel->value_is("email-1-7", "off"); +$sel->value_is("email-1-8", "off"); +$sel->value_is("email-1-9", "off"); +$sel->value_is("email-1-0", "off"); $sel->value_is("neg-email-1-50", "off"); $sel->value_is("neg-email-1-51", "off"); -ok(!$sel->is_editable("email-2-1"), 'The "I\'m added to or removed from this capacity" for Reporter role is disabled'); -$sel->value_is("email-2-10", "off"); -$sel->value_is("email-2-6", "off"); -$sel->value_is("email-2-5", "off"); -$sel->value_is("email-2-2", "off"); -$sel->value_is("email-2-3", "off"); -$sel->value_is("email-2-4", "off"); -$sel->value_is("email-2-7", "off"); -$sel->value_is("email-2-8", "off"); -$sel->value_is("email-2-9", "off"); -$sel->value_is("email-2-0", "off"); +ok(!$sel->is_editable("email-2-1"), + 'The "I\'m added to or removed from this capacity" for Reporter role is disabled' +); +$sel->value_is("email-2-10", "off"); +$sel->value_is("email-2-6", "off"); +$sel->value_is("email-2-5", "off"); +$sel->value_is("email-2-2", "off"); +$sel->value_is("email-2-3", "off"); +$sel->value_is("email-2-4", "off"); +$sel->value_is("email-2-7", "off"); +$sel->value_is("email-2-8", "off"); +$sel->value_is("email-2-9", "off"); +$sel->value_is("email-2-0", "off"); $sel->value_is("neg-email-2-50", "off"); $sel->value_is("neg-email-2-51", "off"); -$sel->value_is("email-3-1", "off"); -$sel->value_is("email-3-10", "off"); -$sel->value_is("email-3-6", "off"); -$sel->value_is("email-3-5", "off"); -$sel->value_is("email-3-2", "off"); -$sel->value_is("email-3-3", "off"); -$sel->value_is("email-3-4", "off"); -$sel->value_is("email-3-7", "off"); -$sel->value_is("email-3-8", "on"); -$sel->value_is("email-3-9", "off"); -$sel->value_is("email-3-0", "off"); +$sel->value_is("email-3-1", "off"); +$sel->value_is("email-3-10", "off"); +$sel->value_is("email-3-6", "off"); +$sel->value_is("email-3-5", "off"); +$sel->value_is("email-3-2", "off"); +$sel->value_is("email-3-3", "off"); +$sel->value_is("email-3-4", "off"); +$sel->value_is("email-3-7", "off"); +$sel->value_is("email-3-8", "on"); +$sel->value_is("email-3-9", "off"); +$sel->value_is("email-3-0", "off"); $sel->value_is("neg-email-3-50", "off"); $sel->value_is("neg-email-3-51", "off"); -$sel->value_is("email-100-100", "off"); -$sel->value_is("email-100-101", "on"); +$sel->value_is("email-100-100", "off"); +$sel->value_is("email-100-101", "on"); $sel->click_ok("update", undef, "Submit modified admin email preferences"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->is_text_present_ok("The changes to your email preferences have been saved."); +$sel->is_text_present_ok( + "The changes to your email preferences have been saved."); # Set "After changing a bug" default preference to "Show the updated bug" # This simplifies bug changes below @@ -148,95 +158,120 @@ log_in($sel, $config, 'editbugs'); $sel->open_ok("$config->{bugzilla_installation}/userprefs.cgi?tab=email"); $sel->is_text_present_ok("Email Preferences"); $sel->click_ok("//input[\@value='Enable All Bugmail']"); -$sel->click_ok("email-3-1", undef, 'Clear "I\'m added to or removed from this capacity" for CCed role'); -$sel->click_ok("email-3-5", undef, 'Clear "The priority, status, severity, or milestone changes" for CCed role'); -$sel->click_ok("email-2-2", undef, 'Clear "New comments are added" for Reporter role'); -$sel->click_ok("email-3-2", undef, 'Clear "New comments are added" for CCed role'); -$sel->click_ok("email-2-8", undef, 'Clear "The CC field changes" for Reporter role'); -$sel->click_ok("email-3-8", undef, 'Clear "The CC field changes" for CCed role'); -$sel->click_ok("email-2-0", undef, 'Clear "Any field not mentioned above changes" for Reporter role'); -$sel->click_ok("email-3-0", undef, 'Clear "Any field not mentioned above changes" for CCed role'); -$sel->click_ok("neg-email-0-51", undef, 'Set "Change was made by me" override for Assignee role'); -$sel->click_ok("email-100-101", undef, 'Clear "Email me when someone sets a flag I asked for" global option'); -$sel->value_is("email-0-1", "on"); -$sel->value_is("email-0-10", "on"); -$sel->value_is("email-0-6", "on"); -$sel->value_is("email-0-5", "on"); -$sel->value_is("email-0-2", "on"); -$sel->value_is("email-0-3", "on"); -$sel->value_is("email-0-4", "on"); -$sel->value_is("email-0-7", "on"); -$sel->value_is("email-0-8", "on"); -$sel->value_is("email-0-9", "on"); -$sel->value_is("email-0-0", "on"); +$sel->click_ok("email-3-1", undef, + 'Clear "I\'m added to or removed from this capacity" for CCed role'); +$sel->click_ok("email-3-5", undef, + 'Clear "The priority, status, severity, or milestone changes" for CCed role'); +$sel->click_ok("email-2-2", undef, + 'Clear "New comments are added" for Reporter role'); +$sel->click_ok("email-3-2", undef, + 'Clear "New comments are added" for CCed role'); +$sel->click_ok("email-2-8", undef, + 'Clear "The CC field changes" for Reporter role'); +$sel->click_ok("email-3-8", undef, + 'Clear "The CC field changes" for CCed role'); +$sel->click_ok("email-2-0", undef, + 'Clear "Any field not mentioned above changes" for Reporter role'); +$sel->click_ok("email-3-0", undef, + 'Clear "Any field not mentioned above changes" for CCed role'); +$sel->click_ok("neg-email-0-51", undef, + 'Set "Change was made by me" override for Assignee role'); +$sel->click_ok("email-100-101", undef, + 'Clear "Email me when someone sets a flag I asked for" global option'); +$sel->value_is("email-0-1", "on"); +$sel->value_is("email-0-10", "on"); +$sel->value_is("email-0-6", "on"); +$sel->value_is("email-0-5", "on"); +$sel->value_is("email-0-2", "on"); +$sel->value_is("email-0-3", "on"); +$sel->value_is("email-0-4", "on"); +$sel->value_is("email-0-7", "on"); +$sel->value_is("email-0-8", "on"); +$sel->value_is("email-0-9", "on"); +$sel->value_is("email-0-0", "on"); $sel->value_is("neg-email-0-50", "off"); $sel->value_is("neg-email-0-51", "on"); -$sel->value_is("email-1-1", "on"); -$sel->value_is("email-1-10", "on"); -$sel->value_is("email-1-6", "on"); -$sel->value_is("email-1-5", "on"); -$sel->value_is("email-1-2", "on"); -$sel->value_is("email-1-3", "on"); -$sel->value_is("email-1-4", "on"); -$sel->value_is("email-1-7", "on"); -$sel->value_is("email-1-8", "on"); -$sel->value_is("email-1-9", "on"); -$sel->value_is("email-1-0", "on"); +$sel->value_is("email-1-1", "on"); +$sel->value_is("email-1-10", "on"); +$sel->value_is("email-1-6", "on"); +$sel->value_is("email-1-5", "on"); +$sel->value_is("email-1-2", "on"); +$sel->value_is("email-1-3", "on"); +$sel->value_is("email-1-4", "on"); +$sel->value_is("email-1-7", "on"); +$sel->value_is("email-1-8", "on"); +$sel->value_is("email-1-9", "on"); +$sel->value_is("email-1-0", "on"); $sel->value_is("neg-email-1-50", "off"); $sel->value_is("neg-email-1-51", "off"); -ok(!$sel->is_editable("email-2-1"), 'The "I\'m added to or removed from this capacity" for Reporter role is disabled'); -$sel->value_is("email-2-10", "on"); -$sel->value_is("email-2-6", "on"); -$sel->value_is("email-2-5", "on"); -$sel->value_is("email-2-2", "off"); -$sel->value_is("email-2-3", "on"); -$sel->value_is("email-2-4", "on"); -$sel->value_is("email-2-7", "on"); -$sel->value_is("email-2-8", "off"); -$sel->value_is("email-2-9", "on"); -$sel->value_is("email-2-0", "off"); +ok(!$sel->is_editable("email-2-1"), + 'The "I\'m added to or removed from this capacity" for Reporter role is disabled' +); +$sel->value_is("email-2-10", "on"); +$sel->value_is("email-2-6", "on"); +$sel->value_is("email-2-5", "on"); +$sel->value_is("email-2-2", "off"); +$sel->value_is("email-2-3", "on"); +$sel->value_is("email-2-4", "on"); +$sel->value_is("email-2-7", "on"); +$sel->value_is("email-2-8", "off"); +$sel->value_is("email-2-9", "on"); +$sel->value_is("email-2-0", "off"); $sel->value_is("neg-email-2-50", "off"); $sel->value_is("neg-email-2-51", "off"); -$sel->value_is("email-3-1", "off"); -$sel->value_is("email-3-10", "on"); -$sel->value_is("email-3-6", "on"); -$sel->value_is("email-3-5", "off"); -$sel->value_is("email-3-2", "off"); -$sel->value_is("email-3-3", "on"); -$sel->value_is("email-3-4", "on"); -$sel->value_is("email-3-7", "on"); -$sel->value_is("email-3-8", "off"); -$sel->value_is("email-3-9", "on"); -$sel->value_is("email-3-0", "off"); +$sel->value_is("email-3-1", "off"); +$sel->value_is("email-3-10", "on"); +$sel->value_is("email-3-6", "on"); +$sel->value_is("email-3-5", "off"); +$sel->value_is("email-3-2", "off"); +$sel->value_is("email-3-3", "on"); +$sel->value_is("email-3-4", "on"); +$sel->value_is("email-3-7", "on"); +$sel->value_is("email-3-8", "off"); +$sel->value_is("email-3-9", "on"); +$sel->value_is("email-3-0", "off"); $sel->value_is("neg-email-3-50", "off"); $sel->value_is("neg-email-3-51", "off"); -$sel->value_is("email-100-100", "on"); -$sel->value_is("email-100-101", "off"); -$sel->click_ok("update", undef, "Submit modified normal user email preferences"); +$sel->value_is("email-100-100", "on"); +$sel->value_is("email-100-101", "off"); +$sel->click_ok("update", undef, + "Submit modified normal user email preferences"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->is_text_present_ok("The changes to your email preferences have been saved."); +$sel->is_text_present_ok( + "The changes to your email preferences have been saved."); # Always show email recipients -ok($sel->create_cookie('show_bugmail_recipients=1'), 'Always show recipient list'); +ok($sel->create_cookie('show_bugmail_recipients=1'), + 'Always show recipient list'); # Create a test bug (bugmail to both normal user and admin) file_bug_in_product($sel, "Another Product"); $sel->select_ok("component", "label=c1"); -$sel->type_ok("short_desc", "Selenium Email Preference test bug", "Enter bug summary"); -$sel->type_ok("comment", "Created by Selenium to test Email Preferences", "Enter bug description"); +$sel->type_ok( + "short_desc", + "Selenium Email Preference test bug", + "Enter bug summary" +); +$sel->type_ok( + "comment", + "Created by Selenium to test Email Preferences", + "Enter bug description" +); $sel->type_ok("assigned_to", $config->{editbugs_user_login}); -$sel->type_ok("qa_contact", $config->{admin_user_login}); -$sel->type_ok("cc", $config->{admin_user_login}); +$sel->type_ok("qa_contact", $config->{admin_user_login}); +$sel->type_ok("cc", $config->{admin_user_login}); $sel->click_ok("commit"); $sel->wait_for_page_to_load(WAIT_TIME); my $bug1_id = $sel->get_value("//input[\@name='id' and \@type='hidden']"); -$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created"); +$sel->is_text_present_ok('has been added to the database', + "Bug $bug1_id created"); my @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_both, "Admin and normal user got bugmail"); # Make normal user changes (first pass) # go_to_bug($sel, $bug1_id); + # Severity change (bugmail to normal user but not admin) $sel->select_ok("bug_severity", "label=blocker"); $sel->selected_label_is("bug_severity", "blocker"); @@ -245,14 +280,20 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_normal, "Normal user got bugmail"); + # Add a comment (bugmail to no one) -$sel->type_ok("comment", "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)"); -$sel->value_is("comment", "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)"); +$sel->type_ok("comment", + "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)" +); +$sel->value_is("comment", + "This is a Selenium generated normal user test comment 1 of 2. (No bugmail should be generated for this.)" +); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); ok($email_sentto[0] eq "no one", "No bugmail sent"); + # Add normal user to CC list (bugmail to admin but not normal user) $sel->type_ok("newcc", $config->{editbugs_user_login}); $sel->value_is("newcc", $config->{editbugs_user_login}); @@ -261,6 +302,7 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail"); + # Request a flag from admin (bugmail to no one, request mail to no one) $sel->select_ok("flag_type-4", "label=?"); $sel->type_ok("requestee_type-4", $config->{admin_user_login}); @@ -276,6 +318,7 @@ ok($email_sentto[0] eq "no one", "No bugmail sent"); logout($sel); log_in($sel, $config, 'admin'); go_to_bug($sel, $bug1_id); + # Severity change (bugmail to normal user but not admin) $sel->select_ok("bug_severity", "label=trivial"); $sel->selected_label_is("bug_severity", "trivial"); @@ -284,14 +327,20 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_normal, "Normal user got bugmail"); + # Add a comment (bugmail to normal user but not admin) -$sel->type_ok("comment", "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)"); -$sel->value_is("comment", "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)"); +$sel->type_ok("comment", + "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)" +); +$sel->value_is("comment", + "This is a Selenium generated admin user test comment. (Only normal user should get bugmail for this.)" +); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_normal, "Normal user got bugmail"); + # Remove normal user from CC list (bugmail to both normal user and admin) $sel->click_ok("removecc"); $sel->add_selection_ok("cc", "label=$config->{editbugs_user_login}"); @@ -302,6 +351,7 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_both, "Admin and normal user got bugmail"); + # Reassign bug to admin user (bugmail to both normal user and admin) $sel->type_ok("assigned_to", $config->{admin_user_login}); $sel->value_is("assigned_to", $config->{admin_user_login}); @@ -310,6 +360,7 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_both, "Admin and normal user got bugmail"); + # Request a flag from normal user (bugmail to admin but not normal user and request mail to admin) $sel->select_ok("flag_type-4", "label=?"); $sel->type_ok("requestee_type-4", $config->{editbugs_user_login}); @@ -318,6 +369,7 @@ $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail"); + # Grant a normal user flag request (bugmail to admin but not normal user and request mail to no one) my $flag1_id = set_flag($sel, $config->{admin_user_login}, "?", "+"); $sel->click_ok("commit"); @@ -330,6 +382,7 @@ is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail"); logout($sel); log_in($sel, $config, 'editbugs'); go_to_bug($sel, $bug1_id); + # Severity change (bugmail to both admin and normal user) $sel->select_ok("bug_severity", "label=normal"); $sel->selected_label_is("bug_severity", "normal"); @@ -338,14 +391,20 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_both, "Admin and normal user got bugmail"); + # Add a comment (bugmail to admin but not normal user) -$sel->type_ok("comment", "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)"); -$sel->value_is("comment", "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)"); +$sel->type_ok("comment", + "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)" +); +$sel->value_is("comment", + "This is a Selenium generated normal user test comment 2 of 2. (Only admin should get bugmail for this.)" +); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail"); + # Reassign to normal user (bugmail to admin but not normal user) $sel->type_ok("assigned_to", $config->{editbugs_user_login}); $sel->value_is("assigned_to", $config->{editbugs_user_login}); @@ -353,12 +412,14 @@ $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); @email_sentto = get_email_sentto($sel); is_deeply(\@email_sentto, \@email_admin, "Admin got bugmail"); + # Deny a flag requested by admin (bugmail to no one and request mail to admin) my $flag2_id = set_flag($sel, $config->{editbugs_user_login}, "?", "-"); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); @email_sentto = get_email_sentto($sel); ok($email_sentto[0] eq "no one", "No bugmail sent"); + # Cancel both flags (bugmail and request mail to no one) set_flag($sel, undef, "+", "X", $flag1_id); set_flag($sel, undef, "-", "X", $flag2_id); @@ -370,31 +431,41 @@ logout($sel); # Help functions sub get_email_sentto { - my ($sel) = @_; - my @email_sentto; - my $index = 1; - while ($sel->is_element_present("//dt[text()='Email sent to:']/following-sibling::dd/code[$index]")) { - push(@email_sentto, - $sel->get_text("//dt[text()='Email sent to:']/following-sibling::dd/code[$index]")); - $index++; - } - return ("no one") if !@email_sentto; - return sort @email_sentto; + my ($sel) = @_; + my @email_sentto; + my $index = 1; + while ($sel->is_element_present( + "//dt[text()='Email sent to:']/following-sibling::dd/code[$index]")) + { + push( + @email_sentto, + $sel->get_text( + "//dt[text()='Email sent to:']/following-sibling::dd/code[$index]") + ); + $index++; + } + return ("no one") if !@email_sentto; + return sort @email_sentto; } sub set_flag { - my ($sel, $login, $curval, $newval, $prev_id) = @_; + my ($sel, $login, $curval, $newval, $prev_id) = @_; - # Retrieve flag id for the flag to be set - my $flag_id = $prev_id; - if (defined $login) { - my $flag_name = $sel->get_attribute("//table[\@id='flags']//input[\@value='$login']\@name"); - $flag_name =~ /^requestee-(\d+)$/; - $flag_id = $1; - } + # Retrieve flag id for the flag to be set + my $flag_id = $prev_id; + if (defined $login) { + my $flag_name + = $sel->get_attribute("//table[\@id='flags']//input[\@value='$login']\@name"); + $flag_name =~ /^requestee-(\d+)$/; + $flag_id = $1; + } - # Set new value for the flag (verifies current value) - $sel->select_ok("//select[\@id=\"flag-$flag_id\"]/option[\@value=\"$curval\" and \@selected]/..", "value=$newval", "Set flag ID $flag_id to $newval from $curval"); + # Set new value for the flag (verifies current value) + $sel->select_ok( + "//select[\@id=\"flag-$flag_id\"]/option[\@value=\"$curval\" and \@selected]/..", + "value=$newval", + "Set flag ID $flag_id to $newval from $curval" + ); - return $flag_id; + return $flag_id; } diff --git a/qa/t/lib/QA/REST.pm b/qa/t/lib/QA/REST.pm index f900cc352..3d37e2277 100644 --- a/qa/t/lib/QA/REST.pm +++ b/qa/t/lib/QA/REST.pm @@ -19,43 +19,46 @@ use QA::Util; use parent qw(LWP::UserAgent Exporter); @QA::REST::EXPORT = qw( - MUST_FAIL - get_rest_client + MUST_FAIL + get_rest_client ); use constant MUST_FAIL => 1; sub get_rest_client { - my $rest_client = LWP::UserAgent->new( ssl_opts => { verify_hostname => 0 } ); - bless($rest_client, 'QA::REST'); - my $config = $rest_client->{bz_config} = get_config(); - $rest_client->{bz_url} = $config->{browser_url} . '/' . $config->{bugzilla_installation} . '/rest/'; - $rest_client->{bz_default_headers} = {'Accept' => 'application/json', 'Content-Type' => 'application/json'}; - return $rest_client; + my $rest_client = LWP::UserAgent->new(ssl_opts => {verify_hostname => 0}); + bless($rest_client, 'QA::REST'); + my $config = $rest_client->{bz_config} = get_config(); + $rest_client->{bz_url} + = $config->{browser_url} . '/' . $config->{bugzilla_installation} . '/rest/'; + $rest_client->{bz_default_headers} + = {'Accept' => 'application/json', 'Content-Type' => 'application/json'}; + return $rest_client; } sub bz_config { return $_[0]->{bz_config}; } sub call { - my ($self, $method, $data, $http_verb, $expect_to_fail) = @_; - $http_verb = lc($http_verb || 'GET'); - $data //= {}; - - my %args = %{ $self->{bz_default_headers} }; - # We do not pass the API key in the URL, so that it's not logged by the web server. - if ($http_verb eq 'get' && $data->{api_key}) { - $args{'X-BUGZILLA-API-KEY'} = $data->{api_key}; - } - elsif ($http_verb ne 'get') { - $args{Content} = encode_json($data); - } - - my $response = $self->$http_verb($self->{bz_url} . $method, %args); - my $res = decode_json($response->decoded_content); - if ($response->is_success xor $expect_to_fail) { - return $res; - } - else { - die 'error ' . $res->{code} . ': ' . $res->{message} . "\n"; - } + my ($self, $method, $data, $http_verb, $expect_to_fail) = @_; + $http_verb = lc($http_verb || 'GET'); + $data //= {}; + + my %args = %{$self->{bz_default_headers}}; + +# We do not pass the API key in the URL, so that it's not logged by the web server. + if ($http_verb eq 'get' && $data->{api_key}) { + $args{'X-BUGZILLA-API-KEY'} = $data->{api_key}; + } + elsif ($http_verb ne 'get') { + $args{Content} = encode_json($data); + } + + my $response = $self->$http_verb($self->{bz_url} . $method, %args); + my $res = decode_json($response->decoded_content); + if ($response->is_success xor $expect_to_fail) { + return $res; + } + else { + die 'error ' . $res->{code} . ': ' . $res->{message} . "\n"; + } } diff --git a/qa/t/lib/QA/RPC.pm b/qa/t/lib/QA/RPC.pm index 4053c4dfe..f1b44eea8 100644 --- a/qa/t/lib/QA/RPC.pm +++ b/qa/t/lib/QA/RPC.pm @@ -16,9 +16,9 @@ use Storable qw(dclone); use Test::More; sub bz_config { - my $self = shift; - $self->{bz_config} ||= QA::Util::get_config(); - return $self->{bz_config}; + my $self = shift; + $self->{bz_config} ||= QA::Util::get_config(); + return $self->{bz_config}; } # True if we're doing calls over GET instead of POST. @@ -29,12 +29,12 @@ sub bz_get_mode { return 0 } # and Bugzilla_password with every future call until User.logout is called # (which actually just calls _bz_clear_credentials, under GET). sub _bz_credentials { - my ($self, $user, $pass) = @_; - if (@_ == 3) { - $self->{_bz_credentials}->{user} = $user; - $self->{_bz_credentials}->{pass} = $pass; - } - return $self->{_bz_credentials}; + my ($self, $user, $pass) = @_; + if (@_ == 3) { + $self->{_bz_credentials}->{user} = $user; + $self->{_bz_credentials}->{pass} = $pass; + } + return $self->{_bz_credentials}; } sub _bz_clear_credentials { delete $_[0]->{_bz_credentials} } @@ -43,240 +43,258 @@ sub _bz_clear_credentials { delete $_[0]->{_bz_credentials} } ################################ sub bz_log_in { - my ($self, $user) = @_; - my $username = $self->bz_config->{"${user}_user_login"}; - my $password = $self->bz_config->{"${user}_user_passwd"}; - - if ($self->bz_get_mode) { - $self->_bz_credentials($username, $password); - return; - } - - my $call = $self->bz_call_success( - 'User.login', { login => $username, password => $password }); - cmp_ok($call->result->{id}, 'gt', 0, $self->TYPE . ": Logged in as $user"); - $self->{_bz_credentials}->{token} = $call->result->{token}; + my ($self, $user) = @_; + my $username = $self->bz_config->{"${user}_user_login"}; + my $password = $self->bz_config->{"${user}_user_passwd"}; + + if ($self->bz_get_mode) { + $self->_bz_credentials($username, $password); + return; + } + + my $call = $self->bz_call_success('User.login', + {login => $username, password => $password}); + cmp_ok($call->result->{id}, 'gt', 0, $self->TYPE . ": Logged in as $user"); + $self->{_bz_credentials}->{token} = $call->result->{token}; } sub bz_call_success { - my ($self, $method, $orig_args, $test_name) = @_; - my $args = $orig_args ? dclone($orig_args) : {}; - - if ($self->bz_get_mode and $method eq 'User.logout') { - $self->_bz_clear_credentials(); - return; - } - - my $call; - # Under XMLRPC::Lite, if we pass undef as the second argument, - # it sends a single param , which shows up as an - # empty string on the Bugzilla side. - if ($self->{_bz_credentials}->{token}) { - $args->{Bugzilla_token} = $self->{_bz_credentials}->{token}; - } - - if (scalar keys %$args) { - $call = $self->call($method, $args); - } - else { - $call = $self->call($method); - } - $test_name ||= "$method returned successfully"; - $self->_handle_undef_response($test_name) if !$call; - ok(!$call->fault, $self->TYPE . ": $test_name") - or diag($call->faultstring); - - if ($method eq 'User.logout') { - delete $self->{_bz_credentials}->{token}; - } - return $call; + my ($self, $method, $orig_args, $test_name) = @_; + my $args = $orig_args ? dclone($orig_args) : {}; + + if ($self->bz_get_mode and $method eq 'User.logout') { + $self->_bz_clear_credentials(); + return; + } + + my $call; + + # Under XMLRPC::Lite, if we pass undef as the second argument, + # it sends a single param , which shows up as an + # empty string on the Bugzilla side. + if ($self->{_bz_credentials}->{token}) { + $args->{Bugzilla_token} = $self->{_bz_credentials}->{token}; + } + + if (scalar keys %$args) { + $call = $self->call($method, $args); + } + else { + $call = $self->call($method); + } + $test_name ||= "$method returned successfully"; + $self->_handle_undef_response($test_name) if !$call; + ok(!$call->fault, $self->TYPE . ": $test_name") or diag($call->faultstring); + + if ($method eq 'User.logout') { + delete $self->{_bz_credentials}->{token}; + } + return $call; } sub bz_call_fail { - my ($self, $method, $orig_args, $faultstring, $test_name) = @_; - my $args = $orig_args ? dclone($orig_args) : {}; - - if ($self->{_bz_credentials}->{token}) { - $args->{Bugzilla_token} = $self->{_bz_credentials}->{token}; - } - - $test_name ||= "$method failed (as intended)"; - my $call = $self->call($method, $args); - $self->_handle_undef_response($test_name) if !$call; - ok($call->fault, $self->TYPE . ": $test_name") - or diag("Returned: " . Dumper($call->result)); - if (defined $faultstring) { - cmp_ok(trim($call->faultstring), '=~', $faultstring, - $self->TYPE . ": Got correct fault for $method"); - } - ok($call->faultcode - && (($call->faultcode < 32000 && $call->faultcode > -32000) - # Fault codes 32610 and above are OK because they are errors - # that we expect and test for sometimes. - || $call->faultcode >= 32610), - $self->TYPE . ': Fault code is set properly') - or diag("Code: " . $call->faultcode - . " Message: " . $call->faultstring); - - return $call; + my ($self, $method, $orig_args, $faultstring, $test_name) = @_; + my $args = $orig_args ? dclone($orig_args) : {}; + + if ($self->{_bz_credentials}->{token}) { + $args->{Bugzilla_token} = $self->{_bz_credentials}->{token}; + } + + $test_name ||= "$method failed (as intended)"; + my $call = $self->call($method, $args); + $self->_handle_undef_response($test_name) if !$call; + ok($call->fault, $self->TYPE . ": $test_name") + or diag("Returned: " . Dumper($call->result)); + if (defined $faultstring) { + cmp_ok(trim($call->faultstring), + '=~', $faultstring, $self->TYPE . ": Got correct fault for $method"); + } + ok( + $call->faultcode && ( + ($call->faultcode < 32000 && $call->faultcode > -32000) + + # Fault codes 32610 and above are OK because they are errors + # that we expect and test for sometimes. + || $call->faultcode >= 32610 + ), + $self->TYPE . ': Fault code is set properly' + ) or diag("Code: " . $call->faultcode . " Message: " . $call->faultstring); + + return $call; } sub _handle_undef_response { - my ($self, $test_name) = @_; - my $response = $self->transport->http_response; - die "$test_name:\n", $response->as_string; + my ($self, $test_name) = @_; + my $response = $self->transport->http_response; + die "$test_name:\n", $response->as_string; } sub bz_get_products { - my ($self) = @_; - $self->bz_log_in('QA_Selenium_TEST'); - - my $accessible = $self->bz_call_success('Product.get_accessible_products'); - my $prod_call = $self->bz_call_success('Product.get', $accessible->result); - my %products; - foreach my $prod (@{ $prod_call->result->{products} }) { - $products{$prod->{name}} = $prod->{id}; - } - - $self->bz_call_success('User.logout'); - return \%products; + my ($self) = @_; + $self->bz_log_in('QA_Selenium_TEST'); + + my $accessible = $self->bz_call_success('Product.get_accessible_products'); + my $prod_call = $self->bz_call_success('Product.get', $accessible->result); + my %products; + foreach my $prod (@{$prod_call->result->{products}}) { + $products{$prod->{name}} = $prod->{id}; + } + + $self->bz_call_success('User.logout'); + return \%products; } -sub _string_array { map { random_string() } (1..$_[0]) } +sub _string_array { + map { random_string() } (1 .. $_[0]); +} sub bz_create_test_bugs { - my ($self, $second_private) = @_; - my $config = $self->bz_config; - - my @whiteboard_strings = _string_array(3); - my @summary_strings = _string_array(3); - - my $public_bug = create_bug_fields($config); - $public_bug->{alias} = random_string(40); - $public_bug->{whiteboard} = join(' ', @whiteboard_strings); - $public_bug->{summary} = join(' ', @summary_strings); - - my $private_bug = dclone($public_bug); - $private_bug->{alias} = random_string(40); - if ($second_private) { - $private_bug->{product} = 'QA-Selenium-TEST'; - $private_bug->{component} = 'QA-Selenium-TEST'; - $private_bug->{target_milestone} = 'QAMilestone'; - $private_bug->{version} = 'QAVersion'; - # Although we don't directly use this, this helps some tests that - # depend on the values in $private_bug. - $private_bug->{creator} = $config->{PRIVATE_BUG_USER . '_user_login'}; - } - - my @create_bugs = ( - { user => 'editbugs', - args => $public_bug, - test => 'Create a public bug' }, - { user => $second_private ? PRIVATE_BUG_USER : 'editbugs', - args => $private_bug, - test => $second_private ? 'Create a private bug' - : 'Create a second public bug' }, - ); - - my $post_success = sub { - my ($call, $t) = @_; - my $id = $call->result->{id}; - $t->{args}->{id} = $id; - }; - - # Creating the bugs isn't really a test, it's just preliminary work - # for the tests. So we just run it with one of the RPC clients. - $self->bz_run_tests(tests => \@create_bugs, method => 'Bug.create', - post_success => $post_success); - - return ($public_bug, $private_bug); + my ($self, $second_private) = @_; + my $config = $self->bz_config; + + my @whiteboard_strings = _string_array(3); + my @summary_strings = _string_array(3); + + my $public_bug = create_bug_fields($config); + $public_bug->{alias} = random_string(40); + $public_bug->{whiteboard} = join(' ', @whiteboard_strings); + $public_bug->{summary} = join(' ', @summary_strings); + + my $private_bug = dclone($public_bug); + $private_bug->{alias} = random_string(40); + if ($second_private) { + $private_bug->{product} = 'QA-Selenium-TEST'; + $private_bug->{component} = 'QA-Selenium-TEST'; + $private_bug->{target_milestone} = 'QAMilestone'; + $private_bug->{version} = 'QAVersion'; + + # Although we don't directly use this, this helps some tests that + # depend on the values in $private_bug. + $private_bug->{creator} = $config->{PRIVATE_BUG_USER . '_user_login'}; + } + + my @create_bugs = ( + {user => 'editbugs', args => $public_bug, test => 'Create a public bug'}, + { + user => $second_private ? PRIVATE_BUG_USER : 'editbugs', + args => $private_bug, + test => $second_private ? 'Create a private bug' : 'Create a second public bug' + }, + ); + + my $post_success = sub { + my ($call, $t) = @_; + my $id = $call->result->{id}; + $t->{args}->{id} = $id; + }; + + # Creating the bugs isn't really a test, it's just preliminary work + # for the tests. So we just run it with one of the RPC clients. + $self->bz_run_tests( + tests => \@create_bugs, + method => 'Bug.create', + post_success => $post_success + ); + + return ($public_bug, $private_bug); } sub bz_run_tests { - my ($self, %params) = @_; - # Required params - my $config = $self->bz_config; - my $tests = $params{tests}; - my $method = $params{method}; - - # Optional params - my $post_success = $params{post_success}; - my $pre_call = $params{pre_call}; - - my $former_user = ''; - foreach my $t (@$tests) { - # Only logout/login if the user has changed since the last test - # (this saves us LOTS of needless logins). - my $user = $t->{user} || ''; - if ($former_user ne $user) { - $self->bz_call_success('User.logout') if $former_user; - $self->bz_log_in($user) if $user; - $former_user = $user; - } - - $pre_call->($t, $self) if $pre_call; - - if ($t->{error}) { - $self->bz_call_fail($method, $t->{args}, $t->{error}, $t->{test}); - } - else { - my $call = $self->bz_call_success($method, $t->{args}, $t->{test}); - if ($call->result && $post_success) { - $post_success->($call, $t, $self); - } - } + my ($self, %params) = @_; + + # Required params + my $config = $self->bz_config; + my $tests = $params{tests}; + my $method = $params{method}; + + # Optional params + my $post_success = $params{post_success}; + my $pre_call = $params{pre_call}; + + my $former_user = ''; + foreach my $t (@$tests) { + + # Only logout/login if the user has changed since the last test + # (this saves us LOTS of needless logins). + my $user = $t->{user} || ''; + if ($former_user ne $user) { + $self->bz_call_success('User.logout') if $former_user; + $self->bz_log_in($user) if $user; + $former_user = $user; + } + + $pre_call->($t, $self) if $pre_call; + + if ($t->{error}) { + $self->bz_call_fail($method, $t->{args}, $t->{error}, $t->{test}); + } + else { + my $call = $self->bz_call_success($method, $t->{args}, $t->{test}); + if ($call->result && $post_success) { + $post_success->($call, $t, $self); + } } + } - $self->bz_call_success('User.logout') if $former_user; + $self->bz_call_success('User.logout') if $former_user; } sub bz_test_bug { - my ($self, $fields, $bug, $expect, $t, $creation_time) = @_; - - foreach my $field (sort @$fields) { - # "description" is used by Bug.create but comments are not returned - # by Bug.get or Bug.search. - next if $field eq 'description'; - - my @include = @{ $t->{args}->{include_fields} || [] }; - my @exclude = @{ $t->{args}->{exclude_fields} || [] }; - if ( (@include and !grep($_ eq $field, @include)) - or (@exclude and grep($_ eq $field, @exclude)) ) - { - ok(!exists $bug->{$field}, "$field is not included") - or diag Dumper($bug); - next; - } - - if ($field =~ /^is_/) { - ok(defined $bug->{$field}, $self->TYPE . ": $field is not null"); - is($bug->{$field} ? 1 : 0, $expect->{$field} ? 1 : 0, - $self->TYPE . ": $field has the right boolean value"); - } - elsif ($field eq 'cc') { - foreach my $cc_item (@{ $expect->{cc} || [] }) { - ok(grep($_ eq $cc_item, @{ $bug->{cc} }), - $self->TYPE . ": $field contains $cc_item"); - } - } - elsif ($field eq 'creation_time' or $field eq 'last_change_time') { - my $creation_day; - # XML-RPC and JSON-RPC have different date formats. - if ($self->isa('QA::RPC::XMLRPC')) { - $creation_day = $creation_time->ymd(''); - } - else { - $creation_day = $creation_time->ymd; - } - - like($bug->{$field}, qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/, - $self->TYPE . ": $field has the right format"); - } - else { - is_deeply($bug->{$field}, $expect->{$field}, - $self->TYPE . ": $field value is correct"); - } + my ($self, $fields, $bug, $expect, $t, $creation_time) = @_; + + foreach my $field (sort @$fields) { + + # "description" is used by Bug.create but comments are not returned + # by Bug.get or Bug.search. + next if $field eq 'description'; + + my @include = @{$t->{args}->{include_fields} || []}; + my @exclude = @{$t->{args}->{exclude_fields} || []}; + if ( (@include and !grep($_ eq $field, @include)) + or (@exclude and grep($_ eq $field, @exclude))) + { + ok(!exists $bug->{$field}, "$field is not included") or diag Dumper($bug); + next; + } + + if ($field =~ /^is_/) { + ok(defined $bug->{$field}, $self->TYPE . ": $field is not null"); + is( + $bug->{$field} ? 1 : 0, + $expect->{$field} ? 1 : 0, + $self->TYPE . ": $field has the right boolean value" + ); + } + elsif ($field eq 'cc') { + foreach my $cc_item (@{$expect->{cc} || []}) { + ok( + grep($_ eq $cc_item, @{$bug->{cc}}), + $self->TYPE . ": $field contains $cc_item" + ); + } + } + elsif ($field eq 'creation_time' or $field eq 'last_change_time') { + my $creation_day; + + # XML-RPC and JSON-RPC have different date formats. + if ($self->isa('QA::RPC::XMLRPC')) { + $creation_day = $creation_time->ymd(''); + } + else { + $creation_day = $creation_time->ymd; + } + + like( + $bug->{$field}, + qr/^\Q${creation_day}\ET\d\d:\d\d:\d\d/, + $self->TYPE . ": $field has the right format" + ); + } + else { + is_deeply($bug->{$field}, $expect->{$field}, + $self->TYPE . ": $field value is correct"); } + } } 1; diff --git a/qa/t/lib/QA/RPC/JSONRPC.pm b/qa/t/lib/QA/RPC/JSONRPC.pm index 4175b10fc..1e43e9eaf 100644 --- a/qa/t/lib/QA/RPC/JSONRPC.pm +++ b/qa/t/lib/QA/RPC/JSONRPC.pm @@ -11,24 +11,26 @@ package QA::RPC::JSONRPC; use strict; use QA::RPC; -BEGIN { - our @ISA = qw(QA::RPC); - if (eval { require JSON::RPC::Client }) { - push(@ISA, 'JSON::RPC::Client'); - } - else { - require JSON::RPC::Legacy::Client; - push(@ISA, 'JSON::RPC::Legacy::Client'); - } +BEGIN { + our @ISA = qw(QA::RPC); + + if (eval { require JSON::RPC::Client }) { + push(@ISA, 'JSON::RPC::Client'); + } + else { + require JSON::RPC::Legacy::Client; + push(@ISA, 'JSON::RPC::Legacy::Client'); + } } use URI::Escape; use constant DATETIME_REGEX => qr/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\dZ$/; + sub TYPE { - my ($self) = @_; - return $self->bz_get_mode ? 'JSON-RPC GET' : 'JSON-RPC'; + my ($self) = @_; + return $self->bz_get_mode ? 'JSON-RPC GET' : 'JSON-RPC'; } ################################# @@ -36,85 +38,88 @@ sub TYPE { ################################# sub ua { - my $self = shift; - if ($self->{ua} and not $self->{ua}->isa('QA::RPC::UserAgent')) { - bless $self->{ua}, 'QA::RPC::UserAgent'; - } - return $self->SUPER::ua(@_); + my $self = shift; + if ($self->{ua} and not $self->{ua}->isa('QA::RPC::UserAgent')) { + bless $self->{ua}, 'QA::RPC::UserAgent'; + } + return $self->SUPER::ua(@_); } sub transport { $_[0]->ua } sub bz_get_mode { - my ($self, $value) = @_; - $self->{bz_get_mode} = $value if @_ > 1; - return $self->{bz_get_mode}; + my ($self, $value) = @_; + $self->{bz_get_mode} = $value if @_ > 1; + return $self->{bz_get_mode}; } sub _bz_callback { - my ($self, $value) = @_; - $self->{bz_callback} = $value if @_ > 1; - return $self->{bz_callback}; + my ($self, $value) = @_; + $self->{bz_callback} = $value if @_ > 1; + return $self->{bz_callback}; } sub call { - my $self = shift; - my ($method, $args) = @_; - my %params = ( method => $method ); - $params{params} = $args ? [$args] : []; - - my $config = $self->bz_config; - my $url = $config->{browser_url} . "/" - . $config->{bugzilla_installation} . "/jsonrpc.cgi"; - my $result; - if ($self->bz_get_mode) { - my $method_escaped = uri_escape($method); - $url .= "?method=$method_escaped"; - if (my $cred = $self->_bz_credentials) { - $args->{Bugzilla_login} = $cred->{user} - if !exists $args->{Bugzilla_login}; - $args->{Bugzilla_password} = $cred->{pass} - if !exists $args->{Bugzilla_password}; - } - if ($args) { - my $params_json = $self->json->encode($args); - my $params_escaped = uri_escape($params_json); - $url .= "¶ms=$params_escaped"; - } - if ($self->version eq '1.1') { - $url .= "&version=1.1"; - } - my $callback = delete $args->{callback}; - if (defined $callback) { - $self->_bz_callback($callback); - $url .= "&callback=" . uri_escape($callback); - } - $result = $self->SUPER::call($url); + my $self = shift; + my ($method, $args) = @_; + my %params = (method => $method); + $params{params} = $args ? [$args] : []; + + my $config = $self->bz_config; + my $url + = $config->{browser_url} . "/" + . $config->{bugzilla_installation} + . "/jsonrpc.cgi"; + my $result; + if ($self->bz_get_mode) { + my $method_escaped = uri_escape($method); + $url .= "?method=$method_escaped"; + if (my $cred = $self->_bz_credentials) { + $args->{Bugzilla_login} = $cred->{user} if !exists $args->{Bugzilla_login}; + $args->{Bugzilla_password} = $cred->{pass} + if !exists $args->{Bugzilla_password}; } - else { - $result = $self->SUPER::call($url, \%params); + if ($args) { + my $params_json = $self->json->encode($args); + my $params_escaped = uri_escape($params_json); + $url .= "¶ms=$params_escaped"; } - - if ($result) { - bless $result, 'QA::RPC::JSONRPC::ReturnObject'; + if ($self->version eq '1.1') { + $url .= "&version=1.1"; + } + my $callback = delete $args->{callback}; + if (defined $callback) { + $self->_bz_callback($callback); + $url .= "&callback=" . uri_escape($callback); } - return $result; + $result = $self->SUPER::call($url); + } + else { + $result = $self->SUPER::call($url, \%params); + } + + if ($result) { + bless $result, 'QA::RPC::JSONRPC::ReturnObject'; + } + return $result; } sub _get { - my $self = shift; - my $result = $self->SUPER::_get(@_); - # Simple JSONP support for tests. We just remove the callback from - # the return value. - my $callback = $self->_bz_callback; - if (defined $callback and $result->is_success) { - my $content = $result->content; - $content =~ s/^(?:\/\*\*\/)?\Q$callback(\E(.*)\)$/$1/s; - $result->content($content); - # We don't need this anymore, and we don't want it to affect - # future calls. - delete $self->{bz_callback}; - } - return $result; + my $self = shift; + my $result = $self->SUPER::_get(@_); + + # Simple JSONP support for tests. We just remove the callback from + # the return value. + my $callback = $self->_bz_callback; + if (defined $callback and $result->is_success) { + my $content = $result->content; + $content =~ s/^(?:\/\*\*\/)?\Q$callback(\E(.*)\)$/$1/s; + $result->content($content); + + # We don't need this anymore, and we don't want it to affect + # future calls. + delete $self->{bz_callback}; + } + return $result; } 1; @@ -123,13 +128,13 @@ package QA::RPC::JSONRPC::ReturnObject; use strict; BEGIN { - if (eval { require JSON::RPC::Client }) { - our @ISA = qw(JSON::RPC::ReturnObject); - } - else { - require JSON::RPC::Legacy::Client; - our @ISA = qw(JSON::RPC::Legacy::ReturnObject); - } + if (eval { require JSON::RPC::Client }) { + our @ISA = qw(JSON::RPC::ReturnObject); + } + else { + require JSON::RPC::Legacy::Client; + our @ISA = qw(JSON::RPC::Legacy::ReturnObject); + } } ################################# @@ -137,8 +142,8 @@ BEGIN { ################################# sub faultstring { $_[0]->{content}->{error}->{message} } -sub faultcode { $_[0]->{content}->{error}->{code} } -sub fault { $_[0]->is_error } +sub faultcode { $_[0]->{content}->{error}->{code} } +sub fault { $_[0]->is_error } 1; @@ -151,18 +156,19 @@ use base qw(LWP::UserAgent); ######################################## sub send_request { - my $self = shift; - my $response = $self->SUPER::send_request(@_); - $self->http_response($response); - # JSON::RPC::Client can't handle 500 responses, even though - # they're required by the JSON-RPC spec. - $response->code(200); - return $response; + my $self = shift; + my $response = $self->SUPER::send_request(@_); + $self->http_response($response); + + # JSON::RPC::Client can't handle 500 responses, even though + # they're required by the JSON-RPC spec. + $response->code(200); + return $response; } # Copied directly from SOAP::Lite::Transport::HTTP. sub http_response { - my $self = shift; - if (@_) { $self->{'_http_response'} = shift; return $self } - return $self->{'_http_response'}; + my $self = shift; + if (@_) { $self->{'_http_response'} = shift; return $self } + return $self->{'_http_response'}; } diff --git a/qa/t/lib/QA/RPC/XMLRPC.pm b/qa/t/lib/QA/RPC/XMLRPC.pm index d88d4092e..6e63852eb 100644 --- a/qa/t/lib/QA/RPC/XMLRPC.pm +++ b/qa/t/lib/QA/RPC/XMLRPC.pm @@ -11,7 +11,7 @@ package QA::RPC::XMLRPC; use strict; use base qw(QA::RPC XMLRPC::Lite); -use constant TYPE => 'XML-RPC'; +use constant TYPE => 'XML-RPC'; use constant DATETIME_REGEX => qr/^\d{8}T\d\d:\d\d:\d\d$/; 1; diff --git a/qa/t/lib/QA/Tests.pm b/qa/t/lib/QA/Tests.pm index 86fc06ad1..3d53cefcc 100644 --- a/qa/t/lib/QA/Tests.pm +++ b/qa/t/lib/QA/Tests.pm @@ -11,96 +11,108 @@ package QA::Tests; use strict; use base qw(Exporter); our @EXPORT_OK = qw( - PRIVATE_BUG_USER - STANDARD_BUG_TESTS - bug_tests - create_bug_fields + PRIVATE_BUG_USER + STANDARD_BUG_TESTS + bug_tests + create_bug_fields ); -use constant INVALID_BUG_ID => -1; +use constant INVALID_BUG_ID => -1; use constant INVALID_BUG_ALIAS => 'aaaaaaa12345'; -use constant PRIVATE_BUG_USER => 'QA_Selenium_TEST'; +use constant PRIVATE_BUG_USER => 'QA_Selenium_TEST'; use constant CREATE_BUG => { - 'priority' => 'Highest', - 'status' => 'CONFIRMED', - 'version' => 'unspecified', - 'creator' => 'editbugs', - 'description' => '-- Comment Created By Bugzilla XML-RPC Tests --', - 'cc' => ['unprivileged'], - 'component' => 'c1', - 'platform' => 'PC', - # It's necessary to assign the bug to somebody who isn't in the - # timetracking group, for the Bug.update tests. - 'assigned_to' => PRIVATE_BUG_USER, - 'summary' => 'WebService Test Bug', - 'product' => 'Another Product', - 'op_sys' => 'Linux', - 'severity' => 'normal', - 'qa_contact' => 'canconfirm', - version => 'Another1', - url => 'https://www.bugzilla.org/', - target_milestone => 'AnotherMS1', + 'priority' => 'Highest', + 'status' => 'CONFIRMED', + 'version' => 'unspecified', + 'creator' => 'editbugs', + 'description' => '-- Comment Created By Bugzilla XML-RPC Tests --', + 'cc' => ['unprivileged'], + 'component' => 'c1', + 'platform' => 'PC', + + # It's necessary to assign the bug to somebody who isn't in the + # timetracking group, for the Bug.update tests. + 'assigned_to' => PRIVATE_BUG_USER, + 'summary' => 'WebService Test Bug', + 'product' => 'Another Product', + 'op_sys' => 'Linux', + 'severity' => 'normal', + 'qa_contact' => 'canconfirm', + version => 'Another1', + url => 'https://www.bugzilla.org/', + target_milestone => 'AnotherMS1', }; sub create_bug_fields { - my ($config) = @_; - my %bug = %{ CREATE_BUG() }; - foreach my $field (qw(creator assigned_to qa_contact)) { - my $value = $bug{$field}; - $bug{$field} = $config->{"${value}_user_login"}; - } - $bug{cc} = [map { $config->{$_ . "_user_login"} } @{ $bug{cc} }]; - return \%bug; + my ($config) = @_; + my %bug = %{CREATE_BUG()}; + foreach my $field (qw(creator assigned_to qa_contact)) { + my $value = $bug{$field}; + $bug{$field} = $config->{"${value}_user_login"}; + } + $bug{cc} = [map { $config->{$_ . "_user_login"} } @{$bug{cc}}]; + return \%bug; } sub bug_tests { - my ($public_id, $private_id) = @_; - return [ - { args => { ids => [$private_id] }, + my ($public_id, $private_id) = @_; + return [ + { + args => {ids => [$private_id]}, error => "You are not authorized to access", test => 'Logged-out user cannot access a private bug', }, - { args => { ids => [$public_id] }, + { + args => {ids => [$public_id]}, test => 'Logged-out user can access a public bug.', }, - { args => { ids => [INVALID_BUG_ID] }, + { + args => {ids => [INVALID_BUG_ID]}, error => "It does not seem like bug number", test => 'Passing invalid bug id returns error "Invalid Bug ID"', }, - { args => { ids => [undef] }, + { + args => {ids => [undef]}, error => "You must enter a valid bug number", test => 'Passing undef as bug id param returns error "Invalid Bug ID"', }, - { args => { ids => [INVALID_BUG_ALIAS] }, + { + args => {ids => [INVALID_BUG_ALIAS]}, error => "nor an alias to a bug", test => 'Passing invalid bug alias returns error "Invalid Bug Alias"', }, - { user => 'editbugs', - args => { ids => [$private_id] }, + { + user => 'editbugs', + args => {ids => [$private_id]}, error => "You are not authorized to access", test => 'Access to a private bug is denied to a user without privs', }, - { user => 'unprivileged', - args => { ids => [$public_id] }, + { + user => 'unprivileged', + args => {ids => [$public_id]}, test => 'User without privs can access a public bug', }, - { user => 'admin', - args => { ids => [$public_id] }, + { + user => 'admin', + args => {ids => [$public_id]}, test => 'Admin can access a public bug.', }, - { user => PRIVATE_BUG_USER, - args => { ids => [$private_id] }, + { + user => PRIVATE_BUG_USER, + args => {ids => [$private_id]}, test => 'User with privs can successfully access a private bug', }, + # This helps webservice_bug_attachment get private attachment ids # from the public bug, and doesn't hurt for the other tests. - { user => PRIVATE_BUG_USER, - args => { ids => [$public_id] }, + { + user => PRIVATE_BUG_USER, + args => {ids => [$public_id]}, test => 'User with privs can also access the public bug', }, - ]; + ]; } use constant STANDARD_BUG_TESTS => bug_tests('public_bug', 'private_bug'); diff --git a/qa/t/lib/QA/Util.pm b/qa/t/lib/QA/Util.pm index bf9151fee..fd584975a 100644 --- a/qa/t/lib/QA/Util.pm +++ b/qa/t/lib/QA/Util.pm @@ -22,72 +22,74 @@ use URI::QueryParam; # Fixes wide character warnings BEGIN { - my $builder = Test::More->builder; - binmode $builder->output, ":encoding(utf8)"; - binmode $builder->failure_output, ":encoding(utf8)"; - binmode $builder->todo_output, ":encoding(utf8)"; + my $builder = Test::More->builder; + binmode $builder->output, ":encoding(utf8)"; + binmode $builder->failure_output, ":encoding(utf8)"; + binmode $builder->todo_output, ":encoding(utf8)"; } use base qw(Exporter); @QA::Util::EXPORT = qw( - trim - url_quote - random_string - - log_in - logout - file_bug_in_product - create_bug - edit_bug - edit_bug_and_return - go_to_bug - go_to_home - go_to_admin - edit_product - add_product - open_advanced_search_page - set_parameters - screenshot_page - - get_selenium - get_rpc_clients - check_page_load - - WAIT_TIME - CHROME_MODE + trim + url_quote + random_string + + log_in + logout + file_bug_in_product + create_bug + edit_bug + edit_bug_and_return + go_to_bug + go_to_home + go_to_admin + edit_product + add_product + open_advanced_search_page + set_parameters + screenshot_page + + get_selenium + get_rpc_clients + check_page_load + + WAIT_TIME + CHROME_MODE ); # How long we wait for pages to load. use constant WAIT_TIME => 60000; -use constant CONF_FILE => $ENV{BZ_QA_CONF_FILE} // "../config/selenium_test.conf"; +use constant CONF_FILE => $ENV{BZ_QA_CONF_FILE} + // "../config/selenium_test.conf"; use constant CHROME_MODE => 1; -use constant NDASH => chr(0x2013); +use constant NDASH => chr(0x2013); ##################### # Utility Functions # ##################### sub random_string { - my $size = shift || 30; # default to 30 chars if nothing specified - return join("", map{ ('0'..'9','a'..'z','A'..'Z')[rand 62] } (1..$size)); + my $size = shift || 30; # default to 30 chars if nothing specified + return + join("", map { ('0' .. '9', 'a' .. 'z', 'A' .. 'Z')[rand 62] } (1 .. $size)); } # Remove consecutive as well as leading and trailing whitespaces. sub trim { - my ($str) = @_; - if ($str) { - $str =~ s/[\r\n\t\s]+/ /g; - $str =~ s/^\s+//g; - $str =~ s/\s+$//g; - } - return $str; + my ($str) = @_; + if ($str) { + $str =~ s/[\r\n\t\s]+/ /g; + $str =~ s/^\s+//g; + $str =~ s/\s+$//g; + } + return $str; } # This originally came from CGI.pm, by Lincoln D. Stein sub url_quote { - my ($toencode) = (@_); - $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; - return $toencode; + my ($toencode) = (@_); + $toencode =~ s/([^a-zA-Z0-9_\-.])/uc sprintf("%%%02x",ord($1))/eg; + return $toencode; } ################### @@ -95,67 +97,73 @@ sub url_quote { ################### sub get_config { - # read the test configuration file - my $conf_file = CONF_FILE; - my $config = do($conf_file) - or die "can't read configuration '$conf_file': $!$@"; - my $uri = URI->new($config->{browser_url}); - if (my $ip_packed = gethostbyname($uri->host)) { - my $ip = inet_ntoa($ip_packed); - $uri->host($ip); - $config->{browser_ip_url} = "$uri"; - } - else { - die "unable to find ip for $config->{browser_url}\n"; - } - return $config; + + # read the test configuration file + my $conf_file = CONF_FILE; + my $config = do($conf_file) + or die "can't read configuration '$conf_file': $!$@"; + my $uri = URI->new($config->{browser_url}); + if (my $ip_packed = gethostbyname($uri->host)) { + my $ip = inet_ntoa($ip_packed); + $uri->host($ip); + $config->{browser_ip_url} = "$uri"; + } + else { + die "unable to find ip for $config->{browser_url}\n"; + } + return $config; } sub get_selenium { - my $chrome_mode = shift; - my $config = get_config(); - - if (!server_is_running) { - die "Selenium Server isn't running!"; - } - - my $sel = Test::WWW::Selenium->new( - host => $config->{host}, - port => $config->{port}, - browser => $chrome_mode ? $config->{experimental_browser_launcher} : $config->{browser}, - browser_url => $config->{browser_url} - ); - - return ($sel, $config); + my $chrome_mode = shift; + my $config = get_config(); + + if (!server_is_running) { + die "Selenium Server isn't running!"; + } + + my $sel = Test::WWW::Selenium->new( + host => $config->{host}, + port => $config->{port}, + browser => $chrome_mode + ? $config->{experimental_browser_launcher} + : $config->{browser}, + browser_url => $config->{browser_url} + ); + + return ($sel, $config); } sub get_xmlrpc_client { - my $config = get_config(); - my $xmlrpc_url = $config->{browser_url} . "/" . - $config->{bugzilla_installation} . "/xmlrpc.cgi"; - - require QA::RPC::XMLRPC; - my $rpc = new QA::RPC::XMLRPC(proxy => $xmlrpc_url); - return ($rpc, $config); + my $config = get_config(); + my $xmlrpc_url + = $config->{browser_url} . "/" + . $config->{bugzilla_installation} + . "/xmlrpc.cgi"; + + require QA::RPC::XMLRPC; + my $rpc = new QA::RPC::XMLRPC(proxy => $xmlrpc_url); + return ($rpc, $config); } sub get_jsonrpc_client { - my ($get_mode) = @_; - require QA::RPC::JSONRPC; - my $rpc = new QA::RPC::JSONRPC(); - # If we don't set a long timeout, then the Bug.add_comment test - # where we add a too-large comment fails. - $rpc->transport->timeout(180); - $rpc->version($get_mode ? '1.1' : '1.0'); - $rpc->bz_get_mode($get_mode); - return $rpc; + my ($get_mode) = @_; + require QA::RPC::JSONRPC; + my $rpc = new QA::RPC::JSONRPC(); + + # If we don't set a long timeout, then the Bug.add_comment test + # where we add a too-large comment fails. + $rpc->transport->timeout(180); + $rpc->version($get_mode ? '1.1' : '1.0'); + $rpc->bz_get_mode($get_mode); + return $rpc; } sub get_rpc_clients { - my ($xmlrpc, $config) = get_xmlrpc_client(); - my $jsonrpc = get_jsonrpc_client(); - my $jsonrpc_get = get_jsonrpc_client('GET'); - return ($config, $xmlrpc, $jsonrpc, $jsonrpc_get); + my ($xmlrpc, $config) = get_xmlrpc_client(); + my $jsonrpc = get_jsonrpc_client(); + my $jsonrpc_get = get_jsonrpc_client('GET'); + return ($config, $xmlrpc, $jsonrpc, $jsonrpc_get); } ################################ @@ -163,182 +171,210 @@ sub get_rpc_clients { ################################ sub go_to_home { - my ($sel, $config) = @_; - $sel->open_ok("/$config->{bugzilla_installation}/", undef, "Go to the home page"); - $sel->set_speed(500); - $sel->title_is("Bugzilla Main Page"); + my ($sel, $config) = @_; + $sel->open_ok("/$config->{bugzilla_installation}/", + undef, "Go to the home page"); + $sel->set_speed(500); + $sel->title_is("Bugzilla Main Page"); } sub screenshot_page { - my ($sel, $filename) = @_; - open my $fh, '>:raw', $filename or die "unable to write $filename: $!"; - binmode $fh; - print $fh decode_base64($sel->capture_entire_page_screenshot_to_string()); - close $fh; + my ($sel, $filename) = @_; + open my $fh, '>:raw', $filename or die "unable to write $filename: $!"; + binmode $fh; + print $fh decode_base64($sel->capture_entire_page_screenshot_to_string()); + close $fh; } # Go to the home/login page and log in. sub log_in { - my ($sel, $config, $user) = @_; - - $sel->open_ok("/$config->{bugzilla_installation}/login", undef, "Go to the home page"); - $sel->title_is("Log in to Bugzilla"); - $sel->type_ok("Bugzilla_login", $config->{"${user}_user_login"}, "Enter $user login name"); - $sel->type_ok("Bugzilla_password", $config->{"${user}_user_passwd"}, "Enter $user password"); - $sel->click_ok("log_in", undef, "Submit credentials"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Bugzilla Main Page", "User is logged in"); + my ($sel, $config, $user) = @_; + + $sel->open_ok("/$config->{bugzilla_installation}/login", + undef, "Go to the home page"); + $sel->title_is("Log in to Bugzilla"); + $sel->type_ok( + "Bugzilla_login", + $config->{"${user}_user_login"}, + "Enter $user login name" + ); + $sel->type_ok( + "Bugzilla_password", + $config->{"${user}_user_passwd"}, + "Enter $user password" + ); + $sel->click_ok("log_in", undef, "Submit credentials"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Bugzilla Main Page", "User is logged in"); } # Log out. Will fail if you are not logged in. sub logout { - my $sel = shift; + my $sel = shift; - $sel->click_ok("link=Log out", undef, "Logout"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Logged Out"); + $sel->click_ok("link=Log out", undef, "Logout"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Logged Out"); } # Display the bug form to enter a bug in the given product. sub file_bug_in_product { - my ($sel, $product, $classification) = @_; - my $config = get_config(); - - $classification ||= "Unclassified"; - $sel->click_ok('//*[@class="link-file"]//a', undef, "Go create a new bug"); + my ($sel, $product, $classification) = @_; + my $config = get_config(); + + $classification ||= "Unclassified"; + $sel->click_ok('//*[@class="link-file"]//a', undef, "Go create a new bug"); + $sel->wait_for_page_to_load(WAIT_TIME); + my $title = $sel->get_title(); + if ($sel->is_text_present("Select Classification")) { + ok(1, + "More than one enterable classification available. Display them in a list"); + $sel->click_ok("link=$classification", undef, "Choose $classification"); $sel->wait_for_page_to_load(WAIT_TIME); - my $title = $sel->get_title(); - if ($sel->is_text_present("Select Classification")) { - ok(1, "More than one enterable classification available. Display them in a list"); - $sel->click_ok("link=$classification", undef, "Choose $classification"); - $sel->wait_for_page_to_load(WAIT_TIME); - $title = $sel->get_title(); - } - if ($sel->is_text_present("Which product is affected by the problem")) { - ok(1, "Which product is affected by the problem"); - $sel->click_ok("link=Other Products", undef, "Choose full product list"); - $sel->wait_for_page_to_load(WAIT_TIME); - $title = $sel->get_title(); - } - if ($sel->is_text_present($product)) { - ok(1, "Display the list of enterable products"); - $sel->open_ok("/" . $config->{bugzilla_installation} . "/enter_bug.cgi?product=$product&format=__default__", undef, "Choose product $product"); - $sel->wait_for_page_to_load(WAIT_TIME); - } - else { - ok(1, "Only one product available in $classification. Skipping the 'Choose product' page.") - } - $sel->title_is("Enter Bug: $product", "Display form to enter bug data"); - # Always make sure all fields are visible - if ($sel->is_element_present('//input[@value="Show Advanced Fields"]')) { - $sel->click_ok('//input[@value="Show Advanced Fields"]'); - } + $title = $sel->get_title(); + } + if ($sel->is_text_present("Which product is affected by the problem")) { + ok(1, "Which product is affected by the problem"); + $sel->click_ok("link=Other Products", undef, "Choose full product list"); + $sel->wait_for_page_to_load(WAIT_TIME); + $title = $sel->get_title(); + } + if ($sel->is_text_present($product)) { + ok(1, "Display the list of enterable products"); + $sel->open_ok( + "/" + . $config->{bugzilla_installation} + . "/enter_bug.cgi?product=$product&format=__default__", + undef, + "Choose product $product" + ); + $sel->wait_for_page_to_load(WAIT_TIME); + } + else { + ok(1, + "Only one product available in $classification. Skipping the 'Choose product' page." + ); + } + $sel->title_is("Enter Bug: $product", "Display form to enter bug data"); + + # Always make sure all fields are visible + if ($sel->is_element_present('//input[@value="Show Advanced Fields"]')) { + $sel->click_ok('//input[@value="Show Advanced Fields"]'); + } } sub create_bug { - my ($sel, $bug_summary) = @_; - my $ndash = NDASH; - - $sel->click_ok('commit'); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - my $bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]'); - $sel->title_like(qr/$bug_id $ndash( \(.*\))? $bug_summary/, "Bug $bug_id created with summary '$bug_summary'"); - return $bug_id; + my ($sel, $bug_summary) = @_; + my $ndash = NDASH; + + $sel->click_ok('commit'); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + my $bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]'); + $sel->title_like( + qr/$bug_id $ndash( \(.*\))? $bug_summary/, + "Bug $bug_id created with summary '$bug_summary'" + ); + return $bug_id; } sub edit_bug { - my ($sel, $bug_id, $bug_summary, $options) = @_; - my $btn_id = $options ? $options->{id} : 'commit'; - $sel->click_ok($btn_id); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->is_text_present_ok("Changes submitted for bug $bug_id"); + my ($sel, $bug_id, $bug_summary, $options) = @_; + my $btn_id = $options ? $options->{id} : 'commit'; + $sel->click_ok($btn_id); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->is_text_present_ok("Changes submitted for bug $bug_id"); } sub edit_bug_and_return { - my ($sel, $bug_id, $bug_summary, $options) = @_; - my $ndash = NDASH; - edit_bug($sel, $bug_id, $bug_summary, $options); - $sel->click_ok("//a[contains(\@href, 'show_bug.cgi?id=$bug_id')]"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("$bug_id $ndash $bug_summary", "Returning back to bug $bug_id"); + my ($sel, $bug_id, $bug_summary, $options) = @_; + my $ndash = NDASH; + edit_bug($sel, $bug_id, $bug_summary, $options); + $sel->click_ok("//a[contains(\@href, 'show_bug.cgi?id=$bug_id')]"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("$bug_id $ndash $bug_summary", "Returning back to bug $bug_id"); } # Go to show_bug.cgi. sub go_to_bug { - my ($sel, $bug_id) = @_; - - $sel->type_ok("quicksearch_top", $bug_id); - $sel->submit("header-search"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - my $bug_title = $sel->get_title(); - utf8::encode($bug_title) if utf8::is_utf8($bug_title); - $sel->title_like(qr/^$bug_id /, $bug_title); + my ($sel, $bug_id) = @_; + + $sel->type_ok("quicksearch_top", $bug_id); + $sel->submit("header-search"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + my $bug_title = $sel->get_title(); + utf8::encode($bug_title) if utf8::is_utf8($bug_title); + $sel->title_like(qr/^$bug_id /, $bug_title); } # Go to admin.cgi. sub go_to_admin { - my $sel = shift; + my $sel = shift; - $sel->click_ok("link=Administration", undef, "Go to the Admin page"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_like(qr/^Administer your installation/, "Display admin.cgi"); + $sel->click_ok("link=Administration", undef, "Go to the Admin page"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_like(qr/^Administer your installation/, "Display admin.cgi"); } # Go to editproducts.cgi and display the given product. sub edit_product { - my ($sel, $product, $classification) = @_; - - $classification ||= "Unclassified"; - go_to_admin($sel); - $sel->click_ok("link=Products", undef, "Go to the Products page"); + my ($sel, $product, $classification) = @_; + + $classification ||= "Unclassified"; + go_to_admin($sel); + $sel->click_ok("link=Products", undef, "Go to the Products page"); + $sel->wait_for_page_to_load(WAIT_TIME); + my $title = $sel->get_title(); + if ($title eq "Select Classification") { + ok(1, + "More than one enterable classification available. Display them in a list"); + $sel->click_ok("link=$classification", undef, "Choose $classification"); $sel->wait_for_page_to_load(WAIT_TIME); - my $title = $sel->get_title(); - if ($title eq "Select Classification") { - ok(1, "More than one enterable classification available. Display them in a list"); - $sel->click_ok("link=$classification", undef, "Choose $classification"); - $sel->wait_for_page_to_load(WAIT_TIME); - } - else { - $sel->title_is("Select product", "Display the list of enterable products"); - } - $sel->click_ok("link=$product", undef, "Choose $product"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Edit Product '$product'", "Display properties of $product"); + } + else { + $sel->title_is("Select product", "Display the list of enterable products"); + } + $sel->click_ok("link=$product", undef, "Choose $product"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Edit Product '$product'", "Display properties of $product"); } sub add_product { - my ($sel, $classification) = @_; - - $classification ||= "Unclassified"; - go_to_admin($sel); - $sel->click_ok("link=Products", undef, "Go to the Products page"); - $sel->wait_for_page_to_load(WAIT_TIME); - my $title = $sel->get_title(); - if ($title eq "Select Classification") { - ok(1, "More than one enterable classification available. Display them in a list"); - $sel->click_ok("//a[contains(\@href, 'editproducts.cgi?action=add&classification=$classification')]", - undef, "Add product to $classification"); - } - else { - $sel->title_is("Select product", "Display the list of enterable products"); - $sel->click_ok("link=Add", undef, "Add a new product"); - } - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Add Product", "Display the new product form"); + my ($sel, $classification) = @_; + + $classification ||= "Unclassified"; + go_to_admin($sel); + $sel->click_ok("link=Products", undef, "Go to the Products page"); + $sel->wait_for_page_to_load(WAIT_TIME); + my $title = $sel->get_title(); + if ($title eq "Select Classification") { + ok(1, + "More than one enterable classification available. Display them in a list"); + $sel->click_ok( + "//a[contains(\@href, 'editproducts.cgi?action=add&classification=$classification')]", + undef, + "Add product to $classification" + ); + } + else { + $sel->title_is("Select product", "Display the list of enterable products"); + $sel->click_ok("link=Add", undef, "Add a new product"); + } + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Add Product", "Display the new product form"); } sub open_advanced_search_page { - my $sel = shift; - - $sel->click_ok('//*[@class="link-search"]//a'); + my $sel = shift; + + $sel->click_ok('//*[@class="link-search"]//a'); + $sel->wait_for_page_to_load(WAIT_TIME); + my $title = $sel->get_title(); + if ($title eq "Simple Search") { + ok(1, "Display the simple search form"); + $sel->click_ok("link=Advanced Search"); $sel->wait_for_page_to_load(WAIT_TIME); - my $title = $sel->get_title(); - if ($title eq "Simple Search") { - ok(1, "Display the simple search form"); - $sel->click_ok("link=Advanced Search"); - $sel->wait_for_page_to_load(WAIT_TIME); - } - $sel->title_is("Search for bugs", "Display the Advanced search form"); + } + $sel->title_is("Search for bugs", "Display the Advanced search form"); } # $params is a hashref of the form: @@ -354,74 +390,75 @@ sub open_advanced_search_page { # undef is for radio buttons (in which case the parameter must be the ID of the radio button) # value => 'foo' is the value of the parameter (either text or label) sub set_parameters { - my ($sel, $params) = @_; - - go_to_admin($sel); - $sel->click_ok("link=Parameters", undef, "Go to the Config Parameters page"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Configuration: General"); - my $last_section = "General"; - - foreach my $section (keys %$params) { - if ($section ne $last_section) { - $sel->click_ok("link=$section"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Configuration: $section"); - $last_section = $section; + my ($sel, $params) = @_; + + go_to_admin($sel); + $sel->click_ok("link=Parameters", undef, "Go to the Config Parameters page"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Configuration: General"); + my $last_section = "General"; + + foreach my $section (keys %$params) { + if ($section ne $last_section) { + $sel->click_ok("link=$section"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Configuration: $section"); + $last_section = $section; + } + my $param_list = $params->{$section}; + foreach my $param (keys %$param_list) { + my $data = $param_list->{$param}; + if (defined $data) { + my $type = $data->{type}; + my $value = $data->{value}; + + if ($type eq 'text') { + $sel->type_ok($param, $value); + } + elsif ($type eq 'select') { + $sel->select_ok($param, "label=$value"); } - my $param_list = $params->{$section}; - foreach my $param (keys %$param_list) { - my $data = $param_list->{$param}; - if (defined $data) { - my $type = $data->{type}; - my $value = $data->{value}; - - if ($type eq 'text') { - $sel->type_ok($param, $value); - } - elsif ($type eq 'select') { - $sel->select_ok($param, "label=$value"); - } - else { - ok(0, "Unknown parameter type: $type"); - } - } - else { - # If the value is undefined, then the param name is - # the ID of the radio button. - $sel->click_ok($param); - } + else { + ok(0, "Unknown parameter type: $type"); } - $sel->click_ok('//input[@type="submit" and @value="Save Changes"]', undef, "Save Changes"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Parameters Updated"); + } + else { + # If the value is undefined, then the param name is + # the ID of the radio button. + $sel->click_ok($param); + } } + $sel->click_ok('//input[@type="submit" and @value="Save Changes"]', + undef, "Save Changes"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Parameters Updated"); + } } my @ANY_KEYS = qw( t token ); sub check_page_load { - my ($sel, $wait, $expected) = @_; - my $expected_uri = URI->new($expected); - $sel->wait_for_page_to_load_ok($wait); - my $uri = URI->new($sel->get_location); - - foreach my $u ($expected_uri, $uri) { - $u->host('HOSTNAME'); - foreach my $any_key (@ANY_KEYS) { - if ($u->query_param($any_key)) { - $u->query_param($any_key => '__ANYTHING__'); - } - } + my ($sel, $wait, $expected) = @_; + my $expected_uri = URI->new($expected); + $sel->wait_for_page_to_load_ok($wait); + my $uri = URI->new($sel->get_location); + + foreach my $u ($expected_uri, $uri) { + $u->host('HOSTNAME'); + foreach my $any_key (@ANY_KEYS) { + if ($u->query_param($any_key)) { + $u->query_param($any_key => '__ANYTHING__'); + } } + } - if ($expected_uri->query_param('id')) { - if ($expected_uri->query_param('id') eq '__BUG_ID__') { - $uri->query_param('id' => '__BUG_ID__'); - } + if ($expected_uri->query_param('id')) { + if ($expected_uri->query_param('id') eq '__BUG_ID__') { + $uri->query_param('id' => '__BUG_ID__'); } - my ($pkg, $file, $line) = caller; - is($uri, $expected_uri, "checking location on $file line $line"); + } + my ($pkg, $file, $line) = caller; + is($uri, $expected_uri, "checking location on $file line $line"); } 1; diff --git a/qa/t/rest_bugzilla.t b/qa/t/rest_bugzilla.t index 01ee6af65..b4185d000 100644 --- a/qa/t/rest_bugzilla.t +++ b/qa/t/rest_bugzilla.t @@ -17,7 +17,7 @@ use lib qw(lib ../../lib ../../local/lib/perl5); use Test::More tests => 11; use QA::REST; -my $rest = get_rest_client(); +my $rest = get_rest_client(); my $config = $rest->bz_config; my $version = $rest->call('version')->{version}; @@ -26,31 +26,42 @@ ok($version, "GET /rest/version returns $version"); my $extensions = $rest->call('extensions')->{extensions}; isa_ok($extensions, 'HASH', 'GET /rest/extensions'); my @ext_names = sort keys %$extensions; + # There is always at least the QA extension enabled. -ok(scalar(@ext_names), scalar(@ext_names) . ' extension(s) found: ' . join(', ', @ext_names)); -ok($extensions->{QA}, 'The QA extension is enabled, with version ' . $extensions->{QA}->{version}); +ok(scalar(@ext_names), + scalar(@ext_names) . ' extension(s) found: ' . join(', ', @ext_names)); +ok($extensions->{QA}, + 'The QA extension is enabled, with version ' . $extensions->{QA}->{version}); my $timezone = $rest->call('timezone')->{timezone}; ok($timezone, "GET /rest/timezone retuns $timezone"); my $time = $rest->call('time'); foreach my $type (qw(db_time web_time)) { - ok($time->{$type}, "GET /rest/time returns $type = " . $time->{$type}); + ok($time->{$type}, "GET /rest/time returns $type = " . $time->{$type}); } # Logged-out users can only access the maintainer and requirelogin parameters. -my $params = $rest->call('parameters')->{parameters}; +my $params = $rest->call('parameters')->{parameters}; my @param_names = sort keys %$params; -ok(@param_names == 2 && defined $params->{maintainer} && defined $params->{requirelogin}, - 'Only 2 parameters accessible to logged-out users: ' . join(', ', @param_names)); +ok( + @param_names == 2 + && defined $params->{maintainer} + && defined $params->{requirelogin}, + 'Only 2 parameters accessible to logged-out users: ' . join(', ', @param_names) +); # Powerless users can access much more parameters. -$params = $rest->call('parameters', { api_key => $config->{unprivileged_user_api_key} })->{parameters}; +$params + = $rest->call('parameters', {api_key => $config->{unprivileged_user_api_key}}) + ->{parameters}; @param_names = sort keys %$params; -ok(@param_names > 2, scalar(@param_names) . ' parameters accessible to powerless users'); +ok(@param_names > 2, + scalar(@param_names) . ' parameters accessible to powerless users'); # Admins can access all parameters. -$params = $rest->call('parameters', { api_key => $config->{admin_user_api_key} })->{parameters}; +$params = $rest->call('parameters', {api_key => $config->{admin_user_api_key}}) + ->{parameters}; @param_names = sort keys %$params; ok(@param_names > 2, scalar(@param_names) . ' parameters accessible to admins'); diff --git a/qa/t/rest_classification.t b/qa/t/rest_classification.t index f5485de2b..e1083e98a 100644 --- a/qa/t/rest_classification.t +++ b/qa/t/rest_classification.t @@ -17,43 +17,67 @@ use lib qw(lib ../../lib ../../local/lib/perl5); use Test::More tests => 7; use QA::REST; -my $rest = get_rest_client(); +my $rest = get_rest_client(); my $config = $rest->bz_config; -my $args = { api_key => $config->{admin_user_api_key} }; +my $args = {api_key => $config->{admin_user_api_key}}; my $params = $rest->call('parameters', $args)->{parameters}; my $use_class = $params->{useclassification}; -ok(defined($use_class), 'Classifications are ' . ($use_class ? 'enabled' : 'disabled')); +ok(defined($use_class), + 'Classifications are ' . ($use_class ? 'enabled' : 'disabled')); # Admins can always access classifications, even when they are disabled. my $class = $rest->call('classification/1', $args)->{classifications}->[0]; -ok($class->{id}, "Admin found classification '" . $class->{name} . "' with the description '" . $class->{description} . "'"); -my @products = sort map { $_->{name} } @{ $class->{products} }; -ok(scalar(@products), scalar(@products) . ' product(s) found: ' . join(', ', @products)); +ok($class->{id}, + "Admin found classification '" + . $class->{name} + . "' with the description '" + . $class->{description} + . "'"); +my @products = sort map { $_->{name} } @{$class->{products}}; +ok(scalar(@products), + scalar(@products) . ' product(s) found: ' . join(', ', @products)); $class = $rest->call('classification/Class2_QA', $args)->{classifications}->[0]; -ok($class->{id}, "Admin found classification '" . $class->{name} . "' with the description '" . $class->{description} . "'"); -@products = sort map { $_->{name} } @{ $class->{products} }; -ok(scalar(@products), scalar(@products) . ' product(s) found: ' . join(', ', @products)); +ok($class->{id}, + "Admin found classification '" + . $class->{name} + . "' with the description '" + . $class->{description} + . "'"); +@products = sort map { $_->{name} } @{$class->{products}}; +ok(scalar(@products), + scalar(@products) . ' product(s) found: ' . join(', ', @products)); if ($use_class) { - # When classifications are enabled, everybody can query classifications... - # ... including logged-out users. - $class = $rest->call('classification/1')->{classifications}->[0]; - ok($class->{id}, 'Logged-out users can access classification ' . $class->{name}); - # ... and non-admins. - $class = $rest->call('classification/1', { api_key => $config->{editbugs_user_api_key} })->{classifications}->[0]; - ok($class->{id}, 'Non-admins can access classification ' . $class->{name}); + + # When classifications are enabled, everybody can query classifications... + # ... including logged-out users. + $class = $rest->call('classification/1')->{classifications}->[0]; + ok($class->{id}, + 'Logged-out users can access classification ' . $class->{name}); + + # ... and non-admins. + $class = $rest->call('classification/1', + {api_key => $config->{editbugs_user_api_key}})->{classifications}->[0]; + ok($class->{id}, 'Non-admins can access classification ' . $class->{name}); } else { - # When classifications are disabled, only users in the 'editclassifications' - # group can access this method... - # ... logged-out users get an error. - my $error = $rest->call('classification/1', undef, undef, MUST_FAIL); - ok($error->{error} && $error->{code} == 900, - 'Logged-out users cannot query classifications when disabled: ' . $error->{message}); - # ... as well as non-admins. - $error = $rest->call('classification/1', { api_key => $config->{editbugs_user_api_key} }, undef, MUST_FAIL); - ok($error->{error} && $error->{code} == 900, - 'Non-admins cannot query classifications when disabled: ' . $error->{message}); + # When classifications are disabled, only users in the 'editclassifications' + # group can access this method... + # ... logged-out users get an error. + my $error = $rest->call('classification/1', undef, undef, MUST_FAIL); + ok( + $error->{error} && $error->{code} == 900, + 'Logged-out users cannot query classifications when disabled: ' + . $error->{message} + ); + + # ... as well as non-admins. + $error + = $rest->call('classification/1', + {api_key => $config->{editbugs_user_api_key}}, + undef, MUST_FAIL); + ok($error->{error} && $error->{code} == 900, + 'Non-admins cannot query classifications when disabled: ' . $error->{message}); } diff --git a/qa/t/selenium_server_start.t b/qa/t/selenium_server_start.t index c08db293c..e70216433 100644 --- a/qa/t/selenium_server_start.t +++ b/qa/t/selenium_server_start.t @@ -9,9 +9,11 @@ use strict; use warnings; use constant DISPLAY => 99; + #use constant DISPLAY => 0; use Test::More tests => 12; + #use Test::More tests => 4; my $pid; @@ -21,63 +23,63 @@ $pid = xserver_start(); ok($pid, "X Server started with PID $pid on display " . DISPLAY); ok(open(XPID, ">testing.x.pid"), "Opening testing.x.pid"); ok((print XPID $pid), "Writing testing.x.pid"); -ok(close(XPID), "Closing testing.x.pid"); +ok(close(XPID), "Closing testing.x.pid"); # Start the VNC service second ok($pid = vnc_start(), "VNC desktop started with PID $pid"); ok(open(VNCPID, ">testing.vnc.pid"), "Opening testing.vnc.pid"); ok((print VNCPID $pid), "Writing testing.vnc.pid"); -ok(close(VNCPID), "Closing testing.vnc.pid"); +ok(close(VNCPID), "Closing testing.vnc.pid"); # Start the selenium server third ok($pid = selenium_start(), "Selenium RC server started with PID $pid"); ok(open(SPID, ">testing.selenium.pid"), "Opening testing.selenium.pid"); ok((print SPID $pid), "Writing testing.selenium.pid"); -ok(close(SPID), "Closing testing.selenium.pid"); +ok(close(SPID), "Closing testing.selenium.pid"); sleep(10); # Subroutines sub xserver_start { - my $pid; - my @x_cmd = qw(Xvfb -ac -screen 0 1600x1200x24 -fbdir /tmp); - push(@x_cmd, ":" . DISPLAY); - $pid = fork(); - if (!$pid) { - open(STDOUT, ">/dev/null"); - open(STDERR, ">/dev/null"); - exec(@x_cmd) || die "unable to execute: $!"; - } - else { - return $pid; - } - return 0; + my $pid; + my @x_cmd = qw(Xvfb -ac -screen 0 1600x1200x24 -fbdir /tmp); + push(@x_cmd, ":" . DISPLAY); + $pid = fork(); + if (!$pid) { + open(STDOUT, ">/dev/null"); + open(STDERR, ">/dev/null"); + exec(@x_cmd) || die "unable to execute: $!"; + } + else { + return $pid; + } + return 0; } sub vnc_start { - my @vnc_cmd = qw(x11vnc -viewonly -forever -nopw -quiet -display); - push(@vnc_cmd, ":" . DISPLAY); - my $pid = fork(); - if (!$pid) { - open(STDOUT, ">/dev/null"); - open(STDERR, ">/dev/null"); - exec(@vnc_cmd) || die "unabled to execute: $!"; - } - return $pid; + my @vnc_cmd = qw(x11vnc -viewonly -forever -nopw -quiet -display); + push(@vnc_cmd, ":" . DISPLAY); + my $pid = fork(); + if (!$pid) { + open(STDOUT, ">/dev/null"); + open(STDERR, ">/dev/null"); + exec(@vnc_cmd) || die "unabled to execute: $!"; + } + return $pid; } sub selenium_start { - my @selenium_cmd = qw(java -jar ../config/selenium-server-standalone.jar - -firefoxProfileTemplate ../config/firefox - -log ../config/selenium.log - -singlewindow); - unshift(@selenium_cmd, "env", "DISPLAY=:" . DISPLAY); - my $pid = fork(); - if (!$pid) { - open(STDOUT, ">/dev/null"); - open(STDERR, ">/dev/null"); - exec(@selenium_cmd) || die "unable to execute: $!"; - } - return $pid; + my @selenium_cmd = qw(java -jar ../config/selenium-server-standalone.jar + -firefoxProfileTemplate ../config/firefox + -log ../config/selenium.log + -singlewindow); + unshift(@selenium_cmd, "env", "DISPLAY=:" . DISPLAY); + my $pid = fork(); + if (!$pid) { + open(STDOUT, ">/dev/null"); + open(STDERR, ">/dev/null"); + exec(@selenium_cmd) || die "unable to execute: $!"; + } + return $pid; } diff --git a/qa/t/selenium_server_stop.t b/qa/t/selenium_server_stop.t index 62a29f38c..7633e57d7 100644 --- a/qa/t/selenium_server_stop.t +++ b/qa/t/selenium_server_stop.t @@ -29,6 +29,6 @@ ok(unlink("testing.vnc.pid"), "Removing testing.vnc.pid"); # Stop the Xvfb server third ok(open(XPID, "), "Reading testing.x.pid"); -ok(close(XPID), "Closing testing.x.pid"); +ok(close(XPID), "Closing testing.x.pid"); ok(kill(9, $pid), "Killing process $pid"); ok(unlink("testing.x.pid"), "Removing testing.x.pid"); diff --git a/qa/t/test_bmo_autolinkification.t b/qa/t/test_bmo_autolinkification.t index af61f09a4..a8bce05cd 100644 --- a/qa/t/test_bmo_autolinkification.t +++ b/qa/t/test_bmo_autolinkification.t @@ -19,7 +19,7 @@ log_in($sel, $config, 'unprivileged'); file_bug_in_product($sel, 'TestProduct'); my $bug_summary = "linkification test bug"; $sel->type_ok("short_desc", $bug_summary); -$sel->type_ok("comment", "linkification test"); +$sel->type_ok("comment", "linkification test"); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_like(qr/\d+ \S $bug_summary/, "Bug created"); @@ -31,7 +31,9 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_like(qr/\d+ \S $bug_summary/, "crash report added"); $sel->click_ok("link=bug $bug_id"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->attribute_is('link=bp-63f096f7-253b-4ee2-ae3d-8bb782090824@href', 'https://crash-stats.mozilla.com/report/index/63f096f7-253b-4ee2-ae3d-8bb782090824'); +$sel->attribute_is('link=bp-63f096f7-253b-4ee2-ae3d-8bb782090824@href', + 'https://crash-stats.mozilla.com/report/index/63f096f7-253b-4ee2-ae3d-8bb782090824' +); $sel->type_ok("comment", "CVE-2010-2884"); $sel->click_ok("commit"); @@ -39,7 +41,8 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_like(qr/\d+ \S $bug_summary/, "cve added"); $sel->click_ok("link=bug $bug_id"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->attribute_is('link=CVE-2010-2884@href', 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2884'); +$sel->attribute_is('link=CVE-2010-2884@href', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2010-2884'); $sel->type_ok("comment", "r12345"); $sel->click_ok("commit"); @@ -47,7 +50,8 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_like(qr/\d+ \S $bug_summary/, "svn revision added"); $sel->click_ok("link=bug $bug_id"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->attribute_is('link=r12345@href', 'https://viewvc.svn.mozilla.org/vc?view=rev&revision=12345'); +$sel->attribute_is('link=r12345@href', + 'https://viewvc.svn.mozilla.org/vc?view=rev&revision=12345'); logout($sel); diff --git a/qa/t/test_bmo_enter_new_bug.t b/qa/t/test_bmo_enter_new_bug.t index 5f015ab64..bdf01a625 100644 --- a/qa/t/test_bmo_enter_new_bug.t +++ b/qa/t/test_bmo_enter_new_bug.t @@ -23,7 +23,7 @@ use QA::Util; my ($sel, $config) = get_selenium(); log_in($sel, $config, 'admin'); -set_parameters($sel, { "Bug Fields" => {"useclassification-off" => undef} }); +set_parameters($sel, {"Bug Fields" => {"useclassification-off" => undef}}); # mktgevent and swag are dependent so we create the mktgevent bug first so # we can provide the bug id to swag @@ -90,15 +90,23 @@ _check_product('Marketing'); _check_component('Marketing', 'Trademark Permissions'); _check_group('marketing-private'); -$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Marketing&format=trademark"); +$sel->open_ok( + "/$config->{bugzilla_installation}/enter_bug.cgi?product=Marketing&format=trademark" +); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Trademark Usage Requests", "Open custom bug entry form - trademark"); +$sel->title_is("Trademark Usage Requests", + "Open custom bug entry form - trademark"); $sel->type_ok("short_desc", "Bug created by Selenium", "Enter bug summary"); -$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description"); +$sel->type_ok( + "comment", + "--- Bug created by Selenium ---", + "Enter bug description" +); $sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok('has been added to the database', 'Bug created'); -my $trademark_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]'); +my $trademark_bug_id + = $sel->get_value('//input[@name="id" and @type="hidden"]'); # itrequest @@ -173,14 +181,25 @@ _check_component('mozilla.org', 'Discussion Forums'); _check_version('mozilla.org', 'other'); _check_component('mozilla.org', 'Discussion Forums'); -$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=mozilla.org&format=mozlist"); +$sel->open_ok( + "/$config->{bugzilla_installation}/enter_bug.cgi?product=mozilla.org&format=mozlist" +); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Mozilla Discussion Forum", "Open custom bug entry form - mozlist"); +$sel->title_is("Mozilla Discussion Forum", + "Open custom bug entry form - mozlist"); $sel->type_ok("listName", "test-list", "Enter name for mailing list"); -$sel->type_ok("listAdmin", $config->{'admin_user_login'}, "Enter list administator"); +$sel->type_ok( + "listAdmin", + $config->{'admin_user_login'}, + "Enter list administator" +); $sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address"); $sel->check_ok("name=groups", "value=infra", "Select private group"); -$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description"); +$sel->type_ok( + "comment", + "--- Bug created by Selenium ---", + "Enter bug description" +); $sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok('has been added to the database', 'Bug created'); @@ -214,15 +233,25 @@ _check_product('Legal'); _check_component('Legal', 'Contract Request'); _check_group('mozilla-employee-confidential'); -$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Legal&format=legal"); +$sel->open_ok( + "/$config->{bugzilla_installation}/enter_bug.cgi?product=Legal&format=legal"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Mozilla Corporation Legal Requests", "Open custom bug entry form - legal"); +$sel->title_is("Mozilla Corporation Legal Requests", + "Open custom bug entry form - legal"); $sel->select_ok("component", "value=Contract Request", "Select request type"); -$sel->select_ok("business_unit", "value=Connected Devices", "Select business unit"); +$sel->select_ok( + "business_unit", + "value=Connected Devices", + "Select business unit" +); $sel->type_ok("short_desc", "Bug created by Selenium", "Enter request summary"); $sel->type_ok("cc", $config->{'unprivileged_user_login'}, "Enter cc address"); $sel->type_ok("important_dates", "Important dates", "Enter important dates"); -$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter request description"); +$sel->type_ok( + "comment", + "--- Bug created by Selenium ---", + "Enter request description" +); $sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok('has been added to the database', 'Bug created'); @@ -234,189 +263,210 @@ _check_product('Websites', 'other'); _check_component('Websites', 'www.mozilla.org'); _check_user('liz@mozilla.com'); -$sel->open_ok("/$config->{bugzilla_installation}/enter_bug.cgi?product=Websites&format=poweredby"); +$sel->open_ok( + "/$config->{bugzilla_installation}/enter_bug.cgi?product=Websites&format=poweredby" +); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Powered by Mozilla Logo Requests", "Open custom bug entry form - poweredby"); +$sel->title_is("Powered by Mozilla Logo Requests", + "Open custom bug entry form - poweredby"); $sel->type_ok("short_desc", "Bug created by Selenium", "Enter bug summary"); -$sel->type_ok("comment", "--- Bug created by Selenium ---", "Enter bug description"); +$sel->type_ok( + "comment", + "--- Bug created by Selenium ---", + "Enter bug description" +); $sel->click_ok("commit", undef, "Submit bug data to post_bug.cgi"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok('has been added to the database', 'Bug created'); -my $poweredby_bug_id = $sel->get_value('//input[@name="id" and @type="hidden"]'); +my $poweredby_bug_id + = $sel->get_value('//input[@name="id" and @type="hidden"]'); -set_parameters($sel, { "Bug Fields" => {"useclassification-on" => undef} }); +set_parameters($sel, {"Bug Fields" => {"useclassification-on" => undef}}); logout($sel); sub _check_product { - my ($product, $version) = @_; - - go_to_admin($sel); - $sel->click_ok("link=Products"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Select product"); - - my $product_description = "$product Description"; - - my $text = trim($sel->get_text("bugzilla-body")); - if ($text =~ /$product_description/) { - # Product exists already - return 1; - } - - $sel->click_ok("link=Add"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Add Product"); - $sel->type_ok("product", $product); - $sel->type_ok("description", $product_description); - $sel->type_ok("version", $version) if $version; - $sel->select_ok("security_group_id", "label=core-security"); - $sel->select_ok("default_op_sys_id", "Unspecified"); - $sel->select_ok("default_platform_id", "Unspecified"); - $sel->click_ok('//input[@type="submit" and @value="Add"]'); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $text = trim($sel->get_text("message")); - ok($text =~ /You will need to add at least one component before anyone can enter bugs against this product/, - "Display a reminder about missing components"); + my ($product, $version) = @_; + go_to_admin($sel); + $sel->click_ok("link=Products"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Select product"); + + my $product_description = "$product Description"; + + my $text = trim($sel->get_text("bugzilla-body")); + if ($text =~ /$product_description/) { + + # Product exists already return 1; + } + + $sel->click_ok("link=Add"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Add Product"); + $sel->type_ok("product", $product); + $sel->type_ok("description", $product_description); + $sel->type_ok("version", $version) if $version; + $sel->select_ok("security_group_id", "label=core-security"); + $sel->select_ok("default_op_sys_id", "Unspecified"); + $sel->select_ok("default_platform_id", "Unspecified"); + $sel->click_ok('//input[@type="submit" and @value="Add"]'); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $text = trim($sel->get_text("message")); + ok( + $text + =~ /You will need to add at least one component before anyone can enter bugs against this product/, + "Display a reminder about missing components" + ); + + return 1; } sub _check_component { - my ($product, $component) = @_; - - go_to_admin($sel); - $sel->click_ok("link=components"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Edit components for which product?"); - - $sel->click_ok("//*[\@id='bugzilla-body']//a[normalize-space(text())='$product']"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Select component of product '$product'"); - - my $component_description = "$component Description"; - - my $text = trim($sel->get_text("bugzilla-body")); - if ($text =~ /$component_description/) { - # Component exists already - return 1; - } - - # Add the watch user for component watching - my $watch_user = lc $component . "@" . lc $product . ".bugs"; - $watch_user =~ s/ & /-/; - $watch_user =~ s/\s+/\-/g; - $watch_user =~ s/://g; - - go_to_admin($sel); - $sel->click_ok("link=components"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Edit components for which product?"); - $sel->click_ok("//*[\@id='bugzilla-body']//a[normalize-space(text())='$product']"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Select component of product '$product'"); - $sel->click_ok("link=Add"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Add component to the $product product"); - $sel->type_ok("component", $component); - $sel->type_ok("description", $component_description); - $sel->type_ok("initialowner", $config->{'admin_user_login'}); - $sel->uncheck_ok("watch_user_auto"); - $sel->type_ok("watch_user", $watch_user); - $sel->check_ok("watch_user_auto"); - $sel->click_ok('//input[@type="submit" and @value="Add"]'); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Component Created"); - $text = trim($sel->get_text("message")); - ok($text eq "The component $component has been created.", "Component successfully created"); + my ($product, $component) = @_; + + go_to_admin($sel); + $sel->click_ok("link=components"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Edit components for which product?"); + $sel->click_ok( + "//*[\@id='bugzilla-body']//a[normalize-space(text())='$product']"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Select component of product '$product'"); + + my $component_description = "$component Description"; + + my $text = trim($sel->get_text("bugzilla-body")); + if ($text =~ /$component_description/) { + + # Component exists already return 1; + } + + # Add the watch user for component watching + my $watch_user = lc $component . "@" . lc $product . ".bugs"; + $watch_user =~ s/ & /-/; + $watch_user =~ s/\s+/\-/g; + $watch_user =~ s/://g; + + go_to_admin($sel); + $sel->click_ok("link=components"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Edit components for which product?"); + $sel->click_ok( + "//*[\@id='bugzilla-body']//a[normalize-space(text())='$product']"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Select component of product '$product'"); + $sel->click_ok("link=Add"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Add component to the $product product"); + $sel->type_ok("component", $component); + $sel->type_ok("description", $component_description); + $sel->type_ok("initialowner", $config->{'admin_user_login'}); + $sel->uncheck_ok("watch_user_auto"); + $sel->type_ok("watch_user", $watch_user); + $sel->check_ok("watch_user_auto"); + $sel->click_ok('//input[@type="submit" and @value="Add"]'); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Component Created"); + $text = trim($sel->get_text("message")); + ok($text eq "The component $component has been created.", + "Component successfully created"); + + return 1; } sub _check_group { - my ($group) = @_; - - go_to_admin($sel); - $sel->click_ok("link=Groups"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Edit Groups"); - - my $group_description = "$group Description"; - - my $text = trim($sel->get_text("bugzilla-body")); - if ($text =~ /$group_description/) { - # Group exists already - return 1; - } - - $sel->title_is("Edit Groups"); - $sel->click_ok("link=Add Group"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Add group"); - $sel->type_ok("name", $group); - $sel->type_ok("desc", $group_description); - $sel->type_ok("owner", $config->{'admin_user_login'}); - $sel->check_ok("isactive"); - $sel->check_ok("insertnew"); - $sel->click_ok("create"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("New Group Created"); - my $group_id = $sel->get_value("group_id"); + my ($group) = @_; + + go_to_admin($sel); + $sel->click_ok("link=Groups"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Edit Groups"); + my $group_description = "$group Description"; + + my $text = trim($sel->get_text("bugzilla-body")); + if ($text =~ /$group_description/) { + + # Group exists already return 1; + } + + $sel->title_is("Edit Groups"); + $sel->click_ok("link=Add Group"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Add group"); + $sel->type_ok("name", $group); + $sel->type_ok("desc", $group_description); + $sel->type_ok("owner", $config->{'admin_user_login'}); + $sel->check_ok("isactive"); + $sel->check_ok("insertnew"); + $sel->click_ok("create"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("New Group Created"); + my $group_id = $sel->get_value("group_id"); + + return 1; } sub _check_version { - my ($product, $version) = @_; - - go_to_admin($sel); - $sel->click_ok("link=versions"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Edit versions for which product?"); - $sel->click_ok("//*[\@id='bugzilla-body']//a[normalize-space(text())='$product']"); - $sel->wait_for_page_to_load(WAIT_TIME); - - my $text = trim($sel->get_text("bugzilla-body")); - if ($text =~ /$version/) { - # Version exists already - return 1; - } - - $sel->click_ok("link=Add"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_like(qr/^Add Version to Product/); - $sel->type_ok("version", $version); - $sel->click_ok("create"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Version Created"); + my ($product, $version) = @_; + + go_to_admin($sel); + $sel->click_ok("link=versions"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Edit versions for which product?"); + $sel->click_ok( + "//*[\@id='bugzilla-body']//a[normalize-space(text())='$product']"); + $sel->wait_for_page_to_load(WAIT_TIME); + + my $text = trim($sel->get_text("bugzilla-body")); + if ($text =~ /$version/) { + # Version exists already return 1; + } + + $sel->click_ok("link=Add"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_like(qr/^Add Version to Product/); + $sel->type_ok("version", $version); + $sel->click_ok("create"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Version Created"); + + return 1; } sub _check_user { - my ($user) = @_; - - go_to_admin($sel); - $sel->click_ok("link=Users"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is("Search users"); - $sel->type_ok("matchstr", $user); - $sel->click_ok("search"); - $sel->wait_for_page_to_load(WAIT_TIME); - - my $text = trim($sel->get_text("bugzilla-body")); - if ($text =~ /$user/) { - # User exists already - return 1; - } - - $sel->click_ok("link=add a new user"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->title_is('Add user'); - $sel->type_ok('login', $user); - $sel->type_ok('password', 'icohF1io2ohw'); - $sel->click_ok("add"); - $sel->wait_for_page_to_load(WAIT_TIME); - $sel->is_text_present('regexp:The user account .* has been created successfully'); + my ($user) = @_; + + go_to_admin($sel); + $sel->click_ok("link=Users"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is("Search users"); + $sel->type_ok("matchstr", $user); + $sel->click_ok("search"); + $sel->wait_for_page_to_load(WAIT_TIME); + + my $text = trim($sel->get_text("bugzilla-body")); + if ($text =~ /$user/) { + # User exists already return 1; + } + + $sel->click_ok("link=add a new user"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->title_is('Add user'); + $sel->type_ok('login', $user); + $sel->type_ok('password', 'icohF1io2ohw'); + $sel->click_ok("add"); + $sel->wait_for_page_to_load(WAIT_TIME); + $sel->is_text_present( + 'regexp:The user account .* has been created successfully'); + + return 1; } diff --git a/qa/t/test_bmo_retire_values.t b/qa/t/test_bmo_retire_values.t index 3a74e8966..a0bfff483 100644 --- a/qa/t/test_bmo_retire_values.t +++ b/qa/t/test_bmo_retire_values.t @@ -19,21 +19,26 @@ my ($text, $bug_id); my $admin_user_login = $config->{admin_user_login}; log_in($sel, $config, 'admin'); -set_parameters($sel, { "Bug Fields" => {"useclassification-off" => undef, - "usetargetmilestone-on" => undef}, - "Administrative Policies" => {"allowbugdeletion-on" => undef}, - }); +set_parameters( + $sel, + { + "Bug Fields" => + {"useclassification-off" => undef, "usetargetmilestone-on" => undef}, + "Administrative Policies" => {"allowbugdeletion-on" => undef}, + } +); # create a clean bug file_bug_in_product($sel, "TestProduct"); $sel->select_ok("component", "label=TestComponent"); $sel->type_ok("short_desc", "testing testComponent"); -$sel->type_ok("comment", "testing"); +$sel->type_ok("comment", "testing"); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); my $clean_bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']"); -$sel->is_text_present_ok('has been added to the database', "Bug $clean_bug_id created"); +$sel->is_text_present_ok('has been added to the database', + "Bug $clean_bug_id created"); # # component @@ -51,19 +56,22 @@ $sel->click_ok("link=Edit components:"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Select component of product 'TestProduct'"); $text = trim($sel->get_text("bugzilla-body")); + if ($text =~ /TempComponent/) { - $sel->click_ok("//a[contains(\@href, 'editcomponents.cgi?action=del&product=TestProduct&component=TempComponent')]"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Delete component 'TempComponent' from 'TestProduct' product"); - $sel->click_ok("delete"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Component Deleted"); + $sel->click_ok( + "//a[contains(\@href, 'editcomponents.cgi?action=del&product=TestProduct&component=TempComponent')]" + ); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Delete component 'TempComponent' from 'TestProduct' product"); + $sel->click_ok("delete"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Component Deleted"); } $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add component to the TestProduct product"); -$sel->type_ok("component", "TempComponent"); -$sel->type_ok("description", "Temp component"); +$sel->type_ok("component", "TempComponent"); +$sel->type_ok("description", "Temp component"); $sel->type_ok("initialowner", $admin_user_login); $sel->uncheck_ok("watch_user_auto"); $sel->type_ok("watch_user", 'tempcomponent@testproduct.bugs'); @@ -77,11 +85,12 @@ $sel->title_is("Component Created"); file_bug_in_product($sel, "TestProduct"); $sel->select_ok("component", "label=TempComponent"); $sel->type_ok("short_desc", "testing tempComponent"); -$sel->type_ok("comment", "testing"); +$sel->type_ok("comment", "testing"); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']"); -$sel->is_text_present_ok('has been added to the database', "Bug $bug_id created"); +$sel->is_text_present_ok('has been added to the database', + "Bug $bug_id created"); # disable TestProduct:TestComponent for bug entry @@ -108,30 +117,37 @@ ok($text =~ /Disabled for bugs/, "Component deactivation confirmed"); # update bug TempComponent bug go_to_bug($sel, $bug_id); + # make sure the component is still tempcomponent $sel->selected_label_is("component", 'TempComponent'); + # update $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug_id"); $sel->click_ok("link=bug $bug_id"); $sel->wait_for_page_to_load_ok(WAIT_TIME); + # make sure the component is still tempcomponent ok($sel->get_selected_labels("component"), 'TempComponent'); # try creating new bug with TempComponent file_bug_in_product($sel, "TestProduct"); -ok(!$sel->is_element_present( +ok( + !$sel->is_element_present( q#//select[@id='component']/option[@value='TempComponent']#), - 'TempComponent is missing from create'); + 'TempComponent is missing from create' +); # try changing compoent of existing bug to TempComponent go_to_bug($sel, $clean_bug_id); -ok(!$sel->is_element_present( +ok( + !$sel->is_element_present( q#//select[@id='component']/option[@value='TempComponent']#), - 'TempComponent is missing from update'); + 'TempComponent is missing from update' +); # delete TempComponent @@ -144,7 +160,9 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Edit Product 'TestProduct'"); $sel->click_ok("link=Edit components:"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->click_ok("//a[contains(\@href, 'editcomponents.cgi?action=del&product=TestProduct&component=TempComponent')]"); +$sel->click_ok( + "//a[contains(\@href, 'editcomponents.cgi?action=del&product=TestProduct&component=TempComponent')]" +); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Delete component 'TempComponent' from 'TestProduct' product"); $sel->click_ok("delete"); @@ -168,13 +186,16 @@ $sel->click_ok("link=Edit versions:"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Select version of product 'TestProduct'"); $text = trim($sel->get_text("bugzilla-body")); + if ($text =~ /TempVersion/) { - $sel->click_ok("//a[contains(\@href, 'editversions.cgi?action=del&product=TestProduct&version=TempVersion')]"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Delete Version of Product 'TestProduct'"); - $sel->click_ok("delete"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Version Deleted"); + $sel->click_ok( + "//a[contains(\@href, 'editversions.cgi?action=del&product=TestProduct&version=TempVersion')]" + ); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Delete Version of Product 'TestProduct'"); + $sel->click_ok("delete"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Version Deleted"); } $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); @@ -189,11 +210,12 @@ $sel->title_is("Version Created"); file_bug_in_product($sel, "TestProduct"); $sel->select_ok("version", "label=TempVersion"); $sel->type_ok("short_desc", "testing tempVersion"); -$sel->type_ok("comment", "testing"); +$sel->type_ok("comment", "testing"); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']"); -$sel->is_text_present_ok('has been added to the database', "Bug $bug_id created"); +$sel->is_text_present_ok('has been added to the database', + "Bug $bug_id created"); # disable new version for bug entry @@ -220,16 +242,20 @@ ok($text =~ /Disabled for bugs/, "Version deactivation confirmed"); # update new version bug go_to_bug($sel, $bug_id); + # make sure the version is still tempversion $sel->selected_label_is("version", 'TempVersion'); + # update $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug_id"); $sel->click_ok("link=bug $bug_id"); $sel->wait_for_page_to_load_ok(WAIT_TIME); + # make sure the version is still tempversion $sel->selected_label_is("version", 'TempVersion'); + # change the version so it can be deleted $sel->select_ok("version", "label=unspecified"); $sel->click_ok("commit"); @@ -239,16 +265,20 @@ $sel->is_text_present_ok("Changes submitted for bug $bug_id"); # try creating new bug with new version file_bug_in_product($sel, "TestProduct"); -ok(!$sel->is_element_present( +ok( + !$sel->is_element_present( q#//select[@id='version']/option[@value='TempVersion']#), - 'TempVersion is missing from create'); + 'TempVersion is missing from create' +); # try changing existing bug to new version go_to_bug($sel, $clean_bug_id); -ok(!$sel->is_element_present( +ok( + !$sel->is_element_present( q#//select[@id='version']/option[@value='TempVersion']#), - 'TempVersion is missing from update'); + 'TempVersion is missing from update' +); # delete new version @@ -263,7 +293,9 @@ $sel->click_ok("link=Edit versions:"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Select version of product 'TestProduct'"); $text = trim($sel->get_text("bugzilla-body")); -$sel->click_ok("//a[contains(\@href, 'editversions.cgi?action=del&product=TestProduct&version=TempVersion')]"); +$sel->click_ok( + "//a[contains(\@href, 'editversions.cgi?action=del&product=TestProduct&version=TempVersion')]" +); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Delete Version of Product 'TestProduct'"); $sel->click_ok("delete"); @@ -287,19 +319,22 @@ $sel->click_ok("link=Edit milestones:"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Select milestone of product 'TestProduct'"); $text = trim($sel->get_text("bugzilla-body")); + if ($text =~ /TempMilestone/) { - $sel->click_ok("//a[contains(\@href, 'editmilestones.cgi?action=del&product=TestProduct&milestone=TempMilestone')]"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Delete Milestone of Product 'TestProduct'"); - $sel->click_ok("delete"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Milestone Deleted"); + $sel->click_ok( + "//a[contains(\@href, 'editmilestones.cgi?action=del&product=TestProduct&milestone=TempMilestone')]" + ); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Delete Milestone of Product 'TestProduct'"); + $sel->click_ok("delete"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Milestone Deleted"); } $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add Milestone to Product 'TestProduct'"); $sel->type_ok("milestone", "TempMilestone"); -$sel->type_ok("sortkey", "999"); +$sel->type_ok("sortkey", "999"); $sel->click_ok("create"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Milestone Created"); @@ -309,11 +344,12 @@ $sel->title_is("Milestone Created"); file_bug_in_product($sel, "TestProduct"); $sel->select_ok("target_milestone", "label=TempMilestone"); $sel->type_ok("short_desc", "testing tempMilestone"); -$sel->type_ok("comment", "testing"); +$sel->type_ok("comment", "testing"); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $bug_id = $sel->get_value("//input[\@name='id' and \@type='hidden']"); -$sel->is_text_present_ok('has been added to the database', "Bug $bug_id created"); +$sel->is_text_present_ok('has been added to the database', + "Bug $bug_id created"); # disable milestone for bug entry @@ -340,30 +376,37 @@ ok($text =~ /Disabled for bugs/, "Milestone deactivation confirmed"); # update milestone bug go_to_bug($sel, $bug_id); + # make sure the milestone is still tempmilestone $sel->selected_label_is("target_milestone", 'TempMilestone'); + # update $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->is_text_present_ok("Changes submitted for bug $bug_id"); $sel->click_ok("link=bug $bug_id"); $sel->wait_for_page_to_load_ok(WAIT_TIME); + # make sure the milestone is still tempmilestone $sel->selected_label_is("target_milestone", 'TempMilestone'); # try creating new bug with milestone file_bug_in_product($sel, "TestProduct"); -ok(!$sel->is_element_present( +ok( + !$sel->is_element_present( q#//select[@id='target_milestone']/option[@value='TempMilestone']#), - 'TempMilestone is missing from create'); + 'TempMilestone is missing from create' +); # try changing existing bug to milestone go_to_bug($sel, $clean_bug_id); -ok(!$sel->is_element_present( +ok( + !$sel->is_element_present( q#//select[@id='target_milestone']/option[@value='TempMilestone']#), - 'TempMilestone is missing from update'); + 'TempMilestone is missing from update' +); # delete milestone @@ -378,7 +421,9 @@ $sel->click_ok("link=Edit milestones:"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Select milestone of product 'TestProduct'"); $text = trim($sel->get_text("bugzilla-body")); -$sel->click_ok("//a[contains(\@href, 'editmilestones.cgi?action=del&product=TestProduct&milestone=TempMilestone')]"); +$sel->click_ok( + "//a[contains(\@href, 'editmilestones.cgi?action=del&product=TestProduct&milestone=TempMilestone')]" +); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Delete Milestone of Product 'TestProduct'"); $sel->click_ok("delete"); diff --git a/qa/t/test_bug_edit.t b/qa/t/test_bug_edit.t index 01037aa1e..b1d800f67 100644 --- a/qa/t/test_bug_edit.t +++ b/qa/t/test_bug_edit.t @@ -16,17 +16,17 @@ use QA::Util; my ($sel, $config) = get_selenium(); log_in($sel, $config, 'admin'); -set_parameters($sel, { "Bug Fields" => {"usestatuswhiteboard-on" => undef} }); +set_parameters($sel, {"Bug Fields" => {"usestatuswhiteboard-on" => undef}}); # Clear the saved search, in case this test didn't complete previously. if ($sel->is_text_present("My bugs from QA_Selenium")) { - $sel->click_ok("link=My bugs from QA_Selenium"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Bug List: My bugs from QA_Selenium"); - $sel->click_ok("link=Forget Search 'My bugs from QA_Selenium'"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Search is gone"); - $sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone"); + $sel->click_ok("link=My bugs from QA_Selenium"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Bug List: My bugs from QA_Selenium"); + $sel->click_ok("link=Forget Search 'My bugs from QA_Selenium'"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Search is gone"); + $sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone"); } # Just in case the test failed before completion previously, reset the CANEDIT bit. @@ -35,7 +35,8 @@ $sel->click_ok("link=Groups"); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editgroups.cgi}); $sel->title_is("Edit Groups"); $sel->click_ok("link=Master"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editgroups.cgi?action=changeform&group=26}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/editgroups.cgi?action=changeform&group=26}); $sel->title_is("Change Group: Master"); my $group_url = $sel->get_location(); $group_url =~ /group=(\d+)$/; @@ -50,30 +51,34 @@ log_in($sel, $config, 'QA_Selenium_TEST'); file_bug_in_product($sel, 'TestProduct'); $sel->select_ok("bug_severity", "label=critical"); $sel->type_ok("short_desc", "Test bug editing"); -$sel->type_ok("comment", "ploc"); +$sel->type_ok("comment", "ploc"); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=__BUG_ID__}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=__BUG_ID__}); my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]'); -$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created"); +$sel->is_text_present_ok('has been added to the database', + "Bug $bug1_id created"); # Now edit field values of the bug you just filed. $sel->select_ok("rep_platform", "label=Other"); -$sel->select_ok("op_sys", "label=Other"); -$sel->select_ok("priority", "label=Highest"); +$sel->select_ok("op_sys", "label=Other"); +$sel->select_ok("priority", "label=Highest"); $sel->select_ok("bug_severity", "label=blocker"); -$sel->type_ok("bug_file_loc", "foo.cgi?action=bar"); +$sel->type_ok("bug_file_loc", "foo.cgi?action=bar"); $sel->type_ok("status_whiteboard", "[Selenium was here]"); -$sel->type_ok("comment", "new comment from me :)"); +$sel->type_ok("comment", "new comment from me :)"); $sel->select_ok("bug_status", "label=RESOLVED"); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); # Now move the bug into another product, which has a mandatory group. $sel->click_ok("link=bug $bug1_id"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->title_like(qr/^$bug1_id /); $sel->select_ok("product", "label=QA-Selenium-TEST"); $sel->type_ok("comment", "moving to QA-Selenium-TEST"); @@ -81,34 +86,49 @@ $sel->click_ok("commit"); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); $sel->title_is("Verify New Product Details..."); $sel->select_ok("component", "label=QA-Selenium-TEST"); -$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'); -ok(!$sel->is_editable('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not editable"); -$sel->is_checked_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]', "QA-Selenium-TEST group is selected"); +$sel->is_element_present_ok( + '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'); +ok( + !$sel->is_editable( + '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), + "QA-Selenium-TEST group not editable" +); +$sel->is_checked_ok( + '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]', + "QA-Selenium-TEST group is selected"); $sel->click_ok("change_product"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); $sel->click_ok("link=bug $bug1_id"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->title_like(qr/^$bug1_id /); $sel->select_ok("bug_severity", "label=normal"); -$sel->select_ok("priority", "label=High"); +$sel->select_ok("priority", "label=High"); $sel->select_ok("rep_platform", "label=All"); -$sel->select_ok("op_sys", "label=All"); +$sel->select_ok("op_sys", "label=All"); $sel->click_ok("cc_edit_area_showhide"); -$sel->type_ok("newcc", $config->{admin_user_login}); +$sel->type_ok("newcc", $config->{admin_user_login}); $sel->type_ok("comment", "Unchecking the reporter_accessible checkbox"); + # This checkbox is checked by default. $sel->click_ok("reporter_accessible"); $sel->select_ok("bug_status", "label=VERIFIED"); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); $sel->click_ok("link=bug $bug1_id"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->title_like(qr/^$bug1_id /); -$sel->type_ok("comment", "I am the reporter, but I can see the bug anyway as I belong to the mandatory group"); +$sel->type_ok("comment", + "I am the reporter, but I can see the bug anyway as I belong to the mandatory group" +); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); logout($sel); @@ -118,23 +138,26 @@ logout($sel); log_in($sel, $config, 'admin'); go_to_bug($sel, $bug1_id); $sel->select_ok("bug_severity", "label=blocker"); -$sel->select_ok("priority", "label=Highest"); +$sel->select_ok("priority", "label=Highest"); $sel->type_ok("status_whiteboard", "[Selenium was here][admin too]"); $sel->select_ok("bug_status", "label=CONFIRMED"); $sel->click_ok("bz_assignee_edit_action"); $sel->type_ok("assigned_to", $config->{admin_user_login}); -$sel->type_ok("comment", "I have editbugs privs. Taking!"); +$sel->type_ok("comment", "I have editbugs privs. Taking!"); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); $sel->click_ok("link=bug $bug1_id"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->title_like(qr/^$bug1_id /); $sel->click_ok("cc_edit_area_showhide"); $sel->type_ok("newcc", $config->{unprivileged_user_login}); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); logout($sel); @@ -151,9 +174,12 @@ logout($sel); log_in($sel, $config, 'admin'); go_to_bug($sel, $bug1_id); $sel->click_ok("cclist_accessible"); -$sel->type_ok("comment", "I am allowed to turn off cclist_accessible despite not being in the mandatory group"); +$sel->type_ok("comment", + "I am allowed to turn off cclist_accessible despite not being in the mandatory group" +); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); logout($sel); @@ -162,7 +188,8 @@ logout($sel); log_in($sel, $config, 'unprivileged'); $sel->type_ok("quicksearch_top", $bug1_id); $sel->submit("header-search"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->title_is("Access Denied"); $sel->is_text_present_ok("You are not authorized to access bug $bug1_id"); logout($sel); @@ -172,6 +199,7 @@ logout($sel); log_in($sel, $config, 'admin'); go_to_bug($sel, $bug1_id); $sel->select_ok("product", "label=TestProduct"); + # When selecting a new product, Bugzilla tries to reassign the bug by default, # so we have to uncheck it. $sel->click_ok("set_default_assignee"); @@ -181,23 +209,46 @@ $sel->click_ok("commit"); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); $sel->title_is("Verify New Product Details..."); $sel->select_ok("component", "label=TestComponent"); -$sel->is_text_present_ok("These groups are not legal for the 'TestProduct' product or you are not allowed to restrict bugs to these groups"); -$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'); -ok(!$sel->is_editable('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not editable"); -ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), "QA-Selenium-TEST group not selected"); -$sel->is_element_present_ok('//input[@type="checkbox" and @name="groups" and @value="Master"]'); -$sel->is_editable_ok('//input[@type="checkbox" and @name="groups" and @value="Master"]', "Master group is editable"); -ok(!$sel->is_checked('//input[@type="checkbox" and @name="groups" and @value="Master"]'), "Master group not selected by default"); +$sel->is_text_present_ok( + "These groups are not legal for the 'TestProduct' product or you are not allowed to restrict bugs to these groups" +); +$sel->is_element_present_ok( + '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'); +ok( + !$sel->is_editable( + '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), + "QA-Selenium-TEST group not editable" +); +ok( + !$sel->is_checked( + '//input[@type="checkbox" and @name="groups" and @value="QA-Selenium-TEST"]'), + "QA-Selenium-TEST group not selected" +); +$sel->is_element_present_ok( + '//input[@type="checkbox" and @name="groups" and @value="Master"]'); +$sel->is_editable_ok( + '//input[@type="checkbox" and @name="groups" and @value="Master"]', + "Master group is editable"); +ok( + !$sel->is_checked( + '//input[@type="checkbox" and @name="groups" and @value="Master"]'), + "Master group not selected by default" +); $sel->click_ok("change_product"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); $sel->click_ok("link=bug $bug1_id"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->title_like(qr/^$bug1_id /); $sel->click_ok("cclist_accessible"); -$sel->type_ok("comment", "I am allowed to turn off cclist_accessible despite not being in the mandatory group"); +$sel->type_ok("comment", + "I am allowed to turn off cclist_accessible despite not being in the mandatory group" +); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); logout($sel); @@ -206,17 +257,24 @@ logout($sel); log_in($sel, $config, 'unprivileged'); go_to_bug($sel, $bug1_id); -$sel->type_ok("comment", "I have no privs, I can only comment (and remove people from the CC list)"); -ok(!$sel->is_element_present('//select[@name="product"]'), "Product field not editable"); -ok(!$sel->is_element_present('//select[@name="bug_severity"]'), "Severity field not editable"); -ok(!$sel->is_element_present('//select[@name="priority"]'), "Priority field not editable"); -ok(!$sel->is_element_present('//select[@name="op_sys"]'), "OS field not editable"); -ok(!$sel->is_element_present('//select[@name="rep_platform"]'), "Hardware field not editable"); +$sel->type_ok("comment", + "I have no privs, I can only comment (and remove people from the CC list)"); +ok(!$sel->is_element_present('//select[@name="product"]'), + "Product field not editable"); +ok(!$sel->is_element_present('//select[@name="bug_severity"]'), + "Severity field not editable"); +ok(!$sel->is_element_present('//select[@name="priority"]'), + "Priority field not editable"); +ok(!$sel->is_element_present('//select[@name="op_sys"]'), + "OS field not editable"); +ok(!$sel->is_element_present('//select[@name="rep_platform"]'), + "Hardware field not editable"); $sel->click_ok("cc_edit_area_showhide"); $sel->add_selection_ok("cc", "label=" . $config->{admin_user_login}); $sel->click_ok("removecc"); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); logout($sel); @@ -225,7 +283,9 @@ logout($sel); log_in($sel, $config, 'admin'); edit_product($sel, "TestProduct"); $sel->click_ok("link=Edit Group Access Controls:"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editproducts.cgi?action=editgroupcontrols&product=TestProduct}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/editproducts.cgi?action=editgroupcontrols&product=TestProduct} +); $sel->title_is("Edit Group Controls for TestProduct"); $sel->check_ok("canedit_$master_gid"); $sel->click_ok("submit"); @@ -237,7 +297,8 @@ $sel->title_is("Update group access controls for TestProduct"); go_to_bug($sel, $bug1_id); $sel->type_ok("comment", "Do nothing except adding a comment..."); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); logout($sel); @@ -249,7 +310,8 @@ $sel->type_ok("comment", "Just a comment too..."); $sel->click_ok("commit"); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); $sel->title_is("Product Edit Access Denied"); -$sel->is_text_present_ok("You are not permitted to edit bugs in product TestProduct."); +$sel->is_text_present_ok( + "You are not permitted to edit bugs in product TestProduct."); logout($sel); # Test searches and "format for printing". @@ -272,24 +334,32 @@ $sel->select_ok("emailtype2", "label=is"); $sel->type_ok("email2", $config->{QA_Selenium_TEST_user_login}); screenshot_page($sel, '/app/artifacts/line271.png'); $sel->click_ok("Search"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?emailreporter2=1&emailtype2=exact&order=Importance&list_id=15&emailtype1=exact&emailcc2=1&query_format=advanced&emailassigned_to1=1&emailqa_contact2=1&email2=QA-Selenium-TEST%40mozilla.test&email1=admin%40mozilla.test&emailassigned_to2=1&product=TestProduct}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?emailreporter2=1&emailtype2=exact&order=Importance&list_id=15&emailtype1=exact&emailcc2=1&query_format=advanced&emailassigned_to1=1&emailqa_contact2=1&email2=QA-Selenium-TEST%40mozilla.test&email1=admin%40mozilla.test&emailassigned_to2=1&product=TestProduct} +); $sel->title_is("Bug List"); screenshot_page($sel, '/app/artifacts/line275.png'); $sel->is_text_present_ok("One bug found."); $sel->type_ok("save_newqueryname", "My bugs from QA_Selenium"); $sel->click_ok("remember"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?newquery=email1%3Dadmin%2540mozilla.test%26email2%3DQA-Selenium-TEST%2540mozilla.test%26emailassigned_to1%3D1%26emailassigned_to2%3D1%26emailcc2%3D1%26emailqa_contact2%3D1%26emailreporter2%3D1%26emailtype1%3Dexact%26emailtype2%3Dexact%26list_id%3D15%26product%3DTestProduct%26query_format%3Dadvanced%26order%3Dpriority%252Cbug_severity&cmdtype=doit&remtype=asnamed&token=1531926552-dc69995d79c786af046436ec6717000b&newqueryname=My%20bugs%20from%20QA_Selenium&list_id=16}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?newquery=email1%3Dadmin%2540mozilla.test%26email2%3DQA-Selenium-TEST%2540mozilla.test%26emailassigned_to1%3D1%26emailassigned_to2%3D1%26emailcc2%3D1%26emailqa_contact2%3D1%26emailreporter2%3D1%26emailtype1%3Dexact%26emailtype2%3Dexact%26list_id%3D15%26product%3DTestProduct%26query_format%3Dadvanced%26order%3Dpriority%252Cbug_severity&cmdtype=doit&remtype=asnamed&token=1531926552-dc69995d79c786af046436ec6717000b&newqueryname=My%20bugs%20from%20QA_Selenium&list_id=16} +); $sel->title_is("Search created"); -$sel->is_text_present_ok("OK, you have a new search named My bugs from QA_Selenium."); +$sel->is_text_present_ok( + "OK, you have a new search named My bugs from QA_Selenium."); $sel->click_ok("link=My bugs from QA_Selenium"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=runnamed&namedcmd=My%20bugs%20from%20QA_Selenium&list_id=17}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=runnamed&namedcmd=My%20bugs%20from%20QA_Selenium&list_id=17} +); $sel->title_is("Bug List: My bugs from QA_Selenium"); $sel->click_ok("long_format"); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/show_bug.cgi}); $sel->title_is("Full Text Bug Listing"); $sel->is_text_present_ok("Bug $bug1_id"); $sel->is_text_present_ok("Status: CONFIRMED"); -$sel->is_text_present_ok("Reporter: QA-Selenium-TEST <$config->{QA_Selenium_TEST_user_login}>"); +$sel->is_text_present_ok( + "Reporter: QA-Selenium-TEST <$config->{QA_Selenium_TEST_user_login}>"); $sel->is_text_present_ok("Assignee: QA Admin <$config->{admin_user_login}>"); $sel->is_text_present_ok("Severity: blocker"); $sel->is_text_present_ok("Priority: Highest"); @@ -303,30 +373,37 @@ log_in($sel, $config, 'QA_Selenium_TEST'); file_bug_in_product($sel, 'TestProduct'); $sel->select_ok("bug_severity", "label=blocker"); $sel->type_ok("short_desc", "New bug from me"); + # We turned on the CANEDIT bit for TestProduct. $sel->type_ok("comment", "I can enter a new bug, but not edit it, right?"); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=__BUG_ID__}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=__BUG_ID__}); my $bug2_id = $sel->get_value('//input[@name="id" and @type="hidden"]'); -$sel->is_text_present_ok('has been added to the database', "Bug $bug2_id created"); +$sel->is_text_present_ok('has been added to the database', + "Bug $bug2_id created"); # Clicking the "Back" button and resubmitting the form again should trigger a suspicous action error. $sel->go_back_ok(); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/enter_bug.cgi?product=TestProduct&format=__default__}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/enter_bug.cgi?product=TestProduct&format=__default__} +); $sel->title_is("Enter Bug: TestProduct"); $sel->click_ok("commit"); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/post_bug.cgi}); $sel->title_is("Suspicious Action"); $sel->is_text_present_ok("you have no valid token for the create_bug action"); $sel->click_ok('//input[@value="Confirm Changes"]'); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/show_bug.cgi?id=15}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/show_bug.cgi?id=15}); $sel->is_text_present_ok('has been added to the database', 'Bug created'); $sel->type_ok("comment", "New comment not allowed"); $sel->click_ok("commit"); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); $sel->title_is("Product Edit Access Denied"); -$sel->is_text_present_ok("You are not permitted to edit bugs in product TestProduct."); +$sel->is_text_present_ok( + "You are not permitted to edit bugs in product TestProduct."); logout($sel); # Reassign the newly created bug to the admin. @@ -335,23 +412,28 @@ log_in($sel, $config, 'admin'); go_to_bug($sel, $bug2_id); $sel->click_ok("bz_assignee_edit_action"); $sel->type_ok("assigned_to", $config->{admin_user_login}); -$sel->type_ok("comment", "Taking!"); +$sel->type_ok("comment", "Taking!"); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug2_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug2_id}); $sel->is_text_present_ok("Changes submitted for bug $bug2_id"); # Test mass-change. $sel->click_ok("link=My bugs from QA_Selenium"); screenshot_page($sel, '/app/artifacts/line344.png'); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=runnamed&namedcmd=My%20bugs%20from%20QA_Selenium&list_id=19}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=runnamed&namedcmd=My%20bugs%20from%20QA_Selenium&list_id=19} +); screenshot_page($sel, '/app/artifacts/line346.png'); $sel->title_is("Bug List: My bugs from QA_Selenium"); screenshot_page($sel, '/app/artifacts/line348.png'); $sel->is_text_present_ok("2 bugs found"); screenshot_page($sel, '/app/artifacts/line350.png'); $sel->click_ok("link=Change Several Bugs at Once"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?email1=admin%40mozilla.test&email2=QA-Selenium-TEST%40mozilla.test&emailassigned_to1=1&emailassigned_to2=1&emailcc2=1&emailqa_contact2=1&emailreporter2=1&emailtype1=exact&emailtype2=exact&product=TestProduct&query_format=advanced&order=priority%2Cbug_severity&tweak=1&list_id=20}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?email1=admin%40mozilla.test&email2=QA-Selenium-TEST%40mozilla.test&emailassigned_to1=1&emailassigned_to2=1&emailcc2=1&emailqa_contact2=1&emailreporter2=1&emailtype1=exact&emailtype2=exact&product=TestProduct&query_format=advanced&order=priority%2Cbug_severity&tweak=1&list_id=20} +); $sel->title_is("Bug List"); $sel->click_ok("check_all"); $sel->type_ok("comment", 'Mass change"'); @@ -362,25 +444,30 @@ check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); $sel->title_is("Bugs processed"); $sel->click_ok("link=bug $bug1_id"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->title_like(qr/$bug1_id /); $sel->selected_label_is("resolution", "WORKSFORME"); $sel->select_ok("resolution", "label=INVALID"); $sel->click_ok("commit"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); $sel->click_ok("link=bug $bug1_id"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); $sel->title_like(qr/$bug1_id /); $sel->selected_label_is("resolution", "INVALID"); $sel->click_ok("link=History"); -check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_activity.cgi?id=$bug1_id}); +check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_activity.cgi?id=$bug1_id}); $sel->title_is("Changes made to bug $bug1_id"); $sel->is_text_present_ok("URL foo.cgi?action=bar"); $sel->is_text_present_ok("Severity critical blocker"); -$sel->is_text_present_ok("Whiteboard [Selenium was here] [Selenium was here][admin too]"); +$sel->is_text_present_ok( + "Whiteboard [Selenium was here] [Selenium was here][admin too]"); $sel->is_text_present_ok("Product QA-Selenium-TEST TestProduct"); $sel->is_text_present_ok("Status CONFIRMED RESOLVED"); @@ -431,53 +518,68 @@ $sel->is_text_present_ok("Status CONFIRMED RESOLVED"); # Make sure token checks are working correctly for single bug editing and mass change, # first with no token, then with an invalid token. -foreach my $params (["no_token_single_bug", ""], ["invalid_token_single_bug", "&token=1"]) { - my ($comment, $token) = @$params; - $sel->open_ok("/$config->{bugzilla_installation}/process_bug.cgi?id=$bug1_id&comment=$comment$token", - undef, "Edit a single bug with " . ($token ? "an invalid" : "no") . " token"); - $sel->title_is("Suspicious Action"); - $sel->is_text_present_ok($token ? "an invalid token" : "web browser directly"); - $sel->click_ok("confirm"); - check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); - $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); - $sel->click_ok("link=bug $bug1_id"); - check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); - $sel->title_like(qr/^$bug1_id /); - $sel->is_text_present_ok($comment); +foreach my $params (["no_token_single_bug", ""], + ["invalid_token_single_bug", "&token=1"]) +{ + my ($comment, $token) = @$params; + $sel->open_ok( + "/$config->{bugzilla_installation}/process_bug.cgi?id=$bug1_id&comment=$comment$token", + undef, "Edit a single bug with " . ($token ? "an invalid" : "no") . " token" + ); + $sel->title_is("Suspicious Action"); + $sel->is_text_present_ok($token ? "an invalid token" : "web browser directly"); + $sel->click_ok("confirm"); + check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); + $sel->is_text_present_ok("Changes submitted for bug $bug1_id"); + $sel->click_ok("link=bug $bug1_id"); + check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug1_id}); + $sel->title_like(qr/^$bug1_id /); + $sel->is_text_present_ok($comment); } -foreach my $params (["no_token_mass_change", ""], ["invalid_token_mass_change", "&token=1"]) { - my ($comment, $token) = @$params; - $sel->open_ok("/$config->{bugzilla_installation}/process_bug.cgi?id_$bug1_id=1&id_$bug2_id=1&comment=$comment$token", - undef, "Mass change with " . ($token ? "an invalid" : "no") . " token"); - $sel->title_is("Suspicious Action"); - $sel->is_text_present_ok("no valid token for the buglist_mass_change action"); - $sel->click_ok("confirm"); +foreach my $params (["no_token_mass_change", ""], + ["invalid_token_mass_change", "&token=1"]) +{ + my ($comment, $token) = @$params; + $sel->open_ok( + "/$config->{bugzilla_installation}/process_bug.cgi?id_$bug1_id=1&id_$bug2_id=1&comment=$comment$token", + undef, "Mass change with " . ($token ? "an invalid" : "no") . " token" + ); + $sel->title_is("Suspicious Action"); + $sel->is_text_present_ok("no valid token for the buglist_mass_change action"); + $sel->click_ok("confirm"); + check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); + $sel->title_is("Bugs processed"); + foreach my $bug_id ($bug1_id, $bug2_id) { + $sel->click_ok("link=bug $bug_id"); + check_page_load($sel, WAIT_TIME, + qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug_id}); + $sel->title_like(qr/^$bug_id /); + $sel->is_text_present_ok($comment); + next if $bug_id == $bug2_id; + $sel->go_back_ok(); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); $sel->title_is("Bugs processed"); - foreach my $bug_id ($bug1_id, $bug2_id) { - $sel->click_ok("link=bug $bug_id"); - check_page_load($sel, WAIT_TIME, qq{http://HOSTNAME:8000/bmo/show_bug.cgi?id=$bug_id}); - $sel->title_like(qr/^$bug_id /); - $sel->is_text_present_ok($comment); - next if $bug_id == $bug2_id; - $sel->go_back_ok(); - check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); - $sel->title_is("Bugs processed"); - } + } } # Now move these bugs out of our radar. $sel->click_ok("link=My bugs from QA_Selenium"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=runnamed&namedcmd=My%20bugs%20from%20QA_Selenium&list_id=21}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=runnamed&namedcmd=My%20bugs%20from%20QA_Selenium&list_id=21} +); $sel->title_is("Bug List: My bugs from QA_Selenium"); $sel->is_text_present_ok("2 bugs found"); $sel->click_ok("link=Change Several Bugs at Once"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?email1=admin%40mozilla.test&email2=QA-Selenium-TEST%40mozilla.test&emailassigned_to1=1&emailassigned_to2=1&emailcc2=1&emailqa_contact2=1&emailreporter2=1&emailtype1=exact&emailtype2=exact&product=TestProduct&query_format=advanced&order=priority%2Cbug_severity&tweak=1&list_id=22}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?email1=admin%40mozilla.test&email2=QA-Selenium-TEST%40mozilla.test&emailassigned_to1=1&emailassigned_to2=1&emailcc2=1&emailqa_contact2=1&emailreporter2=1&emailtype1=exact&emailtype2=exact&product=TestProduct&query_format=advanced&order=priority%2Cbug_severity&tweak=1&list_id=22} +); $sel->title_is("Bug List"); $sel->click_ok("check_all"); -$sel->type_ok("comment", "Reassigning to the reporter"); +$sel->type_ok("comment", "Reassigning to the reporter"); $sel->type_ok("assigned_to", $config->{QA_Selenium_TEST_user_login}); $sel->click_ok("commit"); check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/process_bug.cgi}); @@ -486,10 +588,14 @@ $sel->title_is("Bugs processed"); # Now delete the saved search. $sel->click_ok("link=My bugs from QA_Selenium"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=runnamed&namedcmd=My%20bugs%20from%20QA_Selenium&list_id=23}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=runnamed&namedcmd=My%20bugs%20from%20QA_Selenium&list_id=23} +); $sel->title_is("Bug List: My bugs from QA_Selenium"); $sel->click_ok("link=Forget Search 'My bugs from QA_Selenium'"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=My%20bugs%20from%20QA_Selenium&token=1531926582-f228fa8ebc2f2b3970f2a791e54534ec&list_id=24}); +check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/buglist.cgi?cmdtype=dorem&remaction=forget&namedcmd=My%20bugs%20from%20QA_Selenium&token=1531926582-f228fa8ebc2f2b3970f2a791e54534ec&list_id=24} +); $sel->title_is("Search is gone"); $sel->is_text_present_ok("OK, the My bugs from QA_Selenium search is gone"); @@ -498,14 +604,16 @@ clear_canedit_on_testproduct($sel, $master_gid); logout($sel); sub clear_canedit_on_testproduct { - my ($sel, $master_gid) = @_; - - edit_product($sel, "TestProduct"); - $sel->click_ok("link=Edit Group Access Controls:"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editproducts.cgi?action=editgroupcontrols&product=TestProduct}); - $sel->title_is("Edit Group Controls for TestProduct"); - $sel->uncheck_ok("canedit_$master_gid"); - $sel->click_ok("submit"); -check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editproducts.cgi}); - $sel->title_is("Update group access controls for TestProduct"); + my ($sel, $master_gid) = @_; + + edit_product($sel, "TestProduct"); + $sel->click_ok("link=Edit Group Access Controls:"); + check_page_load($sel, WAIT_TIME, + q{http://HOSTNAME:8000/bmo/editproducts.cgi?action=editgroupcontrols&product=TestProduct} + ); + $sel->title_is("Edit Group Controls for TestProduct"); + $sel->uncheck_ok("canedit_$master_gid"); + $sel->click_ok("submit"); + check_page_load($sel, WAIT_TIME, q{http://HOSTNAME:8000/bmo/editproducts.cgi}); + $sel->title_is("Update group access controls for TestProduct"); } diff --git a/qa/t/test_choose_priority.t b/qa/t/test_choose_priority.t index 95f401e66..6320df652 100644 --- a/qa/t/test_choose_priority.t +++ b/qa/t/test_choose_priority.t @@ -16,11 +16,14 @@ use QA::Util; my ($sel, $config) = get_selenium(); log_in($sel, $config, 'admin'); -set_parameters($sel, { "Bug Change Policies" => {"letsubmitterchoosepriority-off" => undef} }); +set_parameters($sel, + {"Bug Change Policies" => {"letsubmitterchoosepriority-off" => undef}}); file_bug_in_product($sel, "TestProduct"); ok(!$sel->is_text_present("Priority"), "The Priority label is not present"); -ok(!$sel->is_element_present("//select[\@name='priority']"), "The Priority drop-down menu is not present"); -set_parameters($sel, { "Bug Change Policies" => {"letsubmitterchoosepriority-on" => undef} }); +ok(!$sel->is_element_present("//select[\@name='priority']"), + "The Priority drop-down menu is not present"); +set_parameters($sel, + {"Bug Change Policies" => {"letsubmitterchoosepriority-on" => undef}}); file_bug_in_product($sel, "TestProduct"); $sel->is_text_present_ok("Priority"); $sel->is_element_present_ok("//select[\@name='priority']"); diff --git a/qa/t/test_classifications.t b/qa/t/test_classifications.t index a30e72018..a2e8c93b6 100644 --- a/qa/t/test_classifications.t +++ b/qa/t/test_classifications.t @@ -18,7 +18,7 @@ my ($sel, $config) = get_selenium(); # Enable classifications log_in($sel, $config, 'admin'); -set_parameters($sel, { "Bug Fields" => {"useclassification-on" => undef} }); +set_parameters($sel, {"Bug Fields" => {"useclassification-on" => undef}}); # Create a new classification. @@ -31,25 +31,29 @@ $sel->title_is("Select classification"); # Accessing action=delete directly must 1) trigger the security check page, # and 2) automatically reclassify products in this classification. if ($sel->is_text_present("cone")) { - $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&classification=cone"); - $sel->title_is("Suspicious Action"); - $sel->click_ok("confirm"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Classification Deleted"); + $sel->open_ok( + "/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&classification=cone" + ); + $sel->title_is("Suspicious Action"); + $sel->click_ok("confirm"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Classification Deleted"); } if ($sel->is_text_present("ctwo")) { - $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&classification=ctwo"); - $sel->title_is("Suspicious Action"); - $sel->click_ok("confirm"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Classification Deleted"); + $sel->open_ok( + "/$config->{bugzilla_installation}/editclassifications.cgi?action=delete&classification=ctwo" + ); + $sel->title_is("Suspicious Action"); + $sel->click_ok("confirm"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Classification Deleted"); } $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add new classification"); $sel->type_ok("classification", "cone"); -$sel->type_ok("description", "Classification number 1"); +$sel->type_ok("description", "Classification number 1"); $sel->click_ok('//input[@type="submit" and @value="Add"]'); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("New Classification Created"); @@ -62,7 +66,8 @@ $sel->click_ok("add_products"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Reclassify products"); my @products = $sel->get_select_options("myprodlist"); -ok(scalar @products == 1 && $products[0] eq 'TestProduct', "TestProduct successfully added to 'cone'"); +ok(scalar @products == 1 && $products[0] eq 'TestProduct', + "TestProduct successfully added to 'cone'"); # Create a new bug in this product/classification. @@ -72,7 +77,8 @@ $sel->type_ok("comment", "Created by Selenium with classifications turned on"); $sel->click_ok("commit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); my $bug1_id = $sel->get_value('//input[@name="id" and @type="hidden"]'); -$sel->is_text_present_ok('has been added to the database', "Bug $bug1_id created"); +$sel->is_text_present_ok('has been added to the database', + "Bug $bug1_id created"); # Rename 'cone' to 'Unclassified', which must be rejected as it already exists, # then to 'ctwo', which is not yet in use. Should work fine, even with products @@ -109,18 +115,21 @@ go_to_admin($sel); $sel->click_ok("link=Classifications"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Select classification"); -$sel->click_ok('//a[@href="editclassifications.cgi?action=del&classification=ctwo"]'); +$sel->click_ok( + '//a[@href="editclassifications.cgi?action=del&classification=ctwo"]'); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Error"); my $error = trim($sel->get_text("error_msg")); -ok($error =~ /there are products for this classification/, "Reject classification deletion"); +ok($error =~ /there are products for this classification/, + "Reject classification deletion"); # Reclassify the product before deleting the classification. $sel->go_back_ok(); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Select classification"); -$sel->click_ok('//a[@href="editclassifications.cgi?action=reclassify&classification=ctwo"]'); +$sel->click_ok( + '//a[@href="editclassifications.cgi?action=reclassify&classification=ctwo"]'); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Reclassify products"); $sel->add_selection_ok("myprodlist", "label=TestProduct"); @@ -130,7 +139,8 @@ $sel->title_is("Reclassify products"); $sel->click_ok("link=edit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Select classification"); -$sel->click_ok('//a[@href="editclassifications.cgi?action=del&classification=ctwo"]'); +$sel->click_ok( + '//a[@href="editclassifications.cgi?action=del&classification=ctwo"]'); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Delete classification"); $sel->is_text_present_ok("Do you really want to delete this classification?"); @@ -140,7 +150,7 @@ $sel->title_is("Classification Deleted"); # Disable classifications and make sure you cannot edit them anymore. -set_parameters($sel, { "Bug Fields" => {"useclassification-off" => undef} }); +set_parameters($sel, {"Bug Fields" => {"useclassification-off" => undef}}); $sel->open_ok("/$config->{bugzilla_installation}/editclassifications.cgi"); $sel->title_is("Classification Not Enabled"); logout($sel); diff --git a/qa/t/test_config.t b/qa/t/test_config.t index ef1d8d898..7ad97190a 100644 --- a/qa/t/test_config.t +++ b/qa/t/test_config.t @@ -18,12 +18,13 @@ my ($sel, $config) = get_selenium(); # Turn on 'requirelogin' and log out. log_in($sel, $config, 'admin'); -set_parameters($sel, { "User Authentication" => {"requirelogin-on" => undef} }); +set_parameters($sel, {"User Authentication" => {"requirelogin-on" => undef}}); logout($sel); # Accessing config.cgi should display no sensitive data. -$sel->open_ok("/$config->{bugzilla_installation}/config.cgi", undef, "Go to config.cgi (JS format)"); +$sel->open_ok("/$config->{bugzilla_installation}/config.cgi", + undef, "Go to config.cgi (JS format)"); $sel->is_text_present_ok("var status = [ ];"); $sel->is_text_present_ok("var status_open = [ ];"); $sel->is_text_present_ok("var status_closed = [ ];"); @@ -33,13 +34,14 @@ $sel->is_text_present_ok("var platform = [ ];"); $sel->is_text_present_ok("var severity = [ ];"); $sel->is_text_present_ok("var field = [\n];"); -ok(!$sel->is_text_present("cf_"), "No custom field displayed"); +ok(!$sel->is_text_present("cf_"), "No custom field displayed"); ok(!$sel->is_text_present("component["), "No component displayed"); -ok(!$sel->is_text_present("version["), "No version displayed"); -ok(!$sel->is_text_present("target_milestone["), "No target milestone displayed"); +ok(!$sel->is_text_present("version["), "No version displayed"); +ok(!$sel->is_text_present("target_milestone["), + "No target milestone displayed"); # Turn on 'requirelogin' and log out. log_in($sel, $config, 'admin'); -set_parameters($sel, { "User Authentication" => {"requirelogin-off" => undef} }); +set_parameters($sel, {"User Authentication" => {"requirelogin-off" => undef}}); logout($sel); diff --git a/qa/t/test_create_user_accounts.t b/qa/t/test_create_user_accounts.t index ba0f39671..25f93ba65 100644 --- a/qa/t/test_create_user_accounts.t +++ b/qa/t/test_create_user_accounts.t @@ -18,7 +18,13 @@ my ($sel, $config) = get_selenium(); # Set the email regexp for new bugzilla accounts to end with @bugzilla.test. log_in($sel, $config, 'admin'); -set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => '[^@]+@bugzilla\.test$'}} }); +set_parameters( + $sel, + { + "User Authentication" => + {"createemailregexp" => {type => "text", value => '[^@]+@bugzilla\.test$'}} + } +); logout($sel); # Create a valid account. We need to randomize the login address, because a request @@ -50,58 +56,69 @@ $sel->click_ok('//input[@value="Create Account"]'); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Too Soon For New Token"); my $error_msg = trim($sel->get_text("error_msg")); -ok($error_msg =~ /Please wait a while and try again/, "Too soon for this account"); +ok($error_msg =~ /Please wait a while and try again/, + "Too soon for this account"); # These accounts do not pass the regexp. -my @accounts = ('test@yahoo.com', 'test@bugzilla.net', 'test@bugzilla.test.com'); +my @accounts + = ('test@yahoo.com', 'test@bugzilla.net', 'test@bugzilla.test.com'); foreach my $account (@accounts) { - $sel->click_ok("link=New Account"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Create a new Bugzilla account"); - $sel->type_ok("login", $account); - $sel->check_ok("etiquette", "Agree to abide by code of conduct"); - $sel->click_ok('//input[@value="Create Account"]'); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Account Creation Restricted"); - $sel->is_text_present_ok("User account creation has been restricted."); + $sel->click_ok("link=New Account"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Create a new Bugzilla account"); + $sel->type_ok("login", $account); + $sel->check_ok("etiquette", "Agree to abide by code of conduct"); + $sel->click_ok('//input[@value="Create Account"]'); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Account Creation Restricted"); + $sel->is_text_present_ok("User account creation has been restricted."); } # These accounts are illegal and should cause a javascript alert. @accounts = qw( - test\bugzilla@bugzilla.test - testbugzilla.test - test@bugzilla - test@bugzilla. - 'test'@bugzilla.test - test&test@bugzilla.test - [test]@bugzilla.test + test\bugzilla@bugzilla.test + testbugzilla.test + test@bugzilla + test@bugzilla. + 'test'@bugzilla.test + test&test@bugzilla.test + [test]@bugzilla.test ); + foreach my $account (@accounts) { - $sel->click_ok("link=New Account"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Create a new Bugzilla account"); - $sel->type_ok("login", $account); - $sel->check_ok("etiquette", "Agree to abide by code of conduct"); - $sel->click_ok('//input[@value="Create Account"]'); - ok($sel->get_alert() =~ /The e-mail address doesn't pass our syntax checking for a legal email address/, - 'Invalid email address detected'); + $sel->click_ok("link=New Account"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Create a new Bugzilla account"); + $sel->type_ok("login", $account); + $sel->check_ok("etiquette", "Agree to abide by code of conduct"); + $sel->click_ok('//input[@value="Create Account"]'); + ok( + $sel->get_alert() + =~ /The e-mail address doesn't pass our syntax checking for a legal email address/, + 'Invalid email address detected' + ); } # These accounts are illegal but do not cause a javascript alert @accounts = ('test@bugzilla.org@bugzilla.test', 'test@bugzilla..test'); + # Logins larger than 127 characters must be rejected, for security reasons. push @accounts, 'selenium-' . random_string(110) . '@bugzilla.test'; foreach my $account (@accounts) { - $sel->click_ok("link=New Account"); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Create a new Bugzilla account"); - $sel->type_ok("login", $account); - $sel->check_ok("etiquette", "Agree to abide by code of conduct"); - $sel->click_ok('//input[@value="Create Account"]'); - $sel->wait_for_page_to_load_ok(WAIT_TIME); - $sel->title_is("Invalid Email Address"); - my $error_msg = trim($sel->get_text("error_msg")); - ok($error_msg =~ /^The e-mail address you entered (\S+) didn't pass our syntax checking/, "Invalid email address detected"); + $sel->click_ok("link=New Account"); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Create a new Bugzilla account"); + $sel->type_ok("login", $account); + $sel->check_ok("etiquette", "Agree to abide by code of conduct"); + $sel->click_ok('//input[@value="Create Account"]'); + $sel->wait_for_page_to_load_ok(WAIT_TIME); + $sel->title_is("Invalid Email Address"); + my $error_msg = trim($sel->get_text("error_msg")); + ok( + $error_msg + =~ /^The e-mail address you entered (\S+) didn't pass our syntax checking/, + "Invalid email address detected" + ); } # This account already exists. @@ -114,11 +131,20 @@ $sel->click_ok('//input[@value="Create Account"]'); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Account Already Exists"); $error_msg = trim($sel->get_text("error_msg")); -ok($error_msg eq "There is already an account with the login name $config->{admin_user_login}.", "Account already exists"); +ok( + $error_msg eq + "There is already an account with the login name $config->{admin_user_login}.", + "Account already exists" +); # Turn off user account creation. log_in($sel, $config, 'admin'); -set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => ''}} }); +set_parameters( + $sel, + { + "User Authentication" => {"createemailregexp" => {type => "text", value => ''}} + } +); logout($sel); # Make sure that links pointing to createaccount.cgi are all deactivated. @@ -132,13 +158,22 @@ ok(!$sel->is_text_present("New Account"), "No link named 'New Account'"); $sel->open_ok("/$config->{bugzilla_installation}/createaccount.cgi"); $sel->title_is("Account Creation Disabled"); $error_msg = trim($sel->get_text("error_msg")); -ok($error_msg =~ /^User account creation has been disabled. New accounts must be created by an administrator/, - "User account creation disabled"); +ok( + $error_msg + =~ /^User account creation has been disabled. New accounts must be created by an administrator/, + "User account creation disabled" +); # Re-enable user account creation. log_in($sel, $config, 'admin'); -set_parameters($sel, { "User Authentication" => {"createemailregexp" => {type => "text", value => '.*'}} }); +set_parameters( + $sel, + { + "User Authentication" => + {"createemailregexp" => {type => "text", value => '.*'}} + } +); # Make sure selenium-@bugzilla.test has not be added to the DB yet. go_to_admin($sel); diff --git a/qa/t/test_custom_fields.t b/qa/t/test_custom_fields.t index bd2074585..ed65a6f0f 100644 --- a/qa/t/test_custom_fields.t +++ b/qa/t/test_custom_fields.t @@ -21,7 +21,8 @@ log_in($sel, $config, 'admin'); file_bug_in_product($sel, 'TestProduct'); my $bug_summary = "What's your ID?"; $sel->type_ok("short_desc", $bug_summary); -$sel->type_ok("comment", "Use the ID of this bug to generate a unique custom field name."); +$sel->type_ok("comment", + "Use the ID of this bug to generate a unique custom field name."); $sel->type_ok("bug_severity", "label=normal"); my $bug1_id = create_bug($sel, $bug_summary); @@ -38,13 +39,16 @@ $sel->type_ok("name", "cf_qa_freetext_$bug1_id"); $sel->type_ok("desc", "Freetext$bug1_id"); $sel->select_ok("type", "label=Free Text"); $sel->type_ok("sortkey", $bug1_id); + # These values are off by default. $sel->value_is("enter_bug", "off"); -$sel->value_is("obsolete", "off"); +$sel->value_is("obsolete", "off"); $sel->click_ok("create"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Custom Field Created"); -$sel->is_text_present_ok("The new custom field 'cf_qa_freetext_$bug1_id' has been successfully created."); +$sel->is_text_present_ok( + "The new custom field 'cf_qa_freetext_$bug1_id' has been successfully created." +); $sel->click_ok("link=Add a new custom field"); $sel->wait_for_page_to_load_ok(WAIT_TIME); @@ -58,11 +62,12 @@ $sel->value_is("enter_bug", "on"); $sel->click_ok("new_bugmail"); sleep 10; $sel->value_is("new_bugmail", "on"); -$sel->value_is("obsolete", "off"); +$sel->value_is("obsolete", "off"); $sel->click_ok("create"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Custom Field Created"); -$sel->is_text_present_ok("The new custom field 'cf_qa_list_$bug1_id' has been successfully created."); +$sel->is_text_present_ok( + "The new custom field 'cf_qa_list_$bug1_id' has been successfully created."); $sel->click_ok("link=Add a new custom field"); $sel->wait_for_page_to_load_ok(WAIT_TIME); @@ -70,15 +75,16 @@ $sel->title_is("Add a new Custom Field"); $sel->type_ok("name", "cf_qa_bugid_$bug1_id"); $sel->type_ok("desc", "Reference$bug1_id"); $sel->select_ok("type", "label=Bug ID"); -$sel->type_ok("sortkey", $bug1_id); +$sel->type_ok("sortkey", $bug1_id); $sel->type_ok("reverse_desc", "IsRef$bug1_id"); $sel->click_ok("enter_bug"); $sel->value_is("enter_bug", "on"); -$sel->value_is("obsolete", "off"); +$sel->value_is("obsolete", "off"); $sel->click_ok("create"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Custom Field Created"); -$sel->is_text_present_ok("The new custom field 'cf_qa_bugid_$bug1_id' has been successfully created."); +$sel->is_text_present_ok( + "The new custom field 'cf_qa_bugid_$bug1_id' has been successfully created."); # Add values to the custom fields. @@ -87,27 +93,32 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)"); $sel->click_ok("link=Edit legal values for this field"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->title_is( + "Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); -$sel->type_ok("value", "have fun?"); +$sel->type_ok("value", "have fun?"); $sel->type_ok("sortkey", "805"); $sel->click_ok("create"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("New Field Value Created"); -$sel->is_text_present_ok("The value have fun? has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field."); +$sel->is_text_present_ok( + "The value have fun? has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field." +); $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); -$sel->type_ok("value", "storage"); +$sel->type_ok("value", "storage"); $sel->type_ok("sortkey", "49"); $sel->click_ok("create"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("New Field Value Created"); -$sel->is_text_present_ok("The value storage has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field."); +$sel->is_text_present_ok( + "The value storage has been added as a valid choice for the List$bug1_id (cf_qa_list_$bug1_id) field." +); # Also create a new bug status and a new resolution. @@ -121,7 +132,7 @@ $sel->title_is("Select value for the 'Resolution' (resolution) field"); $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add Value for the 'Resolution' (resolution) field"); -$sel->type_ok("value", "UPSTREAM"); +$sel->type_ok("value", "UPSTREAM"); $sel->type_ok("sortkey", 450); $sel->click_ok("create"); $sel->wait_for_page_to_load_ok(WAIT_TIME); @@ -137,7 +148,7 @@ $sel->title_is("Select value for the 'Status' (bug_status) field"); $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add Value for the 'Status' (bug_status) field"); -$sel->type_ok("value", "SUSPENDED"); +$sel->type_ok("value", "SUSPENDED"); $sel->type_ok("sortkey", 250); $sel->click_ok("open_status"); $sel->click_ok("create"); @@ -147,7 +158,7 @@ $sel->title_is("New Field Value Created"); $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add Value for the 'Status' (bug_status) field"); -$sel->type_ok("value", "IN_QA"); +$sel->type_ok("value", "IN_QA"); $sel->type_ok("sortkey", 550); $sel->click_ok("closed_status"); $sel->click_ok("create"); @@ -157,13 +168,20 @@ $sel->title_is("New Field Value Created"); $sel->click_ok("link=status workflow page"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Edit Workflow"); -$sel->click_ok('//td[@title="From UNCONFIRMED to SUSPENDED"]//input[@type="checkbox"]'); -$sel->click_ok('//td[@title="From CONFIRMED to SUSPENDED"]//input[@type="checkbox"]'); -$sel->click_ok('//td[@title="From SUSPENDED to CONFIRMED"]//input[@type="checkbox"]'); -$sel->click_ok('//td[@title="From SUSPENDED to IN_PROGRESS"]//input[@type="checkbox"]'); -$sel->click_ok('//td[@title="From RESOLVED to IN_QA"]//input[@type="checkbox"]'); -$sel->click_ok('//td[@title="From IN_QA to VERIFIED"]//input[@type="checkbox"]'); -$sel->click_ok('//td[@title="From IN_QA to CONFIRMED"]//input[@type="checkbox"]'); +$sel->click_ok( + '//td[@title="From UNCONFIRMED to SUSPENDED"]//input[@type="checkbox"]'); +$sel->click_ok( + '//td[@title="From CONFIRMED to SUSPENDED"]//input[@type="checkbox"]'); +$sel->click_ok( + '//td[@title="From SUSPENDED to CONFIRMED"]//input[@type="checkbox"]'); +$sel->click_ok( + '//td[@title="From SUSPENDED to IN_PROGRESS"]//input[@type="checkbox"]'); +$sel->click_ok( + '//td[@title="From RESOLVED to IN_QA"]//input[@type="checkbox"]'); +$sel->click_ok( + '//td[@title="From IN_QA to VERIFIED"]//input[@type="checkbox"]'); +$sel->click_ok( + '//td[@title="From IN_QA to CONFIRMED"]//input[@type="checkbox"]'); $sel->click_ok('//input[@value="Commit Changes"]'); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Edit Workflow"); @@ -175,13 +193,19 @@ $sel->is_text_present_ok("List$bug1_id:"); $sel->is_element_present_ok("cf_qa_list_$bug1_id"); $sel->is_text_present_ok("Reference$bug1_id:"); $sel->is_element_present_ok("cf_qa_bugid_$bug1_id"); -ok(!$sel->is_text_present("Freetext$bug1_id:"), "Freetext$bug1_id is not displayed"); -ok(!$sel->is_element_present("cf_qa_freetext_$bug1_id"), "cf_qa_freetext_$bug1_id is not available"); +ok( + !$sel->is_text_present("Freetext$bug1_id:"), + "Freetext$bug1_id is not displayed" +); +ok( + !$sel->is_element_present("cf_qa_freetext_$bug1_id"), + "cf_qa_freetext_$bug1_id is not available" +); my $bug_summary2 = "Et de un"; $sel->type_ok("short_desc", $bug_summary2); $sel->select_ok("bug_severity", "critical"); $sel->type_ok("cf_qa_bugid_$bug1_id", $bug1_id); -$sel->type_ok("comment", "hops!"); +$sel->type_ok("comment", "hops!"); my $bug2_id = create_bug($sel, $bug_summary2); # Both fields are editable. @@ -257,27 +281,32 @@ $sel->click_ok("link=cf_qa_list_$bug1_id"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)"); $sel->select_ok("visibility_field_id", "label=Severity (bug_severity)"); -$sel->select_ok("visibility_values", "label=critical"); +$sel->select_ok("visibility_values", "label=critical"); $sel->click_ok("edit"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Custom Field Updated"); go_to_bug($sel, $bug1_id); -$sel->is_element_present_ok("cf_qa_list_$bug1_id", "List$bug1_id is in the DOM of the page..."); -ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "... but is not displayed with severity = 'normal'"); +$sel->is_element_present_ok("cf_qa_list_$bug1_id", + "List$bug1_id is in the DOM of the page..."); +ok(!$sel->is_visible("cf_qa_list_$bug1_id"), + "... but is not displayed with severity = 'normal'"); $sel->select_ok("bug_severity", "major"); ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "... nor with severity = 'major'"); $sel->select_ok("bug_severity", "critical"); -$sel->is_visible_ok("cf_qa_list_$bug1_id", "... but is visible with severity = 'critical'"); +$sel->is_visible_ok("cf_qa_list_$bug1_id", + "... but is visible with severity = 'critical'"); edit_bug_and_return($sel, $bug1_id, $bug_summary); $sel->is_visible_ok("cf_qa_list_$bug1_id"); go_to_bug($sel, $bug2_id); $sel->is_visible_ok("cf_qa_list_$bug1_id"); $sel->select_ok("bug_severity", "minor"); -ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "List$bug1_id is not displayed with severity = 'minor'"); +ok(!$sel->is_visible("cf_qa_list_$bug1_id"), + "List$bug1_id is not displayed with severity = 'minor'"); edit_bug_and_return($sel, $bug2_id, $bug_summary2); -ok(!$sel->is_visible("cf_qa_list_$bug1_id"), "List$bug1_id is not displayed with severity = 'minor'"); +ok(!$sel->is_visible("cf_qa_list_$bug1_id"), + "List$bug1_id is not displayed with severity = 'minor'"); # Add a new value which is only listed under some condition. @@ -297,11 +326,12 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Edit the Custom Field 'cf_qa_list_$bug1_id' (List$bug1_id)"); $sel->click_ok("link=Edit legal values for this field"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->title_is( + "Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); $sel->click_ok("link=Add"); $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Add Value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); -$sel->type_ok("value", "ghost"); +$sel->type_ok("value", "ghost"); $sel->type_ok("sortkey", "500"); $sel->select_ok("visibility_value_id", "label=FIXED"); $sel->click_ok("id=create"); @@ -313,8 +343,8 @@ my @labels = $sel->get_select_options("cf_qa_list_$bug1_id"); ok(grep(/^ghost$/, @labels), "ghost is in the DOM of the page..."); my $disabled = $sel->get_attribute("v4_cf_qa_list_$bug1_id\@disabled"); ok($disabled, "... but is not available for selection by default"); -$sel->select_ok("bug_status", "label=RESOLVED"); -$sel->select_ok("resolution", "label=FIXED"); +$sel->select_ok("bug_status", "label=RESOLVED"); +$sel->select_ok("resolution", "label=FIXED"); $sel->select_ok("cf_qa_list_$bug1_id", "label=ghost"); edit_bug_and_return($sel, $bug1_id, $bug_summary); $sel->selected_label_is("cf_qa_list_$bug1_id", "ghost"); @@ -327,10 +357,14 @@ $sel->wait_for_page_to_load_ok(WAIT_TIME); $sel->title_is("Edit values for which field?"); $sel->click_ok("link=List$bug1_id"); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); -$sel->click_ok("//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=have%20fun%3F')]"); +$sel->title_is( + "Select value for the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->click_ok( + "//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=have%20fun%3F')]" +); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Delete Value 'have fun?' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->title_is( + "Delete Value 'have fun?' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); $sel->is_text_present_ok("Do you really want to delete this value?"); $sel->click_ok("delete"); $sel->wait_for_page_to_load_ok(WAIT_TIME); @@ -338,9 +372,12 @@ $sel->title_is("Field Value Deleted"); # This value cannot be deleted as it's in use. -$sel->click_ok("//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=storage')]"); +$sel->click_ok( + "//a[contains(\@href, 'editvalues.cgi?action=del&field=cf_qa_list_$bug1_id&value=storage')]" +); $sel->wait_for_page_to_load_ok(WAIT_TIME); -$sel->title_is("Delete Value 'storage' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); +$sel->title_is( + "Delete Value 'storage' from the 'List$bug1_id' (cf_qa_list_$bug1_id) field"); $sel->is_text_present_ok("There is 1 bug with this field value"); # Mark the fields. Bugzilla->dbh->bz_populate_enum_tables(); -}); + } +); -$lives_ok->('update_fielddefs_definition' => sub { +$lives_ok->( + 'update_fielddefs_definition' => sub { Bugzilla::Install::DB::update_fielddefs_definition(); -}); + } +); -$lives_ok->('populate_field_definitions' => sub { +$lives_ok->( + 'populate_field_definitions' => sub { Bugzilla::Field::populate_field_definitions(); -}); + } +); -$lives_ok->('init_workflow' => sub { +$lives_ok->( + 'init_workflow' => sub { Bugzilla::Install::init_workflow(); -}); + } +); -$lives_ok->('update_table_definitions' => sub { +$lives_ok->( + 'update_table_definitions' => sub { Bugzilla::Install::DB->update_table_definitions({}); -}); + } +); -$lives_ok->('update_system_groups' => sub { +$lives_ok->( + 'update_system_groups' => sub { Bugzilla::Install::update_system_groups(); -}); + } +); # "Log In" as the fake superuser who can do everything. Bugzilla->set_user(Bugzilla::User->super_user); -$lives_ok->('update_settings' => sub { +$lives_ok->( + 'update_settings' => sub { Bugzilla::Install::update_settings(); -}); + } +); SKIP: { - skip 'default product cannot be created without default assignee', 1; - $lives_ok->('create_default_product' => sub { - Bugzilla::Install::create_default_product(); - }); + skip 'default product cannot be created without default assignee', 1; + $lives_ok->( + 'create_default_product' => sub { + Bugzilla::Install::create_default_product(); + } + ); } done_testing; diff --git a/template/en/default/filterexceptions.pl b/template/en/default/filterexceptions.pl index 39f064035..4ce95b8c1 100644 --- a/template/en/default/filterexceptions.pl +++ b/template/en/default/filterexceptions.pl @@ -38,441 +38,238 @@ %::safe = ( -'whine/schedule.html.tmpl' => [ - 'event.key', - 'query.id', - 'query.sort', - 'schedule.id', - 'option.0', - 'option.1', -], - -'whine/mail.html.tmpl' => [ - 'bug.bug_id', -], - -'flag/list.html.tmpl' => [ - 'flag.status', - 'type.id', -], - -'search/form.html.tmpl' => [ - 'qv.name', - 'qv.description', -], - -'search/search-specific.html.tmpl' => [ - 'status.name', -], - -'search/tabs.html.tmpl' => [ - 'content', -], - -'request/queue.html.tmpl' => [ - 'column_headers.$group_field', - 'column_headers.$column', - 'request.status', - 'request.bug_id', - 'request.attach_id', -], - -'reports/keywords.html.tmpl' => [ - 'keyword.bug_count', -], - -'reports/report-table.csv.tmpl' => [ - 'data.$tbl.$col.$row', - 'colsepchar', -], - -'reports/report-table.html.tmpl' => [ - '"&$col_vals" IF col_vals', - '"&$row_vals" IF row_vals', - 'classes.$row_idx.$col_idx', - 'urlbase', - 'data.$tbl.$col.$row', - 'row_total', - 'col_totals.$col', - 'grand_total', -], - -'reports/report.html.tmpl' => [ - 'width', - 'height', - 'imageurl', - 'formaturl', - 'other_format.name', - 'sizeurl', - 'switchbase', - 'cumulate', -], - -'reports/chart.html.tmpl' => [ - 'width', - 'height', - 'imageurl', - 'sizeurl', - 'height + 100', - 'height - 100', - 'width + 100', - 'width - 100', -], - -'reports/series-common.html.tmpl' => [ - 'sel.name', - '"onchange=\"$sel.onchange\"" IF sel.onchange', -], - -'reports/chart.csv.tmpl' => [ - 'data.$j.$i', - 'colsepchar', -], - -'reports/create-chart.html.tmpl' => [ - 'series.series_id', - 'newidx', -], - -'reports/edit-series.html.tmpl' => [ - 'default.series_id', -], - -'list/edit-multiple.html.tmpl' => [ - 'group.id', - 'menuname', -], - -'list/list.rdf.tmpl' => [ - 'template_version', - 'bug.bug_id', - 'column', -], - -'list/table.html.tmpl' => [ - 'tableheader', - 'bug.bug_id', -], - -'list/list.csv.tmpl' => [ - 'bug.bug_id', - 'colsepchar', -], - -'list/list.js.tmpl' => [ - 'bug.bug_id', -], - -'global/choose-product.html.tmpl' => [ - 'target', -], + 'whine/schedule.html.tmpl' => [ + 'event.key', 'query.id', 'query.sort', 'schedule.id', 'option.0', 'option.1', + ], + + 'whine/mail.html.tmpl' => ['bug.bug_id',], + + 'flag/list.html.tmpl' => ['flag.status', 'type.id',], + + 'search/form.html.tmpl' => ['qv.name', 'qv.description',], + + 'search/search-specific.html.tmpl' => ['status.name',], + + 'search/tabs.html.tmpl' => ['content',], + + 'request/queue.html.tmpl' => [ + 'column_headers.$group_field', 'column_headers.$column', + 'request.status', 'request.bug_id', + 'request.attach_id', + ], + + 'reports/keywords.html.tmpl' => ['keyword.bug_count',], + + 'reports/report-table.csv.tmpl' => ['data.$tbl.$col.$row', 'colsepchar',], + + 'reports/report-table.html.tmpl' => [ + '"&$col_vals" IF col_vals', '"&$row_vals" IF row_vals', + 'classes.$row_idx.$col_idx', 'urlbase', + 'data.$tbl.$col.$row', 'row_total', + 'col_totals.$col', 'grand_total', + ], + + 'reports/report.html.tmpl' => [ + 'width', 'height', 'imageurl', 'formaturl', + 'other_format.name', 'sizeurl', 'switchbase', 'cumulate', + ], + + 'reports/chart.html.tmpl' => [ + 'width', 'height', 'imageurl', 'sizeurl', + 'height + 100', 'height - 100', 'width + 100', 'width - 100', + ], + + 'reports/series-common.html.tmpl' => + ['sel.name', '"onchange=\"$sel.onchange\"" IF sel.onchange',], + + 'reports/chart.csv.tmpl' => ['data.$j.$i', 'colsepchar',], + + 'reports/create-chart.html.tmpl' => ['series.series_id', 'newidx',], + + 'reports/edit-series.html.tmpl' => ['default.series_id',], + + 'list/edit-multiple.html.tmpl' => ['group.id', 'menuname',], + + 'list/list.rdf.tmpl' => ['template_version', 'bug.bug_id', 'column',], + + 'list/table.html.tmpl' => ['tableheader', 'bug.bug_id',], + + 'list/list.csv.tmpl' => ['bug.bug_id', 'colsepchar',], + + 'list/list.js.tmpl' => ['bug.bug_id',], + + 'global/choose-product.html.tmpl' => ['target',], # You are not permitted to add any values here. Everything in this file should # be filtered unless there's an extremely good reason why not, in which case, # use the "none" dummy filter. -'global/code-error.html.tmpl' => [ -], - -'global/header.html.tmpl' => [ - 'javascript', - 'style', - 'onload', - 'title', - 'message', -], - -'global/messages.html.tmpl' => [ - 'series.frequency * 2', -], - -'global/select-menu.html.tmpl' => [ - 'options', - 'size', -], - -'global/tabs.html.tmpl' => [ - 'content', -], + 'global/code-error.html.tmpl' => [], + + 'global/header.html.tmpl' => + ['javascript', 'style', 'onload', 'title', 'message',], + + 'global/messages.html.tmpl' => ['series.frequency * 2',], + + 'global/select-menu.html.tmpl' => ['options', 'size',], + + 'global/tabs.html.tmpl' => ['content',], # You are not permitted to add any values here. Everything in this file should # be filtered unless there's an extremely good reason why not, in which case, # use the "none" dummy filter. -'global/user-error.html.tmpl' => [ -], - -'global/confirm-user-match.html.tmpl' => [ - 'script', -], - -'global/site-navigation.html.tmpl' => [ - 'bug.bug_id', -], - -'bug/comments.html.tmpl' => [ - 'comment.id', - 'comment.count', - 'bug.bug_id', -], - -'bug/dependency-graph.html.tmpl' => [ - 'image_map', # We need to continue to make sure this is safe in the CGI - 'image_url', - 'map_url', - 'bug_id', -], - -'bug/dependency-tree.html.tmpl' => [ - 'bugid', - 'maxdepth', - 'hide_resolved', - 'ids.join(",")', - 'maxdepth + 1', - 'maxdepth > 0 && maxdepth <= realdepth ? maxdepth : ""', - 'maxdepth == 1 ? 1 + 'global/user-error.html.tmpl' => [], + + 'global/confirm-user-match.html.tmpl' => ['script',], + + 'global/site-navigation.html.tmpl' => ['bug.bug_id',], + + 'bug/comments.html.tmpl' => ['comment.id', 'comment.count', 'bug.bug_id',], + + 'bug/dependency-graph.html.tmpl' => [ + 'image_map', # We need to continue to make sure this is safe in the CGI + 'image_url', 'map_url', 'bug_id', + ], + + 'bug/dependency-tree.html.tmpl' => [ + 'bugid', 'maxdepth', 'hide_resolved', 'ids.join(",")', 'maxdepth + 1', + 'maxdepth > 0 && maxdepth <= realdepth ? maxdepth : ""', 'maxdepth == 1 ? 1 : ( maxdepth ? maxdepth - 1 : realdepth - 1 )', -], - -'bug/edit.html.tmpl' => [ - 'bug.remaining_time', - 'bug.delta_ts', - 'bug.bug_id', - 'group.bit', - 'selname', - 'inputname', - '" colspan=\"$colspan\"" IF colspan', - '" size=\"$size\"" IF size', - '" maxlength=\"$maxlength\"" IF maxlength', - '" spellcheck=\"$spellcheck\"" IF spellcheck', -], - -'bug/show-multiple.html.tmpl' => [ - 'attachment.id', - 'flag.status', -], - -'bug/show.html.tmpl' => [ - 'bug.bug_id', -], - -'bug/show.xml.tmpl' => [ - 'constants.BUGZILLA_VERSION', - 'a.id', - 'field', -], - -'bug/summarize-time.html.tmpl' => [ - 'global.grand_total FILTER format("%.2f")', - 'subtotal FILTER format("%.2f")', - 'work_time FILTER format("%.2f")', - 'global.total FILTER format("%.2f")', - 'global.remaining FILTER format("%.2f")', - 'global.estimated FILTER format("%.2f")', - 'bugs.$id.remaining_time FILTER format("%.2f")', - 'bugs.$id.estimated_time FILTER format("%.2f")', -], - - -'bug/time.html.tmpl' => [ - "time_unit.replace('0\\Z', '')", - '(act / (act + rem)) * 100 + ], + + 'bug/edit.html.tmpl' => [ + 'bug.remaining_time', + 'bug.delta_ts', + 'bug.bug_id', + 'group.bit', + 'selname', + 'inputname', + '" colspan=\"$colspan\"" IF colspan', + '" size=\"$size\"" IF size', + '" maxlength=\"$maxlength\"" IF maxlength', + '" spellcheck=\"$spellcheck\"" IF spellcheck', + ], + + 'bug/show-multiple.html.tmpl' => ['attachment.id', 'flag.status',], + + 'bug/show.html.tmpl' => ['bug.bug_id',], + + 'bug/show.xml.tmpl' => ['constants.BUGZILLA_VERSION', 'a.id', 'field',], + + 'bug/summarize-time.html.tmpl' => [ + 'global.grand_total FILTER format("%.2f")', + 'subtotal FILTER format("%.2f")', + 'work_time FILTER format("%.2f")', + 'global.total FILTER format("%.2f")', + 'global.remaining FILTER format("%.2f")', + 'global.estimated FILTER format("%.2f")', + 'bugs.$id.remaining_time FILTER format("%.2f")', + 'bugs.$id.estimated_time FILTER format("%.2f")', + ], + + + 'bug/time.html.tmpl' => [ + "time_unit.replace('0\\Z', '')", '(act / (act + rem)) * 100 FILTER format("%d")', -], - -'bug/process/results.html.tmpl' => [ - 'title.$type', - '"$terms.Bug $id" FILTER bug_link(id)', - '"$terms.bug $id" FILTER bug_link(id)', -], - -'bug/create/create.html.tmpl' => [ - 'cloned_bug_id', -], - -'bug/create/create-guided.html.tmpl' => [ - 'tablecolour', - 'sel', - 'productstring', -], - -'bug/activity/table.html.tmpl' => [ - 'change.attachid', -], - -'attachment/create.html.tmpl' => [ - 'bug.bug_id', - 'attachment.id', -], - -'attachment/created.html.tmpl' => [ - 'attachment.id', - 'attachment.bug_id', -], - -'attachment/edit.html.tmpl' => [ - 'attachment.id', - 'attachment.bug_id', - 'editable_or_hide', -], - -'attachment/list.html.tmpl' => [ - 'attachment.id', - 'flag.status', - 'bugid', - 'obsolete_attachments', -], - -'attachment/midair.html.tmpl' => [ - 'attachment.id', -], - -'attachment/show-multiple.html.tmpl' => [ - 'a.id', - 'flag.status' -], - -'attachment/updated.html.tmpl' => [ - 'attachment.id', -], - -'attachment/diff-header.html.tmpl' => [ - 'attachid', - 'id', - 'bugid', - 'oldid', - 'newid', - 'patch.id', -], - -'attachment/diff-file.html.tmpl' => [ - 'lxr_prefix', - 'file.minus_lines', - 'file.plus_lines', - 'bonsai_prefix', - 'section.old_start', - 'section_num', - 'current_line_old', - 'current_line_new', -], - -'admin/admin.html.tmpl' => [ - 'class' -], - -'admin/table.html.tmpl' => [ - 'link_uri' -], - -'admin/custom_fields/cf-js.js.tmpl' => [ - 'constants.FIELD_TYPE_SINGLE_SELECT', - 'constants.FIELD_TYPE_MULTI_SELECT', - 'constants.FIELD_TYPE_BUG_ID', -], - -'admin/params/common.html.tmpl' => [ - 'sortlist_separator', -], - -'admin/products/groupcontrol/confirm-edit.html.tmpl' => [ - 'group.count', -], - -'admin/products/groupcontrol/edit.html.tmpl' => [ - 'group.id', - 'constants.CONTROLMAPNA', - 'constants.CONTROLMAPSHOWN', - 'constants.CONTROLMAPDEFAULT', - 'constants.CONTROLMAPMANDATORY', -], - -'admin/products/list.html.tmpl' => [ - 'classification_url_part', -], - -'admin/products/footer.html.tmpl' => [ - 'classification_url_part', - 'classification_text', -], - -'admin/flag-type/confirm-delete.html.tmpl' => [ - 'flag_type.flag_count', - 'flag_type.id', -], - -'admin/flag-type/edit.html.tmpl' => [ - 'selname', -], - -'admin/flag-type/list.html.tmpl' => [ - 'type.id', -], - - -'admin/components/confirm-delete.html.tmpl' => [ - 'comp.bug_count' -], - -'admin/groups/delete.html.tmpl' => [ - 'shared_queries' -], - -'admin/users/confirm-delete.html.tmpl' => [ - 'attachments', - 'reporter', - 'assignee_or_qa', - 'cc', - 'component_cc', - 'flags.requestee', - 'flags.setter', - 'longdescs', - 'quips', - 'series', - 'watch.watched', - 'watch.watcher', - 'whine_events', - 'whine_schedules', - 'otheruser.id' -], - -'admin/users/edit.html.tmpl' => [ - 'otheruser.id', - 'group.id', -], - -'admin/components/edit.html.tmpl' => [ - 'comp.bug_count' -], - -'admin/workflow/edit.html.tmpl' => [ - 'status.id', - 'new_status.id', -], - -'admin/workflow/comment.html.tmpl' => [ - 'status.id', - 'new_status.id', -], - -'account/auth/login-small.html.tmpl' => [ - 'qs_suffix', -], - -'account/prefs/email.html.tmpl' => [ - 'relationship.id', - 'event.id', - 'prefname', -], - -'account/prefs/prefs.html.tmpl' => [ - 'current_tab.label', - 'current_tab.name', -], - -'account/prefs/saved-searches.html.tmpl' => [ - 'group.id', -], - -'config.rdf.tmpl' => [ - 'escaped_urlbase', -], + ], + + 'bug/process/results.html.tmpl' => [ + 'title.$type', + '"$terms.Bug $id" FILTER bug_link(id)', + '"$terms.bug $id" FILTER bug_link(id)', + ], + + 'bug/create/create.html.tmpl' => ['cloned_bug_id',], + + 'bug/create/create-guided.html.tmpl' => + ['tablecolour', 'sel', 'productstring',], + + 'bug/activity/table.html.tmpl' => ['change.attachid',], + + 'attachment/create.html.tmpl' => ['bug.bug_id', 'attachment.id',], + + 'attachment/created.html.tmpl' => ['attachment.id', 'attachment.bug_id',], + + 'attachment/edit.html.tmpl' => + ['attachment.id', 'attachment.bug_id', 'editable_or_hide',], + + 'attachment/list.html.tmpl' => + ['attachment.id', 'flag.status', 'bugid', 'obsolete_attachments',], + + 'attachment/midair.html.tmpl' => ['attachment.id',], + + 'attachment/show-multiple.html.tmpl' => ['a.id', 'flag.status'], + + 'attachment/updated.html.tmpl' => ['attachment.id',], + + 'attachment/diff-header.html.tmpl' => + ['attachid', 'id', 'bugid', 'oldid', 'newid', 'patch.id',], + + 'attachment/diff-file.html.tmpl' => [ + 'lxr_prefix', 'file.minus_lines', + 'file.plus_lines', 'bonsai_prefix', + 'section.old_start', 'section_num', + 'current_line_old', 'current_line_new', + ], + + 'admin/admin.html.tmpl' => ['class'], + + 'admin/table.html.tmpl' => ['link_uri'], + + 'admin/custom_fields/cf-js.js.tmpl' => [ + 'constants.FIELD_TYPE_SINGLE_SELECT', 'constants.FIELD_TYPE_MULTI_SELECT', + 'constants.FIELD_TYPE_BUG_ID', + ], + + 'admin/params/common.html.tmpl' => ['sortlist_separator',], + + 'admin/products/groupcontrol/confirm-edit.html.tmpl' => ['group.count',], + + 'admin/products/groupcontrol/edit.html.tmpl' => [ + 'group.id', 'constants.CONTROLMAPNA', + 'constants.CONTROLMAPSHOWN', 'constants.CONTROLMAPDEFAULT', + 'constants.CONTROLMAPMANDATORY', + ], + + 'admin/products/list.html.tmpl' => ['classification_url_part',], + + 'admin/products/footer.html.tmpl' => + ['classification_url_part', 'classification_text',], + + 'admin/flag-type/confirm-delete.html.tmpl' => + ['flag_type.flag_count', 'flag_type.id',], + + 'admin/flag-type/edit.html.tmpl' => ['selname',], + + 'admin/flag-type/list.html.tmpl' => ['type.id',], + + + 'admin/components/confirm-delete.html.tmpl' => ['comp.bug_count'], + + 'admin/groups/delete.html.tmpl' => ['shared_queries'], + + 'admin/users/confirm-delete.html.tmpl' => [ + 'attachments', 'reporter', 'assignee_or_qa', 'cc', + 'component_cc', 'flags.requestee', 'flags.setter', 'longdescs', + 'quips', 'series', 'watch.watched', 'watch.watcher', + 'whine_events', 'whine_schedules', 'otheruser.id' + ], + + 'admin/users/edit.html.tmpl' => ['otheruser.id', 'group.id',], + + 'admin/components/edit.html.tmpl' => ['comp.bug_count'], + + 'admin/workflow/edit.html.tmpl' => ['status.id', 'new_status.id',], + + 'admin/workflow/comment.html.tmpl' => ['status.id', 'new_status.id',], + + 'account/auth/login-small.html.tmpl' => ['qs_suffix',], + + 'account/prefs/email.html.tmpl' => ['relationship.id', 'event.id', 'prefname',], + + 'account/prefs/prefs.html.tmpl' => ['current_tab.label', 'current_tab.name',], + + 'account/prefs/saved-searches.html.tmpl' => ['group.id',], + + 'config.rdf.tmpl' => ['escaped_urlbase',], ); diff --git a/template/en/default/setup/strings.txt.pl b/template/en/default/setup/strings.txt.pl index cb0ac4fe9..f994ee466 100644 --- a/template/en/default/setup/strings.txt.pl +++ b/template/en/default/setup/strings.txt.pl @@ -16,16 +16,17 @@ # Please keep the strings in alphabetical order by their name. %strings = ( - all_optional_features_require => 'All optional features above require the following modules to be found:', - any => 'any', - apachectl_failed => < + 'All optional features above require the following modules to be found:', + any => 'any', + apachectl_failed => < 'not a valid executable: ##bin##', - blacklisted => '(blacklisted)', - bz_schema_exists_before_220 => <<'END', + bad_executable => 'not a valid executable: ##bin##', + blacklisted => '(blacklisted)', + bz_schema_exists_before_220 => <<'END', You are upgrading from a version before 2.20, but the bz_schema table already exists. This means that you restored a mysqldump into the Bugzilla database without first dropping the already-existing Bugzilla database, @@ -37,32 +38,31 @@ not contain the bz_schema table. If for some reason you cannot do this, you can connect to your MySQL database and drop the bz_schema table, as a last resort. END - checking_for => 'Checking for', - chmod_failed => '##path##: Failed to change permissions: ##error##', - chown_failed => '##path##: Failed to change ownership: ##error##', - commands_dbd => < 'Checking for', + chmod_failed => '##path##: Failed to change permissions: ##error##', + chown_failed => '##path##: Failed to change ownership: ##error##', + commands_dbd => < 'COMMANDS TO INSTALL OPTIONAL MODULES:', - commands_required => < 'COMMANDS TO INSTALL OPTIONAL MODULES:', + commands_required => < <<'END', + continue_without_answers => <<'END', Re-run checksetup.pl in interactive mode (without an 'answers' file) to continue. END - cpanfile_created => "##file## created", - cpan_bugzilla_home => - "WARNING: Using the Bugzilla directory as the CPAN home.", - db_enum_setup => "Setting up choices for standard drop-down fields:", - db_schema_init => "Initializing bz_schema...", - db_table_new => "Adding new table ##table##...", - db_table_setup => "Creating tables...", - done => 'done.', - enter_or_ctrl_c => "Press Enter to continue or Ctrl-C to exit...", - error_localconfig_read => <<'END', + cpanfile_created => "##file## created", + cpan_bugzilla_home => "WARNING: Using the Bugzilla directory as the CPAN home.", + db_enum_setup => "Setting up choices for standard drop-down fields:", + db_schema_init => "Initializing bz_schema...", + db_table_new => "Adding new table ##table##...", + db_table_setup => "Creating tables...", + done => 'done.', + enter_or_ctrl_c => "Press Enter to continue or Ctrl-C to exit...", + error_localconfig_read => <<'END', An error has occurred while reading the ##localconfig## file. The text of the error message is: @@ -75,24 +75,24 @@ localconfig file: $ mv -f localconfig localconfig.old $ ./checksetup.pl END - extension_must_return_name => < <1 or a number. See the documentation of Bugzilla::Extension for details. END - file_remove => 'Removing ##name##...', - file_rename => 'Renaming ##from## to ##to##...', - header => "* This is Bugzilla ##bz_ver## on perl ##perl_ver##\n" - . "* Running on ##os_name## ##os_ver##", - installation_failed => '*** Installation aborted. Read the messages above. ***', - install_data_too_long => < 'Removing ##name##...', + file_rename => 'Renaming ##from## to ##to##...', + header => "* This is Bugzilla ##bz_ver## on perl ##perl_ver##\n" + . "* Running on ##os_name## ##os_ver##", + installation_failed => '*** Installation aborted. Read the messages above. ***', + install_data_too_long => < <<'END', + lc_new_vars => <<'END', This version of Bugzilla contains some variables that you may want to change and adapt to your local settings. The following variables are new to ##localconfig## since you last ran checksetup.pl: @@ -102,11 +102,11 @@ new to ##localconfig## since you last ran checksetup.pl: Please edit the file ##localconfig## and then re-run checksetup.pl to complete your installation. END - lc_old_vars => <<'END', + lc_old_vars => <<'END', The following variables are no longer used in ##localconfig##, and have been moved to ##old_file##: ##vars## END - localconfig_attachment_base => <<'END', + localconfig_attachment_base => <<'END', When the runtime allow_attachment_display parameter is on, it is possible for a malicious attachment to steal your cookies or perform an attack using your credentials. @@ -124,7 +124,7 @@ attachments to accessing only other attachments on the same bug. Remember, though, that all those possible domain names must point to this same instance. END - localconfig_create_htaccess => <<'END', + localconfig_create_htaccess => <<'END', If you are using Apache as your web server, Bugzilla can create .htaccess files for you, which will keep this file (localconfig) and other confidential files from being read over the web. @@ -134,62 +134,62 @@ they don't exist. If this is set to 0, checksetup.pl will not create .htaccess files. END - localconfig_cvsbin => <<'END', + localconfig_cvsbin => <<'END', If you want to use the CVS integration of the Patch Viewer, please specify the full path to the "cvs" executable here. END - localconfig_datadog_host => 'hostname of datadog stats daemon', - localconfig_datadog_port => 'port of datadog stats daemon, defaults to 8125', - localconfig_db_check => <<'END', + localconfig_datadog_host => 'hostname of datadog stats daemon', + localconfig_datadog_port => 'port of datadog stats daemon, defaults to 8125', + localconfig_db_check => <<'END', Should checksetup.pl try to verify that your database setup is correct? With some combinations of database servers/Perl modules/moonphase this doesn't work, and so you can try setting this to 0 to make checksetup.pl run. END - localconfig_db_driver => <<'END', + localconfig_db_driver => <<'END', What SQL database to use. Default is mysql. List of supported databases can be obtained by listing Bugzilla/DB directory - every module corresponds to one supported database and the name of the module (before ".pm") corresponds to a valid value for this variable. END - localconfig_db_host => <<'END', + localconfig_db_host => <<'END', The DNS name or IP address of the host that the database server runs on. END - localconfig_db_name => <<'END', + localconfig_db_name => <<'END', The name of the database. For Oracle, this is the database's SID. For SQLite, this is a name (or path) for the DB file. END - localconfig_db_pass => <<'END', + localconfig_db_pass => <<'END', Enter your database password here. It's normally advisable to specify a password for your bugzilla database user. If you use apostrophe (') or a backslash (\) in your password, you'll need to escape it by preceding it with a '\' character. (\') or (\) (It is far simpler to just not use those characters.) END - localconfig_db_port => <<'END', + localconfig_db_port => <<'END', Sometimes the database server is running on a non-standard port. If that's the case for your database server, set this to the port number that your database server is running on. Setting this to 0 means "use the default port for my database server." END - localconfig_db_sock => <<'END', + localconfig_db_sock => <<'END', MySQL Only: Enter a path to the unix socket for MySQL. If this is blank, then MySQL's compiled-in default will be used. You probably want that. END - localconfig_db_user => "Who we connect to the database as.", - localconfig_diffpath => <<'END', + localconfig_db_user => "Who we connect to the database as.", + localconfig_diffpath => <<'END', For the "Difference Between Two Patches" feature to work, we need to know what directory the "diff" bin is in. (You only need to set this if you are using that feature of the Patch Viewer.) END - localconfig_tct_bin => 'Path to tct (tocotrienol) a gpg replacement.', - localconfig_inbound_proxies => <<'END', + localconfig_tct_bin => 'Path to tct (tocotrienol) a gpg replacement.', + localconfig_inbound_proxies => <<'END', This is a list of IP addresses that we expect proxies to come from. This can be '*' if only the load balancer can connect. Setting this to '*' means that we can trust the X-Forwarded-For header. END - localconfig_index_html => <<'END', + localconfig_index_html => <<'END', Most web servers will allow you to use index.cgi as a directory index, and many come preconfigured that way, but if yours doesn't then you'll need an index.html file that provides redirection @@ -199,45 +199,45 @@ NOTE: checksetup.pl will not replace an existing file, so if you wish to have checksetup.pl create one for you, you must make sure that index.html doesn't already exist. END - localconfig_interdiffbin => <<'END', + localconfig_interdiffbin => <<'END', If you want to use the "Difference Between Two Patches" feature of the Patch Viewer, please specify the full path to the "interdiff" executable here. END - localconfig_memcached_servers => <<'END', + localconfig_memcached_servers => <<'END', If this option is set, Bugzilla will integrate with Memcached. Specify one or more servers, separated by spaces, using hostname:port notation (for example: 127.0.0.1:11211). END - localconfig_memcached_namespace => <<'END', + localconfig_memcached_namespace => <<'END', Specify a string to prefix each key on Memcached. END - localconfig_ses_username => <<'END', + localconfig_ses_username => <<'END', Username for HTTP Basic Authentication in front of the SES bounce handler. END - localconfig_ses_password => <<'END', + localconfig_ses_password => <<'END', Password for HTTP Basic Authentication in front of the SES bounce handler. END - localconfig_site_wide_secret => <<'END', + localconfig_site_wide_secret => <<'END', This secret key is used by your installation for the creation and validation of encrypted tokens. These tokens are used to implement security features in Bugzilla, to protect against certain types of attacks. A random string is generated by default. It's very important that this key is kept secret. It also must be very long. END - localconfig_param_override => <<'END', + localconfig_param_override => <<'END', This hash is used by BMO to override select data/params values on a per-webhead basis. Keys set to undef will default to the value in data/params. Only the keys listed below can be overridden. END - localconfig_urlbase => <<'END', + localconfig_urlbase => <<'END', The URL that is the common initial leading part of all URLs. END - localconfig_canonical_urlbase => <<'END', + localconfig_canonical_urlbase => <<'END', The URL that is the canonical initial leading part of all URLs. This will be the production url for a dev site, for instance. END - localconfig_use_suexec => <<'END', + localconfig_use_suexec => <<'END', Set this to 1 if Bugzilla runs in an Apache SuexecUserGroup environment. If your web server runs control panel software (cPanel, Plesk or similar), @@ -252,7 +252,7 @@ a normal webserver environment. If set to 1, checksetup.pl will set file permissions so that Bugzilla works in a SuexecUserGroup environment. END - localconfig_webservergroup => <<'END', + localconfig_webservergroup => <<'END', The name of the group that your web server runs as. On Red Hat distributions, this is usually "apache". On Debian/Ubuntu, it is usually "www-data". @@ -272,28 +272,28 @@ and you cannot set this up any other way. YOU HAVE BEEN WARNED! If you set this to anything other than "", you will need to run checksetup.pl as ##root## or as a user who is a member of the specified group. END - localconfig_apache_size_limit => < < < < < < < < "Minimum version required: ", + min_version_required => "Minimum version required: ", # Note: When translating these "modules" messages, don't change the formatting # if possible, because there is hardcoded formatting in # Bugzilla::Install::Requirements to match the box formatting. - modules_message_apache => < < < < < < < < "found v##ver##", - module_not_found => "not found", - module_ok => 'ok', - module_unknown_version => "found unknown version", - no_such_module => "There is no Perl module on CPAN named ##module##.", - mysql_innodb_disabled => <<'END', + module_found => "found v##ver##", + module_not_found => "not found", + module_ok => 'ok', + module_unknown_version => "found unknown version", + no_such_module => "There is no Perl module on CPAN named ##module##.", + mysql_innodb_disabled => <<'END', InnoDB is disabled in your MySQL installation. Bugzilla requires InnoDB to be enabled. Please enable it and then re-run checksetup.pl. END - mysql_innodb_settings => <<'END', + mysql_innodb_settings => <<'END', Bugzilla requires the following MySQL InnoDB settings: innodb_file_format = Barracuda innodb_file_per_table = 1 innodb_large_prefix = 1 END - mysql_index_renaming => <<'END', + mysql_index_renaming => <<'END', We are about to rename old indexes. The estimated time to complete renaming is ##minutes## minutes. You cannot interrupt this action once it has begun. If you would like to cancel, press Ctrl-C now... (Waiting 45 seconds...) END - mysql_row_format_conversion => "Converting ##table## to row format ##format##.", - mysql_utf8_conversion => <<'END', + mysql_row_format_conversion => "Converting ##table## to row format ##format##.", + mysql_utf8_conversion => <<'END', WARNING: We are about to convert your table storage format to UTF-8. This allows Bugzilla to correctly store and sort international characters. However, if you have any non-UTF-8 data in your database, @@ -376,7 +376,7 @@ WARNING: We are about to convert your table storage format to UTF-8. This If you ever used a version of Bugzilla before 2.22, we STRONGLY recommend that you stop checksetup.pl NOW and run contrib/recode.pl. END - no_checksetup_from_cgi => < < @@ -401,49 +401,49 @@ END END - patchutils_missing => <<'END', + patchutils_missing => <<'END', OPTIONAL NOTE: If you want to be able to use the 'difference between two patches' feature of Bugzilla (which requires the PatchReader Perl module as well), you should install patchutils from: http://cyberelk.net/tim/patchutils/ END - template_precompile => "Precompiling templates...", - template_removal_failed => < "Precompiling templates...", + template_removal_failed => < "Removing existing compiled templates...", - update_cf_invalid_name => - "Removing custom field '##field##', because it has an invalid name...", - update_flags_bad_name => <<'END', + template_removing_dir => "Removing existing compiled templates...", + update_cf_invalid_name => + "Removing custom field '##field##', because it has an invalid name...", + update_flags_bad_name => <<'END', "##flag##" is not a valid name for a flag. Rename it to not have any spaces or commas. END - update_nomail_bad => <<'END', + update_nomail_bad => <<'END', WARNING: The following users were listed in ##data##/nomail, but do not have an account here. The unmatched entries have been moved to ##data##/nomail.bad: END - update_summary_truncate_comment => - "The original value of the Summary field was longer than 255" - . " characters, and so it was truncated during an upgrade." - . " The original summary was:\n\n##summary##", - update_summary_truncated => <<'END', + update_summary_truncate_comment => + "The original value of the Summary field was longer than 255" + . " characters, and so it was truncated during an upgrade." + . " The original summary was:\n\n##summary##", + update_summary_truncated => <<'END', WARNING: Some of your bugs had summaries longer than 255 characters. They have had their original summary copied into a comment, and then the summary was truncated to 255 characters. The affected bug numbers were: END - update_quips => <<'END', + update_quips => <<'END', Quips are now stored in the database, rather than in an external file. The quips previously stored in ##data##/comments have been copied into the database, and that file has been renamed to ##data##/comments.bak You may delete the renamed file once you have confirmed that all your quips were moved successfully. END - update_queries_to_tags => "Populating the new 'tag' table:", - webdot_bad_htaccess => < "Populating the new 'tag' table:", + webdot_bad_htaccess => <{'datadir'}; eval "require LWP; require LWP::UserAgent;"; my $lwp = $@ ? 0 : 1; -if ((@ARGV != 1) || ($ARGV[0] !~ /^https?:/)) -{ - print "Usage: $0 \n"; - print "e.g.: $0 http://www.mycompany.com/bugzilla\n"; - exit(1); +if ((@ARGV != 1) || ($ARGV[0] !~ /^https?:/)) { + print "Usage: $0 \n"; + print "e.g.: $0 http://www.mycompany.com/bugzilla\n"; + exit(1); } # Try to determine the GID used by the web server. -my @pscmds = ('ps -eo comm,gid', 'ps -acxo command,gid', 'ps -acxo command,rgid'); +my @pscmds + = ('ps -eo comm,gid', 'ps -acxo command,gid', 'ps -acxo command,rgid'); my $sgid = 0; if (!ON_WINDOWS) { - foreach my $pscmd (@pscmds) { - open PH, '-|', "$pscmd 2>/dev/null"; - while (my $line = ) { - if ($line =~ /^(?:\S*\/)?(?:httpd|apache)2?\s+(\d+)$/) { - $sgid = $1 if $1 > $sgid; - } - } - close(PH); + foreach my $pscmd (@pscmds) { + open PH, '-|', "$pscmd 2>/dev/null"; + while (my $line = ) { + if ($line =~ /^(?:\S*\/)?(?:httpd|apache)2?\s+(\d+)$/) { + $sgid = $1 if $1 > $sgid; + } } + close(PH); + } } # Determine the numeric GID of $webservergroup -my $webgroupnum = 0; +my $webgroupnum = 0; my $webservergroup = Bugzilla->localconfig->{webservergroup}; if ($webservergroup =~ /^(\d+)$/) { - $webgroupnum = $1; + $webgroupnum = $1; } else { - eval { $webgroupnum = (getgrnam $webservergroup) || 0; }; + eval { $webgroupnum = (getgrnam $webservergroup) || 0; }; } # Check $webservergroup against the server's GID if ($sgid > 0) { - if ($webservergroup eq "") { - print -"WARNING \$webservergroup is set to an empty string. + if ($webservergroup eq "") { + print "WARNING \$webservergroup is set to an empty string. That is a very insecure practice. Please refer to the Bugzilla documentation.\n"; - } - elsif ($webgroupnum == $sgid || Bugzilla->localconfig->{use_suexec}) { - print "TEST-OK Webserver is running under group id in \$webservergroup.\n"; - } - else { - print -"TEST-WARNING Webserver is running under group id not matching \$webservergroup. + } + elsif ($webgroupnum == $sgid || Bugzilla->localconfig->{use_suexec}) { + print "TEST-OK Webserver is running under group id in \$webservergroup.\n"; + } + else { + print + "TEST-WARNING Webserver is running under group id not matching \$webservergroup. This if the tests below fail, this is probably the problem. Please refer to the web server configuration section of the Bugzilla guide. If you are using virtual hosts or suexec, this warning may not apply.\n"; - } + } } elsif (!ON_WINDOWS) { - print -"TEST-WARNING Failed to find the GID for the 'httpd' process, unable + print "TEST-WARNING Failed to find the GID for the 'httpd' process, unable to validate webservergroup.\n"; } @@ -89,196 +87,203 @@ to validate webservergroup.\n"; $ARGV[0] =~ s/\/$//; my $url = $ARGV[0] . "/images/padlock.png"; if (fetch($url)) { - print "TEST-OK Got padlock picture.\n"; -} else { - print -"TEST-FAILED Fetch of images/padlock.png failed + print "TEST-OK Got padlock picture.\n"; +} +else { + print "TEST-FAILED Fetch of images/padlock.png failed Your web server could not fetch $url. Check your web server configuration and try again.\n"; - exit(1); + exit(1); } # Try to execute a cgi script my $response = fetch($ARGV[0] . "/testagent.cgi"); if ($response =~ /^OK (.*)$/) { - print "TEST-OK Webserver is executing CGIs via $1.\n"; -} elsif ($response =~ /^#!/) { - print -"TEST-FAILED Webserver is fetching rather than executing CGI files. + print "TEST-OK Webserver is executing CGIs via $1.\n"; +} +elsif ($response =~ /^#!/) { + print "TEST-FAILED Webserver is fetching rather than executing CGI files. Check the AddHandler statement in your httpd.conf file.\n"; - exit(1); -} else { - print "TEST-FAILED Webserver is not executing CGI files.\n"; + exit(1); +} +else { + print "TEST-FAILED Webserver is not executing CGI files.\n"; } # Make sure that the web server is honoring .htaccess files my $localconfig = bz_locations()->{'localconfig'}; $localconfig =~ s~^\./~~; -$url = $ARGV[0] . "/$localconfig"; +$url = $ARGV[0] . "/$localconfig"; $response = fetch($url); if ($response) { - print -"TEST-FAILED Webserver is permitting fetch of $url. + print "TEST-FAILED Webserver is permitting fetch of $url. This is a serious security problem. Check your web server configuration.\n"; - exit(1); -} else { - print "TEST-OK Webserver is preventing fetch of $url.\n"; + exit(1); +} +else { + print "TEST-OK Webserver is preventing fetch of $url.\n"; } # Test chart generation eval 'use GD'; if ($@ eq '') { - undef $/; - - # Ensure major versions of GD and libgd match - # Windows's GD module include libgd.dll, guaranteed to match - if (!ON_WINDOWS) { - my $gdlib = `gdlib-config --version 2>&1` || ""; - $gdlib =~ s/\n$//; - if (!$gdlib) { - print "TEST-WARNING Failed to run gdlib-config; can't compare " . - "GD versions.\n"; - } - else { - my $gd = $GD::VERSION; - - my $verstring = "GD version $gd, libgd version $gdlib"; - - $gdlib =~ s/^([^\.]+)\..*/$1/; - $gd =~ s/^([^\.]+)\..*/$1/; - - if ($gdlib == $gd) { - print "TEST-OK $verstring; Major versions match.\n"; - } else { - print "TEST-FAILED $verstring; Major versions do not match.\n"; - } - } + undef $/; + + # Ensure major versions of GD and libgd match + # Windows's GD module include libgd.dll, guaranteed to match + if (!ON_WINDOWS) { + my $gdlib = `gdlib-config --version 2>&1` || ""; + $gdlib =~ s/\n$//; + if (!$gdlib) { + print "TEST-WARNING Failed to run gdlib-config; can't compare " + . "GD versions.\n"; } + else { + my $gd = $GD::VERSION; + + my $verstring = "GD version $gd, libgd version $gdlib"; - # Test GD + $gdlib =~ s/^([^\.]+)\..*/$1/; + $gd =~ s/^([^\.]+)\..*/$1/; + + if ($gdlib == $gd) { + print "TEST-OK $verstring; Major versions match.\n"; + } + else { + print "TEST-FAILED $verstring; Major versions do not match.\n"; + } + } + } + + # Test GD + eval { + my $image = new GD::Image(100, 100); + my $black = $image->colorAllocate(0, 0, 0); + my $white = $image->colorAllocate(255, 255, 255); + my $red = $image->colorAllocate(255, 0, 0); + my $blue = $image->colorAllocate(0, 0, 255); + $image->transparent($white); + $image->rectangle(0, 0, 99, 99, $black); + $image->arc(50, 50, 95, 75, 0, 360, $blue); + $image->fill(50, 50, $red); + + if ($image->can('png')) { + create_file("$datadir/testgd-local.png", $image->png); + check_image("$datadir/testgd-local.png", 'GD'); + } + else { + print "TEST-FAILED GD doesn't support PNG generation.\n"; + } + }; + if ($@ ne '') { + print "TEST-FAILED GD returned: $@\n"; + } + + # Test Chart + eval 'use Chart::Lines'; + if ($@) { + print "TEST-FAILED Chart::Lines is not installed.\n"; + } + else { eval { - my $image = new GD::Image(100, 100); - my $black = $image->colorAllocate(0, 0, 0); - my $white = $image->colorAllocate(255, 255, 255); - my $red = $image->colorAllocate(255, 0, 0); - my $blue = $image->colorAllocate(0, 0, 255); - $image->transparent($white); - $image->rectangle(0, 0, 99, 99, $black); - $image->arc(50, 50, 95, 75, 0, 360, $blue); - $image->fill(50, 50, $red); - - if ($image->can('png')) { - create_file("$datadir/testgd-local.png", $image->png); - check_image("$datadir/testgd-local.png", 'GD'); - } else { - print "TEST-FAILED GD doesn't support PNG generation.\n"; - } + my $chart = Chart::Lines->new(400, 400); + + $chart->add_pt('foo', 30, 25); + $chart->add_pt('bar', 16, 32); + + $chart->png("$datadir/testchart-local.png"); + check_image("$datadir/testchart-local.png", "Chart"); }; if ($@ ne '') { - print "TEST-FAILED GD returned: $@\n"; + print "TEST-FAILED Chart returned: $@\n"; } + } + + eval 'use Template::Plugin::GD::Image'; + if ($@) { + print "TEST-FAILED Template::Plugin::GD is not installed.\n"; + } + else { + print "TEST-OK Template::Plugin::GD is installed.\n"; + } +} - # Test Chart - eval 'use Chart::Lines'; - if ($@) { - print "TEST-FAILED Chart::Lines is not installed.\n"; - } else { - eval { - my $chart = Chart::Lines->new(400, 400); - - $chart->add_pt('foo', 30, 25); - $chart->add_pt('bar', 16, 32); - - $chart->png("$datadir/testchart-local.png"); - check_image("$datadir/testchart-local.png", "Chart"); - }; - if ($@ ne '') { - print "TEST-FAILED Chart returned: $@\n"; - } +sub fetch { + my $url = shift; + my $rtn; + if ($lwp) { + my $req = HTTP::Request->new(GET => $url); + my $ua = LWP::UserAgent->new; + my $res = $ua->request($req); + $rtn = ($res->is_success ? $res->content : undef); + } + elsif ($url =~ /^https:/i) { + die("You need LWP installed to use https with testserver.pl"); + } + else { + my ($host, $port, $file) = ('', 80, ''); + if ($url =~ m#^http://([^:]+):(\d+)(/.*)#i) { + ($host, $port, $file) = ($1, $2, $3); } - - eval 'use Template::Plugin::GD::Image'; - if ($@) { - print "TEST-FAILED Template::Plugin::GD is not installed.\n"; + elsif ($url =~ m#^http://([^/]+)(/.*)#i) { + ($host, $file) = ($1, $2); } else { - print "TEST-OK Template::Plugin::GD is installed.\n"; + die("Cannot parse url"); } -} -sub fetch { - my $url = shift; - my $rtn; - if ($lwp) { - my $req = HTTP::Request->new(GET => $url); - my $ua = LWP::UserAgent->new; - my $res = $ua->request($req); - $rtn = ($res->is_success ? $res->content : undef); - } elsif ($url =~ /^https:/i) { - die("You need LWP installed to use https with testserver.pl"); - } else { - my($host, $port, $file) = ('', 80, ''); - if ($url =~ m#^http://([^:]+):(\d+)(/.*)#i) { - ($host, $port, $file) = ($1, $2, $3); - } elsif ($url =~ m#^http://([^/]+)(/.*)#i) { - ($host, $file) = ($1, $2); - } else { - die("Cannot parse url"); - } - - my $proto = getprotobyname('tcp'); - socket(SOCK, PF_INET, SOCK_STREAM, $proto); - my $sin = sockaddr_in($port, inet_aton($host)); - if (connect(SOCK, $sin)) { - binmode SOCK; - select((select(SOCK), $| = 1)[0]); - - # get content - print SOCK "GET $file HTTP/1.0\015\012host: $host:$port\015\012\015\012"; - my $header = ''; - while (defined(my $line = )) { - last if $line eq "\015\012"; - $header .= $line; - } - my $content = ''; - while (defined(my $line = )) { - $content .= $line; - } - - my ($status) = $header =~ m#^HTTP/\d+\.\d+ (\d+)#; - $rtn = (($status =~ /^2\d\d/) ? $content : undef); - } + my $proto = getprotobyname('tcp'); + socket(SOCK, PF_INET, SOCK_STREAM, $proto); + my $sin = sockaddr_in($port, inet_aton($host)); + if (connect(SOCK, $sin)) { + binmode SOCK; + select((select(SOCK), $| = 1)[0]); + + # get content + print SOCK "GET $file HTTP/1.0\015\012host: $host:$port\015\012\015\012"; + my $header = ''; + while (defined(my $line = )) { + last if $line eq "\015\012"; + $header .= $line; + } + my $content = ''; + while (defined(my $line = )) { + $content .= $line; + } + + my ($status) = $header =~ m#^HTTP/\d+\.\d+ (\d+)#; + $rtn = (($status =~ /^2\d\d/) ? $content : undef); } - return($rtn); + } + return ($rtn); } sub check_image { - my ($local_file, $library) = @_; - my $filedata = read_file($local_file); - if ($filedata =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/) { - print "TEST-OK $library library generated a good PNG image.\n"; - unlink $local_file; - } else { - print "TEST-WARNING $library library did not generate a good PNG.\n"; - } + my ($local_file, $library) = @_; + my $filedata = read_file($local_file); + if ($filedata =~ /^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A/) { + print "TEST-OK $library library generated a good PNG image.\n"; + unlink $local_file; + } + else { + print "TEST-WARNING $library library did not generate a good PNG.\n"; + } } sub create_file { - my ($filename, $content) = @_; - open(FH, ">", $filename) - or die "Failed to create $filename: $!\n"; - binmode FH; - print FH $content; - close FH; + my ($filename, $content) = @_; + open(FH, ">", $filename) or die "Failed to create $filename: $!\n"; + binmode FH; + print FH $content; + close FH; } sub read_file { - my ($filename) = @_; - open(FH, "<", $filename) - or die "Failed to open $filename: $!\n"; - binmode FH; - my $content = ; - close FH; - return $content; + my ($filename) = @_; + open(FH, "<", $filename) or die "Failed to open $filename: $!\n"; + binmode FH; + my $content = ; + close FH; + return $content; } diff --git a/token.cgi b/token.cgi index 990040050..d006d1b9f 100755 --- a/token.cgi +++ b/token.cgi @@ -23,13 +23,13 @@ use Date::Format; use Date::Parse; use JSON qw( decode_json ); -local our $dbh = Bugzilla->dbh; -local our $cgi = Bugzilla->cgi; +local our $dbh = Bugzilla->dbh; +local our $cgi = Bugzilla->cgi; local our $template = Bugzilla->template; -local our $vars = {}; +local our $vars = {}; my $action = $cgi->param('a'); -my $token = $cgi->param('t'); +my $token = $cgi->param('t'); Bugzilla->login(LOGIN_OPTIONAL); @@ -50,35 +50,38 @@ if ($token) { trick_taint($token); # Make sure the token exists in the database. - my ($db_token, $tokentype) = $dbh->selectrow_array('SELECT token, tokentype FROM tokens - WHERE token = ?', undef, $token); + my ($db_token, $tokentype) = $dbh->selectrow_array( + 'SELECT token, tokentype FROM tokens + WHERE token = ?', undef, + $token + ); (defined $db_token && $db_token eq $token) || ThrowUserError("token_does_not_exist"); # Make sure the token is the correct type for the action being taken. - if ( grep($action eq $_ , qw(cfmpw cxlpw chgpw)) && $tokentype ne 'password' ) { + if (grep($action eq $_, qw(cfmpw cxlpw chgpw)) && $tokentype ne 'password') { Bugzilla::Token::Cancel($token, "wrong_token_for_changing_passwd"); ThrowUserError("wrong_token_for_changing_passwd"); } if ( ($action eq 'cxlem') - && (($tokentype ne 'emailold') && ($tokentype ne 'emailnew')) ) { + && (($tokentype ne 'emailold') && ($tokentype ne 'emailnew'))) + { Bugzilla::Token::Cancel($token, "wrong_token_for_cancelling_email_change"); ThrowUserError("wrong_token_for_cancelling_email_change"); } - if ( grep($action eq $_ , qw(cfmem chgem)) - && ($tokentype ne 'emailnew') ) { + if (grep($action eq $_, qw(cfmem chgem)) && ($tokentype ne 'emailnew')) { Bugzilla::Token::Cancel($token, "wrong_token_for_confirming_email_change"); ThrowUserError("wrong_token_for_confirming_email_change"); } - if (($action =~ /^(request|confirm|cancel)_new_account$/) - && ($tokentype ne 'account')) + if ( ($action =~ /^(request|confirm|cancel)_new_account$/) + && ($tokentype ne 'account')) { - Bugzilla::Token::Cancel($token, 'wrong_token_for_creating_account'); - ThrowUserError('wrong_token_for_creating_account'); + Bugzilla::Token::Cancel($token, 'wrong_token_for_creating_account'); + ThrowUserError('wrong_token_for_creating_account'); } if (substr($action, 0, 4) eq 'mfa_' && $tokentype ne 'session.short') { - Bugzilla::Token::Cancel($token, 'wrong_token_for_mfa'); - ThrowUserError('wrong_token_for_mfa'); + Bugzilla::Token::Cancel($token, 'wrong_token_for_mfa'); + ThrowUserError('wrong_token_for_mfa'); } } @@ -87,46 +90,46 @@ if ($token) { # their login name and it exists in the database, and that the DB module is in # the list of allowed verification methods. my $user_account; -if ( $action eq 'reqpw' ) { - my $login_name = $cgi->param('loginname') - || ThrowUserError("login_needed_for_password_change"); +if ($action eq 'reqpw') { + my $login_name = $cgi->param('loginname') + || ThrowUserError("login_needed_for_password_change"); - # check verification methods - unless (Bugzilla->user->authorizer->can_change_password) { - ThrowUserError("password_change_requests_not_allowed"); - } + # check verification methods + unless (Bugzilla->user->authorizer->can_change_password) { + ThrowUserError("password_change_requests_not_allowed"); + } - # Check the hash token to make sure this user actually submitted - # the forgotten password form. - my $token = $cgi->param('token'); - check_hash_token($token, ['reqpw']); + # Check the hash token to make sure this user actually submitted + # the forgotten password form. + my $token = $cgi->param('token'); + check_hash_token($token, ['reqpw']); - validate_email_syntax($login_name) - || ThrowUserError('illegal_email_address', {addr => $login_name}); + validate_email_syntax($login_name) + || ThrowUserError('illegal_email_address', {addr => $login_name}); - $user_account = Bugzilla::User->check($login_name); + $user_account = Bugzilla::User->check($login_name); - # Make sure the user account is active. - if (!$user_account->is_enabled) { - ThrowUserError('account_disabled', - {disabled_reason => get_text('account_disabled', {account => $login_name})}); - } + # Make sure the user account is active. + if (!$user_account->is_enabled) { + ThrowUserError('account_disabled', + {disabled_reason => get_text('account_disabled', {account => $login_name})}); + } } # If the user is changing their password, make sure they submitted a new # password and that the new password is valid. my $password; -if ( $action eq 'chgpw' ) { - $password = $cgi->param('password'); - my $matchpassword = $cgi->param('matchpassword'); - ThrowUserError("require_new_password") - unless defined $password && defined $matchpassword; +if ($action eq 'chgpw') { + $password = $cgi->param('password'); + my $matchpassword = $cgi->param('matchpassword'); + ThrowUserError("require_new_password") + unless defined $password && defined $matchpassword; - Bugzilla->assert_password_is_secure($password); - Bugzilla->assert_passwords_match($password, $matchpassword); + Bugzilla->assert_password_is_secure($password); + Bugzilla->assert_passwords_match($password, $matchpassword); - # Make sure that these never show up in the UI under any circumstances. - $cgi->delete('password', 'matchpassword'); + # Make sure that these never show up in the UI under any circumstances. + $cgi->delete('password', 'matchpassword'); } ################################################################################ @@ -138,31 +141,43 @@ if ( $action eq 'chgpw' ) { # that variable and runs the appropriate code. if ($action eq 'reqpw') { - requestChangePassword($user_account); -} elsif ($action eq 'cfmpw') { - confirmChangePassword($token); -} elsif ($action eq 'cxlpw') { - cancelChangePassword($token); -} elsif ($action eq 'chgpw') { - changePassword($token, $password); -} elsif ($action eq 'cfmem') { - confirmChangeEmail($token); -} elsif ($action eq 'cxlem') { - cancelChangeEmail($token); -} elsif ($action eq 'chgem') { - changeEmail($token); -} elsif ($action eq 'request_new_account') { - request_create_account($token); -} elsif ($action eq 'confirm_new_account') { - confirm_create_account($token); -} elsif ($action eq 'cancel_new_account') { - cancel_create_account($token); -} elsif ($action eq 'mfa_l') { - verify_mfa_login($token); -} elsif ($action eq 'mfa_p') { - verify_mfa_password($token); -} else { - ThrowUserError('unknown_action', {action => $action}); + requestChangePassword($user_account); +} +elsif ($action eq 'cfmpw') { + confirmChangePassword($token); +} +elsif ($action eq 'cxlpw') { + cancelChangePassword($token); +} +elsif ($action eq 'chgpw') { + changePassword($token, $password); +} +elsif ($action eq 'cfmem') { + confirmChangeEmail($token); +} +elsif ($action eq 'cxlem') { + cancelChangeEmail($token); +} +elsif ($action eq 'chgem') { + changeEmail($token); +} +elsif ($action eq 'request_new_account') { + request_create_account($token); +} +elsif ($action eq 'confirm_new_account') { + confirm_create_account($token); +} +elsif ($action eq 'cancel_new_account') { + cancel_create_account($token); +} +elsif ($action eq 'mfa_l') { + verify_mfa_login($token); +} +elsif ($action eq 'mfa_p') { + verify_mfa_password($token); +} +else { + ThrowUserError('unknown_action', {action => $action}); } exit; @@ -172,294 +187,302 @@ exit; ################################################################################ sub requestChangePassword { - my ($user) = @_; - Bugzilla::Token::IssuePasswordToken($user); + my ($user) = @_; + Bugzilla::Token::IssuePasswordToken($user); - $vars->{'message'} = "password_change_request"; + $vars->{'message'} = "password_change_request"; - print $cgi->header(); - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub confirmChangePassword { - my $token = shift; - $vars->{'token'} = $token; + my $token = shift; + $vars->{'token'} = $token; - my ($user_id) = Bugzilla::Token::GetTokenData($token); - $vars->{token_user} = Bugzilla::User->check({ id => $user_id, cache => 1 }); + my ($user_id) = Bugzilla::Token::GetTokenData($token); + $vars->{token_user} = Bugzilla::User->check({id => $user_id, cache => 1}); - print $cgi->header(); - $template->process("account/password/set-forgotten-password.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("account/password/set-forgotten-password.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub cancelChangePassword { - my $token = shift; - $vars->{'message'} = "password_change_canceled"; - Bugzilla::Token::Cancel($token, $vars->{'message'}); + my $token = shift; + $vars->{'message'} = "password_change_canceled"; + Bugzilla::Token::Cancel($token, $vars->{'message'}); - print $cgi->header(); - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub changePassword { - my ($token, $password) = @_; - my $dbh = Bugzilla->dbh; - - my ($user_id) = Bugzilla::Token::GetTokenData($token); - my $user = Bugzilla::User->check({ id => $user_id }); - - if ($user->mfa) { - $user->mfa_provider->verify_prompt({ - user => $user, - reason => 'Setting your password', - password => $password, - token => $token, - postback => { - action => 'token.cgi', - token_field => 't', - fields => { - a => 'mfa_p', - }, - }, - }); - } - else { - set_user_password($token, $user, $password); - } + my ($token, $password) = @_; + my $dbh = Bugzilla->dbh; + + my ($user_id) = Bugzilla::Token::GetTokenData($token); + my $user = Bugzilla::User->check({id => $user_id}); + + if ($user->mfa) { + $user->mfa_provider->verify_prompt({ + user => $user, + reason => 'Setting your password', + password => $password, + token => $token, + postback => + {action => 'token.cgi', token_field => 't', fields => {a => 'mfa_p',},}, + }); + } + else { + set_user_password($token, $user, $password); + } } sub verify_mfa_password { - my $token = shift; - my ($user, $event) = mfa_event_from_token($token); - set_user_password($event->{token}, $user, $event->{password}); + my $token = shift; + my ($user, $event) = mfa_event_from_token($token); + set_user_password($event->{token}, $user, $event->{password}); } sub set_user_password { - my ($token, $user, $password) = @_; + my ($token, $user, $password) = @_; - $user->set_password($password); - $user->update(); - delete_token($token); - $dbh->do("DELETE FROM tokens WHERE userid = ? AND tokentype = 'password'", undef, $user->id); + $user->set_password($password); + $user->update(); + delete_token($token); + $dbh->do("DELETE FROM tokens WHERE userid = ? AND tokentype = 'password'", + undef, $user->id); - Bugzilla->logout_user_by_id($user->id); + Bugzilla->logout_user_by_id($user->id); - $vars->{'message'} = "password_changed"; + $vars->{'message'} = "password_changed"; - print $cgi->header(); - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub confirmChangeEmail { - my $token = shift; - $vars->{'token'} = $token; + my $token = shift; + $vars->{'token'} = $token; - print $cgi->header(); - $template->process("account/email/confirm.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process("account/email/confirm.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub changeEmail { - my $token = shift; - my $dbh = Bugzilla->dbh; - - # Get the user's ID from the tokens table. - my ($userid, $eventdata) = $dbh->selectrow_array( - q{SELECT userid, eventdata FROM tokens - WHERE token = ?}, undef, $token); - my ($old_email, $new_email) = split(/:/,$eventdata); - - # Check the user entered the correct old email address - if(lc($cgi->param('email')) ne lc($old_email)) { - ThrowUserError("email_confirmation_failed"); - } - # The new email address should be available as this was - # confirmed initially so cancel token if it is not still available - if (! is_available_username($new_email,$old_email)) { - $vars->{'email'} = $new_email; # Needed for Bugzilla::Token::Cancel's mail - Bugzilla::Token::Cancel($token, "account_exists", $vars); - ThrowUserError("account_exists", { email => $new_email } ); - } + my $token = shift; + my $dbh = Bugzilla->dbh; + + # Get the user's ID from the tokens table. + my ($userid, $eventdata) = $dbh->selectrow_array( + q{SELECT userid, eventdata FROM tokens + WHERE token = ?}, undef, $token + ); + my ($old_email, $new_email) = split(/:/, $eventdata); + + # Check the user entered the correct old email address + if (lc($cgi->param('email')) ne lc($old_email)) { + ThrowUserError("email_confirmation_failed"); + } + + # The new email address should be available as this was + # confirmed initially so cancel token if it is not still available + if (!is_available_username($new_email, $old_email)) { + $vars->{'email'} = $new_email; # Needed for Bugzilla::Token::Cancel's mail + Bugzilla::Token::Cancel($token, "account_exists", $vars); + ThrowUserError("account_exists", {email => $new_email}); + } - # Update the user's login name in the profiles table and delete the token - # from the tokens table. - $dbh->bz_start_transaction(); - $dbh->do(q{UPDATE profiles + # Update the user's login name in the profiles table and delete the token + # from the tokens table. + $dbh->bz_start_transaction(); + $dbh->do( + q{UPDATE profiles SET login_name = ? - WHERE userid = ?}, - undef, ($new_email, $userid)); - Bugzilla->memcached->clear({ table => 'profiles', id => $userid }); - $dbh->do('DELETE FROM tokens WHERE token = ?', undef, $token); - $dbh->do(q{DELETE FROM tokens WHERE userid = ? - AND tokentype = 'emailnew'}, undef, $userid); + WHERE userid = ?}, undef, ($new_email, $userid) + ); + Bugzilla->memcached->clear({table => 'profiles', id => $userid}); + $dbh->do('DELETE FROM tokens WHERE token = ?', undef, $token); + $dbh->do( + q{DELETE FROM tokens WHERE userid = ? + AND tokentype = 'emailnew'}, undef, $userid + ); - # The email address has been changed, so we need to rederive the groups - my $user = new Bugzilla::User($userid); - $user->derive_regexp_groups; + # The email address has been changed, so we need to rederive the groups + my $user = new Bugzilla::User($userid); + $user->derive_regexp_groups; - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Return HTTP response headers. - print $cgi->header(); + # Return HTTP response headers. + print $cgi->header(); - # Let the user know their email address has been changed. + # Let the user know their email address has been changed. - $vars->{'message'} = "login_changed"; + $vars->{'message'} = "login_changed"; - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub cancelChangeEmail { - my $token = shift; - my $dbh = Bugzilla->dbh; - - $dbh->bz_start_transaction(); - - # Get the user's ID from the tokens table. - my ($userid, $tokentype, $eventdata) = $dbh->selectrow_array( - q{SELECT userid, tokentype, eventdata FROM tokens - WHERE token = ?}, undef, $token); - my ($old_email, $new_email) = split(/:/,$eventdata); - - if($tokentype eq "emailold") { - $vars->{'message'} = "emailold_change_canceled"; - - my $actualemail = $dbh->selectrow_array( - q{SELECT login_name FROM profiles - WHERE userid = ?}, undef, $userid); - - # check to see if it has been altered - if($actualemail ne $old_email) { - # XXX - This is NOT safe - if A has change to B, another profile - # could have grabbed A's username in the meantime. - # The DB constraint will catch this, though - $dbh->do(q{UPDATE profiles + my $token = shift; + my $dbh = Bugzilla->dbh; + + $dbh->bz_start_transaction(); + + # Get the user's ID from the tokens table. + my ($userid, $tokentype, $eventdata) = $dbh->selectrow_array( + q{SELECT userid, tokentype, eventdata FROM tokens + WHERE token = ?}, undef, $token + ); + my ($old_email, $new_email) = split(/:/, $eventdata); + + if ($tokentype eq "emailold") { + $vars->{'message'} = "emailold_change_canceled"; + + my $actualemail = $dbh->selectrow_array( + q{SELECT login_name FROM profiles + WHERE userid = ?}, undef, $userid + ); + + # check to see if it has been altered + if ($actualemail ne $old_email) { + + # XXX - This is NOT safe - if A has change to B, another profile + # could have grabbed A's username in the meantime. + # The DB constraint will catch this, though + $dbh->do( + q{UPDATE profiles SET login_name = ? - WHERE userid = ?}, - undef, ($old_email, $userid)); - Bugzilla->memcached->clear({ table => 'profiles', id => $userid }); + WHERE userid = ?}, undef, ($old_email, $userid) + ); + Bugzilla->memcached->clear({table => 'profiles', id => $userid}); - # email has changed, so rederive groups + # email has changed, so rederive groups - my $user = new Bugzilla::User($userid); - $user->derive_regexp_groups; + my $user = new Bugzilla::User($userid); + $user->derive_regexp_groups; - $vars->{'message'} = "email_change_canceled_reinstated"; - } + $vars->{'message'} = "email_change_canceled_reinstated"; } - else { - $vars->{'message'} = 'email_change_canceled' - } + } + else { + $vars->{'message'} = 'email_change_canceled'; + } - $vars->{'old_email'} = $old_email; - $vars->{'new_email'} = $new_email; - Bugzilla::Token::Cancel($token, $vars->{'message'}, $vars); + $vars->{'old_email'} = $old_email; + $vars->{'new_email'} = $new_email; + Bugzilla::Token::Cancel($token, $vars->{'message'}, $vars); - $dbh->do(q{DELETE FROM tokens WHERE userid = ? - AND tokentype = 'emailold' OR tokentype = 'emailnew'}, - undef, $userid); + $dbh->do( + q{DELETE FROM tokens WHERE userid = ? + AND tokentype = 'emailold' OR tokentype = 'emailnew'}, undef, $userid + ); - $dbh->bz_commit_transaction(); + $dbh->bz_commit_transaction(); - # Return HTTP response headers. - print $cgi->header(); + # Return HTTP response headers. + print $cgi->header(); - $template->process("global/message.html.tmpl", $vars) - || ThrowTemplateError($template->error()); + $template->process("global/message.html.tmpl", $vars) + || ThrowTemplateError($template->error()); } sub request_create_account { - my $token = shift; + my $token = shift; - Bugzilla->user->check_account_creation_enabled; - my (undef, $date, $login_name) = Bugzilla::Token::GetTokenData($token); - $vars->{'token'} = $token; - $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'}; - $vars->{'expiration_ts'} = ctime(str2time($date) + MAX_TOKEN_AGE * 86400); + Bugzilla->user->check_account_creation_enabled; + my (undef, $date, $login_name) = Bugzilla::Token::GetTokenData($token); + $vars->{'token'} = $token; + $vars->{'email'} = $login_name . Bugzilla->params->{'emailsuffix'}; + $vars->{'expiration_ts'} = ctime(str2time($date) + MAX_TOKEN_AGE * 86400); - print $cgi->header(); - $template->process('account/email/confirm-new.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process('account/email/confirm-new.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } sub confirm_create_account { - my $token = shift; + my $token = shift; - Bugzilla->user->check_account_creation_enabled; - my (undef, undef, $login_name) = Bugzilla::Token::GetTokenData($token); + Bugzilla->user->check_account_creation_enabled; + my (undef, undef, $login_name) = Bugzilla::Token::GetTokenData($token); - my $password1 = $cgi->param('passwd1'); - my $password2 = $cgi->param('passwd2'); - # Make sure that these never show up anywhere in the UI. - $cgi->delete('passwd1', 'passwd2'); - Bugzilla->assert_password_is_secure($password1); - Bugzilla->assert_passwords_match($password1, $password2); + my $password1 = $cgi->param('passwd1'); + my $password2 = $cgi->param('passwd2'); - my $otheruser = Bugzilla::User->create({ - login_name => $login_name, - realname => scalar $cgi->param('realname'), - cryptpassword => $password1}); + # Make sure that these never show up anywhere in the UI. + $cgi->delete('passwd1', 'passwd2'); + Bugzilla->assert_password_is_secure($password1); + Bugzilla->assert_passwords_match($password1, $password2); - # Now delete this token. - delete_token($token); + my $otheruser = Bugzilla::User->create({ + login_name => $login_name, + realname => scalar $cgi->param('realname'), + cryptpassword => $password1 + }); - # Let the user know that his user account has been successfully created. - $vars->{'message'} = 'account_created'; - $vars->{'otheruser'} = $otheruser; + # Now delete this token. + delete_token($token); - # Log in the new user using credentials he just gave. - $cgi->param('Bugzilla_login', $otheruser->login); - $cgi->param('Bugzilla_password', $password1); - Bugzilla->login(LOGIN_OPTIONAL); + # Let the user know that his user account has been successfully created. + $vars->{'message'} = 'account_created'; + $vars->{'otheruser'} = $otheruser; - print $cgi->header(); + # Log in the new user using credentials he just gave. + $cgi->param('Bugzilla_login', $otheruser->login); + $cgi->param('Bugzilla_password', $password1); + Bugzilla->login(LOGIN_OPTIONAL); - $template->process('index.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + + $template->process('index.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } sub cancel_create_account { - my $token = shift; + my $token = shift; - my (undef, undef, $login_name) = Bugzilla::Token::GetTokenData($token); + my (undef, undef, $login_name) = Bugzilla::Token::GetTokenData($token); - $vars->{'message'} = 'account_creation_canceled'; - $vars->{'account'} = $login_name; - Bugzilla::Token::Cancel($token, $vars->{'message'}); + $vars->{'message'} = 'account_creation_canceled'; + $vars->{'account'} = $login_name; + Bugzilla::Token::Cancel($token, $vars->{'message'}); - print $cgi->header(); - $template->process('global/message.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + print $cgi->header(); + $template->process('global/message.html.tmpl', $vars) + || ThrowTemplateError($template->error()); } sub verify_mfa_login { - my $token = shift; - my ($user, $event) = mfa_event_from_token($token); - $user->authorizer->mfa_verified($user, $event); - print Bugzilla->cgi->redirect($event->{url} // 'index.cgi'); - exit; + my $token = shift; + my ($user, $event) = mfa_event_from_token($token); + $user->authorizer->mfa_verified($user, $event); + print Bugzilla->cgi->redirect($event->{url} // 'index.cgi'); + exit; } sub mfa_event_from_token { - my $token = shift; + my $token = shift; - # create user from token - my ($user_id) = Bugzilla::Token::GetTokenData($token); - my $user = Bugzilla::User->check({ id => $user_id, cache => 1 }); + # create user from token + my ($user_id) = Bugzilla::Token::GetTokenData($token); + my $user = Bugzilla::User->check({id => $user_id, cache => 1}); - # sanity check - if (!$user->mfa) { - delete_token($token); - print Bugzilla->cgi->redirect('index.cgi'); - exit; - } + # sanity check + if (!$user->mfa) { + delete_token($token); + print Bugzilla->cgi->redirect('index.cgi'); + exit; + } - # verify - my $event = $user->mfa_provider->verify_token($token); - return ($user, $event); + # verify + my $event = $user->mfa_provider->verify_token($token); + return ($user, $event); } diff --git a/userprefs.cgi b/userprefs.cgi index 830c49eed..45a1c5194 100755 --- a/userprefs.cgi +++ b/userprefs.cgi @@ -30,7 +30,7 @@ use DateTime; use constant SESSION_MAX => 20; local our $template = Bugzilla->template; -local our $vars = {}; +local our $vars = {}; ############################################################################### # Each panel has two functions - panel Foo has a DoFoo, to get the data @@ -39,898 +39,922 @@ local our $vars = {}; # SaveFoo may be called before DoFoo. ############################################################################### sub DoAccount { - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - ($vars->{'realname'}) = $dbh->selectrow_array( - "SELECT realname FROM profiles WHERE userid = ?", undef, $user->id); - - if(Bugzilla->params->{'allowemailchange'} - && Bugzilla->user->authorizer->can_change_email) { - # First delete old tokens. - Bugzilla::Token::CleanTokenTable(); - - my @token = $dbh->selectrow_array( - "SELECT tokentype, " . - $dbh->sql_date_math('issuedate', '+', MAX_TOKEN_AGE, 'DAY') - . ", eventdata + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + ($vars->{'realname'}) + = $dbh->selectrow_array("SELECT realname FROM profiles WHERE userid = ?", + undef, $user->id); + + if ( Bugzilla->params->{'allowemailchange'} + && Bugzilla->user->authorizer->can_change_email) + { + # First delete old tokens. + Bugzilla::Token::CleanTokenTable(); + + my @token = $dbh->selectrow_array( + "SELECT tokentype, " + . $dbh->sql_date_math('issuedate', '+', MAX_TOKEN_AGE, 'DAY') + . ", eventdata FROM tokens WHERE userid = ? AND tokentype LIKE 'email%' - ORDER BY tokentype ASC " . $dbh->sql_limit(1), undef, $user->id); - if (scalar(@token) > 0) { - my ($tokentype, $change_date, $eventdata) = @token; - $vars->{'login_change_date'} = $change_date; - - if($tokentype eq 'emailnew') { - my ($oldemail,$newemail) = split(/:/,$eventdata); - $vars->{'new_login_name'} = $newemail; - } - } + ORDER BY tokentype ASC " . $dbh->sql_limit(1), undef, $user->id + ); + if (scalar(@token) > 0) { + my ($tokentype, $change_date, $eventdata) = @token; + $vars->{'login_change_date'} = $change_date; + + if ($tokentype eq 'emailnew') { + my ($oldemail, $newemail) = split(/:/, $eventdata); + $vars->{'new_login_name'} = $newemail; + } } + } } sub SaveAccount { - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; - $dbh->bz_start_transaction; + $dbh->bz_start_transaction; - my $user = Bugzilla->user; + my $user = Bugzilla->user; - my $oldpassword = $cgi->param('old_password'); - my $pwd1 = $cgi->param('new_password1'); - my $pwd2 = $cgi->param('new_password2'); - my $new_login_name = trim($cgi->param('new_login_name')); - my @mfa_events; + my $oldpassword = $cgi->param('old_password'); + my $pwd1 = $cgi->param('new_password1'); + my $pwd2 = $cgi->param('new_password2'); + my $new_login_name = trim($cgi->param('new_login_name')); + my @mfa_events; - if ($user->authorizer->can_change_password - && ($oldpassword ne "" || $pwd1 ne "" || $pwd2 ne "")) - { - my $oldcryptedpwd = $user->cryptpassword; - $oldcryptedpwd || ThrowCodeError("unable_to_retrieve_password"); + if ($user->authorizer->can_change_password + && ($oldpassword ne "" || $pwd1 ne "" || $pwd2 ne "")) + { + my $oldcryptedpwd = $user->cryptpassword; + $oldcryptedpwd || ThrowCodeError("unable_to_retrieve_password"); - if (bz_crypt($oldpassword, $oldcryptedpwd) ne $oldcryptedpwd) { - ThrowUserError("old_password_incorrect"); - } + if (bz_crypt($oldpassword, $oldcryptedpwd) ne $oldcryptedpwd) { + ThrowUserError("old_password_incorrect"); + } + + if ($pwd1 ne "" || $pwd2 ne "") { + ThrowUserError("new_password_missing") unless $pwd1; + Bugzilla->assert_password_is_secure($pwd1); + Bugzilla->assert_passwords_match($pwd1, $pwd2); - if ($pwd1 ne "" || $pwd2 ne "") { - ThrowUserError("new_password_missing") unless $pwd1; - Bugzilla->assert_password_is_secure($pwd1); - Bugzilla->assert_passwords_match($pwd1, $pwd2); - - if ($oldpassword ne $pwd1) { - if ($user->mfa) { - push @mfa_events, { - type => 'set_password', - reason => 'changing your password', - password => $pwd1, - }; - } - else { - $user->set_password($pwd1); - # Invalidate all logins except for the current one - Bugzilla->logout(LOGOUT_KEEP_CURRENT); - } - } + if ($oldpassword ne $pwd1) { + if ($user->mfa) { + push @mfa_events, + { + type => 'set_password', + reason => 'changing your password', + password => $pwd1, + }; } - } + else { + $user->set_password($pwd1); - if ($user->authorizer->can_change_email - && Bugzilla->params->{"allowemailchange"} - && $new_login_name) - { - if ($user->login ne $new_login_name) { - $oldpassword || ThrowUserError("old_password_required"); - - # Block multiple email changes for the same user. - if (Bugzilla::Token::HasEmailChangeToken($user->id)) { - ThrowUserError("email_change_in_progress"); - } - - # Before changing an email address, confirm one does not exist. - validate_email_syntax($new_login_name) - || ThrowUserError('illegal_email_address', {addr => $new_login_name}); - is_available_username($new_login_name) - || ThrowUserError("account_exists", {email => $new_login_name}); - - if ($user->mfa) { - push @mfa_events, { - type => 'set_login', - reason => 'changing your email address', - login => $new_login_name, - }; - } - else { - Bugzilla::Token::IssueEmailChangeToken($user, $new_login_name); - $vars->{email_changes_saved} = 1; - } + # Invalidate all logins except for the current one + Bugzilla->logout(LOGOUT_KEEP_CURRENT); } + } } + } + + if ( $user->authorizer->can_change_email + && Bugzilla->params->{"allowemailchange"} + && $new_login_name) + { + if ($user->login ne $new_login_name) { + $oldpassword || ThrowUserError("old_password_required"); + + # Block multiple email changes for the same user. + if (Bugzilla::Token::HasEmailChangeToken($user->id)) { + ThrowUserError("email_change_in_progress"); + } + + # Before changing an email address, confirm one does not exist. + validate_email_syntax($new_login_name) + || ThrowUserError('illegal_email_address', {addr => $new_login_name}); + is_available_username($new_login_name) + || ThrowUserError("account_exists", {email => $new_login_name}); + + if ($user->mfa) { + push @mfa_events, + { + type => 'set_login', + reason => 'changing your email address', + login => $new_login_name, + }; + } + else { + Bugzilla::Token::IssueEmailChangeToken($user, $new_login_name); + $vars->{email_changes_saved} = 1; + } + } + } - $user->set_name($cgi->param('realname')); - $user->update({ keep_session => 1, keep_tokens => 1 }); - $dbh->bz_commit_transaction; + $user->set_name($cgi->param('realname')); + $user->update({keep_session => 1, keep_tokens => 1}); + $dbh->bz_commit_transaction; - if (@mfa_events) { - # build the fields for the postback - my $mfa_event = { - postback => { - action => 'userprefs.cgi', - fields => { - tab => 'account', - }, - }, - reason => ucfirst(join(' and ', map { $_->{reason} } @mfa_events)), - actions => \@mfa_events, - }; - # display 2fa verification - $user->mfa_provider->verify_prompt($mfa_event); - } + if (@mfa_events) { + + # build the fields for the postback + my $mfa_event = { + postback => {action => 'userprefs.cgi', fields => {tab => 'account',},}, + reason => ucfirst(join(' and ', map { $_->{reason} } @mfa_events)), + actions => \@mfa_events, + }; + + # display 2fa verification + $user->mfa_provider->verify_prompt($mfa_event); + } } sub MfaAccount { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - return unless $user->mfa; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + return unless $user->mfa; - my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token')); + my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token')); - foreach my $action (@{ $event->{actions} }) { - if ($action->{type} eq 'set_login') { - Bugzilla::Token::IssueEmailChangeToken($user, $action->{login}); - $vars->{email_changes_saved} = 1; - } + foreach my $action (@{$event->{actions}}) { + if ($action->{type} eq 'set_login') { + Bugzilla::Token::IssueEmailChangeToken($user, $action->{login}); + $vars->{email_changes_saved} = 1; + } - elsif ($action->{type} eq 'set_password') { - $dbh->bz_start_transaction; - $user->set_password($action->{password}); - Bugzilla->logout(LOGOUT_KEEP_CURRENT); - $user->update({ keep_session => 1, keep_tokens => 1 }); - $dbh->bz_commit_transaction; - } + elsif ($action->{type} eq 'set_password') { + $dbh->bz_start_transaction; + $user->set_password($action->{password}); + Bugzilla->logout(LOGOUT_KEEP_CURRENT); + $user->update({keep_session => 1, keep_tokens => 1}); + $dbh->bz_commit_transaction; } + } } sub DisableAccount { - my $user = Bugzilla->user; + my $user = Bugzilla->user; - my $new_login = 'u' . $user->id . '@disabled.tld'; + my $new_login = 'u' . $user->id . '@disabled.tld'; - Bugzilla->audit(sprintf('<%s> self-disabled %s (now %s)', remote_ip(), $user->login, $new_login)); + Bugzilla->audit(sprintf( + '<%s> self-disabled %s (now %s)', + remote_ip(), $user->login, $new_login + )); - $user->set_login($new_login); - $user->set_name(''); - $user->set_disabledtext('Disabled by account owner.'); - $user->set_disable_mail(1); - $user->set_password('*'); - $user->update(); + $user->set_login($new_login); + $user->set_name(''); + $user->set_disabledtext('Disabled by account owner.'); + $user->set_disable_mail(1); + $user->set_password('*'); + $user->update(); - Bugzilla->logout(); - print Bugzilla->cgi->redirect(Bugzilla->localconfig->{urlbase}); - exit; + Bugzilla->logout(); + print Bugzilla->cgi->redirect(Bugzilla->localconfig->{urlbase}); + exit; } sub DoSettings { - my $user = Bugzilla->user; - - my %settings; - my $has_settings_enabled = 0; - foreach my $name (sort keys %{ $user->settings }) { - my $setting = $user->settings->{$name}; - next if !$setting->{is_enabled}; - my $category = $setting->{category}; - $settings{$category} ||= []; - push(@{ $settings{$category} }, $setting); - $has_settings_enabled = 1 if $setting->{is_enabled}; - } - - $vars->{settings} = \%settings; - $vars->{has_settings_enabled} = $has_settings_enabled; - $vars->{dont_show_button} = !$has_settings_enabled; + my $user = Bugzilla->user; + + my %settings; + my $has_settings_enabled = 0; + foreach my $name (sort keys %{$user->settings}) { + my $setting = $user->settings->{$name}; + next if !$setting->{is_enabled}; + my $category = $setting->{category}; + $settings{$category} ||= []; + push(@{$settings{$category}}, $setting); + $has_settings_enabled = 1 if $setting->{is_enabled}; + } + + $vars->{settings} = \%settings; + $vars->{has_settings_enabled} = $has_settings_enabled; + $vars->{dont_show_button} = !$has_settings_enabled; } sub SaveSettings { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - - my $settings = $user->settings; - my @setting_list = keys %$settings; - my $mfa_event = undef; - - foreach my $name (@setting_list) { - next if ! ($settings->{$name}->{'is_enabled'}); - my $value = $cgi->param($name); - next unless defined $value; - my $setting = new Bugzilla::User::Setting($name); - - if ($name eq 'api_key_only' && $user->mfa - && ($value eq 'off' - || ($value eq 'api_key_only-isdefault' && $setting->{default_value} eq 'off')) - ) { - $mfa_event = {}; - } + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + + my $settings = $user->settings; + my @setting_list = keys %$settings; + my $mfa_event = undef; + + foreach my $name (@setting_list) { + next if !($settings->{$name}->{'is_enabled'}); + my $value = $cgi->param($name); + next unless defined $value; + my $setting = new Bugzilla::User::Setting($name); + + if ( + $name eq 'api_key_only' + && $user->mfa + && ($value eq 'off' + || ($value eq 'api_key_only-isdefault' && $setting->{default_value} eq 'off')) + ) + { + $mfa_event = {}; + } - if ($value eq "${name}-isdefault" ) { - if (! $settings->{$name}->{'is_default'}) { - if ($mfa_event) { - $mfa_event->{reset} = 1; - } - else { - $settings->{$name}->reset_to_default; - } - } + if ($value eq "${name}-isdefault") { + if (!$settings->{$name}->{'is_default'}) { + if ($mfa_event) { + $mfa_event->{reset} = 1; } else { - $setting->validate_value($value); - if ($name eq 'api_key_only' && $mfa_event) { - $mfa_event->{set} = $value; - } - else { - $settings->{$name}->set($value); - } + $settings->{$name}->reset_to_default; } + } } + else { + $setting->validate_value($value); + if ($name eq 'api_key_only' && $mfa_event) { + $mfa_event->{set} = $value; + } + else { + $settings->{$name}->set($value); + } + } + } - Bugzilla::Hook::process('settings_after_update'); + Bugzilla::Hook::process('settings_after_update'); - $vars->{'settings'} = $user->settings(1); - clear_settings_cache($user->id); + $vars->{'settings'} = $user->settings(1); + clear_settings_cache($user->id); - if ($mfa_event) { - $mfa_event->{reason} = 'Disabling API key authentication requirements'; - $mfa_event->{postback} = { - action => 'userprefs.cgi', - fields => { - tab => 'settings', - }, - }; - $user->mfa_provider->verify_prompt($mfa_event); - } + if ($mfa_event) { + $mfa_event->{reason} = 'Disabling API key authentication requirements'; + $mfa_event->{postback} + = {action => 'userprefs.cgi', fields => {tab => 'settings',},}; + $user->mfa_provider->verify_prompt($mfa_event); + } } sub MfaSettings { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - return unless $user->mfa; - - my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token')); - - my $settings = $user->settings; - if ($event->{reset}) { - $settings->{api_key_only}->reset_to_default(); - } - elsif (my $value = $event->{set}) { - $settings->{api_key_only}->set($value); - } - - $vars->{settings} = $user->settings(1); - clear_settings_cache($user->id); + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + return unless $user->mfa; + + my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token')); + + my $settings = $user->settings; + if ($event->{reset}) { + $settings->{api_key_only}->reset_to_default(); + } + elsif (my $value = $event->{set}) { + $settings->{api_key_only}->set($value); + } + + $vars->{settings} = $user->settings(1); + clear_settings_cache($user->id); } sub DoEmail { - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - ########################################################################### - # User watching - ########################################################################### - my $watched_ref = $dbh->selectcol_arrayref( - "SELECT profiles.login_name FROM watch INNER JOIN profiles" . - " ON watch.watched = profiles.userid" . - " WHERE watcher = ?" . - " ORDER BY profiles.login_name", - undef, $user->id); - $vars->{'watchedusers'} = $watched_ref; - - my $watcher_ids = $dbh->selectcol_arrayref( - "SELECT watcher FROM watch WHERE watched = ?", - undef, $user->id); - - my @watchers; - foreach my $watcher_id (@$watcher_ids) { - my $watcher = new Bugzilla::User($watcher_id); - push(@watchers, Bugzilla::User::identity($watcher)); - } - - @watchers = sort { lc($a) cmp lc($b) } @watchers; - $vars->{'watchers'} = \@watchers; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + ########################################################################### + # User watching + ########################################################################### + my $watched_ref = $dbh->selectcol_arrayref( + "SELECT profiles.login_name FROM watch INNER JOIN profiles" + . " ON watch.watched = profiles.userid" + . " WHERE watcher = ?" + . " ORDER BY profiles.login_name", + undef, $user->id + ); + $vars->{'watchedusers'} = $watched_ref; + + my $watcher_ids + = $dbh->selectcol_arrayref("SELECT watcher FROM watch WHERE watched = ?", + undef, $user->id); + + my @watchers; + foreach my $watcher_id (@$watcher_ids) { + my $watcher = new Bugzilla::User($watcher_id); + push(@watchers, Bugzilla::User::identity($watcher)); + } + + @watchers = sort { lc($a) cmp lc($b) } @watchers; + $vars->{'watchers'} = \@watchers; } sub SaveEmail { - my $dbh = Bugzilla->dbh; - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - - Bugzilla::User::match_field({ 'new_watchedusers' => {'type' => 'multi'} }); - - ########################################################################### - # Role-based preferences - ########################################################################### - $dbh->bz_start_transaction(); - - my $sth_insert = $dbh->prepare('INSERT INTO email_setting - (user_id, relationship, event) VALUES (?, ?, ?)'); - - my $sth_delete = $dbh->prepare('DELETE FROM email_setting - WHERE user_id = ? AND relationship = ? AND event = ?'); - # Load current email preferences into memory before updating them. - my $settings = $user->mail_settings; - - # Update the table - first, with normal events in the - # relationship/event matrix. - my %relationships = Bugzilla::BugMail::relationships(); - foreach my $rel (keys %relationships) { - next if ($rel == REL_QA && !Bugzilla->params->{'useqacontact'}); - # Positive events: a ticked box means "send me mail." - foreach my $event (POS_EVENTS) { - my $is_set = $cgi->param("email-$rel-$event"); - if ($is_set xor $settings->{$rel}{$event}) { - if ($is_set) { - $sth_insert->execute($user->id, $rel, $event); - } - else { - $sth_delete->execute($user->id, $rel, $event); - } - } + my $dbh = Bugzilla->dbh; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + + Bugzilla::User::match_field({'new_watchedusers' => {'type' => 'multi'}}); + + ########################################################################### + # Role-based preferences + ########################################################################### + $dbh->bz_start_transaction(); + + my $sth_insert = $dbh->prepare( + 'INSERT INTO email_setting + (user_id, relationship, event) VALUES (?, ?, ?)' + ); + + my $sth_delete = $dbh->prepare( + 'DELETE FROM email_setting + WHERE user_id = ? AND relationship = ? AND event = ?' + ); + + # Load current email preferences into memory before updating them. + my $settings = $user->mail_settings; + + # Update the table - first, with normal events in the + # relationship/event matrix. + my %relationships = Bugzilla::BugMail::relationships(); + foreach my $rel (keys %relationships) { + next if ($rel == REL_QA && !Bugzilla->params->{'useqacontact'}); + + # Positive events: a ticked box means "send me mail." + foreach my $event (POS_EVENTS) { + my $is_set = $cgi->param("email-$rel-$event"); + if ($is_set xor $settings->{$rel}{$event}) { + if ($is_set) { + $sth_insert->execute($user->id, $rel, $event); } - - # Negative events: a ticked box means "don't send me mail." - foreach my $event (NEG_EVENTS) { - my $is_set = $cgi->param("neg-email-$rel-$event"); - if (!$is_set xor $settings->{$rel}{$event}) { - if (!$is_set) { - $sth_insert->execute($user->id, $rel, $event); - } - else { - $sth_delete->execute($user->id, $rel, $event); - } - } + else { + $sth_delete->execute($user->id, $rel, $event); } + } } - # Global positive events: a ticked box means "send me mail." - foreach my $event (GLOBAL_EVENTS) { - my $is_set = $cgi->param("email-" . REL_ANY . "-$event"); - if ($is_set xor $settings->{+REL_ANY}{$event}) { - if ($is_set) { - $sth_insert->execute($user->id, REL_ANY, $event); - } - else { - $sth_delete->execute($user->id, REL_ANY, $event); - } + # Negative events: a ticked box means "don't send me mail." + foreach my $event (NEG_EVENTS) { + my $is_set = $cgi->param("neg-email-$rel-$event"); + if (!$is_set xor $settings->{$rel}{$event}) { + if (!$is_set) { + $sth_insert->execute($user->id, $rel, $event); } + else { + $sth_delete->execute($user->id, $rel, $event); + } + } } + } + + # Global positive events: a ticked box means "send me mail." + foreach my $event (GLOBAL_EVENTS) { + my $is_set = $cgi->param("email-" . REL_ANY . "-$event"); + if ($is_set xor $settings->{+REL_ANY}{$event}) { + if ($is_set) { + $sth_insert->execute($user->id, REL_ANY, $event); + } + else { + $sth_delete->execute($user->id, REL_ANY, $event); + } + } + } - $dbh->bz_commit_transaction(); - - # We have to clear the cache about email preferences. - delete $user->{'mail_settings'}; - - ########################################################################### - # User watching - ########################################################################### - if (defined $cgi->param('new_watchedusers') - || defined $cgi->param('remove_watched_users')) - { - $dbh->bz_start_transaction(); - - # Use this to protect error messages on duplicate submissions - my $old_watch_ids = - $dbh->selectcol_arrayref("SELECT watched FROM watch" - . " WHERE watcher = ?", undef, $user->id); + $dbh->bz_commit_transaction(); - # The new information given to us by the user. - my $new_watched_users = join(',', $cgi->param('new_watchedusers')) || ''; - my @new_watch_names = split(/[,\s]+/, $new_watched_users); - my %new_watch_ids; + # We have to clear the cache about email preferences. + delete $user->{'mail_settings'}; - foreach my $username (@new_watch_names) { - my $watched_userid = login_to_id(trim($username), THROW_ERROR); - $new_watch_ids{$watched_userid} = 1; - } + ########################################################################### + # User watching + ########################################################################### + if ( defined $cgi->param('new_watchedusers') + || defined $cgi->param('remove_watched_users')) + { + $dbh->bz_start_transaction(); - # Add people who were added. - my $insert_sth = $dbh->prepare('INSERT INTO watch (watched, watcher)' - . ' VALUES (?, ?)'); - foreach my $add_me (keys(%new_watch_ids)) { - next if grep($_ == $add_me, @$old_watch_ids); - $insert_sth->execute($add_me, $user->id); - } + # Use this to protect error messages on duplicate submissions + my $old_watch_ids + = $dbh->selectcol_arrayref("SELECT watched FROM watch" . " WHERE watcher = ?", + undef, $user->id); - if (defined $cgi->param('remove_watched_users')) { - my @removed = $cgi->param('watched_by_you'); - # Remove people who were removed. - my $delete_sth = $dbh->prepare('DELETE FROM watch WHERE watched = ?' - . ' AND watcher = ?'); - - my %remove_watch_ids; - foreach my $username (@removed) { - my $watched_userid = login_to_id(trim($username), THROW_ERROR); - $remove_watch_ids{$watched_userid} = 1; - } - foreach my $remove_me (keys(%remove_watch_ids)) { - $delete_sth->execute($remove_me, $user->id); - } - } + # The new information given to us by the user. + my $new_watched_users = join(',', $cgi->param('new_watchedusers')) || ''; + my @new_watch_names = split(/[,\s]+/, $new_watched_users); + my %new_watch_ids; - $dbh->bz_commit_transaction(); + foreach my $username (@new_watch_names) { + my $watched_userid = login_to_id(trim($username), THROW_ERROR); + $new_watch_ids{$watched_userid} = 1; } - ########################################################################### - # Ignore Bugs - ########################################################################### - my %ignored_bugs = map { $_->{'id'} => 1 } @{$user->bugs_ignored}; - - # Validate the new bugs to ignore by checking that they exist and also - # if the user gave an alias - my @add_ignored = split(/[\s,]+/, $cgi->param('add_ignored_bugs')); - @add_ignored = map { Bugzilla::Bug->check($_)->id } @add_ignored; - map { $ignored_bugs{$_} = 1 } @add_ignored; - - # Remove any bug ids the user no longer wants to ignore - foreach my $key (grep(/^remove_ignored_bug_/, $cgi->param)) { - my ($bug_id) = $key =~ /(\d+)$/; - delete $ignored_bugs{$bug_id}; + # Add people who were added. + my $insert_sth + = $dbh->prepare('INSERT INTO watch (watched, watcher)' . ' VALUES (?, ?)'); + foreach my $add_me (keys(%new_watch_ids)) { + next if grep($_ == $add_me, @$old_watch_ids); + $insert_sth->execute($add_me, $user->id); } - # Update the database with any changes made - my ($removed, $added) = diff_arrays([ map { $_->{'id'} } @{$user->bugs_ignored} ], - [ keys %ignored_bugs ]); + if (defined $cgi->param('remove_watched_users')) { + my @removed = $cgi->param('watched_by_you'); + + # Remove people who were removed. + my $delete_sth + = $dbh->prepare('DELETE FROM watch WHERE watched = ?' . ' AND watcher = ?'); + + my %remove_watch_ids; + foreach my $username (@removed) { + my $watched_userid = login_to_id(trim($username), THROW_ERROR); + $remove_watch_ids{$watched_userid} = 1; + } + foreach my $remove_me (keys(%remove_watch_ids)) { + $delete_sth->execute($remove_me, $user->id); + } + } - if (scalar @$removed || scalar @$added) { - $dbh->bz_start_transaction(); + $dbh->bz_commit_transaction(); + } + + ########################################################################### + # Ignore Bugs + ########################################################################### + my %ignored_bugs = map { $_->{'id'} => 1 } @{$user->bugs_ignored}; + + # Validate the new bugs to ignore by checking that they exist and also + # if the user gave an alias + my @add_ignored = split(/[\s,]+/, $cgi->param('add_ignored_bugs')); + @add_ignored = map { Bugzilla::Bug->check($_)->id } @add_ignored; + map { $ignored_bugs{$_} = 1 } @add_ignored; + + # Remove any bug ids the user no longer wants to ignore + foreach my $key (grep(/^remove_ignored_bug_/, $cgi->param)) { + my ($bug_id) = $key =~ /(\d+)$/; + delete $ignored_bugs{$bug_id}; + } + + # Update the database with any changes made + my ($removed, $added) + = diff_arrays([map { $_->{'id'} } @{$user->bugs_ignored}], + [keys %ignored_bugs]); + + if (scalar @$removed || scalar @$added) { + $dbh->bz_start_transaction(); - if (scalar @$removed) { - $dbh->do('DELETE FROM email_bug_ignore WHERE user_id = ? AND ' . - $dbh->sql_in('bug_id', $removed), - undef, $user->id); - } - if (scalar @$added) { - my $sth = $dbh->prepare('INSERT INTO email_bug_ignore - (user_id, bug_id) VALUES (?, ?)'); - $sth->execute($user->id, $_) foreach @$added; - } + if (scalar @$removed) { + $dbh->do( + 'DELETE FROM email_bug_ignore WHERE user_id = ? AND ' + . $dbh->sql_in('bug_id', $removed), + undef, $user->id + ); + } + if (scalar @$added) { + my $sth = $dbh->prepare( + 'INSERT INTO email_bug_ignore + (user_id, bug_id) VALUES (?, ?)' + ); + $sth->execute($user->id, $_) foreach @$added; + } - # Reset the cache of ignored bugs if the list changed. - delete $user->{bugs_ignored}; + # Reset the cache of ignored bugs if the list changed. + delete $user->{bugs_ignored}; - $dbh->bz_commit_transaction(); - } + $dbh->bz_commit_transaction(); + } } sub DoPermissions { - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my (@has_bits, @set_bits); - - my $groups = $dbh->selectall_arrayref( - "SELECT DISTINCT name, description FROM groups WHERE id IN (" . - $user->groups_as_string . ") ORDER BY name"); - foreach my $group (@$groups) { - my ($nam, $desc) = @$group; - push(@has_bits, {"desc" => $desc, "name" => $nam}); - } - $groups = $dbh->selectall_arrayref('SELECT DISTINCT id, name, description + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my (@has_bits, @set_bits); + + my $groups + = $dbh->selectall_arrayref( + "SELECT DISTINCT name, description FROM groups WHERE id IN (" + . $user->groups_as_string + . ") ORDER BY name"); + foreach my $group (@$groups) { + my ($nam, $desc) = @$group; + push(@has_bits, {"desc" => $desc, "name" => $nam}); + } + $groups = $dbh->selectall_arrayref( + 'SELECT DISTINCT id, name, description FROM groups - ORDER BY name'); - foreach my $group (@$groups) { - my ($group_id, $nam, $desc) = @$group; - if ($user->can_bless($group_id)) { - push(@set_bits, {"desc" => $desc, "name" => $nam}); - } + ORDER BY name' + ); + foreach my $group (@$groups) { + my ($group_id, $nam, $desc) = @$group; + if ($user->can_bless($group_id)) { + push(@set_bits, {"desc" => $desc, "name" => $nam}); } + } - # If the user has product specific privileges, inform him about that. - foreach my $privs (PER_PRODUCT_PRIVILEGES) { - next if $user->in_group($privs); - $vars->{"local_$privs"} = $user->get_products_by_permission($privs); - } + # If the user has product specific privileges, inform him about that. + foreach my $privs (PER_PRODUCT_PRIVILEGES) { + next if $user->in_group($privs); + $vars->{"local_$privs"} = $user->get_products_by_permission($privs); + } - $vars->{'has_bits'} = \@has_bits; - $vars->{'set_bits'} = \@set_bits; + $vars->{'has_bits'} = \@has_bits; + $vars->{'set_bits'} = \@set_bits; } # No SavePermissions() because this panel has no changeable fields. sub DoSavedSearches { - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - if ($user->queryshare_groups_as_string) { - $vars->{'queryshare_groups'} = - Bugzilla::Group->new_from_list($user->queryshare_groups); - } - $vars->{'bless_group_ids'} = [map { $_->id } @{$user->bless_groups}]; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + if ($user->queryshare_groups_as_string) { + $vars->{'queryshare_groups'} + = Bugzilla::Group->new_from_list($user->queryshare_groups); + } + $vars->{'bless_group_ids'} = [map { $_->id } @{$user->bless_groups}]; } sub SaveSavedSearches { - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - # We'll need this in a loop, so do the call once. - my $user_id = $user->id; + # We'll need this in a loop, so do the call once. + my $user_id = $user->id; - my $sth_insert_nl = $dbh->prepare('INSERT INTO namedqueries_link_in_footer + my $sth_insert_nl = $dbh->prepare( + 'INSERT INTO namedqueries_link_in_footer (namedquery_id, user_id) - VALUES (?, ?)'); - my $sth_delete_nl = $dbh->prepare('DELETE FROM namedqueries_link_in_footer + VALUES (?, ?)' + ); + my $sth_delete_nl = $dbh->prepare( + 'DELETE FROM namedqueries_link_in_footer WHERE namedquery_id = ? - AND user_id = ?'); - my $sth_insert_ngm = $dbh->prepare('INSERT INTO namedquery_group_map + AND user_id = ?' + ); + my $sth_insert_ngm = $dbh->prepare( + 'INSERT INTO namedquery_group_map (namedquery_id, group_id) - VALUES (?, ?)'); - my $sth_update_ngm = $dbh->prepare('UPDATE namedquery_group_map + VALUES (?, ?)' + ); + my $sth_update_ngm = $dbh->prepare( + 'UPDATE namedquery_group_map SET group_id = ? - WHERE namedquery_id = ?'); - my $sth_delete_ngm = $dbh->prepare('DELETE FROM namedquery_group_map - WHERE namedquery_id = ?'); - - # Update namedqueries_link_in_footer for this user. - foreach my $q (@{$user->queries}, @{$user->queries_available}) { - if (defined $cgi->param("link_in_footer_" . $q->id)) { - $sth_insert_nl->execute($q->id, $user_id) if !$q->link_in_footer; - } - else { - $sth_delete_nl->execute($q->id, $user_id) if $q->link_in_footer; - } + WHERE namedquery_id = ?' + ); + my $sth_delete_ngm = $dbh->prepare( + 'DELETE FROM namedquery_group_map + WHERE namedquery_id = ?' + ); + + # Update namedqueries_link_in_footer for this user. + foreach my $q (@{$user->queries}, @{$user->queries_available}) { + if (defined $cgi->param("link_in_footer_" . $q->id)) { + $sth_insert_nl->execute($q->id, $user_id) if !$q->link_in_footer; } + else { + $sth_delete_nl->execute($q->id, $user_id) if $q->link_in_footer; + } + } - # For user's own queries, update namedquery_group_map. - foreach my $q (@{$user->queries}) { - my $group_id; + # For user's own queries, update namedquery_group_map. + foreach my $q (@{$user->queries}) { + my $group_id; - if ($user->in_group(Bugzilla->params->{'querysharegroup'})) { - $group_id = $cgi->param("share_" . $q->id) || ''; - } + if ($user->in_group(Bugzilla->params->{'querysharegroup'})) { + $group_id = $cgi->param("share_" . $q->id) || ''; + } - if ($group_id) { - # Don't allow the user to share queries with groups he's not - # allowed to. - next unless grep($_ eq $group_id, @{$user->queryshare_groups}); - - # $group_id is now definitely a valid ID of a group the - # user can share queries with, so we can trick_taint. - detaint_natural($group_id); - if ($q->shared_with_group) { - $sth_update_ngm->execute($group_id, $q->id); - } - else { - $sth_insert_ngm->execute($q->id, $group_id); - } - - # If we're sharing our query with a group we can bless, we - # have the ability to add link to our search to the footer of - # direct group members automatically. - if ($user->can_bless($group_id) && $cgi->param('force_' . $q->id)) { - my $group = new Bugzilla::Group($group_id); - my $members = $group->members_non_inherited; - foreach my $member (@$members) { - next if $member->id == $user->id; - $sth_insert_nl->execute($q->id, $member->id) - if !$q->link_in_footer($member); - } - } - } - else { - # They have unshared that query. - if ($q->shared_with_group) { - $sth_delete_ngm->execute($q->id); - } - - # Don't remove namedqueries_link_in_footer entries for users - # subscribing to the shared query. The idea is that they will - # probably want to be subscribers again should the sharing - # user choose to share the query again. + if ($group_id) { + + # Don't allow the user to share queries with groups he's not + # allowed to. + next unless grep($_ eq $group_id, @{$user->queryshare_groups}); + + # $group_id is now definitely a valid ID of a group the + # user can share queries with, so we can trick_taint. + detaint_natural($group_id); + if ($q->shared_with_group) { + $sth_update_ngm->execute($group_id, $q->id); + } + else { + $sth_insert_ngm->execute($q->id, $group_id); + } + + # If we're sharing our query with a group we can bless, we + # have the ability to add link to our search to the footer of + # direct group members automatically. + if ($user->can_bless($group_id) && $cgi->param('force_' . $q->id)) { + my $group = new Bugzilla::Group($group_id); + my $members = $group->members_non_inherited; + foreach my $member (@$members) { + next if $member->id == $user->id; + $sth_insert_nl->execute($q->id, $member->id) if !$q->link_in_footer($member); } + } + } + else { + # They have unshared that query. + if ($q->shared_with_group) { + $sth_delete_ngm->execute($q->id); + } + + # Don't remove namedqueries_link_in_footer entries for users + # subscribing to the shared query. The idea is that they will + # probably want to be subscribers again should the sharing + # user choose to share the query again. } + } - $user->flush_queries_cache; + $user->flush_queries_cache; - # Update profiles.mybugslink. - my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0; - $dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?", - undef, ($showmybugslink, $user->id)); - $user->{'showmybugslink'} = $showmybugslink; - Bugzilla->memcached->clear({ table => 'profiles', id => $user->id }); + # Update profiles.mybugslink. + my $showmybugslink = defined($cgi->param("showmybugslink")) ? 1 : 0; + $dbh->do("UPDATE profiles SET mybugslink = ? WHERE userid = ?", + undef, ($showmybugslink, $user->id)); + $user->{'showmybugslink'} = $showmybugslink; + Bugzilla->memcached->clear({table => 'profiles', id => $user->id}); } sub SaveMFA { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - my $action = $cgi->param('mfa_action') // ''; - my $params = Bugzilla->input_params; - - my $crypt_password = $user->cryptpassword; - if (bz_crypt(delete $params->{password}, $crypt_password) ne $crypt_password) { - ThrowUserError('password_incorrect'); - } + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + my $action = $cgi->param('mfa_action') // ''; + my $params = Bugzilla->input_params; + + my $crypt_password = $user->cryptpassword; + if (bz_crypt(delete $params->{password}, $crypt_password) ne $crypt_password) { + ThrowUserError('password_incorrect'); + } + + my $mfa = $cgi->param('mfa') // $user->mfa; + my $provider = Bugzilla::MFA->new_from($user, $mfa) // return; + + my $reason; + if ($action eq 'enable') { + $provider->enroll(Bugzilla->input_params); + $reason = 'Two-factor enrollment'; + } + elsif ($action eq 'recovery') { + $reason = 'Recovery code generation'; + } + elsif ($action eq 'disable') { + $reason = 'Disabling two-factor authentication'; + } + + if ($provider->can_verify_inline) { + $provider->verify_check($params); + SaveMFAupdate($cgi->param('mfa_action'), $mfa); + } + else { + my $mfa_event = { + postback => + {action => 'userprefs.cgi', fields => {tab => 'mfa', mfa => $mfa,},}, + reason => $reason, + action => $action, + }; + $provider->verify_prompt($mfa_event); + } +} - my $mfa = $cgi->param('mfa') // $user->mfa; - my $provider = Bugzilla::MFA->new_from($user, $mfa) // return; +sub SaveMFAupdate { + my ($action, $mfa) = @_; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + $action //= ''; - my $reason; - if ($action eq 'enable') { - $provider->enroll(Bugzilla->input_params); - $reason = 'Two-factor enrollment'; - } - elsif ($action eq 'recovery') { - $reason = 'Recovery code generation'; - } - elsif ($action eq 'disable') { - $reason = 'Disabling two-factor authentication'; - } + if ($action eq 'enable') { + $dbh->bz_start_transaction; - if ($provider->can_verify_inline) { - $provider->verify_check($params); - SaveMFAupdate($cgi->param('mfa_action'), $mfa); - } - else { - my $mfa_event = { - postback => { - action => 'userprefs.cgi', - fields => { - tab => 'mfa', - mfa => $mfa, - }, - }, - reason => $reason, - action => $action, - }; - $provider->verify_prompt($mfa_event); - } -} + $user->set_mfa($mfa); + $user->mfa_provider->enrolled(); + Bugzilla->request_cache->{mfa_warning} = 0; + my $settings = Bugzilla->user->settings; + $settings->{api_key_only}->set('on'); + clear_settings_cache(Bugzilla->user->id); -sub SaveMFAupdate { - my ($action, $mfa) = @_; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - $action //= ''; - - if ($action eq 'enable') { - $dbh->bz_start_transaction; - - $user->set_mfa($mfa); - $user->mfa_provider->enrolled(); - Bugzilla->request_cache->{mfa_warning} = 0; - my $settings = Bugzilla->user->settings; - $settings->{api_key_only}->set('on'); - clear_settings_cache(Bugzilla->user->id); - - $user->update({ keep_session => 1, keep_tokens => 1 }); - $dbh->bz_commit_transaction; - } + $user->update({keep_session => 1, keep_tokens => 1}); + $dbh->bz_commit_transaction; + } - elsif ($action eq 'recovery') { - my $codes = $user->mfa_provider->generate_recovery_codes(); - my $token = issue_short_lived_session_token('mfa-recovery'); - set_token_extra_data($token, $codes); - $vars->{mfa_recovery_token} = $token; + elsif ($action eq 'recovery') { + my $codes = $user->mfa_provider->generate_recovery_codes(); + my $token = issue_short_lived_session_token('mfa-recovery'); + set_token_extra_data($token, $codes); + $vars->{mfa_recovery_token} = $token; - } + } - elsif ($action eq 'disable') { - $user->set_mfa(''); - $user->update({ keep_session => 1, keep_tokens => 1 }); + elsif ($action eq 'disable') { + $user->set_mfa(''); + $user->update({keep_session => 1, keep_tokens => 1}); - } + } } sub SaveMFAcallback { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; - my $mfa = $cgi->param('mfa'); - my $provider = Bugzilla::MFA->new_from($user, $mfa) // return; - my $event = $provider->verify_token($cgi->param('mfa_token')); + my $mfa = $cgi->param('mfa'); + my $provider = Bugzilla::MFA->new_from($user, $mfa) // return; + my $event = $provider->verify_token($cgi->param('mfa_token')); - SaveMFAupdate($event->{action}, $mfa); + SaveMFAupdate($event->{action}, $mfa); } sub DoMFA { - my $cgi = Bugzilla->cgi; - return unless my $provider = $cgi->param('frame'); - - print $cgi->header( - -Cache_Control => 'no-cache, no-store, must-revalidate', - -Expires => 'Thu, 01 Dec 1994 16:00:00 GMT', - -Pragma => 'no-cache', - ); - if ($provider eq 'recovery') { - my $token = $cgi->param('t'); - $vars->{codes} = get_token_extra_data($token); - delete_token($token); - $template->process("mfa/recovery.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - } - elsif ($provider =~ /^[a-z]+$/) { - trick_taint($provider); - $template->process("mfa/$provider/enroll.html.tmpl", $vars) - || ThrowTemplateError($template->error()); - } - exit; + my $cgi = Bugzilla->cgi; + return unless my $provider = $cgi->param('frame'); + + print $cgi->header( + -Cache_Control => 'no-cache, no-store, must-revalidate', + -Expires => 'Thu, 01 Dec 1994 16:00:00 GMT', + -Pragma => 'no-cache', + ); + if ($provider eq 'recovery') { + my $token = $cgi->param('t'); + $vars->{codes} = get_token_extra_data($token); + delete_token($token); + $template->process("mfa/recovery.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + } + elsif ($provider =~ /^[a-z]+$/) { + trick_taint($provider); + $template->process("mfa/$provider/enroll.html.tmpl", $vars) + || ThrowTemplateError($template->error()); + } + exit; } sub SaveSessions { - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - - # Do it in a transaction. - $dbh->bz_start_transaction; - if ($cgi->param("session_logout_all")) { - my $info_getter = $user->authorizer && $user->authorizer->successful_info_getter(); - if ($info_getter->cookie) { - $dbh->do("DELETE FROM logincookies WHERE userid = ? AND cookie != ?", undef, - $user->id, $info_getter->cookie); - } + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + + # Do it in a transaction. + $dbh->bz_start_transaction; + if ($cgi->param("session_logout_all")) { + my $info_getter + = $user->authorizer && $user->authorizer->successful_info_getter(); + if ($info_getter->cookie) { + $dbh->do("DELETE FROM logincookies WHERE userid = ? AND cookie != ?", + undef, $user->id, $info_getter->cookie); } - else { - my @logout_ids = $cgi->param('session_logout_id'); - my $sessions = Bugzilla::User::Session->new_from_list(\@logout_ids); - foreach my $session (@$sessions) { - $session->remove_from_db if $session->userid == $user->id; - } + } + else { + my @logout_ids = $cgi->param('session_logout_id'); + my $sessions = Bugzilla::User::Session->new_from_list(\@logout_ids); + foreach my $session (@$sessions) { + $session->remove_from_db if $session->userid == $user->id; } + } - $dbh->bz_commit_transaction; + $dbh->bz_commit_transaction; } sub DoSessions { - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - my $sessions = Bugzilla::User::Session->match({ userid => $user->id, LIMIT => SESSION_MAX + 1 }); - my $info_getter = $user->authorizer && $user->authorizer->successful_info_getter(); - - if ($info_getter && $info_getter->can('cookie')) { - foreach my $session (@$sessions) { - $session->{current} = $info_getter->cookie eq $session->{cookie}; - } + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + my $sessions = Bugzilla::User::Session->match( + {userid => $user->id, LIMIT => SESSION_MAX + 1}); + my $info_getter + = $user->authorizer && $user->authorizer->successful_info_getter(); + + if ($info_getter && $info_getter->can('cookie')) { + foreach my $session (@$sessions) { + $session->{current} = $info_getter->cookie eq $session->{cookie}; } - my ($count) = $dbh->selectrow_array("SELECT count(*) FROM logincookies WHERE userid = ?", undef, - $user->id); - - $vars->{too_many_sessions} = @$sessions == SESSION_MAX + 1; - $vars->{sessions} = $sessions; - $vars->{session_count} = $count; - $vars->{session_max} = SESSION_MAX; - pop @$sessions if $vars->{too_many_sessions}; + } + my ($count) + = $dbh->selectrow_array("SELECT count(*) FROM logincookies WHERE userid = ?", + undef, $user->id); + + $vars->{too_many_sessions} = @$sessions == SESSION_MAX + 1; + $vars->{sessions} = $sessions; + $vars->{session_count} = $count; + $vars->{session_max} = SESSION_MAX; + pop @$sessions if $vars->{too_many_sessions}; } sub DoApiKey { - my $user = Bugzilla->user; + my $user = Bugzilla->user; - my $api_keys = Bugzilla::User::APIKey->match({ user_id => $user->id }); - $vars->{api_keys} = $api_keys; - $vars->{any_revoked} = grep { $_->revoked } @$api_keys; + my $api_keys = Bugzilla::User::APIKey->match({user_id => $user->id}); + $vars->{api_keys} = $api_keys; + $vars->{any_revoked} = grep { $_->revoked } @$api_keys; } sub SaveApiKey { - my $cgi = Bugzilla->cgi; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; - my @mfa_events; - - # Do it in a transaction. - $dbh->bz_start_transaction; - - # Update any existing keys - my $api_keys = Bugzilla::User::APIKey->match({ user_id => $user->id }); - foreach my $api_key (@$api_keys) { - my $description = $cgi->param('description_' . $api_key->id); - my $revoked = !!$cgi->param('revoked_' . $api_key->id); - - if ($description ne $api_key->description || $revoked != $api_key->revoked) { - if ($user->mfa && !$revoked && $api_key->revoked) { - push @mfa_events, { - type => 'update', - reason => 'enabling an API key', - id => $api_key->id, - description => $description, - }; - } - else { - $api_key->set_all({ - description => $description, - revoked => $revoked, - }); - $api_key->update(); - if ($revoked) { - Bugzilla->log_user_request(undef, undef, 'api-key-revoke') - } - else { - Bugzilla->log_user_request(undef, undef, 'api-key-unrevoke') - } - } - } - } - - # Create a new API key if requested. - if ($cgi->param('new_key')) { - my $description = $cgi->param('new_description'); - if ($user->mfa) { - push @mfa_events, { - type => 'create', - reason => 'creating an API key', - description => $description, - }; + my $cgi = Bugzilla->cgi; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; + my @mfa_events; + + # Do it in a transaction. + $dbh->bz_start_transaction; + + # Update any existing keys + my $api_keys = Bugzilla::User::APIKey->match({user_id => $user->id}); + foreach my $api_key (@$api_keys) { + my $description = $cgi->param('description_' . $api_key->id); + my $revoked = !!$cgi->param('revoked_' . $api_key->id); + + if ($description ne $api_key->description || $revoked != $api_key->revoked) { + if ($user->mfa && !$revoked && $api_key->revoked) { + push @mfa_events, + { + type => 'update', + reason => 'enabling an API key', + id => $api_key->id, + description => $description, + }; + } + else { + $api_key->set_all({description => $description, revoked => $revoked,}); + $api_key->update(); + if ($revoked) { + Bugzilla->log_user_request(undef, undef, 'api-key-revoke'); } else { - $vars->{new_key} = _create_api_key($description); + Bugzilla->log_user_request(undef, undef, 'api-key-unrevoke'); } + } } - - $dbh->bz_commit_transaction; - - if (@mfa_events) { - # build the fields for the postback - my $mfa_event = { - postback => { - action => 'userprefs.cgi', - fields => { - tab => 'apikey', - }, - }, - reason => ucfirst(join(' and ', map { $_->{reason} } @mfa_events)), - actions => \@mfa_events, + } + + # Create a new API key if requested. + if ($cgi->param('new_key')) { + my $description = $cgi->param('new_description'); + if ($user->mfa) { + push @mfa_events, + { + type => 'create', + reason => 'creating an API key', + description => $description, }; - # display 2fa verification - $user->mfa_provider->verify_prompt($mfa_event); } + else { + $vars->{new_key} = _create_api_key($description); + } + } + + $dbh->bz_commit_transaction; + + if (@mfa_events) { + + # build the fields for the postback + my $mfa_event = { + postback => {action => 'userprefs.cgi', fields => {tab => 'apikey',},}, + reason => ucfirst(join(' and ', map { $_->{reason} } @mfa_events)), + actions => \@mfa_events, + }; + + # display 2fa verification + $user->mfa_provider->verify_prompt($mfa_event); + } } sub MfaApiKey { - my $cgi = Bugzilla->cgi; - my $user = Bugzilla->user; - my $dbh = Bugzilla->dbh; - return unless $user->mfa; + my $cgi = Bugzilla->cgi; + my $user = Bugzilla->user; + my $dbh = Bugzilla->dbh; + return unless $user->mfa; - my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token')); + my $event = $user->mfa_provider->verify_token($cgi->param('mfa_token')); - foreach my $action (@{ $event->{actions} }) { - if ($action->{type} eq 'create') { - $vars->{new_key} = _create_api_key($action->{description}); - } + foreach my $action (@{$event->{actions}}) { + if ($action->{type} eq 'create') { + $vars->{new_key} = _create_api_key($action->{description}); + } - elsif ($action->{type} eq 'update') { - $dbh->bz_start_transaction; - my $api_key = Bugzilla::User::APIKey->check({ id => $action->{id} }); - $api_key->set_all({ - description => $action->{description}, - revoked => 0, - }); - $api_key->update(); - Bugzilla->log_user_request(undef, undef, 'api-key-unrevoke'); - $dbh->bz_commit_transaction; - } + elsif ($action->{type} eq 'update') { + $dbh->bz_start_transaction; + my $api_key = Bugzilla::User::APIKey->check({id => $action->{id}}); + $api_key->set_all({description => $action->{description}, revoked => 0,}); + $api_key->update(); + Bugzilla->log_user_request(undef, undef, 'api-key-unrevoke'); + $dbh->bz_commit_transaction; } + } } sub _create_api_key { - my ($description) = @_; - my $user = Bugzilla->user; + my ($description) = @_; + my $user = Bugzilla->user; - my $key = Bugzilla::User::APIKey->create({ - user_id => $user->id, - description => $description, + my $key + = Bugzilla::User::APIKey->create({ + user_id => $user->id, description => $description, }); - Bugzilla->log_user_request(undef, undef, 'api-key-create'); + Bugzilla->log_user_request(undef, undef, 'api-key-create'); - # As a security precaution, we always sent out an e-mail when - # an API key is created - my $template = Bugzilla->template_inner($user->setting('lang')); - my $message; - $template->process('email/new-api-key.txt.tmpl', $vars, \$message) - || ThrowTemplateError($template->error()); + # As a security precaution, we always sent out an e-mail when + # an API key is created + my $template = Bugzilla->template_inner($user->setting('lang')); + my $message; + $template->process('email/new-api-key.txt.tmpl', $vars, \$message) + || ThrowTemplateError($template->error()); - MessageToMTA($message); + MessageToMTA($message); - return $key; + return $key; } ############################################################################### @@ -947,15 +971,16 @@ $cgi->delete('GoAheadAndLogIn'); Bugzilla->login(LOGIN_OPTIONAL); if (!Bugzilla->user->id) { - # Use credentials given in the form if login cookies are not available. - $cgi->param('Bugzilla_login', $cgi->param('old_login')); - $cgi->param('Bugzilla_password', $cgi->param('old_password')); + + # Use credentials given in the form if login cookies are not available. + $cgi->param('Bugzilla_login', $cgi->param('old_login')); + $cgi->param('Bugzilla_password', $cgi->param('old_password')); } Bugzilla->login(LOGIN_REQUIRED); -my $save_changes = $cgi->param('dosave'); +my $save_changes = $cgi->param('dosave'); my $disable_account = $cgi->param('account_disable'); -my $mfa_token = $cgi->param('mfa_token'); +my $mfa_token = $cgi->param('mfa_token'); $vars->{'changes_saved'} = $save_changes || $mfa_token; my $current_tab_name = $cgi->param('tab') || "account"; @@ -966,72 +991,76 @@ trick_taint($current_tab_name); $vars->{'current_tab_name'} = $current_tab_name; my $token = $cgi->param('token'); -check_token_data($token, 'edit_user_prefs') if $save_changes || $disable_account; +check_token_data($token, 'edit_user_prefs') + if $save_changes || $disable_account; # Do any saving, and then display the current tab. SWITCH: for ($current_tab_name) { - # Extensions must set it to 1 to confirm the tab is valid. - my $handled = 0; - Bugzilla::Hook::process('user_preferences', - { 'vars' => $vars, - save_changes => $save_changes, - current_tab => $current_tab_name, - handled => \$handled }); - last SWITCH if $handled; - - /^account$/ && do { - MfaAccount() if $mfa_token; - DisableAccount() if $disable_account; - SaveAccount() if $save_changes; - DoAccount(); - last SWITCH; - }; - /^settings$/ && do { - MfaSettings() if $mfa_token; - SaveSettings() if $save_changes; - DoSettings(); - last SWITCH; - }; - /^email$/ && do { - SaveEmail() if $save_changes; - DoEmail(); - last SWITCH; - }; - /^permissions$/ && do { - DoPermissions(); - last SWITCH; - }; - /^saved-searches$/ && do { - SaveSavedSearches() if $save_changes; - DoSavedSearches(); - last SWITCH; - }; - /^apikey$/ && do { - MfaApiKey() if $mfa_token; - SaveApiKey() if $save_changes; - DoApiKey(); - last SWITCH; - }; - /^sessions$/ && do { - SaveSessions() if $save_changes; - DoSessions(); - last SWITCH; - }; - /^mfa$/ && do { - SaveMFAcallback() if $mfa_token; - SaveMFA() if $save_changes; - DoMFA(); - last SWITCH; - }; - - ThrowUserError("unknown_tab", - { current_tab_name => $current_tab_name }); + # Extensions must set it to 1 to confirm the tab is valid. + my $handled = 0; + Bugzilla::Hook::process( + 'user_preferences', + { + 'vars' => $vars, + save_changes => $save_changes, + current_tab => $current_tab_name, + handled => \$handled + } + ); + last SWITCH if $handled; + + /^account$/ && do { + MfaAccount() if $mfa_token; + DisableAccount() if $disable_account; + SaveAccount() if $save_changes; + DoAccount(); + last SWITCH; + }; + /^settings$/ && do { + MfaSettings() if $mfa_token; + SaveSettings() if $save_changes; + DoSettings(); + last SWITCH; + }; + /^email$/ && do { + SaveEmail() if $save_changes; + DoEmail(); + last SWITCH; + }; + /^permissions$/ && do { + DoPermissions(); + last SWITCH; + }; + /^saved-searches$/ && do { + SaveSavedSearches() if $save_changes; + DoSavedSearches(); + last SWITCH; + }; + /^apikey$/ && do { + MfaApiKey() if $mfa_token; + SaveApiKey() if $save_changes; + DoApiKey(); + last SWITCH; + }; + /^sessions$/ && do { + SaveSessions() if $save_changes; + DoSessions(); + last SWITCH; + }; + /^mfa$/ && do { + SaveMFAcallback() if $mfa_token; + SaveMFA() if $save_changes; + DoMFA(); + last SWITCH; + }; + + ThrowUserError("unknown_tab", {current_tab_name => $current_tab_name}); } delete_token($token) if $save_changes; if ($current_tab_name ne 'permissions') { - $vars->{'token'} = issue_session_token('edit_user_prefs'); + $vars->{'token'} = issue_session_token('edit_user_prefs'); } # Generate and return the UI (HTML page) from the appropriate template. diff --git a/vagrant_support/re.pl b/vagrant_support/re.pl index d6deeca8c..fdb844eb4 100755 --- a/vagrant_support/re.pl +++ b/vagrant_support/re.pl @@ -1,6 +1,4 @@ #!/bin/bash -exec perl \ - -I$HOME/perl/lib/perl5 \ - -I/vagrant/local/lib/perl5 \ - $HOME/perl/bin/re.pl "$@" +exec perl \-I $HOME / perl / lib / perl5 \-I / vagrant / local + /lib/perl5 \$HOME / perl / bin / re . pl "$@" diff --git a/view_job_queue.cgi b/view_job_queue.cgi index 5ea871104..a8e585e1a 100755 --- a/view_job_queue.cgi +++ b/view_job_queue.cgi @@ -21,9 +21,8 @@ use Storable qw(read_magic thaw); my $user = Bugzilla->login(LOGIN_REQUIRED); ($user->in_group("admin") || $user->in_group('infra')) - || ThrowUserError("auth_failure", { group => "admin", - action => "access", - object => "job_queue" }); + || ThrowUserError("auth_failure", + {group => "admin", action => "access", object => "job_queue"}); my $vars = {}; generate_report($vars); @@ -31,14 +30,14 @@ generate_report($vars); print Bugzilla->cgi->header(); my $template = Bugzilla->template; $template->process('admin/reports/job_queue.html.tmpl', $vars) - || ThrowTemplateError($template->error()); + || ThrowTemplateError($template->error()); sub generate_report { - my ($vars) = @_; - my $dbh = Bugzilla->dbh; - my $user = Bugzilla->user; + my ($vars) = @_; + my $dbh = Bugzilla->dbh; + my $user = Bugzilla->user; - my $query = " + my $query = " SELECT j.jobid, j.arg, @@ -66,56 +65,59 @@ sub generate_report { j.run_after, j.grabbed_until, j.insert_time, j.jobid " . $dbh->sql_limit(JOB_QUEUE_VIEW_MAX_JOBS + 1); - $vars->{jobs} = $dbh->selectall_arrayref($query, { Slice => {} }); - if (@{ $vars->{jobs} } == JOB_QUEUE_VIEW_MAX_JOBS + 1) { - pop @{ $vars->{jobs} }; - $vars->{job_count} = $dbh->selectrow_array("SELECT COUNT(*) FROM ts_job"); - $vars->{too_many_jobs} = 1; - } + $vars->{jobs} = $dbh->selectall_arrayref($query, {Slice => {}}); + if (@{$vars->{jobs}} == JOB_QUEUE_VIEW_MAX_JOBS + 1) { + pop @{$vars->{jobs}}; + $vars->{job_count} = $dbh->selectrow_array("SELECT COUNT(*) FROM ts_job"); + $vars->{too_many_jobs} = 1; + } - my $bug_word = template_var('terms')->{bug}; - foreach my $job (@{ $vars->{jobs} }) { - my ($recipient, $description); - eval { - if ($job->{func} eq 'Bugzilla::Job::BugMail') { - my $arg = _cond_thaw(delete $job->{arg}); - next unless $arg; - my $vars = $arg->{vars}; - $recipient = $vars->{to_user}->{login_name}; - $description = "[$bug_word " . $vars->{bug}->{bug_id} . '] ' - . $vars->{bug}->{short_desc}; - } + my $bug_word = template_var('terms')->{bug}; + foreach my $job (@{$vars->{jobs}}) { + my ($recipient, $description); + eval { + if ($job->{func} eq 'Bugzilla::Job::BugMail') { + my $arg = _cond_thaw(delete $job->{arg}); + next unless $arg; + my $vars = $arg->{vars}; + $recipient = $vars->{to_user}->{login_name}; + $description + = "[$bug_word " . $vars->{bug}->{bug_id} . '] ' . $vars->{bug}->{short_desc}; + } - elsif ($job->{func} eq 'Bugzilla::Job::Mailer') { - my $arg = _cond_thaw(delete $job->{arg}); - next unless $arg; - my $msg = $arg->{msg}; - if (ref($msg) && blessed($msg) eq 'Email::MIME') { - $recipient = $msg->header('to'); - $description = $msg->header('subject'); - } else { - ($recipient) = $msg =~ /\nTo: ([^\n]+)/i; - ($description) = $msg =~ /\nSubject: ([^\n]+)/i; - } - } - }; - if ($recipient) { - $job->{subject} = "<$recipient> $description"; + elsif ($job->{func} eq 'Bugzilla::Job::Mailer') { + my $arg = _cond_thaw(delete $job->{arg}); + next unless $arg; + my $msg = $arg->{msg}; + if (ref($msg) && blessed($msg) eq 'Email::MIME') { + $recipient = $msg->header('to'); + $description = $msg->header('subject'); + } + else { + ($recipient) = $msg =~ /\nTo: ([^\n]+)/i; + ($description) = $msg =~ /\nSubject: ([^\n]+)/i; } + } + }; + if ($recipient) { + $job->{subject} = "<$recipient> $description"; } + } } sub _cond_thaw { - my $data = shift; - my $magic = eval { read_magic($data); }; - if ($magic && $magic->{major} && $magic->{major} >= 2 && $magic->{major} <= 5) { - my $thawed = eval { thaw($data) }; - if ($@) { - # false alarm... looked like a Storable, but wasn't - return undef; - } - return $thawed; - } else { - return undef; + my $data = shift; + my $magic = eval { read_magic($data); }; + if ($magic && $magic->{major} && $magic->{major} >= 2 && $magic->{major} <= 5) { + my $thawed = eval { thaw($data) }; + if ($@) { + + # false alarm... looked like a Storable, but wasn't + return undef; } + return $thawed; + } + else { + return undef; + } } diff --git a/votes.cgi b/votes.cgi index 7c1fff67a..b2e1f2a7e 100755 --- a/votes.cgi +++ b/votes.cgi @@ -30,21 +30,22 @@ use lib qw(. lib local/lib/perl5); use Bugzilla; use Bugzilla::Error; -Bugzilla->has_extension('Voting') || ThrowCodeError('extension_disabled', { name => 'Voting' }); +Bugzilla->has_extension('Voting') + || ThrowCodeError('extension_disabled', {name => 'Voting'}); my $cgi = Bugzilla->cgi; my $action = $cgi->param('action') || 'show_user'; if ($action eq "show_bug") { - $cgi->delete('action'); - $cgi->param('id', 'voting/bug.html'); + $cgi->delete('action'); + $cgi->param('id', 'voting/bug.html'); } elsif ($action eq "show_user" or $action eq 'vote') { - $cgi->delete('action') unless $action eq 'vote'; - $cgi->param('id', 'voting/user.html'); + $cgi->delete('action') unless $action eq 'vote'; + $cgi->param('id', 'voting/user.html'); } else { - ThrowUserError('unknown_action', {action => $action}); + ThrowUserError('unknown_action', {action => $action}); } print $cgi->redirect('page.cgi?' . $cgi->query_string); diff --git a/whine.pl b/whine.pl index 6343b501f..bfa8df5e6 100755 --- a/whine.pl +++ b/whine.pl @@ -37,38 +37,35 @@ my @seen_schedules = (); # These statement handles should live outside of their functions in order to # allow the database to keep their SQL compiled. -my $sth_run_queries = - $dbh->prepare("SELECT " . - "query_name, title, onemailperbug " . - "FROM whine_queries " . - "WHERE eventid=? " . - "ORDER BY sortkey"); -my $sth_get_query = - $dbh->prepare("SELECT query FROM namedqueries " . - "WHERE userid = ? AND name = ?"); +my $sth_run_queries + = $dbh->prepare("SELECT " + . "query_name, title, onemailperbug " + . "FROM whine_queries " + . "WHERE eventid=? " + . "ORDER BY sortkey"); +my $sth_get_query = $dbh->prepare( + "SELECT query FROM namedqueries " . "WHERE userid = ? AND name = ?"); # get the event that's scheduled with the lowest run_next value -my $sth_next_scheduled_event = $dbh->prepare( - "SELECT " . - " whine_schedules.eventid, " . - " whine_events.owner_userid, " . - " whine_events.subject, " . - " whine_events.body, " . - " whine_events.mailifnobugs " . - "FROM whine_schedules " . - "LEFT JOIN whine_events " . - " ON whine_events.id = whine_schedules.eventid " . - "WHERE run_next <= NOW() " . - "ORDER BY run_next " . - $dbh->sql_limit(1) -); +my $sth_next_scheduled_event + = $dbh->prepare("SELECT " + . " whine_schedules.eventid, " + . " whine_events.owner_userid, " + . " whine_events.subject, " + . " whine_events.body, " + . " whine_events.mailifnobugs " + . "FROM whine_schedules " + . "LEFT JOIN whine_events " + . " ON whine_events.id = whine_schedules.eventid " + . "WHERE run_next <= NOW() " + . "ORDER BY run_next " + . $dbh->sql_limit(1)); # get all pending schedules matching an eventid -my $sth_schedules_by_event = $dbh->prepare( - "SELECT id, mailto_type, mailto " . - "FROM whine_schedules " . - "WHERE eventid=? AND run_next <= NOW()" -); +my $sth_schedules_by_event + = $dbh->prepare("SELECT id, mailto_type, mailto " + . "FROM whine_schedules " + . "WHERE eventid=? AND run_next <= NOW()"); ################################################################################ @@ -88,20 +85,25 @@ my $sth_schedules_by_event = $dbh->prepare( my $fromaddress = Bugzilla->params->{'mailfrom'}; # get the current date and time -my ($now_sec, $now_minute, $now_hour, $now_day, $now_month, $now_year, - $now_weekday) = localtime; +my ( + $now_sec, $now_minute, $now_hour, $now_day, + $now_month, $now_year, $now_weekday +) = localtime; + # Convert year to two digits $now_year = sprintf("%02d", $now_year % 100); + # Convert the month to January being "1" instead of January being "0". $now_month++; my @daysinmonth = qw(0 31 28 31 30 31 30 31 31 30 31 30 31); + # Alter February in case of a leap year. This simple way to do it only # applies if you won't be looking at February of next year, which whining # doesn't need to do. -if (($now_year % 4 == 0) && - (($now_year % 100 != 0) || ($now_year % 400 == 0))) { - $daysinmonth[2] = 29; +if (($now_year % 4 == 0) && (($now_year % 100 != 0) || ($now_year % 400 == 0))) +{ + $daysinmonth[2] = 29; } # run_day can contain either a calendar day (1, 2, 3...), a day of the week @@ -113,67 +115,79 @@ if (($now_year % 4 == 0) && # # We go over each uninitialized schedule record and use its settings to # determine what the next time it runs should be -my $sched_h = $dbh->prepare("SELECT id, run_day, run_time " . - "FROM whine_schedules " . - "WHERE run_next IS NULL" ); +my $sched_h + = $dbh->prepare("SELECT id, run_day, run_time " + . "FROM whine_schedules " + . "WHERE run_next IS NULL"); $sched_h->execute(); while (my ($schedule_id, $day, $time) = $sched_h->fetchrow_array) { - # fill in some defaults in case they're blank - $day ||= '0'; - $time ||= '0'; - - # If this schedule is supposed to run today, we see if it's supposed to be - # run at a particular hour. If so, we set it for that hour, and if not, - # it runs at an interval over the course of a day, which means we should - # set it to run immediately. - if (&check_today($day)) { - # Values that are not entirely numeric are intervals, like "30min" - if ($time !~ /^\d+$/) { - # set it to now - $sth = $dbh->prepare( "UPDATE whine_schedules " . - "SET run_next=NOW() " . - "WHERE id=?"); - $sth->execute($schedule_id); - } - # A time greater than now means it still has to run today - elsif ($time >= $now_hour) { - # set it to today + number of hours - $sth = $dbh->prepare( - "UPDATE whine_schedules " . - "SET run_next = " . - $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'HOUR') . - " WHERE id = ?"); - $sth->execute($time, $schedule_id); - } - # the target time is less than the current time - else { # set it for the next applicable day - $day = &get_next_date($day); - my $run_next = $dbh->sql_date_math('(' - . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') - . ')', '+', '?', 'HOUR'); - $sth = $dbh->prepare("UPDATE whine_schedules " . - "SET run_next = $run_next - WHERE id = ?"); - $sth->execute($day, $time, $schedule_id); - } + # fill in some defaults in case they're blank + $day ||= '0'; + $time ||= '0'; + + # If this schedule is supposed to run today, we see if it's supposed to be + # run at a particular hour. If so, we set it for that hour, and if not, + # it runs at an interval over the course of a day, which means we should + # set it to run immediately. + if (&check_today($day)) { + + # Values that are not entirely numeric are intervals, like "30min" + if ($time !~ /^\d+$/) { + + # set it to now + $sth = $dbh->prepare( + "UPDATE whine_schedules " . "SET run_next=NOW() " . "WHERE id=?"); + $sth->execute($schedule_id); } - # If the schedule is not supposed to run today, we set it to run on the - # appropriate date and time - else { - my $target_date = &get_next_date($day); - # If configured for a particular time, set it to that, otherwise - # midnight - my $target_time = ($time =~ /^\d+$/) ? $time : 0; - - my $run_next = $dbh->sql_date_math('(' - . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') - . ')', '+', '?', 'HOUR'); - $sth = $dbh->prepare("UPDATE whine_schedules " . - "SET run_next = $run_next - WHERE id = ?"); - $sth->execute($target_date, $target_time, $schedule_id); + + # A time greater than now means it still has to run today + elsif ($time >= $now_hour) { + + # set it to today + number of hours + $sth + = $dbh->prepare("UPDATE whine_schedules " + . "SET run_next = " + . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'HOUR') + . " WHERE id = ?"); + $sth->execute($time, $schedule_id); + } + + # the target time is less than the current time + else { # set it for the next applicable day + $day = &get_next_date($day); + my $run_next + = $dbh->sql_date_math( + '(' . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') . ')', + '+', '?', 'HOUR'); + $sth = $dbh->prepare( + "UPDATE whine_schedules " . "SET run_next = $run_next + WHERE id = ?" + ); + $sth->execute($day, $time, $schedule_id); } + + } + + # If the schedule is not supposed to run today, we set it to run on the + # appropriate date and time + else { + my $target_date = &get_next_date($day); + + # If configured for a particular time, set it to that, otherwise + # midnight + my $target_time = ($time =~ /^\d+$/) ? $time : 0; + + my $run_next + = $dbh->sql_date_math( + '(' . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') . ')', + '+', '?', 'HOUR'); + $sth = $dbh->prepare( + "UPDATE whine_schedules " . "SET run_next = $run_next + WHERE id = ?" + ); + $sth->execute($target_date, $target_time, $schedule_id); + } } $sched_h->finish(); @@ -195,91 +209,85 @@ $sched_h->finish(); # mailifnobugs - send message even if there are no query or query results sub get_next_event { - my $event = {}; - - # Loop until there's something to return - until (scalar keys %{$event}) { - - $dbh->bz_start_transaction(); - - # Get the event ID for the first pending schedule - $sth_next_scheduled_event->execute; - my $fetched = $sth_next_scheduled_event->fetch; - $sth_next_scheduled_event->finish; - return undef unless $fetched; - my ($eventid, $owner_id, $subject, $body, $mailifnobugs) = @{$fetched}; - - my $owner = Bugzilla::User->new($owner_id); - - my $whineatothers = $owner->in_group('bz_canusewhineatothers'); - - my %user_objects; # Used for keeping track of who has been added - - # Get all schedules that match that event ID and are pending - $sth_schedules_by_event->execute($eventid); - - # Add the users from those schedules to the list - while (my $row = $sth_schedules_by_event->fetch) { - my ($sid, $mailto_type, $mailto) = @{$row}; - - # Only bother doing any work if this user has whine permission - if ($owner->in_group('bz_canusewhines')) { - - if ($mailto_type == MAILTO_USER) { - if (not defined $user_objects{$mailto}) { - if ($mailto == $owner_id) { - $user_objects{$mailto} = $owner; - } - elsif ($whineatothers) { - $user_objects{$mailto} = Bugzilla::User->new($mailto); - } - } - } - elsif ($mailto_type == MAILTO_GROUP) { - my $sth = $dbh->prepare("SELECT name FROM groups " . - "WHERE id=?"); - $sth->execute($mailto); - my $groupname = $sth->fetch->[0]; - my $group_id = Bugzilla::Group::ValidateGroupName( - $groupname, $owner); - if ($group_id) { - my $glist = join(',', - @{Bugzilla::Group->flatten_group_membership( - $group_id)}); - $sth = $dbh->prepare("SELECT user_id FROM " . - "user_group_map " . - "WHERE group_id IN ($glist)"); - $sth->execute(); - for my $row (@{$sth->fetchall_arrayref}) { - if (not defined $user_objects{$row->[0]}) { - $user_objects{$row->[0]} = - Bugzilla::User->new($row->[0]); - } - } - } - } + my $event = {}; - } + # Loop until there's something to return + until (scalar keys %{$event}) { - reset_timer($sid); - } + $dbh->bz_start_transaction(); + + # Get the event ID for the first pending schedule + $sth_next_scheduled_event->execute; + my $fetched = $sth_next_scheduled_event->fetch; + $sth_next_scheduled_event->finish; + return undef unless $fetched; + my ($eventid, $owner_id, $subject, $body, $mailifnobugs) = @{$fetched}; - $dbh->bz_commit_transaction(); - - # Only set $event if the user is allowed to do whining - if ($owner->in_group('bz_canusewhines')) { - my @users = values %user_objects; - $event = { - 'eventid' => $eventid, - 'author' => $owner, - 'mailto' => \@users, - 'subject' => $subject, - 'body' => $body, - 'mailifnobugs' => $mailifnobugs, - }; + my $owner = Bugzilla::User->new($owner_id); + + my $whineatothers = $owner->in_group('bz_canusewhineatothers'); + + my %user_objects; # Used for keeping track of who has been added + + # Get all schedules that match that event ID and are pending + $sth_schedules_by_event->execute($eventid); + + # Add the users from those schedules to the list + while (my $row = $sth_schedules_by_event->fetch) { + my ($sid, $mailto_type, $mailto) = @{$row}; + + # Only bother doing any work if this user has whine permission + if ($owner->in_group('bz_canusewhines')) { + + if ($mailto_type == MAILTO_USER) { + if (not defined $user_objects{$mailto}) { + if ($mailto == $owner_id) { + $user_objects{$mailto} = $owner; + } + elsif ($whineatothers) { + $user_objects{$mailto} = Bugzilla::User->new($mailto); + } + } } + elsif ($mailto_type == MAILTO_GROUP) { + my $sth = $dbh->prepare("SELECT name FROM groups " . "WHERE id=?"); + $sth->execute($mailto); + my $groupname = $sth->fetch->[0]; + my $group_id = Bugzilla::Group::ValidateGroupName($groupname, $owner); + if ($group_id) { + my $glist = join(',', @{Bugzilla::Group->flatten_group_membership($group_id)}); + $sth = $dbh->prepare( + "SELECT user_id FROM " . "user_group_map " . "WHERE group_id IN ($glist)"); + $sth->execute(); + for my $row (@{$sth->fetchall_arrayref}) { + if (not defined $user_objects{$row->[0]}) { + $user_objects{$row->[0]} = Bugzilla::User->new($row->[0]); + } + } + } + } + + } + + reset_timer($sid); + } + + $dbh->bz_commit_transaction(); + + # Only set $event if the user is allowed to do whining + if ($owner->in_group('bz_canusewhines')) { + my @users = values %user_objects; + $event = { + 'eventid' => $eventid, + 'author' => $owner, + 'mailto' => \@users, + 'subject' => $subject, + 'body' => $body, + 'mailifnobugs' => $mailifnobugs, + }; } - return $event; + } + return $event; } # Run the queries for each event @@ -293,38 +301,38 @@ sub get_next_event { # mailifnobugs (send message even if there are no query or query results) while (my $event = get_next_event) { - my $eventid = $event->{'eventid'}; - - # We loop for each target user because some of the queries will be using - # subjective pronouns - $dbh = Bugzilla->switch_to_shadow_db(); - for my $target (@{$event->{'mailto'}}) { - my $args = { - 'subject' => $event->{'subject'}, - 'body' => $event->{'body'}, - 'eventid' => $event->{'eventid'}, - 'author' => $event->{'author'}, - 'recipient' => $target, - 'from' => $fromaddress, - }; - - # run the queries for this schedule - my $queries = run_queries($args); - - # If mailifnobugs is false, make sure there is something to output - if (!$event->{'mailifnobugs'}) { - my $there_are_bugs = 0; - for my $query (@{$queries}) { - $there_are_bugs = 1 if scalar @{$query->{'bugs'}}; - } - next unless $there_are_bugs; - } + my $eventid = $event->{'eventid'}; + + # We loop for each target user because some of the queries will be using + # subjective pronouns + $dbh = Bugzilla->switch_to_shadow_db(); + for my $target (@{$event->{'mailto'}}) { + my $args = { + 'subject' => $event->{'subject'}, + 'body' => $event->{'body'}, + 'eventid' => $event->{'eventid'}, + 'author' => $event->{'author'}, + 'recipient' => $target, + 'from' => $fromaddress, + }; + + # run the queries for this schedule + my $queries = run_queries($args); + + # If mailifnobugs is false, make sure there is something to output + if (!$event->{'mailifnobugs'}) { + my $there_are_bugs = 0; + for my $query (@{$queries}) { + $there_are_bugs = 1 if scalar @{$query->{'bugs'}}; + } + next unless $there_are_bugs; + } - $args->{'queries'} = $queries; + $args->{'queries'} = $queries; - mail($args); - } - $dbh = Bugzilla->switch_to_main_db(); + mail($args); + } + $dbh = Bugzilla->switch_to_main_db(); } ################################################################################ @@ -350,46 +358,39 @@ while (my $event = get_next_event) { # - boundary a MIME boundary generated using the process id and time # sub mail { - my $args = shift; - my $addressee = $args->{recipient}; - # Don't send mail to someone whose bugmail notification is disabled. - return if $addressee->email_disabled; + my $args = shift; + my $addressee = $args->{recipient}; + + # Don't send mail to someone whose bugmail notification is disabled. + return if $addressee->email_disabled; - my $template = Bugzilla->template_inner($addressee->setting('lang')); - my $msg = ''; # it's a temporary variable to hold the template output - $args->{'alternatives'} ||= []; + my $template = Bugzilla->template_inner($addressee->setting('lang')); + my $msg = ''; # it's a temporary variable to hold the template output + $args->{'alternatives'} ||= []; - # put together the different multipart mime segments + # put together the different multipart mime segments - $template->process("whine/mail.txt.tmpl", $args, \$msg) - or die($template->error()); - push @{$args->{'alternatives'}}, - { - 'content' => $msg, - 'type' => 'text/plain', - }; - $msg = ''; + $template->process("whine/mail.txt.tmpl", $args, \$msg) + or die($template->error()); + push @{$args->{'alternatives'}}, {'content' => $msg, 'type' => 'text/plain',}; + $msg = ''; - $template->process("whine/mail.html.tmpl", $args, \$msg) - or die($template->error()); - push @{$args->{'alternatives'}}, - { - 'content' => $msg, - 'type' => 'text/html', - }; - $msg = ''; + $template->process("whine/mail.html.tmpl", $args, \$msg) + or die($template->error()); + push @{$args->{'alternatives'}}, {'content' => $msg, 'type' => 'text/html',}; + $msg = ''; - # now produce a ready-to-mail mime-encoded message + # now produce a ready-to-mail mime-encoded message - $args->{'boundary'} = "----------" . $$ . "--" . time() . "-----"; + $args->{'boundary'} = "----------" . $$ . "--" . time() . "-----"; - $template->process("whine/multipart-mime.txt.tmpl", $args, \$msg) - or die($template->error()); + $template->process("whine/multipart-mime.txt.tmpl", $args, \$msg) + or die($template->error()); - MessageToMTA($msg); + MessageToMTA($msg); - delete $args->{'boundary'}; - delete $args->{'alternatives'}; + delete $args->{'boundary'}; + delete $args->{'alternatives'}; } @@ -397,87 +398,90 @@ sub mail { # the results to $args or mailing off the template if a query wants individual # messages for each bug sub run_queries { - my $args = shift; - - my $return_queries = []; - - $sth_run_queries->execute($args->{'eventid'}); - my @queries = (); - for (@{$sth_run_queries->fetchall_arrayref}) { - push(@queries, - { - 'name' => $_->[0], - 'title' => $_->[1], - 'onemailperbug' => $_->[2], - 'bugs' => [], - } - ); + my $args = shift; + + my $return_queries = []; + + $sth_run_queries->execute($args->{'eventid'}); + my @queries = (); + for (@{$sth_run_queries->fetchall_arrayref}) { + push( + @queries, + { + 'name' => $_->[0], + 'title' => $_->[1], + 'onemailperbug' => $_->[2], + 'bugs' => [], + } + ); + } + + foreach my $thisquery (@queries) { + next unless $thisquery->{'name'}; # named query is blank + + my $savedquery = get_query($thisquery->{'name'}, $args->{'author'}); + next unless $savedquery; # silently ignore missing queries + + # Execute the saved query + my @searchfields = qw( + bug_id + bug_severity + priority + rep_platform + assigned_to + bug_status + resolution + short_desc + ); + + # A new Bugzilla::CGI object needs to be created to allow + # Bugzilla::Search to execute a saved query. It's exceedingly weird, + # but that's how it works. + my $searchparams = new Bugzilla::CGI($savedquery); + my $search = new Bugzilla::Search( + 'fields' => \@searchfields, + 'params' => scalar $searchparams->Vars, + 'user' => $args->{'recipient'}, # the search runs as the recipient + ); + + # If a query fails for whatever reason, it shouldn't kill the script. + my $data = eval { $search->data }; + if ($@) { + print STDERR get_text('whine_query_failed', + {query_name => $thisquery->{'name'}, author => $args->{'author'}, reason => $@}) + . "\n"; + next; } - foreach my $thisquery (@queries) { - next unless $thisquery->{'name'}; # named query is blank - - my $savedquery = get_query($thisquery->{'name'}, $args->{'author'}); - next unless $savedquery; # silently ignore missing queries - - # Execute the saved query - my @searchfields = qw( - bug_id - bug_severity - priority - rep_platform - assigned_to - bug_status - resolution - short_desc - ); - # A new Bugzilla::CGI object needs to be created to allow - # Bugzilla::Search to execute a saved query. It's exceedingly weird, - # but that's how it works. - my $searchparams = new Bugzilla::CGI($savedquery); - my $search = new Bugzilla::Search( - 'fields' => \@searchfields, - 'params' => scalar $searchparams->Vars, - 'user' => $args->{'recipient'}, # the search runs as the recipient - ); - # If a query fails for whatever reason, it shouldn't kill the script. - my $data = eval { $search->data }; - if ($@) { - print STDERR get_text('whine_query_failed', { query_name => $thisquery->{'name'}, - author => $args->{'author'}, - reason => $@ }) . "\n"; - next; - } - - foreach my $row (@$data) { - my $bug = {}; - for my $field (@searchfields) { - my $fieldname = $field; - $fieldname =~ s/^bugs\.//; # No need for bugs.whatever - $bug->{$fieldname} = shift @$row; - } - - if ($thisquery->{'onemailperbug'}) { - $args->{'queries'} = [ - { - 'name' => $thisquery->{'name'}, - 'title' => $thisquery->{'title'}, - 'bugs' => [ $bug ], - }, - ]; - mail($args); - delete $args->{'queries'}; - } - else { # It belongs in one message with any other lists - push @{$thisquery->{'bugs'}}, $bug; - } - } - if (!$thisquery->{'onemailperbug'} && @{$thisquery->{'bugs'}}) { - push @{$return_queries}, $thisquery; - } + foreach my $row (@$data) { + my $bug = {}; + for my $field (@searchfields) { + my $fieldname = $field; + $fieldname =~ s/^bugs\.//; # No need for bugs.whatever + $bug->{$fieldname} = shift @$row; + } + + if ($thisquery->{'onemailperbug'}) { + $args->{'queries'} = [ + { + 'name' => $thisquery->{'name'}, + 'title' => $thisquery->{'title'}, + 'bugs' => [$bug], + }, + ]; + mail($args); + delete $args->{'queries'}; + } + else { # It belongs in one message with any other lists + push @{$thisquery->{'bugs'}}, $bug; + } + } + if (!$thisquery->{'onemailperbug'} && @{$thisquery->{'bugs'}}) { + push @{$return_queries}, $thisquery; } + } - return $return_queries; + return $return_queries; } # get_query gets the namedquery. It's similar to LookupNamedQuery (in @@ -485,12 +489,12 @@ sub run_queries { # individual named queries might go away without the whine_queries that point # to them being removed. sub get_query { - my ($name, $user) = @_; - my $qname = $name; - $sth_get_query->execute($user->id, $qname); - my $fetched = $sth_get_query->fetch; - $sth_get_query->finish; - return $fetched ? $fetched->[0] : ''; + my ($name, $user) = @_; + my $qname = $name; + $sth_get_query->execute($user->id, $qname); + my $fetched = $sth_get_query->fetch; + $sth_get_query->finish; + return $fetched ? $fetched->[0] : ''; } # check_today gets a run day from the schedule and sees if it matches today @@ -502,25 +506,23 @@ sub get_query { # - 'MF' for every weekday sub check_today { - my $run_day = shift; - - if (($run_day eq 'MF') - && ($now_weekday > 0) - && ($now_weekday < 6)) { - return 1; - } - elsif ( - length($run_day) == 3 && - index("SunMonTueWedThuFriSat", $run_day)/3 == $now_weekday) { - return 1; - } - elsif (($run_day eq 'All') - || (($run_day eq 'last') && - ($now_day == $daysinmonth[$now_month] )) - || ($run_day eq $now_day)) { - return 1; - } - return 0; + my $run_day = shift; + + if (($run_day eq 'MF') && ($now_weekday > 0) && ($now_weekday < 6)) { + return 1; + } + elsif (length($run_day) == 3 + && index("SunMonTueWedThuFriSat", $run_day) / 3 == $now_weekday) + { + return 1; + } + elsif (($run_day eq 'All') + || (($run_day eq 'last') && ($now_day == $daysinmonth[$now_month])) + || ($run_day eq $now_day)) + { + return 1; + } + return 0; } # reset_timer sets the next time a whine is supposed to run, assuming it just @@ -529,95 +531,101 @@ sub check_today { # reset_timer does not lock the whine_schedules table. Anything that calls it # should do that itself. sub reset_timer { - my $schedule_id = shift; - - # Schedules may not be executed more than once for each invocation of - # whine.pl -- there are legitimate circumstances that can cause this, like - # a set of whines that take a very long time to execute, so it's done - # quietly. - if (grep($_ == $schedule_id, @seen_schedules)) { - null_schedule($schedule_id); - return; - } - push @seen_schedules, $schedule_id; - - $sth = $dbh->prepare( "SELECT run_day, run_time FROM whine_schedules " . - "WHERE id=?" ); - $sth->execute($schedule_id); - my ($run_day, $run_time) = $sth->fetchrow_array; - - # It may happen that the run_time field is NULL or blank due to - # a bug in editwhines.cgi when this field was initially 0. - $run_time ||= 0; - - my $run_today = 0; - my $minute_offset = 0; - - # If the schedule is to run today, and it runs many times per day, - # it shall be set to run immediately. - $run_today = &check_today($run_day); - if (($run_today) && ($run_time !~ /^\d+$/)) { - # The default of 60 catches any bad value - my $minute_interval = 60; - if ($run_time =~ /^(\d+)min$/i) { - $minute_interval = $1; - } - - # set the minute offset to the next interval point - $minute_offset = $minute_interval - ($now_minute % $minute_interval); - } - elsif (($run_today) && ($run_time > $now_hour)) { - # timed event for later today - # (This should only happen if, for example, an 11pm scheduled event - # didn't happen until after midnight) - $minute_offset = (60 * ($run_time - $now_hour)) - $now_minute; - } - else { - # it's not something that runs later today. - $minute_offset = 0; - - # Set the target time if it's a specific hour - my $target_time = ($run_time =~ /^\d+$/) ? $run_time : 0; - - my $nextdate = &get_next_date($run_day); - my $run_next = $dbh->sql_date_math('(' - . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') - . ')', '+', '?', 'HOUR'); - $sth = $dbh->prepare("UPDATE whine_schedules " . - "SET run_next = $run_next - WHERE id = ?"); - $sth->execute($nextdate, $target_time, $schedule_id); - return; + my $schedule_id = shift; + + # Schedules may not be executed more than once for each invocation of + # whine.pl -- there are legitimate circumstances that can cause this, like + # a set of whines that take a very long time to execute, so it's done + # quietly. + if (grep($_ == $schedule_id, @seen_schedules)) { + null_schedule($schedule_id); + return; + } + push @seen_schedules, $schedule_id; + + $sth = $dbh->prepare( + "SELECT run_day, run_time FROM whine_schedules " . "WHERE id=?"); + $sth->execute($schedule_id); + my ($run_day, $run_time) = $sth->fetchrow_array; + + # It may happen that the run_time field is NULL or blank due to + # a bug in editwhines.cgi when this field was initially 0. + $run_time ||= 0; + + my $run_today = 0; + my $minute_offset = 0; + + # If the schedule is to run today, and it runs many times per day, + # it shall be set to run immediately. + $run_today = &check_today($run_day); + if (($run_today) && ($run_time !~ /^\d+$/)) { + + # The default of 60 catches any bad value + my $minute_interval = 60; + if ($run_time =~ /^(\d+)min$/i) { + $minute_interval = $1; } - if ($minute_offset > 0) { - # Scheduling is done in terms of whole minutes. - - my $next_run = $dbh->selectrow_array( - 'SELECT ' . $dbh->sql_date_math('NOW()', '+', '?', 'MINUTE'), - undef, $minute_offset); - $next_run = format_time($next_run, "%Y-%m-%d %R", "UTC"); - - $sth = $dbh->prepare("UPDATE whine_schedules " . - "SET run_next = ? WHERE id = ?"); - $sth->execute($next_run, $schedule_id); - } else { - # The minute offset is zero or less, which is not supposed to happen. - # complain to STDERR - null_schedule($schedule_id); - print STDERR "Error: bad minute_offset for schedule ID $schedule_id\n"; - } + # set the minute offset to the next interval point + $minute_offset = $minute_interval - ($now_minute % $minute_interval); + } + elsif (($run_today) && ($run_time > $now_hour)) { + + # timed event for later today + # (This should only happen if, for example, an 11pm scheduled event + # didn't happen until after midnight) + $minute_offset = (60 * ($run_time - $now_hour)) - $now_minute; + } + else { + # it's not something that runs later today. + $minute_offset = 0; + + # Set the target time if it's a specific hour + my $target_time = ($run_time =~ /^\d+$/) ? $run_time : 0; + + my $nextdate = &get_next_date($run_day); + my $run_next + = $dbh->sql_date_math( + '(' . $dbh->sql_date_math('CURRENT_DATE', '+', '?', 'DAY') . ')', + '+', '?', 'HOUR'); + $sth = $dbh->prepare( + "UPDATE whine_schedules " . "SET run_next = $run_next + WHERE id = ?" + ); + $sth->execute($nextdate, $target_time, $schedule_id); + return; + } + + if ($minute_offset > 0) { + + # Scheduling is done in terms of whole minutes. + + my $next_run + = $dbh->selectrow_array( + 'SELECT ' . $dbh->sql_date_math('NOW()', '+', '?', 'MINUTE'), + undef, $minute_offset); + $next_run = format_time($next_run, "%Y-%m-%d %R", "UTC"); + + $sth + = $dbh->prepare("UPDATE whine_schedules " . "SET run_next = ? WHERE id = ?"); + $sth->execute($next_run, $schedule_id); + } + else { + # The minute offset is zero or less, which is not supposed to happen. + # complain to STDERR + null_schedule($schedule_id); + print STDERR "Error: bad minute_offset for schedule ID $schedule_id\n"; + } } # null_schedule is used to safeguard against infinite loops. Schedules with # run_next set to NULL will not be available to get_next_event until they are # rescheduled, which only happens when whine.pl starts. sub null_schedule { - my $schedule_id = shift; - $sth = $dbh->prepare("UPDATE whine_schedules " . - "SET run_next = NULL " . - "WHERE id=?"); - $sth->execute($schedule_id); + my $schedule_id = shift; + $sth = $dbh->prepare( + "UPDATE whine_schedules " . "SET run_next = NULL " . "WHERE id=?"); + $sth->execute($schedule_id); } # get_next_date determines the difference in days between now and the next @@ -626,56 +634,58 @@ sub null_schedule { # It takes a run_day argument (see check_today, above, for an explanation), # and returns an integer, representing a number of days. sub get_next_date { - my $day = shift; + my $day = shift; - my $add_days = 0; + my $add_days = 0; - if ($day eq 'All') { - $add_days = 1; + if ($day eq 'All') { + $add_days = 1; + } + elsif ($day eq 'last') { + + # next_date should contain the last day of this month, or next month + # if it's today + if ($daysinmonth[$now_month] == $now_day) { + my $month = $now_month + 1; + $month = 1 if $month > 12; + $add_days = $daysinmonth[$month] + 1; } - elsif ($day eq 'last') { - # next_date should contain the last day of this month, or next month - # if it's today - if ($daysinmonth[$now_month] == $now_day) { - my $month = $now_month + 1; - $month = 1 if $month > 12; - $add_days = $daysinmonth[$month] + 1; - } - else { - $add_days = $daysinmonth[$now_month] - $now_day; - } + else { + $add_days = $daysinmonth[$now_month] - $now_day; } - elsif ($day eq 'MF') { # any day Monday through Friday - if ($now_weekday < 5) { # Sun-Thurs - $add_days = 1; - } - elsif ($now_weekday == 5) { # Friday - $add_days = 3; - } - else { # it's 6, Saturday - $add_days = 2; - } + } + elsif ($day eq 'MF') { # any day Monday through Friday + if ($now_weekday < 5) { # Sun-Thurs + $add_days = 1; + } + elsif ($now_weekday == 5) { # Friday + $add_days = 3; } - elsif ($day !~ /^\d+$/) { # A specific day of the week + else { # it's 6, Saturday + $add_days = 2; + } + } + elsif ($day !~ /^\d+$/) { # A specific day of the week # The default is used if there is a bad value in the database, in # which case we mark it to a less-popular day (Sunday) - my $day_num = 0; + my $day_num = 0; - if (length($day) == 3) { - $day_num = (index("SunMonTueWedThuFriSat", $day)/3) or 0; - } + if (length($day) == 3) { + $day_num = (index("SunMonTueWedThuFriSat", $day) / 3) or 0; + } - $add_days = $day_num - $now_weekday; - if ($add_days <= 0) { # it's next week - $add_days += 7; - } + $add_days = $day_num - $now_weekday; + if ($add_days <= 0) { # it's next week + $add_days += 7; } - else { # it's a number, so we set it for that calendar day - $add_days = $day - $now_day; - # If it's already beyond that day this month, set it to the next one - if ($add_days <= 0) { - $add_days += $daysinmonth[$now_month]; - } + } + else { # it's a number, so we set it for that calendar day + $add_days = $day - $now_day; + + # If it's already beyond that day this month, set it to the next one + if ($add_days <= 0) { + $add_days += $daysinmonth[$now_month]; } - return $add_days; + } + return $add_days; } diff --git a/whineatnews.pl b/whineatnews.pl index 6f8643855..e79d3cf19 100755 --- a/whineatnews.pl +++ b/whineatnews.pl @@ -28,58 +28,59 @@ use Bugzilla::User; # Whining is disabled if whinedays is zero exit unless Bugzilla->params->{'whinedays'} >= 1; -my $dbh = Bugzilla->dbh; +my $dbh = Bugzilla->dbh; my $query = q{SELECT bug_id, short_desc, login_name FROM bugs INNER JOIN profiles ON userid = assigned_to WHERE bug_status IN (?,?,?) AND disable_mail = 0 - AND } . $dbh->sql_to_days('NOW()') . " - " . - $dbh->sql_to_days('delta_ts') . " > " . - Bugzilla->params->{'whinedays'} . - " ORDER BY bug_id"; + AND } + . $dbh->sql_to_days('NOW()') . " - " + . $dbh->sql_to_days('delta_ts') . " > " + . Bugzilla->params->{'whinedays'} + . " ORDER BY bug_id"; my %bugs; my %desc; -my $slt_bugs = $dbh->selectall_arrayref($query, undef, 'CONFIRMED', 'NEW', - 'REOPENED'); +my $slt_bugs + = $dbh->selectall_arrayref($query, undef, 'CONFIRMED', 'NEW', 'REOPENED'); foreach my $bug (@$slt_bugs) { - my ($id, $desc, $email) = @$bug; - if (!defined $bugs{$email}) { - $bugs{$email} = []; - } - if (!defined $desc{$email}) { - $desc{$email} = []; - } - push @{$bugs{$email}}, $id; - push @{$desc{$email}}, $desc; + my ($id, $desc, $email) = @$bug; + if (!defined $bugs{$email}) { + $bugs{$email} = []; + } + if (!defined $desc{$email}) { + $desc{$email} = []; + } + push @{$bugs{$email}}, $id; + push @{$desc{$email}}, $desc; } foreach my $email (sort (keys %bugs)) { - my $user = new Bugzilla::User({name => $email}); - next if $user->email_disabled; + my $user = new Bugzilla::User({name => $email}); + next if $user->email_disabled; - my $vars = {'email' => $email}; + my $vars = {'email' => $email}; - my @bugs = (); - foreach my $i (@{$bugs{$email}}) { - my $bug = {}; - $bug->{'summary'} = shift(@{$desc{$email}}); - $bug->{'id'} = $i; - push @bugs, $bug; - } - $vars->{'bugs'} = \@bugs; + my @bugs = (); + foreach my $i (@{$bugs{$email}}) { + my $bug = {}; + $bug->{'summary'} = shift(@{$desc{$email}}); + $bug->{'id'} = $i; + push @bugs, $bug; + } + $vars->{'bugs'} = \@bugs; - my $msg; - my $template = Bugzilla->template_inner($user->setting('lang')); - $template->process("email/whine.txt.tmpl", $vars, \$msg) - or die($template->error()); + my $msg; + my $template = Bugzilla->template_inner($user->setting('lang')); + $template->process("email/whine.txt.tmpl", $vars, \$msg) + or die($template->error()); - MessageToMTA($msg); + MessageToMTA($msg); - print "$email " . join(" ", @{$bugs{$email}}) . "\n"; + print "$email " . join(" ", @{$bugs{$email}}) . "\n"; } diff --git a/xml.cgi b/xml.cgi index 1b5fac9fa..4676383f8 100755 --- a/xml.cgi +++ b/xml.cgi @@ -35,7 +35,7 @@ my $cgi = Bugzilla->cgi; my @ids = (); if (defined $cgi->param('id')) { - @ids = split (/[, ]+/, $cgi->param('id')); + @ids = split(/[, ]+/, $cgi->param('id')); } my $ids = join('', map { $_ = "&id=" . $_ } @ids); diff --git a/xt/lib/Bugzilla/Test/Search.pm b/xt/lib/Bugzilla/Test/Search.pm index 2666455e8..b1651916e 100644 --- a/xt/lib/Bugzilla/Test/Search.pm +++ b/xt/lib/Bugzilla/Test/Search.pm @@ -73,8 +73,8 @@ use Scalar::Util qw(blessed); ############### sub new { - my ($class, $options) = @_; - return bless { options => $options }, $class; + my ($class, $options) = @_; + return bless {options => $options}, $class; } ############# @@ -82,198 +82,212 @@ sub new { ############# sub options { return $_[0]->{options} } -sub option { return $_[0]->{options}->{$_[1]} } +sub option { return $_[0]->{options}->{$_[1]} } sub num_tests { - my ($self) = @_; - my @top_operators = $self->top_level_operators; - my @all_operators = $self->all_operators; - my $top_operator_tests = $self->_total_operator_tests(\@top_operators); - my $all_operator_tests = $self->_total_operator_tests(\@all_operators); - - my @fields = $self->all_fields; - - # Basically, we run TESTS_PER_RUN tests for each field/operator combination. - my $top_combinations = $top_operator_tests * scalar(@fields); - my $all_combinations = $all_operator_tests * scalar(@fields); - # But we also have ORs, for which we run combinations^2 tests. - my $join_tests = $self->option('long') - ? ($top_combinations * $all_combinations) : 0; - # And AND tests, which means we run 2x $join_tests; - $join_tests = $join_tests * 2; - # Also, because of NOT tests and Normal tests, we run 3x $top_combinations. - my $basic_tests = $top_combinations * 3; - my $operator_field_tests = ($basic_tests + $join_tests) * TESTS_PER_RUN; - - # Then we test each field/operator combination for SQL injection. - my @injection_values = INJECTION_TESTS; - my $sql_injection_tests = scalar(@fields) * scalar(@top_operators) - * scalar(@injection_values) * NUM_SEARCH_TESTS; - - # This @{ [] } thing is the only reasonable way to get a count out of a - # constant array. - my $special_tests = scalar(@{ [SPECIAL_PARAM_TESTS, CUSTOM_SEARCH_TESTS] }) - * TESTS_PER_RUN; - - return $operator_field_tests + $sql_injection_tests + $special_tests; + my ($self) = @_; + my @top_operators = $self->top_level_operators; + my @all_operators = $self->all_operators; + my $top_operator_tests = $self->_total_operator_tests(\@top_operators); + my $all_operator_tests = $self->_total_operator_tests(\@all_operators); + + my @fields = $self->all_fields; + + # Basically, we run TESTS_PER_RUN tests for each field/operator combination. + my $top_combinations = $top_operator_tests * scalar(@fields); + my $all_combinations = $all_operator_tests * scalar(@fields); + + # But we also have ORs, for which we run combinations^2 tests. + my $join_tests + = $self->option('long') ? ($top_combinations * $all_combinations) : 0; + + # And AND tests, which means we run 2x $join_tests; + $join_tests = $join_tests * 2; + + # Also, because of NOT tests and Normal tests, we run 3x $top_combinations. + my $basic_tests = $top_combinations * 3; + my $operator_field_tests = ($basic_tests + $join_tests) * TESTS_PER_RUN; + + # Then we test each field/operator combination for SQL injection. + my @injection_values = INJECTION_TESTS; + my $sql_injection_tests + = scalar(@fields) + * scalar(@top_operators) + * scalar(@injection_values) + * NUM_SEARCH_TESTS; + + # This @{ [] } thing is the only reasonable way to get a count out of a + # constant array. + my $special_tests + = scalar(@{[SPECIAL_PARAM_TESTS, CUSTOM_SEARCH_TESTS]}) * TESTS_PER_RUN; + + return $operator_field_tests + $sql_injection_tests + $special_tests; } sub _total_operator_tests { - my ($self, $operators) = @_; - - # Some operators have more than one test. Find those ones and add - # them to the total operator tests - my $extra_operator_tests; - foreach my $operator (@$operators) { - my $tests = TESTS->{$operator}; - next if !$tests; - my $extra_num = scalar(@$tests) - 1; - $extra_operator_tests += $extra_num; - } - return scalar(@$operators) + $extra_operator_tests; + my ($self, $operators) = @_; + + # Some operators have more than one test. Find those ones and add + # them to the total operator tests + my $extra_operator_tests; + foreach my $operator (@$operators) { + my $tests = TESTS->{$operator}; + next if !$tests; + my $extra_num = scalar(@$tests) - 1; + $extra_operator_tests += $extra_num; + } + return scalar(@$operators) + $extra_operator_tests; } sub all_operators { - my ($self) = @_; - if (not $self->{all_operators}) { - - my @operators; - if (my $limit_operators = $self->option('operators')) { - @operators = split(',', $limit_operators); - } - else { - @operators = sort (keys %{ Bugzilla::Search::OPERATORS() }); - } - # "substr" is just a backwards-compatibility operator, same as "substring". - @operators = grep { $_ ne 'substr' } @operators; - $self->{all_operators} = \@operators; + my ($self) = @_; + if (not $self->{all_operators}) { + + my @operators; + if (my $limit_operators = $self->option('operators')) { + @operators = split(',', $limit_operators); + } + else { + @operators = sort (keys %{Bugzilla::Search::OPERATORS()}); } - return @{ $self->{all_operators} }; + + # "substr" is just a backwards-compatibility operator, same as "substring". + @operators = grep { $_ ne 'substr' } @operators; + $self->{all_operators} = \@operators; + } + return @{$self->{all_operators}}; } sub all_fields { - my $self = shift; - if (not $self->{all_fields}) { - $self->_create_custom_fields(); - my @fields = @{ Bugzilla->fields }; - @fields = sort { $a->name cmp $b->name } @fields; - $self->{all_fields} = \@fields; - } - return @{ $self->{all_fields} }; + my $self = shift; + if (not $self->{all_fields}) { + $self->_create_custom_fields(); + my @fields = @{Bugzilla->fields}; + @fields = sort { $a->name cmp $b->name } @fields; + $self->{all_fields} = \@fields; + } + return @{$self->{all_fields}}; } sub top_level_operators { - my ($self) = @_; - if (!$self->{top_level_operators}) { - my @operators; - my $limit_top = $self->option('top-operators'); - if ($limit_top) { - @operators = split(',', $limit_top); - } - else { - @operators = $self->all_operators; - } - $self->{top_level_operators} = \@operators; + my ($self) = @_; + if (!$self->{top_level_operators}) { + my @operators; + my $limit_top = $self->option('top-operators'); + if ($limit_top) { + @operators = split(',', $limit_top); } - return @{ $self->{top_level_operators} }; + else { + @operators = $self->all_operators; + } + $self->{top_level_operators} = \@operators; + } + return @{$self->{top_level_operators}}; } sub text_fields { - my ($self) = @_; - my @text_fields = grep { $_->type == FIELD_TYPE_TEXTAREA - or $_->type == FIELD_TYPE_FREETEXT } $self->all_fields; - @text_fields = map { $_->name } @text_fields; - push(@text_fields, qw(short_desc status_whiteboard bug_file_loc see_also)); - return @text_fields; + my ($self) = @_; + my @text_fields + = grep { $_->type == FIELD_TYPE_TEXTAREA or $_->type == FIELD_TYPE_FREETEXT } + $self->all_fields; + @text_fields = map { $_->name } @text_fields; + push(@text_fields, qw(short_desc status_whiteboard bug_file_loc see_also)); + return @text_fields; } sub bugs { - my $self = shift; - $self->{bugs} ||= [map { $self->_create_one_bug($_) } (1..NUM_BUGS)]; - return @{ $self->{bugs} }; + my $self = shift; + $self->{bugs} ||= [map { $self->_create_one_bug($_) } (1 .. NUM_BUGS)]; + return @{$self->{bugs}}; } # Get a numbered bug. sub bug { - my ($self, $number) = @_; - return ($self->bugs)[$number - 1]; + my ($self, $number) = @_; + return ($self->bugs)[$number - 1]; } sub admin { - my $self = shift; - if (!$self->{admin_user}) { - my $admin = create_user("admin"); - Bugzilla::Install::make_admin($admin); - $self->{admin_user} = $admin; - } - # We send back a fresh object every time, to make sure that group - # memberships are always up-to-date. - return new Bugzilla::User($self->{admin_user}->id); + my $self = shift; + if (!$self->{admin_user}) { + my $admin = create_user("admin"); + Bugzilla::Install::make_admin($admin); + $self->{admin_user} = $admin; + } + + # We send back a fresh object every time, to make sure that group + # memberships are always up-to-date. + return new Bugzilla::User($self->{admin_user}->id); } sub nobody { - my $self = shift; - $self->{nobody} ||= Bugzilla::Group->create({ name => "nobody-" . random(), - description => "Nobody", isbuggroup => 1 }); - return $self->{nobody}; + my $self = shift; + $self->{nobody} + ||= Bugzilla::Group->create({ + name => "nobody-" . random(), description => "Nobody", isbuggroup => 1 + }); + return $self->{nobody}; } + sub everybody { - my ($self) = @_; - $self->{everybody} ||= create_group('To The Limit'); - return $self->{everybody}; + my ($self) = @_; + $self->{everybody} ||= create_group('To The Limit'); + return $self->{everybody}; } sub bug_create_value { - my ($self, $number, $field) = @_; - $field = $field->name if blessed($field); - if ($number == 6 and $field ne 'alias') { - $number = 1; - } - my $extra_values = $self->_extra_bug_create_values->{$number}; - if (exists $extra_values->{$field}) { - return $extra_values->{$field}; - } - return $self->_bug_create_values->{$number}->{$field}; + my ($self, $number, $field) = @_; + $field = $field->name if blessed($field); + if ($number == 6 and $field ne 'alias') { + $number = 1; + } + my $extra_values = $self->_extra_bug_create_values->{$number}; + if (exists $extra_values->{$field}) { + return $extra_values->{$field}; + } + return $self->_bug_create_values->{$number}->{$field}; } + sub bug_update_value { - my ($self, $number, $field) = @_; - $field = $field->name if blessed($field); - if ($number == 6 and $field ne 'alias') { - $number = 1; - } - return $self->_bug_update_values->{$number}->{$field}; + my ($self, $number, $field) = @_; + $field = $field->name if blessed($field); + if ($number == 6 and $field ne 'alias') { + $number = 1; + } + return $self->_bug_update_values->{$number}->{$field}; } # Values used to create the bugs. sub _bug_create_values { - my $self = shift; - return $self->{bug_create_values} if $self->{bug_create_values}; - my %values; - foreach my $number (1..NUM_BUGS) { - $values{$number} = $self->_create_field_values($number, 'for create'); - } - $self->{bug_create_values} = \%values; - return $self->{bug_create_values}; + my $self = shift; + return $self->{bug_create_values} if $self->{bug_create_values}; + my %values; + foreach my $number (1 .. NUM_BUGS) { + $values{$number} = $self->_create_field_values($number, 'for create'); + } + $self->{bug_create_values} = \%values; + return $self->{bug_create_values}; } + # Values as they existed on the bug, at creation time. Used by the # changedfrom tests. sub _extra_bug_create_values { - my $self = shift; - $self->{extra_bug_create_values} ||= { map { $_ => {} } (1..NUM_BUGS) }; - return $self->{extra_bug_create_values}; + my $self = shift; + $self->{extra_bug_create_values} ||= {map { $_ => {} } (1 .. NUM_BUGS)}; + return $self->{extra_bug_create_values}; } # Values used to update the bugs after they are created. sub _bug_update_values { - my $self = shift; - return $self->{bug_update_values} if $self->{bug_update_values}; - my %values; - foreach my $number (1..NUM_BUGS) { - $values{$number} = $self->_create_field_values($number); - } - $self->{bug_update_values} = \%values; - return $self->{bug_update_values}; + my $self = shift; + return $self->{bug_update_values} if $self->{bug_update_values}; + my %values; + foreach my $number (1 .. NUM_BUGS) { + $values{$number} = $self->_create_field_values($number); + } + $self->{bug_update_values} = \%values; + return $self->{bug_update_values}; } ############################## @@ -281,8 +295,8 @@ sub _bug_update_values { ############################## sub random { - $_[0] ||= FIELD_SIZE; - generate_random_password(@_); + $_[0] ||= FIELD_SIZE; + generate_random_password(@_); } # We need to use a custom timestamp for each create() and update(), @@ -290,53 +304,55 @@ sub random { # for the entire transaction, and we need each created bug to have # its own creation_ts and delta_ts. sub timestamp { - my ($day, $second) = @_; - return DateTime->new( - year => 2037, - month => 1, - day => $day, - hour => 12, - minute => $second, - second => 0, - # We make it floating because the timezone doesn't matter for our uses, - # and we want totally consistent behavior across all possible machines. - time_zone => 'floating', - ); + my ($day, $second) = @_; + return DateTime->new( + year => 2037, + month => 1, + day => $day, + hour => 12, + minute => $second, + second => 0, + + # We make it floating because the timezone doesn't matter for our uses, + # and we want totally consistent behavior across all possible machines. + time_zone => 'floating', + ); } sub create_keyword { - my ($number) = @_; - return Bugzilla::Keyword->create({ - name => "$number-keyword-" . random(), - description => "Keyword $number" }); + my ($number) = @_; + return Bugzilla::Keyword->create({ + name => "$number-keyword-" . random(), description => "Keyword $number" + }); } sub create_user { - my ($prefix) = @_; - my $user_name = $prefix . '-' . random(15) . "@" . random(12) - . "." . random(3); - my $user_realname = $prefix . '-' . random(); - my $user = Bugzilla::User->create({ - login_name => $user_name, - realname => $user_realname, - cryptpassword => '*', + my ($prefix) = @_; + my $user_name = $prefix . '-' . random(15) . "@" . random(12) . "." . random(3); + my $user_realname = $prefix . '-' . random(); + my $user + = Bugzilla::User->create({ + login_name => $user_name, realname => $user_realname, cryptpassword => '*', }); - return $user; + return $user; } sub create_group { - my ($prefix) = @_; - return Bugzilla::Group->create({ - name => "$prefix-group-" . random(), description => "Everybody $prefix", - userregexp => '.*', isbuggroup => 1 }); + my ($prefix) = @_; + return Bugzilla::Group->create({ + name => "$prefix-group-" . random(), + description => "Everybody $prefix", + userregexp => '.*', + isbuggroup => 1 + }); } sub create_legal_value { - my ($field, $number) = @_; - my $type = Bugzilla::Field::Choice->type($field); - my $field_name = $field->name; - return $type->create({ value => "$number-$field_name-" . random(), - is_open => 0 }); + my ($field, $number) = @_; + my $type = Bugzilla::Field::Choice->type($field); + my $field_name = $field->name; + return $type->create({value => "$number-$field_name-" . random(), is_open => 0 + }); } ######################### @@ -344,22 +360,22 @@ sub create_legal_value { ######################### sub _create_custom_fields { - my ($self) = @_; - return if !$self->option('add-custom-fields'); - - while (my ($type, $name) = each %{ CUSTOM_FIELDS() }) { - my $exists = new Bugzilla::Field({ name => $name }); - next if $exists; - Bugzilla::Field->create({ - name => $name, - type => $type, - description => "Search Test Field $name", - enter_bug => 1, - custom => 1, - buglist => 1, - is_mandatory => 0, - }); - } + my ($self) = @_; + return if !$self->option('add-custom-fields'); + + while (my ($type, $name) = each %{CUSTOM_FIELDS()}) { + my $exists = new Bugzilla::Field({name => $name}); + next if $exists; + Bugzilla::Field->create({ + name => $name, + type => $type, + description => "Search Test Field $name", + enter_bug => 1, + custom => 1, + buglist => 1, + is_mandatory => 0, + }); + } } ######################## @@ -367,221 +383,232 @@ sub _create_custom_fields { ######################## sub _create_field_values { - my ($self, $number, $for_create) = @_; - my $dbh = Bugzilla->dbh; - - Bugzilla->set_user($self->admin); - - my @selects = grep { $_->is_select } $self->all_fields; - my %values; - foreach my $field (@selects) { - next if $field->is_abnormal; - $values{$field->name} = create_legal_value($field, $number)->name; + my ($self, $number, $for_create) = @_; + my $dbh = Bugzilla->dbh; + + Bugzilla->set_user($self->admin); + + my @selects = grep { $_->is_select } $self->all_fields; + my %values; + foreach my $field (@selects) { + next if $field->is_abnormal; + $values{$field->name} = create_legal_value($field, $number)->name; + } + + my $group = create_group($number); + $values{groups} = [$group->name]; + + $values{'keywords'} = create_keyword($number)->name; + + foreach my $field (qw(assigned_to qa_contact reporter cc)) { + $values{$field} = create_user("$number-$field")->login; + } + + my $classification = Bugzilla::Classification->create( + {name => "$number-classification-" . random()}); + $classification = $classification->name; + + my $version = "$number-version-" . random(); + my $milestone = "$number-tm-" . random(15); + my $product = Bugzilla::Product->create({ + name => "$number-product-" . random(), + description => 'Created by t/search.t', + defaultmilestone => $milestone, + classification => $classification, + version => $version, + allows_unconfirmed => 1, + }); + foreach my $item ($group, $self->nobody) { + $product->set_group_controls($item, + {membercontrol => CONTROLMAPSHOWN, othercontrol => CONTROLMAPNA}); + } + + # $product->update() is called lower down. + my $component = Bugzilla::Component->create({ + product => $product, + name => "$number-component-" . random(), + initialowner => create_user("$number-defaultowner")->login, + initialqacontact => create_user("$number-defaultqa")->login, + initial_cc => [create_user("$number-initcc")->login], + description => "Component $number" + }); + + $values{'product'} = $product->name; + $values{'component'} = $component->name; + $values{'target_milestone'} = $milestone; + $values{'version'} = $version; + + foreach my $field ($self->text_fields) { + + # We don't add a - after $field for the text fields, because + # if we do, fulltext searching for short_desc pulls out + # "short_desc" as a word and matches it in every bug. + my $value = "$number-$field" . random(); + if ($field eq 'bug_file_loc' or $field eq 'see_also') { + $value = "http://$value-" . random(3) . "/show_bug.cgi?id=$number"; } - - my $group = create_group($number); - $values{groups} = [$group->name]; - - $values{'keywords'} = create_keyword($number)->name; - - foreach my $field (qw(assigned_to qa_contact reporter cc)) { - $values{$field} = create_user("$number-$field")->login; + $values{$field} = $value; + } + $values{'tag'} = ["$number-tag-" . random()]; + + my @date_fields = grep { $_->type == FIELD_TYPE_DATETIME } $self->all_fields; + foreach my $field (@date_fields) { + + # We use 03 as the month because that differs from our creation_ts, + # delta_ts, and deadline. (It's nice to have recognizable values + # for each field when debugging.) + my $second = $for_create ? $number : $number + 1; + $values{$field->name} = "2037-03-0$number 12:34:0$second"; + } + + $values{alias} = "$number-alias-" . random(12); + + # Prefixing the original comment with "description" makes the + # lesserthan and greaterthan tests behave predictably. + my $comm_prefix = $for_create ? "description-" : ''; + $values{comment} = "$comm_prefix$number-comment-" . random() . ' ' . random(); + + my @flags; + my $setter = create_user("$number-setters.login_name"); + my $requestee = create_user("$number-requestees.login_name"); + $values{set_flags} = _create_flags($number, $setter, $requestee); + + my $month = $for_create ? "12" : "02"; + $values{'deadline'} = "2037-$month-0$number"; + my $estimate_times = $for_create ? 10 : 1; + $values{estimated_time} = $estimate_times * $number; + + $values{attachment} = _get_attach_values($number, $for_create); + + # Some things only happen on the first bug. + if ($number == 1) { + + # We use 6 as the prefix for the extra values, because bug 6's values + # don't otherwise get used (since bug 6 is created as a clone of + # bug 1). This also makes sure that our greaterthan/lessthan + # tests work properly. + my $extra_group = create_group(6); + $product->set_group_controls($extra_group, + {membercontrol => CONTROLMAPSHOWN, othercontrol => CONTROLMAPNA}); + $values{groups} = [$values{groups}->[0], $extra_group->name]; + my $extra_keyword = create_keyword(6); + $values{keywords} = [$values{keywords}, $extra_keyword->name]; + my $extra_cc = create_user("6-cc"); + $values{cc} = [$values{cc}, $extra_cc->login]; + my @multi_selects + = grep { $_->type == FIELD_TYPE_MULTI_SELECT } $self->all_fields; + + foreach my $field (@multi_selects) { + my $new_value = create_legal_value($field, 6); + my $name = $field->name; + $values{$name} = [$values{$name}, $new_value->name]; } - - my $classification = Bugzilla::Classification->create( - { name => "$number-classification-" . random() }); - $classification = $classification->name; - - my $version = "$number-version-" . random(); - my $milestone = "$number-tm-" . random(15); - my $product = Bugzilla::Product->create({ - name => "$number-product-" . random(), - description => 'Created by t/search.t', - defaultmilestone => $milestone, - classification => $classification, - version => $version, - allows_unconfirmed => 1, - }); - foreach my $item ($group, $self->nobody) { - $product->set_group_controls($item, - { membercontrol => CONTROLMAPSHOWN, - othercontrol => CONTROLMAPNA }); - } - # $product->update() is called lower down. - my $component = Bugzilla::Component->create({ - product => $product, name => "$number-component-" . random(), - initialowner => create_user("$number-defaultowner")->login, - initialqacontact => create_user("$number-defaultqa")->login, - initial_cc => [create_user("$number-initcc")->login], - description => "Component $number" }); - - $values{'product'} = $product->name; - $values{'component'} = $component->name; - $values{'target_milestone'} = $milestone; - $values{'version'} = $version; - - foreach my $field ($self->text_fields) { - # We don't add a - after $field for the text fields, because - # if we do, fulltext searching for short_desc pulls out - # "short_desc" as a word and matches it in every bug. - my $value = "$number-$field" . random(); - if ($field eq 'bug_file_loc' or $field eq 'see_also') { - $value = "http://$value-" . random(3) - . "/show_bug.cgi?id=$number"; - } - $values{$field} = $value; + push(@{$values{'tag'}}, "6-tag-" . random()); + } + + # On bug 5, any field that *can* be left empty, *is* left empty. + if ($number == 5) { + my @set_fields + = grep { $_->type == FIELD_TYPE_SINGLE_SELECT } $self->all_fields; + @set_fields = map { $_->name } @set_fields; + push(@set_fields, qw(short_desc version reporter)); + foreach my $key (keys %values) { + delete $values{$key} unless grep { $_ eq $key } @set_fields; } - $values{'tag'} = ["$number-tag-" . random()]; - - my @date_fields = grep { $_->type == FIELD_TYPE_DATETIME } $self->all_fields; - foreach my $field (@date_fields) { - # We use 03 as the month because that differs from our creation_ts, - # delta_ts, and deadline. (It's nice to have recognizable values - # for each field when debugging.) - my $second = $for_create ? $number : $number + 1; - $values{$field->name} = "2037-03-0$number 12:34:0$second"; - } - - $values{alias} = "$number-alias-" . random(12); + } - # Prefixing the original comment with "description" makes the - # lesserthan and greaterthan tests behave predictably. - my $comm_prefix = $for_create ? "description-" : ''; - $values{comment} = "$comm_prefix$number-comment-" . random() - . ' ' . random(); + $product->update(); - my @flags; - my $setter = create_user("$number-setters.login_name"); - my $requestee = create_user("$number-requestees.login_name"); - $values{set_flags} = _create_flags($number, $setter, $requestee); - - my $month = $for_create ? "12" : "02"; - $values{'deadline'} = "2037-$month-0$number"; - my $estimate_times = $for_create ? 10 : 1; - $values{estimated_time} = $estimate_times * $number; - - $values{attachment} = _get_attach_values($number, $for_create); - - # Some things only happen on the first bug. - if ($number == 1) { - # We use 6 as the prefix for the extra values, because bug 6's values - # don't otherwise get used (since bug 6 is created as a clone of - # bug 1). This also makes sure that our greaterthan/lessthan - # tests work properly. - my $extra_group = create_group(6); - $product->set_group_controls($extra_group, - { membercontrol => CONTROLMAPSHOWN, - othercontrol => CONTROLMAPNA }); - $values{groups} = [$values{groups}->[0], $extra_group->name]; - my $extra_keyword = create_keyword(6); - $values{keywords} = [$values{keywords}, $extra_keyword->name]; - my $extra_cc = create_user("6-cc"); - $values{cc} = [$values{cc}, $extra_cc->login]; - my @multi_selects = grep { $_->type == FIELD_TYPE_MULTI_SELECT } - $self->all_fields; - foreach my $field (@multi_selects) { - my $new_value = create_legal_value($field, 6); - my $name = $field->name; - $values{$name} = [$values{$name}, $new_value->name]; - } - push(@{ $values{'tag'} }, "6-tag-" . random()); - } - - # On bug 5, any field that *can* be left empty, *is* left empty. - if ($number == 5) { - my @set_fields = grep { $_->type == FIELD_TYPE_SINGLE_SELECT } - $self->all_fields; - @set_fields = map { $_->name } @set_fields; - push(@set_fields, qw(short_desc version reporter)); - foreach my $key (keys %values) { - delete $values{$key} unless grep { $_ eq $key } @set_fields; - } - } - - $product->update(); - - return \%values; + return \%values; } # Flags sub _create_flags { - my ($number, $setter, $requestee) = @_; + my ($number, $setter, $requestee) = @_; - my $flagtypes = _create_flagtypes($number); + my $flagtypes = _create_flagtypes($number); - my %flags; - foreach my $type (qw(a b)) { - $flags{$type} = _get_flag_values(@_, $flagtypes->{$type}); - } - return \%flags; + my %flags; + foreach my $type (qw(a b)) { + $flags{$type} = _get_flag_values(@_, $flagtypes->{$type}); + } + return \%flags; } sub _create_flagtypes { - my ($number) = @_; - my $dbh = Bugzilla->dbh; - my $name = "$number-flag-" . random(); - my $desc = "FlagType $number"; - - my %flagtypes; - foreach my $target (qw(a b)) { - $dbh->do("INSERT INTO flagtypes + my ($number) = @_; + my $dbh = Bugzilla->dbh; + my $name = "$number-flag-" . random(); + my $desc = "FlagType $number"; + + my %flagtypes; + foreach my $target (qw(a b)) { + $dbh->do( + "INSERT INTO flagtypes (name, description, target_type, is_requestable, is_requesteeble, is_multiplicable, cc_list) - VALUES (?,?,?,1,1,1,'')", - undef, $name, $desc, $target); - my $id = $dbh->bz_last_key('flagtypes', 'id'); - $dbh->do('INSERT INTO flaginclusions (type_id) VALUES (?)', - undef, $id); - my $flagtype = new Bugzilla::FlagType($id); - $flagtypes{$target} = $flagtype; - } - return \%flagtypes; + VALUES (?,?,?,1,1,1,'')", undef, $name, $desc, $target + ); + my $id = $dbh->bz_last_key('flagtypes', 'id'); + $dbh->do('INSERT INTO flaginclusions (type_id) VALUES (?)', undef, $id); + my $flagtype = new Bugzilla::FlagType($id); + $flagtypes{$target} = $flagtype; + } + return \%flagtypes; } sub _get_flag_values { - my ($number, $setter, $requestee, $flagtype) = @_; - - my @set_flags; - if ($number <= 2) { - foreach my $value (qw(? - + ?)) { - my $flag = { type_id => $flagtype->id, status => $value, - setter => $setter, flagtype => $flagtype }; - push(@set_flags, $flag); - } - $set_flags[0]->{requestee} = $requestee->login; - } - else { - @set_flags = ({ type_id => $flagtype->id, status => '+', - setter => $setter, flagtype => $flagtype }); + my ($number, $setter, $requestee, $flagtype) = @_; + + my @set_flags; + if ($number <= 2) { + foreach my $value (qw(? - + ?)) { + my $flag = { + type_id => $flagtype->id, + status => $value, + setter => $setter, + flagtype => $flagtype + }; + push(@set_flags, $flag); } - return \@set_flags; + $set_flags[0]->{requestee} = $requestee->login; + } + else { + @set_flags = ({ + type_id => $flagtype->id, + status => '+', + setter => $setter, + flagtype => $flagtype + }); + } + return \@set_flags; } # Attachments sub _get_attach_values { - my ($number, $for_create) = @_; - - my $boolean = $number == 1 ? 1 : 0; - if ($for_create) { - $boolean = !$boolean ? 1 : 0; - } - my $ispatch = $for_create ? 'ispatch' : 'is_patch'; - my $isobsolete = $for_create ? 'isobsolete' : 'is_obsolete'; - my $isprivate = $for_create ? 'isprivate' : 'is_private'; - my $mimetype = $for_create ? 'mimetype' : 'content_type'; - - my %values = ( - description => "$number-attach_desc-" . random(), - filename => "$number-filename-" . random(), - $ispatch => $boolean, - $isobsolete => $boolean, - $isprivate => $boolean, - $mimetype => "text/x-$number-" . random(), - ); - if ($for_create) { - $values{data} = "$number-data-" . random() . random(); - } - return \%values; + my ($number, $for_create) = @_; + + my $boolean = $number == 1 ? 1 : 0; + if ($for_create) { + $boolean = !$boolean ? 1 : 0; + } + my $ispatch = $for_create ? 'ispatch' : 'is_patch'; + my $isobsolete = $for_create ? 'isobsolete' : 'is_obsolete'; + my $isprivate = $for_create ? 'isprivate' : 'is_private'; + my $mimetype = $for_create ? 'mimetype' : 'content_type'; + + my %values = ( + description => "$number-attach_desc-" . random(), + filename => "$number-filename-" . random(), + $ispatch => $boolean, + $isobsolete => $boolean, + $isprivate => $boolean, + $mimetype => "text/x-$number-" . random(), + ); + if ($for_create) { + $values{data} = "$number-data-" . random() . random(); + } + return \%values; } ################ @@ -589,194 +616,209 @@ sub _get_attach_values { ################ sub _create_one_bug { - my ($self, $number) = @_; - my $dbh = Bugzilla->dbh; + my ($self, $number) = @_; + my $dbh = Bugzilla->dbh; + + # We need bug 6 to have a unique alias that is not a clone of bug 1's, + # so we get the alias separately from the other parameters. + my $alias = $self->bug_create_value($number, 'alias'); + my $update_alias = $self->bug_update_value($number, 'alias'); + + # Otherwise, make bug 6 a clone of bug 1. + my $real_number = $number; + $number = 1 if $number == 6; + + my $reporter = $self->bug_create_value($number, 'reporter'); + Bugzilla->set_user(Bugzilla::User->check($reporter)); + + # We create the bug with one set of values, and then we change it + # to have different values. + my %params = %{$self->_bug_create_values->{$number}}; + $params{alias} = $alias; + + # There are some things in bug_create_values that shouldn't go into + # create(). + delete @params{qw(attachment set_flags tag)}; + + my ($status, $resolution, $see_also) + = delete @params{qw(bug_status resolution see_also)}; + + # All the bugs are created with everconfirmed = 0. + $params{bug_status} = 'UNCONFIRMED'; + my $bug = Bugzilla::Bug->create(\%params); + + # These are necessary for the changedfrom tests. + my $extra_values = $self->_extra_bug_create_values->{$number}; + foreach my $field ( + qw(comments remaining_time percentage_complete + keyword_objects everconfirmed dependson blocked + groups_in classification actual_time) + ) + { + $extra_values->{$field} = $bug->$field; + } + $extra_values->{reporter_accessible} = $number == 1 ? 0 : 1; + $extra_values->{cclist_accessible} = $number == 1 ? 0 : 1; + + if ($number == 5) { + + # Bypass Bugzilla::Bug--we don't want any changes in bugs_activity + # for bug 5. + $dbh->do( + 'UPDATE bugs SET qa_contact = NULL, reporter_accessible = 0, + cclist_accessible = 0 WHERE bug_id = ?', undef, + $bug->id + ); + $dbh->do('DELETE FROM cc WHERE bug_id = ?', undef, $bug->id); + my $ts = '1970-01-01 00:00:00'; + $dbh->do( + 'UPDATE bugs SET creation_ts = ?, delta_ts = ? + WHERE bug_id = ?', undef, $ts, $ts, $bug->id + ); + $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', + undef, $ts, $bug->id); + $bug->{creation_ts} = $ts; + $extra_values->{see_also} = []; + } + else { + # Manually set the creation_ts so that each bug has a different one. + # + # Also, manually update the resolution and bug_status, because + # we want to see both of them change in bugs_activity, so we + # have to start with values for both (and as of the time when I'm + # writing this test, Bug->create doesn't support setting resolution). + # + # Same for see_also. + my $timestamp = timestamp($number, $number - 1); + my $creation_ts = $timestamp->ymd . ' ' . $timestamp->hms; + $bug->{creation_ts} = $creation_ts; + $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', + undef, $creation_ts, $bug->id); + $dbh->do( + 'UPDATE bugs SET creation_ts = ?, bug_status = ?, + resolution = ? WHERE bug_id = ?', undef, $creation_ts, $status, + $resolution, $bug->id + ); + $dbh->do('INSERT INTO bug_see_also (bug_id, value, class) VALUES (?,?,?)', + undef, $bug->id, $see_also, 'Bugzilla::BugUrl::Bugzilla'); + $extra_values->{see_also} = $bug->see_also; - # We need bug 6 to have a unique alias that is not a clone of bug 1's, - # so we get the alias separately from the other parameters. - my $alias = $self->bug_create_value($number, 'alias'); - my $update_alias = $self->bug_update_value($number, 'alias'); + # All the tags must be created as the admin user, so that the + # admin user can find them, later. + my $original_user = Bugzilla->user; + Bugzilla->set_user($self->admin); + my $tags = $self->bug_create_value($number, 'tag'); + $bug->add_tag($_) foreach @$tags; + $extra_values->{tags} = $tags; + Bugzilla->set_user($original_user); - # Otherwise, make bug 6 a clone of bug 1. - my $real_number = $number; - $number = 1 if $number == 6; + if ($number == 1) { - my $reporter = $self->bug_create_value($number, 'reporter'); - Bugzilla->set_user(Bugzilla::User->check($reporter)); - - # We create the bug with one set of values, and then we change it - # to have different values. - my %params = %{ $self->_bug_create_values->{$number} }; - $params{alias} = $alias; - - # There are some things in bug_create_values that shouldn't go into - # create(). - delete @params{qw(attachment set_flags tag)}; - - my ($status, $resolution, $see_also) = - delete @params{qw(bug_status resolution see_also)}; - # All the bugs are created with everconfirmed = 0. - $params{bug_status} = 'UNCONFIRMED'; - my $bug = Bugzilla::Bug->create(\%params); - - # These are necessary for the changedfrom tests. - my $extra_values = $self->_extra_bug_create_values->{$number}; - foreach my $field (qw(comments remaining_time percentage_complete - keyword_objects everconfirmed dependson blocked - groups_in classification actual_time)) - { - $extra_values->{$field} = $bug->$field; + # Bug 1 needs to start off with reporter_accessible and + # cclist_accessible being 0, so that when we change them to 1, + # that change shows up in bugs_activity. + $dbh->do( + 'UPDATE bugs SET reporter_accessible = 0, + cclist_accessible = 0 WHERE bug_id = ?', undef, $bug->id + ); + + # Bug 1 gets three comments, so that longdescs.count matches it + # uniquely. The third comment is added in the middle, so that the + # last comment contains all of the important data, like work_time. + $bug->add_comment("1-comment-" . random(100)); } - $extra_values->{reporter_accessible} = $number == 1 ? 0 : 1; - $extra_values->{cclist_accessible} = $number == 1 ? 0 : 1; - - if ($number == 5) { - # Bypass Bugzilla::Bug--we don't want any changes in bugs_activity - # for bug 5. - $dbh->do('UPDATE bugs SET qa_contact = NULL, reporter_accessible = 0, - cclist_accessible = 0 WHERE bug_id = ?', - undef, $bug->id); - $dbh->do('DELETE FROM cc WHERE bug_id = ?', undef, $bug->id); - my $ts = '1970-01-01 00:00:00'; - $dbh->do('UPDATE bugs SET creation_ts = ?, delta_ts = ? - WHERE bug_id = ?', undef, $ts, $ts, $bug->id); - $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', - undef, $ts, $bug->id); - $bug->{creation_ts} = $ts; - $extra_values->{see_also} = []; + + my %update_params = %{$self->_bug_update_values->{$number}}; + my %reverse_map = reverse %{Bugzilla::Bug->FIELD_MAP}; + foreach my $db_name (keys %reverse_map) { + next if $db_name eq 'comment'; + next if $db_name eq 'status_whiteboard'; + if (exists $update_params{$db_name}) { + my $update_name = $reverse_map{$db_name}; + $update_params{$update_name} = delete $update_params{$db_name}; + } } - else { - # Manually set the creation_ts so that each bug has a different one. - # - # Also, manually update the resolution and bug_status, because - # we want to see both of them change in bugs_activity, so we - # have to start with values for both (and as of the time when I'm - # writing this test, Bug->create doesn't support setting resolution). - # - # Same for see_also. - my $timestamp = timestamp($number, $number - 1); - my $creation_ts = $timestamp->ymd . ' ' . $timestamp->hms; - $bug->{creation_ts} = $creation_ts; - $dbh->do('UPDATE longdescs SET bug_when = ? WHERE bug_id = ?', - undef, $creation_ts, $bug->id); - $dbh->do('UPDATE bugs SET creation_ts = ?, bug_status = ?, - resolution = ? WHERE bug_id = ?', - undef, $creation_ts, $status, $resolution, $bug->id); - $dbh->do('INSERT INTO bug_see_also (bug_id, value, class) VALUES (?,?,?)', - undef, $bug->id, $see_also, 'Bugzilla::BugUrl::Bugzilla'); - $extra_values->{see_also} = $bug->see_also; - - # All the tags must be created as the admin user, so that the - # admin user can find them, later. - my $original_user = Bugzilla->user; - Bugzilla->set_user($self->admin); - my $tags = $self->bug_create_value($number, 'tag'); - $bug->add_tag($_) foreach @$tags; - $extra_values->{tags} = $tags; - Bugzilla->set_user($original_user); - - if ($number == 1) { - # Bug 1 needs to start off with reporter_accessible and - # cclist_accessible being 0, so that when we change them to 1, - # that change shows up in bugs_activity. - $dbh->do('UPDATE bugs SET reporter_accessible = 0, - cclist_accessible = 0 WHERE bug_id = ?', - undef, $bug->id); - # Bug 1 gets three comments, so that longdescs.count matches it - # uniquely. The third comment is added in the middle, so that the - # last comment contains all of the important data, like work_time. - $bug->add_comment("1-comment-" . random(100)); - } - - my %update_params = %{ $self->_bug_update_values->{$number} }; - my %reverse_map = reverse %{ Bugzilla::Bug->FIELD_MAP }; - foreach my $db_name (keys %reverse_map) { - next if $db_name eq 'comment'; - next if $db_name eq 'status_whiteboard'; - if (exists $update_params{$db_name}) { - my $update_name = $reverse_map{$db_name}; - $update_params{$update_name} = delete $update_params{$db_name}; - } - } - - my ($new_status, $new_res) = - delete @update_params{qw(status resolution)}; - # Bypass the status workflow. - $bug->{bug_status} = $new_status; - $bug->{resolution} = $new_res; - $bug->{everconfirmed} = 1 if $number == 1; - - # add/remove/set fields. - $update_params{keywords} = { set => $update_params{keywords} }; - $update_params{groups} = { add => $update_params{groups}, - remove => $bug->groups_in }; - my @cc_remove = map { $_->login } @{ $bug->cc_users }; - my $cc_new = $update_params{cc}; - my @cc_add = ref($cc_new) ? @$cc_new : ($cc_new); - # We make the admin an explicit CC on bug 1 (but not on bug 6), so - # that we can test the %user% pronoun properly. - if ($real_number == 1) { - push(@cc_add, $self->admin->login); - } - $update_params{cc} = { add => \@cc_add, remove => \@cc_remove }; - my $see_also_remove = $bug->see_also; - my $see_also_add = [$update_params{see_also}]; - $update_params{see_also} = { add => $see_also_add, - remove => $see_also_remove }; - $update_params{comment} = { body => $update_params{comment} }; - $update_params{work_time} = $number; - # Setting work_time kills the remaining_time, so we need to - # preserve that. We add 8 because that produces an integer - # percentage_complete for bug 1, which is necessary for - # accurate "equals"-type searching. - $update_params{remaining_time} = $number + 8; - $update_params{reporter_accessible} = $number == 1 ? 1 : 0; - $update_params{cclist_accessible} = $number == 1 ? 1 : 0; - $update_params{alias} = $update_alias; - - $bug->set_all(\%update_params); - my $flags = $self->bug_create_value($number, 'set_flags')->{b}; - $bug->set_flags([], $flags); - $timestamp->set(second => $number); - $bug->update($timestamp->ymd . ' ' . $timestamp->hms); - $extra_values->{flags} = $bug->flags; - - # It's not generally safe to do update() multiple times on - # the same Bug object. - $bug = new Bugzilla::Bug($bug->id); - my $update_flags = $self->bug_update_value($number, 'set_flags')->{b}; - $_->{status} = 'X' foreach @{ $bug->flags }; - $bug->set_flags($bug->flags, $update_flags); - if ($number == 1) { - my $comment_id = $bug->comments->[-1]->id; - $bug->set_comment_is_private({ $comment_id => 1 }); - } - $bug->update($bug->delta_ts); - - my $attach_create = $self->bug_create_value($number, 'attachment'); - my $attachment = Bugzilla::Attachment->create({ - bug => $bug, - creation_ts => $creation_ts, - %$attach_create }); - # Store for the changedfrom tests. - $extra_values->{attachments} = - [new Bugzilla::Attachment($attachment->id)]; - - my $attach_update = $self->bug_update_value($number, 'attachment'); - $attachment->set_all($attach_update); - # In order to keep the mimetype on the ispatch attachment, - # we need to bypass the validator. - $attachment->{mimetype} = $attach_update->{content_type}; - my $attach_flags = $self->bug_update_value($number, 'set_flags')->{a}; - $attachment->set_flags([], $attach_flags); - $attachment->update($bug->delta_ts); + + my ($new_status, $new_res) = delete @update_params{qw(status resolution)}; + + # Bypass the status workflow. + $bug->{bug_status} = $new_status; + $bug->{resolution} = $new_res; + $bug->{everconfirmed} = 1 if $number == 1; + + # add/remove/set fields. + $update_params{keywords} = {set => $update_params{keywords}}; + $update_params{groups} + = {add => $update_params{groups}, remove => $bug->groups_in}; + my @cc_remove = map { $_->login } @{$bug->cc_users}; + my $cc_new = $update_params{cc}; + my @cc_add = ref($cc_new) ? @$cc_new : ($cc_new); + + # We make the admin an explicit CC on bug 1 (but not on bug 6), so + # that we can test the %user% pronoun properly. + if ($real_number == 1) { + push(@cc_add, $self->admin->login); + } + $update_params{cc} = {add => \@cc_add, remove => \@cc_remove}; + my $see_also_remove = $bug->see_also; + my $see_also_add = [$update_params{see_also}]; + $update_params{see_also} = {add => $see_also_add, remove => $see_also_remove}; + $update_params{comment} = {body => $update_params{comment}}; + $update_params{work_time} = $number; + + # Setting work_time kills the remaining_time, so we need to + # preserve that. We add 8 because that produces an integer + # percentage_complete for bug 1, which is necessary for + # accurate "equals"-type searching. + $update_params{remaining_time} = $number + 8; + $update_params{reporter_accessible} = $number == 1 ? 1 : 0; + $update_params{cclist_accessible} = $number == 1 ? 1 : 0; + $update_params{alias} = $update_alias; + + $bug->set_all(\%update_params); + my $flags = $self->bug_create_value($number, 'set_flags')->{b}; + $bug->set_flags([], $flags); + $timestamp->set(second => $number); + $bug->update($timestamp->ymd . ' ' . $timestamp->hms); + $extra_values->{flags} = $bug->flags; + + # It's not generally safe to do update() multiple times on + # the same Bug object. + $bug = new Bugzilla::Bug($bug->id); + my $update_flags = $self->bug_update_value($number, 'set_flags')->{b}; + $_->{status} = 'X' foreach @{$bug->flags}; + $bug->set_flags($bug->flags, $update_flags); + if ($number == 1) { + my $comment_id = $bug->comments->[-1]->id; + $bug->set_comment_is_private({$comment_id => 1}); } + $bug->update($bug->delta_ts); - # Values for changedfrom. - $extra_values->{creation_ts} = $bug->creation_ts; - $extra_values->{delta_ts} = $bug->creation_ts; + my $attach_create = $self->bug_create_value($number, 'attachment'); + my $attachment + = Bugzilla::Attachment->create({ + bug => $bug, creation_ts => $creation_ts, %$attach_create + }); - return new Bugzilla::Bug($bug->id); + # Store for the changedfrom tests. + $extra_values->{attachments} = [new Bugzilla::Attachment($attachment->id)]; + + my $attach_update = $self->bug_update_value($number, 'attachment'); + $attachment->set_all($attach_update); + + # In order to keep the mimetype on the ispatch attachment, + # we need to bypass the validator. + $attachment->{mimetype} = $attach_update->{content_type}; + my $attach_flags = $self->bug_update_value($number, 'set_flags')->{a}; + $attachment->set_flags([], $attach_flags); + $attachment->update($bug->delta_ts); + } + + # Values for changedfrom. + $extra_values->{creation_ts} = $bug->creation_ts; + $extra_values->{delta_ts} = $bug->creation_ts; + + return new Bugzilla::Bug($bug->id); } ################################### @@ -792,44 +834,45 @@ sub _create_one_bug { # field, which we store more efficiently, in an array, and then we re-populate # the Test_Results in Test::Builder at the end of the test. sub clean_test_history { - my ($self) = @_; - return if !$self->option('long'); - my $builder = Test::More->builder; - my $current_test = $builder->current_test; - - # I don't use details() because I don't want to copy the array. - my $results = $builder->{Test_Results}; - my $check_test = $current_test - 1; - while (my $result = $results->[$check_test]) { - last if !$result; - $self->test_success($check_test, $result->{ok}); - $check_test--; - } - - # Truncate the test history array, but retain the current test number. - $builder->{Test_Results} = []; - $builder->{Curr_Test} = $current_test; + my ($self) = @_; + return if !$self->option('long'); + my $builder = Test::More->builder; + my $current_test = $builder->current_test; + + # I don't use details() because I don't want to copy the array. + my $results = $builder->{Test_Results}; + my $check_test = $current_test - 1; + while (my $result = $results->[$check_test]) { + last if !$result; + $self->test_success($check_test, $result->{ok}); + $check_test--; + } + + # Truncate the test history array, but retain the current test number. + $builder->{Test_Results} = []; + $builder->{Curr_Test} = $current_test; } sub test_success { - my ($self, $index, $status) = @_; - $self->{test_success}->[$index] = $status; - return $self->{test_success}; + my ($self, $index, $status) = @_; + $self->{test_success}->[$index] = $status; + return $self->{test_success}; } sub repopulate_test_results { - my ($self) = @_; - return if !$self->option('long'); - $self->clean_test_history(); - # We create only two hashes, for memory efficiency. - my %ok = ( ok => 1 ); - my %not_ok = ( ok => 0 ); - my @results; - foreach my $success (@{ $self->{test_success} }) { - push(@results, $success ? \%ok : \%not_ok); - } - my $builder = Test::More->builder; - $builder->{Test_Results} = \@results; + my ($self) = @_; + return if !$self->option('long'); + $self->clean_test_history(); + + # We create only two hashes, for memory efficiency. + my %ok = (ok => 1); + my %not_ok = (ok => 0); + my @results; + foreach my $success (@{$self->{test_success}}) { + push(@results, $success ? \%ok : \%not_ok); + } + my $builder = Test::More->builder; + $builder->{Test_Results} = \@results; } ########## @@ -842,13 +885,13 @@ sub repopulate_test_results { # have to re-run the value-translation code every time (which can be pretty # slow). sub value_translation_cache { - my ($self, $field_test, $value) = @_; - return if !$self->option('long'); - my $test_name = $field_test->name; - if (@_ == 3) { - $self->{value_translation_cache}->{$test_name} = $value; - } - return $self->{value_translation_cache}->{$test_name}; + my ($self, $field_test, $value) = @_; + return if !$self->option('long'); + my $test_name = $field_test->name; + if (@_ == 3) { + $self->{value_translation_cache}->{$test_name} = $value; + } + return $self->{value_translation_cache}->{$test_name}; } # When doing AND/OR tests, the value for transformed_value_was_equal @@ -856,13 +899,13 @@ sub value_translation_cache { # if we pull our values from the value_translation_cache. So we need # to also cache the values for transformed_value_was_equal. sub was_equal_cache { - my ($self, $field_test, $number, $value) = @_; - return if !$self->option('long'); - my $test_name = $field_test->name; - if (@_ == 4) { - $self->{tvwe_cache}->{$test_name}->{$number} = $value; - } - return $self->{tvwe_cache}->{$test_name}->{$number}; + my ($self, $field_test, $number, $value) = @_; + return if !$self->option('long'); + my $test_name = $field_test->name; + if (@_ == 4) { + $self->{tvwe_cache}->{$test_name}->{$number} = $value; + } + return $self->{tvwe_cache}->{$test_name}->{$number}; } ############# @@ -870,133 +913,136 @@ sub was_equal_cache { ############# sub run { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - # We want backtraces on any "die" message or any warning. - # Otherwise it's hard to trace errors inside of Bugzilla::Search from - # reading automated test run results. - local $SIG{__WARN__} = \&Carp::cluck; - local $SIG{__DIE__} = \&Carp::confess; - - $dbh->bz_start_transaction(); - - # Some parameters need to be set in order for the tests to function - # properly. - my $everybody = $self->everybody; - my $params = Bugzilla->params; - local $params->{'useclassification'} = 1; - local $params->{'useqacontact'} = 1; - local $params->{'usebugaliases'} = 1; - local $params->{'usetargetmilestone'} = 1; - local $params->{'mail_delivery_method'} = 'None'; - local $params->{'timetrackinggroup'} = $everybody->name; - local $params->{'insidergroup'} = $everybody->name; - - $self->_setup_bugs(); - - # Even though _setup_bugs set us as an admin, we want to be sure at - # this point that we have an admin with refreshed group memberships. - Bugzilla->set_user($self->admin); - foreach my $test (CUSTOM_SEARCH_TESTS) { - my $custom_test = new Bugzilla::Test::Search::CustomTest($test, $self); - $custom_test->run(); - } - foreach my $test (SPECIAL_PARAM_TESTS) { - my $operator_test = - new Bugzilla::Test::Search::OperatorTest($test->{operator}, $self); - my $field = Bugzilla::Field->check($test->{field}); - my $special_test = new Bugzilla::Test::Search::FieldTestNormal( - $operator_test, $field, $test); - $special_test->run(); - } - foreach my $operator ($self->top_level_operators) { - my $operator_test = - new Bugzilla::Test::Search::OperatorTest($operator, $self); - $operator_test->run(); - } - - # Rollbacks won't get rid of bugs_fulltext entries, so we do that ourselves. - my @bug_ids = map { $_->id } $self->bugs; - my $bug_id_string = join(',', @bug_ids); - $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id IN ($bug_id_string)"); - $dbh->bz_rollback_transaction(); - $self->repopulate_test_results(); + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + # We want backtraces on any "die" message or any warning. + # Otherwise it's hard to trace errors inside of Bugzilla::Search from + # reading automated test run results. + local $SIG{__WARN__} = \&Carp::cluck; + local $SIG{__DIE__} = \&Carp::confess; + + $dbh->bz_start_transaction(); + + # Some parameters need to be set in order for the tests to function + # properly. + my $everybody = $self->everybody; + my $params = Bugzilla->params; + local $params->{'useclassification'} = 1; + local $params->{'useqacontact'} = 1; + local $params->{'usebugaliases'} = 1; + local $params->{'usetargetmilestone'} = 1; + local $params->{'mail_delivery_method'} = 'None'; + local $params->{'timetrackinggroup'} = $everybody->name; + local $params->{'insidergroup'} = $everybody->name; + + $self->_setup_bugs(); + + # Even though _setup_bugs set us as an admin, we want to be sure at + # this point that we have an admin with refreshed group memberships. + Bugzilla->set_user($self->admin); + foreach my $test (CUSTOM_SEARCH_TESTS) { + my $custom_test = new Bugzilla::Test::Search::CustomTest($test, $self); + $custom_test->run(); + } + foreach my $test (SPECIAL_PARAM_TESTS) { + my $operator_test + = new Bugzilla::Test::Search::OperatorTest($test->{operator}, $self); + my $field = Bugzilla::Field->check($test->{field}); + my $special_test + = new Bugzilla::Test::Search::FieldTestNormal($operator_test, $field, $test); + $special_test->run(); + } + foreach my $operator ($self->top_level_operators) { + my $operator_test = new Bugzilla::Test::Search::OperatorTest($operator, $self); + $operator_test->run(); + } + + # Rollbacks won't get rid of bugs_fulltext entries, so we do that ourselves. + my @bug_ids = map { $_->id } $self->bugs; + my $bug_id_string = join(',', @bug_ids); + $dbh->do("DELETE FROM bugs_fulltext WHERE bug_id IN ($bug_id_string)"); + $dbh->bz_rollback_transaction(); + $self->repopulate_test_results(); } # This makes a few changes to the bugs after they're created--changes # that can only be done after all the bugs have been created. sub _setup_bugs { - my ($self) = @_; - $self->_setup_dependencies(); - $self->_set_bug_id_fields(); - $self->_protect_bug_6(); + my ($self) = @_; + $self->_setup_dependencies(); + $self->_set_bug_id_fields(); + $self->_protect_bug_6(); } + sub _setup_dependencies { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - # Set up depedency relationships between the bugs. - # Bug 1 + 6 depend on bug 2 and block bug 3. - my $bug2 = $self->bug(2); - my $bug3 = $self->bug(3); - foreach my $number (1,6) { - my $bug = $self->bug($number); - my @original_delta = ($bug2->delta_ts, $bug3->delta_ts); - Bugzilla->set_user($bug->reporter); - $bug->set_dependencies([$bug2->id], [$bug3->id]); - $bug->update($bug->delta_ts); - # Setting dependencies changed the delta_ts on bug2 and bug3, so - # re-set them back to what they were before. However, we leave - # the correct update times in bugs_activity, so that the changed* - # searches still work right. - my $set_delta = $dbh->prepare( - 'UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); - foreach my $row ([$original_delta[0], $bug2->id], - [$original_delta[1], $bug3->id]) - { - $set_delta->execute(@$row); - } + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + # Set up depedency relationships between the bugs. + # Bug 1 + 6 depend on bug 2 and block bug 3. + my $bug2 = $self->bug(2); + my $bug3 = $self->bug(3); + foreach my $number (1, 6) { + my $bug = $self->bug($number); + my @original_delta = ($bug2->delta_ts, $bug3->delta_ts); + Bugzilla->set_user($bug->reporter); + $bug->set_dependencies([$bug2->id], [$bug3->id]); + $bug->update($bug->delta_ts); + + # Setting dependencies changed the delta_ts on bug2 and bug3, so + # re-set them back to what they were before. However, we leave + # the correct update times in bugs_activity, so that the changed* + # searches still work right. + my $set_delta = $dbh->prepare('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?'); + foreach + my $row ([$original_delta[0], $bug2->id], [$original_delta[1], $bug3->id]) + { + $set_delta->execute(@$row); } + } } sub _set_bug_id_fields { - my ($self) = @_; - # BUG_ID fields couldn't be set before, because before we create bug 1, - # we don't necessarily have any valid bug ids.) - my @bug_id_fields = grep { $_->type == FIELD_TYPE_BUG_ID } - $self->all_fields; - foreach my $number (1..NUM_BUGS) { - my $bug = $self->bug($number); - $number = 1 if $number == 6; - next if $number == 5; - my $other_bug = $self->bug($number + 1); - Bugzilla->set_user($bug->reporter); - foreach my $field (@bug_id_fields) { - $bug->set_custom_field($field, $other_bug->id); - $bug->update($bug->delta_ts); - } + my ($self) = @_; + + # BUG_ID fields couldn't be set before, because before we create bug 1, + # we don't necessarily have any valid bug ids.) + my @bug_id_fields = grep { $_->type == FIELD_TYPE_BUG_ID } $self->all_fields; + foreach my $number (1 .. NUM_BUGS) { + my $bug = $self->bug($number); + $number = 1 if $number == 6; + next if $number == 5; + my $other_bug = $self->bug($number + 1); + Bugzilla->set_user($bug->reporter); + foreach my $field (@bug_id_fields) { + $bug->set_custom_field($field, $other_bug->id); + $bug->update($bug->delta_ts); } + } } sub _protect_bug_6 { - my ($self) = @_; - my $dbh = Bugzilla->dbh; - - Bugzilla->set_user($self->admin); - - # Put bug6 in the nobody group. - my $nobody = $self->nobody; - # We pull it newly from the DB to be sure it's safe to call update() - # on. - my $bug6 = new Bugzilla::Bug($self->bug(6)->id); - $bug6->add_group($nobody); - $bug6->update($bug6->delta_ts); - - # Remove the admin (and everybody else) from the $nobody group. - $dbh->do('DELETE FROM group_group_map - WHERE grantor_id = ? OR member_id = ?', undef, - $nobody->id, $nobody->id); + my ($self) = @_; + my $dbh = Bugzilla->dbh; + + Bugzilla->set_user($self->admin); + + # Put bug6 in the nobody group. + my $nobody = $self->nobody; + + # We pull it newly from the DB to be sure it's safe to call update() + # on. + my $bug6 = new Bugzilla::Bug($self->bug(6)->id); + $bug6->add_group($nobody); + $bug6->update($bug6->delta_ts); + + # Remove the admin (and everybody else) from the $nobody group. + $dbh->do( + 'DELETE FROM group_group_map + WHERE grantor_id = ? OR member_id = ?', undef, $nobody->id, + $nobody->id + ); } 1; diff --git a/xt/lib/Bugzilla/Test/Search/AndTest.pm b/xt/lib/Bugzilla/Test/Search/AndTest.pm index b7f8b3cab..77bdf8911 100644 --- a/xt/lib/Bugzilla/Test/Search/AndTest.pm +++ b/xt/lib/Bugzilla/Test/Search/AndTest.pm @@ -36,13 +36,13 @@ use constant type => 'AND'; # In an AND test, bugs ARE supposed to be contained only if they are contained # by ALL tests. sub bug_is_contained { - my ($self, $number) = @_; - return all { $_->bug_is_contained($number) } $self->field_tests; + my ($self, $number) = @_; + return all { $_->bug_is_contained($number) } $self->field_tests; } sub _bug_will_actually_be_contained { - my ($self, $number) = @_; - return all { $_->will_actually_contain_bug($number) } $self->field_tests; + my ($self, $number) = @_; + return all { $_->will_actually_contain_bug($number) } $self->field_tests; } ############################## @@ -50,17 +50,17 @@ sub _bug_will_actually_be_contained { ############################## sub search_params { - my ($self) = @_; - my @all_params = map { $_->search_params } $self->field_tests; - my %params; - my $chart = 0; - foreach my $item (@all_params) { - $params{"field0-$chart-0"} = $item->{'field0-0-0'}; - $params{"type0-$chart-0"} = $item->{'type0-0-0'}; - $params{"value0-$chart-0"} = $item->{'value0-0-0'}; - $chart++; - } - return \%params; + my ($self) = @_; + my @all_params = map { $_->search_params } $self->field_tests; + my %params; + my $chart = 0; + foreach my $item (@all_params) { + $params{"field0-$chart-0"} = $item->{'field0-0-0'}; + $params{"type0-$chart-0"} = $item->{'type0-0-0'}; + $params{"value0-$chart-0"} = $item->{'value0-0-0'}; + $chart++; + } + return \%params; } 1; diff --git a/xt/lib/Bugzilla/Test/Search/Constants.pm b/xt/lib/Bugzilla/Test/Search/Constants.pm index 53f8851fb..34953d697 100644 --- a/xt/lib/Bugzilla/Test/Search/Constants.pm +++ b/xt/lib/Bugzilla/Test/Search/Constants.pm @@ -31,28 +31,28 @@ use Bugzilla::Constants; use Bugzilla::Util qw(generate_random_password); our @EXPORT = qw( - ATTACHMENT_FIELDS - BROKEN_NOT - COLUMN_TRANSLATION - COMMENT_FIELDS - CUSTOM_FIELDS - CUSTOM_SEARCH_TESTS - FIELD_SIZE - FIELD_SUBSTR_SIZE - FLAG_FIELDS - INJECTION_BROKEN_FIELD - INJECTION_BROKEN_OPERATOR - INJECTION_TESTS - KNOWN_BROKEN - NUM_BUGS - NUM_SEARCH_TESTS - SKIP_FIELDS - SPECIAL_PARAM_TESTS - SUBSTR_NO_FIELD_ADD - SUBSTR_SIZE - TESTS - TESTS_PER_RUN - USER_FIELDS + ATTACHMENT_FIELDS + BROKEN_NOT + COLUMN_TRANSLATION + COMMENT_FIELDS + CUSTOM_FIELDS + CUSTOM_SEARCH_TESTS + FIELD_SIZE + FIELD_SUBSTR_SIZE + FLAG_FIELDS + INJECTION_BROKEN_FIELD + INJECTION_BROKEN_OPERATOR + INJECTION_TESTS + KNOWN_BROKEN + NUM_BUGS + NUM_SEARCH_TESTS + SKIP_FIELDS + SPECIAL_PARAM_TESTS + SUBSTR_NO_FIELD_ADD + SUBSTR_SIZE + TESTS + TESTS_PER_RUN + USER_FIELDS ); # Bug 1 is designed to be found by all the "equals" tests. It has @@ -77,6 +77,7 @@ use constant NUM_BUGS => 6; # How many tests there are for each operator/field combination other # than the "contains" tests. use constant NUM_SEARCH_TESTS => 3; + # This is how many tests get run for each field/operator. use constant TESTS_PER_RUN => NUM_SEARCH_TESTS + NUM_BUGS; @@ -88,40 +89,37 @@ use constant FIELD_SIZE => 30; # These are the custom fields that are created if the BZ_MODIFY_DATABASE_TESTS # environment variable is set. use constant CUSTOM_FIELDS => { - FIELD_TYPE_FREETEXT, 'cf_freetext', - FIELD_TYPE_SINGLE_SELECT, 'cf_single_select', - FIELD_TYPE_MULTI_SELECT, 'cf_multi_select', - FIELD_TYPE_TEXTAREA, 'cf_textarea', - FIELD_TYPE_DATETIME, 'cf_datetime', - FIELD_TYPE_BUG_ID, 'cf_bugid', + FIELD_TYPE_FREETEXT, 'cf_freetext', + FIELD_TYPE_SINGLE_SELECT, 'cf_single_select', + FIELD_TYPE_MULTI_SELECT, 'cf_multi_select', + FIELD_TYPE_TEXTAREA, 'cf_textarea', + FIELD_TYPE_DATETIME, 'cf_datetime', + FIELD_TYPE_BUG_ID, 'cf_bugid', }; # This translates fielddefs names into Search column names. use constant COLUMN_TRANSLATION => { - creation_ts => 'opendate', - delta_ts => 'changeddate', - work_time => 'actual_time', + creation_ts => 'opendate', + delta_ts => 'changeddate', + work_time => 'actual_time', }; # Make comment field names to their Bugzilla::Comment accessor. use constant COMMENT_FIELDS => { - longdesc => 'body', - commenter => 'author', - 'longdescs.isprivate' => 'is_private', + longdesc => 'body', + commenter => 'author', + 'longdescs.isprivate' => 'is_private', }; # Same as above, for Bugzilla::Attachment. -use constant ATTACHMENT_FIELDS => { - mimetype => 'contenttype', - submitter => 'attacher', - thedata => 'data', -}; +use constant ATTACHMENT_FIELDS => + {mimetype => 'contenttype', submitter => 'attacher', thedata => 'data',}; # Same, for Bugzilla::Flag. use constant FLAG_FIELDS => { - 'flagtypes.name' => 'name', - 'setters.login_name' => 'setter', - 'requestees.login_name' => 'requestee', + 'flagtypes.name' => 'name', + 'setters.login_name' => 'setter', + 'requestees.login_name' => 'requestee', }; # These are fields that we don't test. Test::More will mark these @@ -129,40 +127,43 @@ use constant FLAG_FIELDS => { # # We don't support days_elapsed or owner_idle_time yet. use constant SKIP_FIELDS => qw( - owner_idle_time - days_elapsed + owner_idle_time + days_elapsed ); # All the fields that represent users. use constant USER_FIELDS => qw( - assigned_to - cc - reporter - qa_contact - commenter - attachments.submitter - setters.login_name - requestees.login_name + assigned_to + cc + reporter + qa_contact + commenter + attachments.submitter + setters.login_name + requestees.login_name ); # For the "substr"-type searches, how short of a substring should # we use? The goal is to be shorter than the full string, but # long enough to still be globally unique. use constant SUBSTR_SIZE => 20; + # However, for some fields, we use a different size. use constant FIELD_SUBSTR_SIZE => { - alias => 11, - # Just the month and day. - deadline => -5, - creation_ts => -8, - delta_ts => -8, - percentage_complete => 1, - work_time => 3, - remaining_time => 3, - target_milestone => 15, - longdesc => 25, - # Just the hour and minute. - FIELD_TYPE_DATETIME, -5, + alias => 11, + + # Just the month and day. + deadline => -5, + creation_ts => -8, + delta_ts => -8, + percentage_complete => 1, + work_time => 3, + remaining_time => 3, + target_milestone => 15, + longdesc => 25, + + # Just the hour and minute. + FIELD_TYPE_DATETIME, -5, }; # For most fields, we add the length of the name of the field plus @@ -170,9 +171,9 @@ use constant FIELD_SUBSTR_SIZE => { # we're going to use. However, for some fields, it doesn't make sense to # add in their field name this way. use constant SUBSTR_NO_FIELD_ADD => FIELD_TYPE_DATETIME, qw( - target_milestone remaining_time percentage_complete work_time - attachments.mimetype attachments.submitter attachments.filename - attachments.description flagtypes.name + target_milestone remaining_time percentage_complete work_time + attachments.mimetype attachments.submitter attachments.filename + attachments.description flagtypes.name ); ################ @@ -192,42 +193,42 @@ use constant SUBSTR_NO_FIELD_ADD => FIELD_TYPE_DATETIME, qw( # lessthaneq. What we're really saying here by marking these broken # is that there ought to be some way of searching "all ccs" vs "any cc" # (and same for the other fields). -use constant GREATERTHAN_BROKEN => ( - cc => { contains => [1] }, -); +use constant GREATERTHAN_BROKEN => (cc => {contains => [1]},); # allwords and allwordssubstr have these broken tests in common. use constant ALLWORDS_BROKEN => ( - # allwordssubstr on cc fields matches against a single cc, - # instead of matching against all ccs on a bug. - cc => { contains => [1] }, - # bug 828344 changed how these searches operate to revert back to the 4.0 - # behavour, so these tests need to be updated (bug 849117). - 'flagtypes.name' => { contains => [1] }, - longdesc => { contains => [1] }, + + # allwordssubstr on cc fields matches against a single cc, + # instead of matching against all ccs on a bug. + cc => {contains => [1]}, + + # bug 828344 changed how these searches operate to revert back to the 4.0 + # behavour, so these tests need to be updated (bug 849117). + 'flagtypes.name' => {contains => [1]}, + longdesc => {contains => [1]}, ); # Fields that don't generally work at all with changed* searches, but # probably should. use constant CHANGED_BROKEN => ( - classification => { contains => [1] }, - commenter => { contains => [1] }, - percentage_complete => { contains => [1] }, - 'requestees.login_name' => { contains => [1] }, - 'setters.login_name' => { contains => [1] }, - delta_ts => { contains => [1] }, + classification => {contains => [1]}, + commenter => {contains => [1]}, + percentage_complete => {contains => [1]}, + 'requestees.login_name' => {contains => [1]}, + 'setters.login_name' => {contains => [1]}, + delta_ts => {contains => [1]}, ); # These are additional broken tests that changedfrom and changedto # have in common. use constant CHANGED_VALUE_BROKEN => ( - bug_group => { contains => [1] }, - cc => { contains => [1] }, - estimated_time => { contains => [1] }, - 'flagtypes.name' => { contains => [1] }, - keywords => { contains => [1] }, - 'longdescs.count' => { search => 1 }, - FIELD_TYPE_MULTI_SELECT, { contains => [1] }, + bug_group => {contains => [1]}, + cc => {contains => [1]}, + estimated_time => {contains => [1]}, + 'flagtypes.name' => {contains => [1]}, + keywords => {contains => [1]}, + 'longdescs.count' => {search => 1}, + FIELD_TYPE_MULTI_SELECT, {contains => [1]}, ); @@ -252,114 +253,94 @@ use constant CHANGED_VALUE_BROKEN => ( # while the other fails. In this case, we have a special override for # "operator-value", which uniquely identifies tests. use constant KNOWN_BROKEN => { - "equals-%group.<1-bug_group>%" => { - commenter => { contains => [1,2,3,4,5] }, - }, - - greaterthan => { GREATERTHAN_BROKEN }, - greaterthaneq => { GREATERTHAN_BROKEN }, - - 'allwordssubstr-<1>' => { ALLWORDS_BROKEN }, - 'allwords-<1>' => { - ALLWORDS_BROKEN, - }, - 'anywords-<1>' => { - 'flagtypes.name' => { contains => [1,2,3,4,5] }, - }, - 'anywords-<1> <2>' => { - 'flagtypes.name' => { contains => [3,4,5] }, - }, - 'anywordssubstr-<1> <2>' => { - 'flagtypes.name' => { contains => [3,4,5] }, - }, - - # setters.login_name and requestees.login name aren't tracked individually - # in bugs_activity, so can't be searched using this method. - # - # percentage_complete isn't tracked in bugs_activity (and it would be - # really hard to track). However, it adds a 0=0 term instead of using - # the changed* charts or simply denying them. - # - # delta_ts changedbefore/after should probably search for bugs based - # on their delta_ts. - # - # creation_ts changedbefore/after should search for bug creation dates. - # - # The commenter field changedbefore/after should search for comment - # creation dates. - # - # classification isn't being tracked properly in bugs_activity, I think. - # - # attach_data.thedata should search when attachments were created and - # who they were created by. - 'changedbefore' => { - CHANGED_BROKEN, - 'attach_data.thedata' => { contains => [1] }, - }, - 'changedafter' => { - 'attach_data.thedata' => { contains => [2,3,4] }, - classification => { contains => [2,3,4] }, - commenter => { contains => [2,3,4] }, - delta_ts => { contains => [2,3,4] }, - percentage_complete => { contains => [2,3,4] }, - 'requestees.login_name' => { contains => [2,3,4] }, - 'setters.login_name' => { contains => [2,3,4] }, - }, - changedfrom => { - CHANGED_BROKEN, - CHANGED_VALUE_BROKEN, - # All fields should have a way to search for "changing - # from a blank value" probably. - blocked => { contains => [3,4,5], no_criteria => 1 }, - dependson => { contains => [2,4,5], no_criteria => 1 }, - work_time => { contains => [1] }, - FIELD_TYPE_BUG_ID, { contains => [5], no_criteria => 1 }, - }, - # changeto doesn't find remaining_time changes (possibly due to us not - # tracking that data properly). - # - # multi-valued fields are stored as comma-separated strings, so you - # can't do changedfrom/to on them. - # - # Perhaps commenter can either tell you who the last commenter was, - # or if somebody commented at a given time (combined with other - # charts). - # - # longdesc changedto/from doesn't do anything; maybe it should. - # Same for attach_data.thedata. - changedto => { - CHANGED_BROKEN, - CHANGED_VALUE_BROKEN, - 'attach_data.thedata' => { contains => [1] }, - longdesc => { contains => [1] }, - remaining_time => { contains => [1] }, - }, - changedby => { - CHANGED_BROKEN, - # This should probably search the attacher or anybody who changed - # anything about an attachment at all. - 'attach_data.thedata' => { contains => [1] }, - # This should probably search the reporter. - creation_ts => { contains => [1] }, - }, - notequals => { - 'flagtypes.name' => { contains => [1, 5] }, - longdesc => { contains => [1] }, - }, - notregexp => { - 'flagtypes.name' => { contains => [1, 5] }, - longdesc => { contains => [1] }, - }, - notsubstring => { - 'flagtypes.name' => { contains => [5] }, - longdesc => { contains => [1] }, - }, - nowords => { - 'flagtypes.name' => { contains => [1, 5] }, - }, - nowordssubstr => { - 'flagtypes.name' => { contains => [5] }, - }, + "equals-%group.<1-bug_group>%" => {commenter => {contains => [1, 2, 3, 4, 5]},}, + + greaterthan => {GREATERTHAN_BROKEN}, + greaterthaneq => {GREATERTHAN_BROKEN}, + + 'allwordssubstr-<1>' => {ALLWORDS_BROKEN}, + 'allwords-<1>' => {ALLWORDS_BROKEN,}, + 'anywords-<1>' => {'flagtypes.name' => {contains => [1, 2, 3, 4, 5]},}, + 'anywords-<1> <2>' => {'flagtypes.name' => {contains => [3, 4, 5]},}, + 'anywordssubstr-<1> <2>' => {'flagtypes.name' => {contains => [3, 4, 5]},}, + + # setters.login_name and requestees.login name aren't tracked individually + # in bugs_activity, so can't be searched using this method. + # + # percentage_complete isn't tracked in bugs_activity (and it would be + # really hard to track). However, it adds a 0=0 term instead of using + # the changed* charts or simply denying them. + # + # delta_ts changedbefore/after should probably search for bugs based + # on their delta_ts. + # + # creation_ts changedbefore/after should search for bug creation dates. + # + # The commenter field changedbefore/after should search for comment + # creation dates. + # + # classification isn't being tracked properly in bugs_activity, I think. + # + # attach_data.thedata should search when attachments were created and + # who they were created by. + 'changedbefore' => + {CHANGED_BROKEN, 'attach_data.thedata' => {contains => [1]},}, + 'changedafter' => { + 'attach_data.thedata' => {contains => [2, 3, 4]}, + classification => {contains => [2, 3, 4]}, + commenter => {contains => [2, 3, 4]}, + delta_ts => {contains => [2, 3, 4]}, + percentage_complete => {contains => [2, 3, 4]}, + 'requestees.login_name' => {contains => [2, 3, 4]}, + 'setters.login_name' => {contains => [2, 3, 4]}, + }, + changedfrom => { + CHANGED_BROKEN, CHANGED_VALUE_BROKEN, + + # All fields should have a way to search for "changing + # from a blank value" probably. + blocked => {contains => [3, 4, 5], no_criteria => 1}, + dependson => {contains => [2, 4, 5], no_criteria => 1}, + work_time => {contains => [1]}, + FIELD_TYPE_BUG_ID, {contains => [5], no_criteria => 1}, + }, + + # changeto doesn't find remaining_time changes (possibly due to us not + # tracking that data properly). + # + # multi-valued fields are stored as comma-separated strings, so you + # can't do changedfrom/to on them. + # + # Perhaps commenter can either tell you who the last commenter was, + # or if somebody commented at a given time (combined with other + # charts). + # + # longdesc changedto/from doesn't do anything; maybe it should. + # Same for attach_data.thedata. + changedto => { + CHANGED_BROKEN, CHANGED_VALUE_BROKEN, + 'attach_data.thedata' => {contains => [1]}, + longdesc => {contains => [1]}, + remaining_time => {contains => [1]}, + }, + changedby => { + CHANGED_BROKEN, + + # This should probably search the attacher or anybody who changed + # anything about an attachment at all. + 'attach_data.thedata' => {contains => [1]}, + + # This should probably search the reporter. + creation_ts => {contains => [1]}, + }, + notequals => + {'flagtypes.name' => {contains => [1, 5]}, longdesc => {contains => [1]},}, + notregexp => + {'flagtypes.name' => {contains => [1, 5]}, longdesc => {contains => [1]},}, + notsubstring => + {'flagtypes.name' => {contains => [5]}, longdesc => {contains => [1]},}, + nowords => {'flagtypes.name' => {contains => [1, 5]},}, + nowordssubstr => {'flagtypes.name' => {contains => [5]},}, }; ################### @@ -368,135 +349,90 @@ use constant KNOWN_BROKEN => { # Common BROKEN_NOT values for the changed* fields. use constant CHANGED_BROKEN_NOT => ( - "attach_data.thedata" => { contains => [1] }, - "classification" => { contains => [1] }, - "commenter" => { contains => [1] }, - "delta_ts" => { contains => [1] }, - percentage_complete => { contains => [1] }, - "requestees.login_name" => { contains => [1] }, - "setters.login_name" => { contains => [1] }, + "attach_data.thedata" => {contains => [1]}, + "classification" => {contains => [1]}, + "commenter" => {contains => [1]}, + "delta_ts" => {contains => [1]}, + percentage_complete => {contains => [1]}, + "requestees.login_name" => {contains => [1]}, + "setters.login_name" => {contains => [1]}, ); # For changedfrom and changedto. use constant CHANGED_FROM_TO_BROKEN_NOT => ( - 'longdescs.count' => { search => 1 }, - "bug_group" => { contains => [1] }, - "cc" => { contains => [1] }, - "estimated_time" => { contains => [1] }, - "flagtypes.name" => { contains => [1] }, - "keywords" => { contains => [1] }, - FIELD_TYPE_MULTI_SELECT, { contains => [1] }, + 'longdescs.count' => {search => 1}, + "bug_group" => {contains => [1]}, + "cc" => {contains => [1]}, + "estimated_time" => {contains => [1]}, + "flagtypes.name" => {contains => [1]}, + "keywords" => {contains => [1]}, + FIELD_TYPE_MULTI_SELECT, {contains => [1]}, ); # These are field/operator combinations that are broken when run under NOT(). use constant BROKEN_NOT => { - allwords => { - cc => { contains => [1] }, - 'flagtypes.name' => { contains => [1, 5] }, - longdesc => { contains => [1] }, - }, - 'allwords-<1> <2>' => { - cc => { }, - }, - allwordssubstr => { - cc => { contains => [1] }, - 'flagtypes.name' => { contains => [5, 6] }, - longdesc => { contains => [1] }, - }, - 'allwordssubstr-<1>,<2>' => { - cc => { }, - longdesc => { contains => [1] }, - }, - anyexact => { - 'flagtypes.name' => { contains => [1, 2, 5] }, - }, - 'anywords-<1>' => { - 'flagtypes.name' => { contains => [1, 2, 3, 4, 5] }, - }, - 'anywords-<1> <2>' => { - 'flagtypes.name' => { contains => [3, 4, 5] }, - }, - anywordssubstr => { - 'flagtypes.name' => { contains => [5] }, - }, - 'anywordssubstr-<1> <2>' => { - 'flagtypes.name' => { contains => [3,4,5] }, - }, - casesubstring => { - 'flagtypes.name' => { contains => [5] }, - }, - changedafter => { - "attach_data.thedata" => { contains => [2, 3, 4] }, - "classification" => { contains => [2, 3, 4] }, - "commenter" => { contains => [2, 3, 4] }, - percentage_complete => { contains => [2, 3, 4] }, - "delta_ts" => { contains => [2, 3, 4] }, - "requestees.login_name" => { contains => [2, 3, 4] }, - "setters.login_name" => { contains => [2, 3, 4] }, - }, - changedbefore => { - CHANGED_BROKEN_NOT, - }, - changedby => { - CHANGED_BROKEN_NOT, - creation_ts => { contains => [1] }, - work_time => { contains => [1] }, - }, - changedfrom => { - CHANGED_BROKEN_NOT, - CHANGED_FROM_TO_BROKEN_NOT, - 'attach_data.thedata' => { }, - blocked => { contains => [1, 2] }, - dependson => { contains => [1, 3] }, - work_time => { contains => [1] }, - FIELD_TYPE_BUG_ID, { contains => [1 .. 4] }, - }, - changedto => { - CHANGED_BROKEN_NOT, - CHANGED_FROM_TO_BROKEN_NOT, - longdesc => { contains => [1] }, - "remaining_time" => { contains => [1] }, - }, - greaterthan => { - cc => { contains => [1] }, - 'flagtypes.name' => { contains => [5] }, - }, - greaterthaneq => { - cc => { contains => [1] }, - 'flagtypes.name' => { contains => [2, 5] }, - }, - equals => { - 'flagtypes.name' => { contains => [1, 5] }, - }, - notequals => { - longdesc => { contains => [1] }, - }, - notregexp => { - longdesc => { contains => [1] }, - }, - notsubstring => { - longdesc => { contains => [1] }, - }, - 'nowords-<1>' => { - 'flagtypes.name' => { contains => [5] }, - }, - 'nowordssubstr-<1>' => { - 'flagtypes.name' => { contains => [5] }, - }, - lessthan => { - 'flagtypes.name' => { contains => [5] }, - }, - lessthaneq => { - 'flagtypes.name' => { contains => [1, 5] }, - }, - regexp => { - 'flagtypes.name' => { contains => [1, 5] }, - longdesc => { contains => [1] }, - }, - substring => { - 'flagtypes.name' => { contains => [5] }, - longdesc => { contains => [1] }, - }, + allwords => { + cc => {contains => [1]}, + 'flagtypes.name' => {contains => [1, 5]}, + longdesc => {contains => [1]}, + }, + 'allwords-<1> <2>' => {cc => {},}, + allwordssubstr => { + cc => {contains => [1]}, + 'flagtypes.name' => {contains => [5, 6]}, + longdesc => {contains => [1]}, + }, + 'allwordssubstr-<1>,<2>' => {cc => {}, longdesc => {contains => [1]},}, + anyexact => {'flagtypes.name' => {contains => [1, 2, 5]},}, + 'anywords-<1>' => {'flagtypes.name' => {contains => [1, 2, 3, 4, 5]},}, + 'anywords-<1> <2>' => {'flagtypes.name' => {contains => [3, 4, 5]},}, + anywordssubstr => {'flagtypes.name' => {contains => [5]},}, + 'anywordssubstr-<1> <2>' => {'flagtypes.name' => {contains => [3, 4, 5]},}, + casesubstring => {'flagtypes.name' => {contains => [5]},}, + changedafter => { + "attach_data.thedata" => {contains => [2, 3, 4]}, + "classification" => {contains => [2, 3, 4]}, + "commenter" => {contains => [2, 3, 4]}, + percentage_complete => {contains => [2, 3, 4]}, + "delta_ts" => {contains => [2, 3, 4]}, + "requestees.login_name" => {contains => [2, 3, 4]}, + "setters.login_name" => {contains => [2, 3, 4]}, + }, + changedbefore => {CHANGED_BROKEN_NOT,}, + changedby => { + CHANGED_BROKEN_NOT, + creation_ts => {contains => [1]}, + work_time => {contains => [1]}, + }, + changedfrom => { + CHANGED_BROKEN_NOT, CHANGED_FROM_TO_BROKEN_NOT, + 'attach_data.thedata' => {}, + blocked => {contains => [1, 2]}, + dependson => {contains => [1, 3]}, + work_time => {contains => [1]}, + FIELD_TYPE_BUG_ID, {contains => [1 .. 4]}, + }, + changedto => { + CHANGED_BROKEN_NOT, CHANGED_FROM_TO_BROKEN_NOT, + longdesc => {contains => [1]}, + "remaining_time" => {contains => [1]}, + }, + greaterthan => + {cc => {contains => [1]}, 'flagtypes.name' => {contains => [5]},}, + greaterthaneq => + {cc => {contains => [1]}, 'flagtypes.name' => {contains => [2, 5]},}, + equals => {'flagtypes.name' => {contains => [1, 5]},}, + notequals => {longdesc => {contains => [1]},}, + notregexp => {longdesc => {contains => [1]},}, + notsubstring => {longdesc => {contains => [1]},}, + 'nowords-<1>' => {'flagtypes.name' => {contains => [5]},}, + 'nowordssubstr-<1>' => {'flagtypes.name' => {contains => [5]},}, + lessthan => {'flagtypes.name' => {contains => [5]},}, + lessthaneq => {'flagtypes.name' => {contains => [1, 5]},}, + regexp => + {'flagtypes.name' => {contains => [1, 5]}, longdesc => {contains => [1]},}, + substring => + {'flagtypes.name' => {contains => [5]}, longdesc => {contains => [1]},}, }; ############# @@ -507,113 +443,117 @@ use constant BROKEN_NOT => { # Regex tests need unique test values for certain fields. use constant REGEX_OVERRIDE => { - 'attachments.mimetype' => { value => '^text/x-1-' }, - bug_file_loc => { value => '^http://1-' }, - see_also => { value => '^http://1-' }, - blocked => { value => '^<1>$' }, - dependson => { value => '^<1>$' }, - bug_id => { value => '^<1>$' }, - 'attachments.isobsolete' => { value => '^1'}, - 'attachments.ispatch' => { value => '^1'}, - 'attachments.isprivate' => { value => '^1' }, - cclist_accessible => { value => '^1' }, - reporter_accessible => { value => '^1' }, - everconfirmed => { value => '^1' }, - 'longdescs.count' => { value => '^3' }, - 'longdescs.isprivate' => { value => '^1' }, - creation_ts => { value => '^2037-01-01' }, - delta_ts => { value => '^2037-01-01' }, - deadline => { value => '^2037-02-01' }, - estimated_time => { value => '^1.0' }, - remaining_time => { value => '^9.0' }, - work_time => { value => '^1.0' }, - longdesc => { value => '^1-' }, - percentage_complete => { value => '^10' }, - FIELD_TYPE_BUG_ID, { value => '^<1>$' }, - FIELD_TYPE_DATETIME, { value => '^2037-03-01' } + 'attachments.mimetype' => {value => '^text/x-1-'}, + bug_file_loc => {value => '^http://1-'}, + see_also => {value => '^http://1-'}, + blocked => {value => '^<1>$'}, + dependson => {value => '^<1>$'}, + bug_id => {value => '^<1>$'}, + 'attachments.isobsolete' => {value => '^1'}, + 'attachments.ispatch' => {value => '^1'}, + 'attachments.isprivate' => {value => '^1'}, + cclist_accessible => {value => '^1'}, + reporter_accessible => {value => '^1'}, + everconfirmed => {value => '^1'}, + 'longdescs.count' => {value => '^3'}, + 'longdescs.isprivate' => {value => '^1'}, + creation_ts => {value => '^2037-01-01'}, + delta_ts => {value => '^2037-01-01'}, + deadline => {value => '^2037-02-01'}, + estimated_time => {value => '^1.0'}, + remaining_time => {value => '^9.0'}, + work_time => {value => '^1.0'}, + longdesc => {value => '^1-'}, + percentage_complete => {value => '^10'}, + FIELD_TYPE_BUG_ID, {value => '^<1>$'}, FIELD_TYPE_DATETIME, + {value => '^2037-03-01'} }; # Common overrides between lessthan and lessthaneq. use constant LESSTHAN_OVERRIDE => ( - alias => { contains => [1,5] }, - estimated_time => { contains => [1,5] }, - qa_contact => { contains => [1,5] }, - resolution => { contains => [1,5] }, - status_whiteboard => { contains => [1,5] }, - FIELD_TYPE_TEXTAREA, { contains => [1,5] }, - FIELD_TYPE_FREETEXT, { contains => [1,5] }, + alias => {contains => [1, 5]}, + estimated_time => {contains => [1, 5]}, + qa_contact => {contains => [1, 5]}, + resolution => {contains => [1, 5]}, + status_whiteboard => {contains => [1, 5]}, + FIELD_TYPE_TEXTAREA, {contains => [1, 5]}, FIELD_TYPE_FREETEXT, + {contains => [1, 5]}, ); # The mandatorily-set fields have values higher than <1>, # so bug 5 shows up. use constant GREATERTHAN_OVERRIDE => ( - classification => { contains => [2,3,4,5] }, - assigned_to => { contains => [2,3,4,5] }, - bug_id => { contains => [2,3,4,5] }, - bug_group => { contains => [1,2,3,4] }, - bug_severity => { contains => [2,3,4,5] }, - bug_status => { contains => [2,3,4,5] }, - component => { contains => [2,3,4,5] }, - commenter => { contains => [2,3,4,5] }, - # keywords matches if *any* keyword matches - keywords => { contains => [1,2,3,4] }, - longdesc => { contains => [1,2,3,4] }, - op_sys => { contains => [2,3,4,5] }, - priority => { contains => [2,3,4,5] }, - product => { contains => [2,3,4,5] }, - reporter => { contains => [2,3,4,5] }, - rep_platform => { contains => [2,3,4,5] }, - short_desc => { contains => [2,3,4,5] }, - version => { contains => [2,3,4,5] }, - tag => { contains => [1,2,3,4] }, - target_milestone => { contains => [2,3,4,5] }, - # Bug 2 is the only bug besides 1 that has a Requestee set. - 'requestees.login_name' => { contains => [2] }, - FIELD_TYPE_SINGLE_SELECT, { contains => [2,3,4,5] }, - # Override SINGLE_SELECT for resolution. - resolution => { contains => [2,3,4] }, - # MULTI_SELECTs match if *any* value matches - FIELD_TYPE_MULTI_SELECT, { contains => [1,2,3,4] }, + classification => {contains => [2, 3, 4, 5]}, + assigned_to => {contains => [2, 3, 4, 5]}, + bug_id => {contains => [2, 3, 4, 5]}, + bug_group => {contains => [1, 2, 3, 4]}, + bug_severity => {contains => [2, 3, 4, 5]}, + bug_status => {contains => [2, 3, 4, 5]}, + component => {contains => [2, 3, 4, 5]}, + commenter => {contains => [2, 3, 4, 5]}, + + # keywords matches if *any* keyword matches + keywords => {contains => [1, 2, 3, 4]}, + longdesc => {contains => [1, 2, 3, 4]}, + op_sys => {contains => [2, 3, 4, 5]}, + priority => {contains => [2, 3, 4, 5]}, + product => {contains => [2, 3, 4, 5]}, + reporter => {contains => [2, 3, 4, 5]}, + rep_platform => {contains => [2, 3, 4, 5]}, + short_desc => {contains => [2, 3, 4, 5]}, + version => {contains => [2, 3, 4, 5]}, + tag => {contains => [1, 2, 3, 4]}, + target_milestone => {contains => [2, 3, 4, 5]}, + + # Bug 2 is the only bug besides 1 that has a Requestee set. + 'requestees.login_name' => {contains => [2]}, + FIELD_TYPE_SINGLE_SELECT, {contains => [2, 3, 4, 5]}, + + # Override SINGLE_SELECT for resolution. + resolution => {contains => [2, 3, 4]}, + + # MULTI_SELECTs match if *any* value matches + FIELD_TYPE_MULTI_SELECT, {contains => [1, 2, 3, 4]}, ); # For all positive multi-value types. use constant MULTI_BOOLEAN_OVERRIDE => ( - 'attachments.ispatch' => { value => '1,1', contains => [1] }, - 'attachments.isobsolete' => { value => '1,1', contains => [1] }, - 'attachments.isprivate' => { value => '1,1', contains => [1] }, - cclist_accessible => { value => '1,1', contains => [1] }, - reporter_accessible => { value => '1,1', contains => [1] }, - 'longdescs.isprivate' => { value => '1,1', contains => [1] }, - everconfirmed => { value => '1,1', contains => [1] }, + 'attachments.ispatch' => {value => '1,1', contains => [1]}, + 'attachments.isobsolete' => {value => '1,1', contains => [1]}, + 'attachments.isprivate' => {value => '1,1', contains => [1]}, + cclist_accessible => {value => '1,1', contains => [1]}, + reporter_accessible => {value => '1,1', contains => [1]}, + 'longdescs.isprivate' => {value => '1,1', contains => [1]}, + everconfirmed => {value => '1,1', contains => [1]}, ); # Same as above, for negative multi-value types. use constant NEGATIVE_MULTI_BOOLEAN_OVERRIDE => ( - 'attachments.ispatch' => { value => '1,1', contains => [2,3,4,5] }, - 'attachments.isobsolete' => { value => '1,1', contains => [2,3,4,5] }, - 'attachments.isprivate' => { value => '1,1', contains => [2,3,4,5] }, - cclist_accessible => { value => '1,1', contains => [2,3,4,5] }, - reporter_accessible => { value => '1,1', contains => [2,3,4,5] }, - 'longdescs.isprivate' => { value => '1,1', contains => [2,3,4,5] }, - everconfirmed => { value => '1,1', contains => [2,3,4,5] }, + 'attachments.ispatch' => {value => '1,1', contains => [2, 3, 4, 5]}, + 'attachments.isobsolete' => {value => '1,1', contains => [2, 3, 4, 5]}, + 'attachments.isprivate' => {value => '1,1', contains => [2, 3, 4, 5]}, + cclist_accessible => {value => '1,1', contains => [2, 3, 4, 5]}, + reporter_accessible => {value => '1,1', contains => [2, 3, 4, 5]}, + 'longdescs.isprivate' => {value => '1,1', contains => [2, 3, 4, 5]}, + everconfirmed => {value => '1,1', contains => [2, 3, 4, 5]}, ); # For anyexact and anywordssubstr use constant ANY_OVERRIDE => ( - 'longdescs.count' => { contains => [1,2,3,4] }, - 'work_time' => { value => '1.0,2.0' }, - dependson => { value => '<1>,<3>', contains => [1,3] }, - MULTI_BOOLEAN_OVERRIDE, + 'longdescs.count' => {contains => [1, 2, 3, 4]}, + 'work_time' => {value => '1.0,2.0'}, + dependson => {value => '<1>,<3>', contains => [1, 3]}, + MULTI_BOOLEAN_OVERRIDE, ); # For all the changed* searches. The ones that have empty contains # are fields that never change in value, or will never be rationally # tracked in bugs_activity. use constant CHANGED_OVERRIDE => ( - 'attachments.submitter' => { contains => [] }, - bug_id => { contains => [] }, - reporter => { contains => [] }, - tag => { contains => [] }, + 'attachments.submitter' => {contains => []}, + bug_id => {contains => []}, + reporter => {contains => []}, + tag => {contains => []}, ); ######### @@ -673,298 +613,326 @@ use constant CHANGED_OVERRIDE => ( # override: This allows you to override "contains" and "values" for # certain fields. use constant TESTS => { - equals => [ - { contains => [1], value => '<1>' }, - ], - notequals => [ - { contains => [2,3,4,5], value => '<1>' }, - ], - substring => [ - { contains => [1], value => '<1>', - override => { - percentage_complete => { contains => [1,2,3] }, - } - }, - ], - casesubstring => [ - { contains => [1], value => '<1>', - override => { - percentage_complete => { contains => [1,2,3] }, - } - }, - { contains => [], value => '<1>', transform => sub { lc($_[0]) }, - extra_name => 'lc', if_equal => { contains => [1] }, - override => { - percentage_complete => { contains => [1,2,3] }, - } - }, - ], - notsubstring => [ - { contains => [2,3,4,5], value => '<1>', - override => { - percentage_complete => { contains => [4,5] }, - }, - } - ], - regexp => [ - { contains => [1], value => '<1>', escape => 1, - override => { - percentage_complete => { value => '^10' }, - } - }, - { contains => [1], value => '^1-', override => REGEX_OVERRIDE }, - ], - notregexp => [ - { contains => [2,3,4,5], value => '<1>', escape => 1, - override => { - percentage_complete => { value => '^10' }, - } - }, - { contains => [2,3,4,5], value => '^1-', override => REGEX_OVERRIDE }, - ], - lessthan => [ - { contains => [1], value => 2, - override => { - # A lot of these contain bug 5 because an empty value is validly - # less than the specified value. - bug_file_loc => { value => 'http://2-', contains => [1,5] }, - see_also => { value => 'http://2-' }, - 'attachments.mimetype' => { value => 'text/x-2-' }, - blocked => { value => '<4-id>', contains => [1,2] }, - dependson => { value => '<3-id>', contains => [1,3] }, - bug_id => { value => '<2-id>' }, - 'attachments.isprivate' => { value => 1, contains => [2,3,4] }, - 'attachments.isobsolete' => { value => 1, contains => [2,3,4] }, - 'attachments.ispatch' => { value => 1, contains => [2,3,4] }, - cclist_accessible => { value => 1, contains => [2,3,4,5] }, - reporter_accessible => { value => 1, contains => [2,3,4,5] }, - 'longdescs.count' => { value => 3, contains => [2,3,4,5] }, - 'longdescs.isprivate' => { value => 1, contains => [1,2,3,4,5] }, - everconfirmed => { value => 1, contains => [2,3,4,5] }, - creation_ts => { value => '2037-01-02', contains => [1,5] }, - delta_ts => { value => '2037-01-02', contains => [1,5] }, - deadline => { value => '2037-02-02', contains => [1,5] }, - remaining_time => { value => 10, contains => [1,5] }, - percentage_complete => { value => 11, contains => [1,5] }, - longdesc => { value => '2-', contains => [1,5] }, - work_time => { value => 1, contains => [5] }, - FIELD_TYPE_BUG_ID, { value => '<2>', contains => [1,5] }, - FIELD_TYPE_DATETIME, { value => '2037-03-02', contains => [1,5] }, - LESSTHAN_OVERRIDE, - } - }, - ], - lessthaneq => [ - { contains => [1], value => '<1>', - override => { - 'attachments.isobsolete' => { value => 0, contains => [2,3,4] }, - 'attachments.ispatch' => { value => 0, contains => [2,3,4] }, - 'attachments.isprivate' => { value => 0, contains => [2,3,4] }, - cclist_accessible => { value => 0, contains => [2,3,4,5] }, - reporter_accessible => { value => 0, contains => [2,3,4,5] }, - 'longdescs.count' => { value => 2, contains => [2,3,4,5] }, - 'longdescs.isprivate' => { value => -1, contains => [] }, - everconfirmed => { value => 0, contains => [2,3,4,5] }, - bug_file_loc => { contains => [1,5] }, - blocked => { contains => [1,2] }, - deadline => { contains => [1,5] }, - dependson => { contains => [1,3] }, - creation_ts => { contains => [1,5] }, - delta_ts => { contains => [1,5] }, - remaining_time => { contains => [1,5] }, - longdesc => { contains => [1,5] }, - percentage_complete => { contains => [1,5] }, - work_time => { value => 1, contains => [1,5] }, - FIELD_TYPE_BUG_ID, { contains => [1,5] }, - FIELD_TYPE_DATETIME, { contains => [1,5] }, - LESSTHAN_OVERRIDE, - }, - }, - ], - greaterthan => [ - { contains => [2,3,4], value => '<1>', - override => { - dependson => { contains => [3] }, - blocked => { contains => [2] }, - 'attachments.ispatch' => { value => 0, contains => [1] }, - 'attachments.isobsolete' => { value => 0, contains => [1] }, - 'attachments.isprivate' => { value => 0, contains => [1] }, - cclist_accessible => { value => 0, contains => [1] }, - reporter_accessible => { value => 0, contains => [1] }, - 'longdescs.count' => { value => 2, contains => [1] }, - 'longdescs.isprivate' => { value => 0, contains => [1] }, - everconfirmed => { value => 0, contains => [1] }, - 'flagtypes.name' => { value => 2, contains => [2,3,4] }, - GREATERTHAN_OVERRIDE, - }, - }, - ], - greaterthaneq => [ - { contains => [2,3,4], value => '<2>', - override => { - 'attachments.ispatch' => { value => 1, contains => [1] }, - 'attachments.isobsolete' => { value => 1, contains => [1] }, - 'attachments.isprivate' => { value => 1, contains => [1] }, - cclist_accessible => { value => 1, contains => [1] }, - reporter_accessible => { value => 1, contains => [1] }, - 'longdescs.count' => { value => 3, contains => [1] }, - 'longdescs.isprivate' => { value => 1, contains => [1] }, - everconfirmed => { value => 1, contains => [1] }, - dependson => { value => '<3>', contains => [1,3] }, - blocked => { contains => [1,2] }, - GREATERTHAN_OVERRIDE, - } - }, - ], - matches => [ - { contains => [1], value => '<1>' }, - ], - notmatches => [ - { contains => [2,3,4,5], value => '<1>' }, - ], - anyexact => [ - { contains => [1,2], value => '<1>, <2>', - override => { ANY_OVERRIDE } }, - ], - anywordssubstr => [ - { contains => [1,2], value => '<1> <2>', - override => { - ANY_OVERRIDE, - percentage_complete => { contains => [1,2,3] }, - } - }, - ], - allwordssubstr => [ - { contains => [1], value => '<1>', - override => { - MULTI_BOOLEAN_OVERRIDE, - # We search just the number "1" for percentage_complete, - # which matches a lot of bugs. - percentage_complete => { contains => [1,2,3] }, - }, - }, - { contains => [], value => '<1>,<2>', - override => { - dependson => { value => '<1-id> <3-id>', contains => [] }, - # bug 3 has the value "21" here, so matches "2,1" - percentage_complete => { value => '<2>,<3>', contains => [3] }, - # 1 0 matches bug 1, which has both public and private comments. - 'longdescs.isprivate' => { contains => [1] }, - } - }, - ], - nowordssubstr => [ - { contains => [2,3,4,5], value => '<1>', - override => { - # longdescs.isprivate translates to "1 0", so no bugs should - # show up. - 'longdescs.isprivate' => { contains => [] }, - percentage_complete => { contains => [4,5] }, - work_time => { contains => [2,3,4,5] }, - } - }, - ], - anywords => [ - { contains => [1], value => '<1>', - override => { - MULTI_BOOLEAN_OVERRIDE, - } - }, - { contains => [1,2], value => '<1> <2>', - override => { - MULTI_BOOLEAN_OVERRIDE, - dependson => { value => '<1> <3>', contains => [1,3] }, - 'longdescs.count' => { contains => [1,2,3,4] }, - }, - }, - ], - allwords => [ - { contains => [1], value => '<1>', - override => { MULTI_BOOLEAN_OVERRIDE } }, - { contains => [], value => '<1> <2>', - override => { - dependson => { contains => [], value => '<2-id> <3-id>' }, - # 1 0 matches bug 1, which has both public and private comments. - 'longdescs.isprivate' => { contains => [1] }, - } - }, - ], - nowords => [ - { contains => [2,3,4,5], value => '<1>', - override => { - # longdescs.isprivate translates to "1 0", so no bugs should - # show up. - 'longdescs.isprivate' => { contains => [] }, - work_time => { contains => [2,3,4,5] }, - } - }, - ], - - changedbefore => [ - { contains => [1], value => '<1-delta>', - override => { - CHANGED_OVERRIDE, - creation_ts => { contains => [1,5] }, - blocked => { contains => [1,2] }, - dependson => { contains => [1,3] }, - longdesc => { contains => [1,5] }, - 'longdescs.count' => { contains => [1,5] }, - } - }, - ], - changedafter => [ - { contains => [2,3,4], value => '<2-delta>', - override => { - CHANGED_OVERRIDE, - creation_ts => { contains => [3,4] }, - # We only change this for one bug, and it doesn't match. - 'longdescs.isprivate' => { contains => [] }, - # Same for everconfirmed. - 'everconfirmed' => { contains => [] }, - # For blocked and dependson, they have the delta_ts of bug1 - # in the bugs_activity table, so they won't ever match. - blocked => { contains => [] }, - dependson => { contains => [] }, - } - }, - ], - changedfrom => [ - { contains => [1], value => '<1>', - override => { - CHANGED_OVERRIDE, - # The test never changes an already-set dependency field, but - # we *can* attempt to test searching against an empty value, - # which should get us some bugs. - blocked => { value => '', contains => [1,2] }, - dependson => { value => '', contains => [1,3] }, - FIELD_TYPE_BUG_ID, { value => '', contains => [1,2,3,4] }, - # longdesc changedfrom doesn't make any sense. - longdesc => { contains => [] }, - # Nor does creation_ts changedfrom. - creation_ts => { contains => [] }, - 'attach_data.thedata' => { contains => [] }, - bug_id => { value => '<1-id>', contains => [] }, - }, - }, - ], - changedto => [ - { contains => [1], value => '<1>', - override => { - CHANGED_OVERRIDE, - # I can't imagine any use for creation_ts changedto. - creation_ts => { contains => [] }, - } - }, - ], - changedby => [ - { contains => [1], value => '<1-reporter>', - override => { - CHANGED_OVERRIDE, - blocked => { contains => [1,2] }, - dependson => { contains => [1,3] }, - }, - }, - ], + equals => [{contains => [1], value => '<1>'},], + notequals => [{contains => [2, 3, 4, 5], value => '<1>'},], + substring => [ + { + contains => [1], + value => '<1>', + override => {percentage_complete => {contains => [1, 2, 3]},} + }, + ], + casesubstring => [ + { + contains => [1], + value => '<1>', + override => {percentage_complete => {contains => [1, 2, 3]},} + }, + { + contains => [], + value => '<1>', + transform => sub { lc($_[0]) }, + extra_name => 'lc', + if_equal => {contains => [1]}, + override => {percentage_complete => {contains => [1, 2, 3]},} + }, + ], + notsubstring => [{ + contains => [2, 3, 4, 5], + value => '<1>', + override => {percentage_complete => {contains => [4, 5]},}, + }], + regexp => [ + { + contains => [1], + value => '<1>', + escape => 1, + override => {percentage_complete => {value => '^10'},} + }, + {contains => [1], value => '^1-', override => REGEX_OVERRIDE}, + ], + notregexp => [ + { + contains => [2, 3, 4, 5], + value => '<1>', + escape => 1, + override => {percentage_complete => {value => '^10'},} + }, + {contains => [2, 3, 4, 5], value => '^1-', override => REGEX_OVERRIDE}, + ], + lessthan => [ + { + contains => [1], + value => 2, + override => { + + # A lot of these contain bug 5 because an empty value is validly + # less than the specified value. + bug_file_loc => {value => 'http://2-', contains => [1, 5]}, + see_also => {value => 'http://2-'}, + 'attachments.mimetype' => {value => 'text/x-2-'}, + blocked => {value => '<4-id>', contains => [1, 2]}, + dependson => {value => '<3-id>', contains => [1, 3]}, + bug_id => {value => '<2-id>'}, + 'attachments.isprivate' => {value => 1, contains => [2, 3, 4]}, + 'attachments.isobsolete' => {value => 1, contains => [2, 3, 4]}, + 'attachments.ispatch' => {value => 1, contains => [2, 3, 4]}, + cclist_accessible => {value => 1, contains => [2, 3, 4, 5]}, + reporter_accessible => {value => 1, contains => [2, 3, 4, 5]}, + 'longdescs.count' => {value => 3, contains => [2, 3, 4, 5]}, + 'longdescs.isprivate' => {value => 1, contains => [1, 2, 3, 4, 5]}, + everconfirmed => {value => 1, contains => [2, 3, 4, 5]}, + creation_ts => {value => '2037-01-02', contains => [1, 5]}, + delta_ts => {value => '2037-01-02', contains => [1, 5]}, + deadline => {value => '2037-02-02', contains => [1, 5]}, + remaining_time => {value => 10, contains => [1, 5]}, + percentage_complete => {value => 11, contains => [1, 5]}, + longdesc => {value => '2-', contains => [1, 5]}, + work_time => {value => 1, contains => [5]}, + FIELD_TYPE_BUG_ID, {value => '<2>', contains => [1, 5]}, FIELD_TYPE_DATETIME, + {value => '2037-03-02', contains => [1, 5]}, LESSTHAN_OVERRIDE, + } + }, + ], + lessthaneq => [ + { + contains => [1], + value => '<1>', + override => { + 'attachments.isobsolete' => {value => 0, contains => [2, 3, 4]}, + 'attachments.ispatch' => {value => 0, contains => [2, 3, 4]}, + 'attachments.isprivate' => {value => 0, contains => [2, 3, 4]}, + cclist_accessible => {value => 0, contains => [2, 3, 4, 5]}, + reporter_accessible => {value => 0, contains => [2, 3, 4, 5]}, + 'longdescs.count' => {value => 2, contains => [2, 3, 4, 5]}, + 'longdescs.isprivate' => {value => -1, contains => []}, + everconfirmed => {value => 0, contains => [2, 3, 4, 5]}, + bug_file_loc => {contains => [1, 5]}, + blocked => {contains => [1, 2]}, + deadline => {contains => [1, 5]}, + dependson => {contains => [1, 3]}, + creation_ts => {contains => [1, 5]}, + delta_ts => {contains => [1, 5]}, + remaining_time => {contains => [1, 5]}, + longdesc => {contains => [1, 5]}, + percentage_complete => {contains => [1, 5]}, + work_time => {value => 1, contains => [1, 5]}, + FIELD_TYPE_BUG_ID, {contains => [1, 5]}, FIELD_TYPE_DATETIME, + {contains => [1, 5]}, LESSTHAN_OVERRIDE, + }, + }, + ], + greaterthan => [ + { + contains => [2, 3, 4], + value => '<1>', + override => { + dependson => {contains => [3]}, + blocked => {contains => [2]}, + 'attachments.ispatch' => {value => 0, contains => [1]}, + 'attachments.isobsolete' => {value => 0, contains => [1]}, + 'attachments.isprivate' => {value => 0, contains => [1]}, + cclist_accessible => {value => 0, contains => [1]}, + reporter_accessible => {value => 0, contains => [1]}, + 'longdescs.count' => {value => 2, contains => [1]}, + 'longdescs.isprivate' => {value => 0, contains => [1]}, + everconfirmed => {value => 0, contains => [1]}, + 'flagtypes.name' => {value => 2, contains => [2, 3, 4]}, + GREATERTHAN_OVERRIDE, + }, + }, + ], + greaterthaneq => [ + { + contains => [2, 3, 4], + value => '<2>', + override => { + 'attachments.ispatch' => {value => 1, contains => [1]}, + 'attachments.isobsolete' => {value => 1, contains => [1]}, + 'attachments.isprivate' => {value => 1, contains => [1]}, + cclist_accessible => {value => 1, contains => [1]}, + reporter_accessible => {value => 1, contains => [1]}, + 'longdescs.count' => {value => 3, contains => [1]}, + 'longdescs.isprivate' => {value => 1, contains => [1]}, + everconfirmed => {value => 1, contains => [1]}, + dependson => {value => '<3>', contains => [1, 3]}, + blocked => {contains => [1, 2]}, + GREATERTHAN_OVERRIDE, + } + }, + ], + matches => [{contains => [1], value => '<1>'},], + notmatches => [{contains => [2, 3, 4, 5], value => '<1>'},], + anyexact => + [{contains => [1, 2], value => '<1>, <2>', override => {ANY_OVERRIDE}},], + anywordssubstr => [ + { + contains => [1, 2], + value => '<1> <2>', + override => {ANY_OVERRIDE, percentage_complete => {contains => [1, 2, 3]},} + }, + ], + allwordssubstr => [ + { + contains => [1], + value => '<1>', + override => { + MULTI_BOOLEAN_OVERRIDE, + + # We search just the number "1" for percentage_complete, + # which matches a lot of bugs. + percentage_complete => {contains => [1, 2, 3]}, + }, + }, + { + contains => [], + value => '<1>,<2>', + override => { + dependson => {value => '<1-id> <3-id>', contains => []}, + + # bug 3 has the value "21" here, so matches "2,1" + percentage_complete => {value => '<2>,<3>', contains => [3]}, + + # 1 0 matches bug 1, which has both public and private comments. + 'longdescs.isprivate' => {contains => [1]}, + } + }, + ], + nowordssubstr => [ + { + contains => [2, 3, 4, 5], + value => '<1>', + override => { + + # longdescs.isprivate translates to "1 0", so no bugs should + # show up. + 'longdescs.isprivate' => {contains => []}, + percentage_complete => {contains => [4, 5]}, + work_time => {contains => [2, 3, 4, 5]}, + } + }, + ], + anywords => [ + {contains => [1], value => '<1>', override => {MULTI_BOOLEAN_OVERRIDE,}}, + { + contains => [1, 2], + value => '<1> <2>', + override => { + MULTI_BOOLEAN_OVERRIDE, + dependson => {value => '<1> <3>', contains => [1, 3]}, + 'longdescs.count' => {contains => [1, 2, 3, 4]}, + }, + }, + ], + allwords => [ + {contains => [1], value => '<1>', override => {MULTI_BOOLEAN_OVERRIDE}}, + { + contains => [], + value => '<1> <2>', + override => { + dependson => {contains => [], value => '<2-id> <3-id>'}, + + # 1 0 matches bug 1, which has both public and private comments. + 'longdescs.isprivate' => {contains => [1]}, + } + }, + ], + nowords => [ + { + contains => [2, 3, 4, 5], + value => '<1>', + override => { + + # longdescs.isprivate translates to "1 0", so no bugs should + # show up. + 'longdescs.isprivate' => {contains => []}, + work_time => {contains => [2, 3, 4, 5]}, + } + }, + ], + + changedbefore => [ + { + contains => [1], + value => '<1-delta>', + override => { + CHANGED_OVERRIDE, + creation_ts => {contains => [1, 5]}, + blocked => {contains => [1, 2]}, + dependson => {contains => [1, 3]}, + longdesc => {contains => [1, 5]}, + 'longdescs.count' => {contains => [1, 5]}, + } + }, + ], + changedafter => [ + { + contains => [2, 3, 4], + value => '<2-delta>', + override => { + CHANGED_OVERRIDE, + creation_ts => {contains => [3, 4]}, + + # We only change this for one bug, and it doesn't match. + 'longdescs.isprivate' => {contains => []}, + + # Same for everconfirmed. + 'everconfirmed' => {contains => []}, + + # For blocked and dependson, they have the delta_ts of bug1 + # in the bugs_activity table, so they won't ever match. + blocked => {contains => []}, + dependson => {contains => []}, + } + }, + ], + changedfrom => [ + { + contains => [1], + value => '<1>', + override => { + CHANGED_OVERRIDE, + + # The test never changes an already-set dependency field, but + # we *can* attempt to test searching against an empty value, + # which should get us some bugs. + blocked => {value => '', contains => [1, 2]}, + dependson => {value => '', contains => [1, 3]}, + FIELD_TYPE_BUG_ID, {value => '', contains => [1, 2, 3, 4]}, + + # longdesc changedfrom doesn't make any sense. + longdesc => {contains => []}, + + # Nor does creation_ts changedfrom. + creation_ts => {contains => []}, + 'attach_data.thedata' => {contains => []}, + bug_id => {value => '<1-id>', contains => []}, + }, + }, + ], + changedto => [ + { + contains => [1], + value => '<1>', + override => { + CHANGED_OVERRIDE, + + # I can't imagine any use for creation_ts changedto. + creation_ts => {contains => []}, + } + }, + ], + changedby => [ + { + contains => [1], + value => '<1-reporter>', + override => { + CHANGED_OVERRIDE, + blocked => {contains => [1, 2]}, + dependson => {contains => [1, 3]}, + }, + }, + ], }; # Fields that do not behave as we expect, for InjectionTest. @@ -973,57 +941,62 @@ use constant TESTS => { # operator_ok overrides the "brokenness" of certain operators, so that they # are always OK for that field/operator combination. use constant INJECTION_BROKEN_FIELD => { - # Pg can't run injection tests against integer or date fields. See bug 577557. - 'attachments.isobsolete' => { db_skip => ['Pg'] }, - 'attachments.ispatch' => { db_skip => ['Pg'] }, - 'attachments.isprivate' => { db_skip => ['Pg'] }, - blocked => { db_skip => ['Pg'] }, - bug_id => { db_skip => ['Pg'] }, - cclist_accessible => { db_skip => ['Pg'] }, - creation_ts => { db_skip => ['Pg'] }, - days_elapsed => { db_skip => ['Pg'] }, - dependson => { db_skip => ['Pg'] }, - deadline => { db_skip => ['Pg'] }, - delta_ts => { db_skip => ['Pg'] }, - estimated_time => { db_skip => ['Pg'] }, - everconfirmed => { db_skip => ['Pg'] }, - 'longdescs.isprivate' => { db_skip => ['Pg'] }, - percentage_complete => { db_skip => ['Pg'] }, - remaining_time => { db_skip => ['Pg'] }, - reporter_accessible => { db_skip => ['Pg'] }, - work_time => { db_skip => ['Pg'] }, - FIELD_TYPE_BUG_ID, { db_skip => ['Pg'] }, - FIELD_TYPE_DATETIME, { db_skip => ['Pg'] }, - owner_idle_time => { search => 1 }, - 'longdescs.count' => { - search => 1, - db_skip => ['Pg'], - operator_ok => [qw(allwords allwordssubstr anywordssubstr casesubstring - changedbefore changedafter greaterthan greaterthaneq - lessthan lessthaneq notregexp notsubstring - nowordssubstr regexp substring anywords - notequals nowords equals anyexact)], - }, + + # Pg can't run injection tests against integer or date fields. See bug 577557. + 'attachments.isobsolete' => {db_skip => ['Pg']}, + 'attachments.ispatch' => {db_skip => ['Pg']}, + 'attachments.isprivate' => {db_skip => ['Pg']}, + blocked => {db_skip => ['Pg']}, + bug_id => {db_skip => ['Pg']}, + cclist_accessible => {db_skip => ['Pg']}, + creation_ts => {db_skip => ['Pg']}, + days_elapsed => {db_skip => ['Pg']}, + dependson => {db_skip => ['Pg']}, + deadline => {db_skip => ['Pg']}, + delta_ts => {db_skip => ['Pg']}, + estimated_time => {db_skip => ['Pg']}, + everconfirmed => {db_skip => ['Pg']}, + 'longdescs.isprivate' => {db_skip => ['Pg']}, + percentage_complete => {db_skip => ['Pg']}, + remaining_time => {db_skip => ['Pg']}, + reporter_accessible => {db_skip => ['Pg']}, + work_time => {db_skip => ['Pg']}, + FIELD_TYPE_BUG_ID, + {db_skip => ['Pg']}, + FIELD_TYPE_DATETIME, + {db_skip => ['Pg']}, + owner_idle_time => {search => 1}, + 'longdescs.count' => { + search => 1, + db_skip => ['Pg'], + operator_ok => [ + qw(allwords allwordssubstr anywordssubstr casesubstring + changedbefore changedafter greaterthan greaterthaneq + lessthan lessthaneq notregexp notsubstring + nowordssubstr regexp substring anywords + notequals nowords equals anyexact) + ], + }, }; # Operators that do not behave as we expect, for InjectionTest. # search => 1 means the Bugzilla::Search creation fails, but # field_ok contains fields that it does actually succeed for. use constant INJECTION_BROKEN_OPERATOR => { - changedafter => { search => 1, field_ok => ['creation_ts'] }, - changedbefore => { search => 1, field_ok => ['creation_ts'] }, - changedby => { search => 1 }, + changedafter => {search => 1, field_ok => ['creation_ts']}, + changedbefore => {search => 1, field_ok => ['creation_ts']}, + changedby => {search => 1}, }; # Tests run by Bugzilla::Test::Search::InjectionTest. # We have to make sure the values are all one word or they'll be split # up by the multi-word tests. use constant INJECTION_TESTS => ( - { value => ';SEMICOLON_TEST' }, - { value => '--COMMENT_TEST' }, - { value => "'QUOTE_TEST" }, - { value => "';QUOTE_SEMICOLON_TEST" }, - { value => '/*STAR_COMMENT_TEST' } + {value => ';SEMICOLON_TEST'}, + {value => '--COMMENT_TEST'}, + {value => "'QUOTE_TEST"}, + {value => "';QUOTE_SEMICOLON_TEST"}, + {value => '/*STAR_COMMENT_TEST'} ); ################# @@ -1031,185 +1004,257 @@ use constant INJECTION_TESTS => ( ################# use constant SPECIAL_PARAM_TESTS => ( - { field => 'bug_status', operator => 'anyexact', value => '__open__', - contains => [5] }, - { field => 'bug_status', operator => 'anyexact', value => '__closed__', - contains => [1,2,3,4] }, - { field => 'bug_status', operator => 'anyexact', value => '__all__', - contains => [1,2,3,4,5] }, - - { field => 'resolution', operator => 'anyexact', value => '---', - contains => [5] }, - - # email* query parameters. - { field => 'assigned_to', operator => 'anyexact', - value => '<1>, <2-reporter>', contains => [1,2], - extra_params => { emailreporter1 => 1 } }, - { field => 'assigned_to', operator => 'equals', - value => '<1>', extra_name => 'email2', contains => [], - extra_params => { - email2 => generate_random_password(100), emaillongdesc2 => 1, - }, - }, - - # standard pronouns - { field => 'assigned_to', operator => 'equals', value => '%assignee%', - contains => [1,2,3,4,5] }, - { field => 'reporter', operator => 'equals', value => '%reporter%', - contains => [1,2,3,4,5] }, - { field => 'qa_contact', operator => 'equals', value => '%qacontact%', - contains => [1,2,3,4,5] }, - { field => 'cc', operator => 'equals', value => '%user%', - contains => [1] }, - # group pronouns - { field => 'reporter', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] }, - { field => 'assigned_to', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] }, - { field => 'qa_contact', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4] }, - { field => 'cc', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4] }, - { field => 'commenter', operator => 'equals', - value => '%group.<1-bug_group>%', contains => [1,2,3,4,5] }, + { + field => 'bug_status', + operator => 'anyexact', + value => '__open__', + contains => [5] + }, + { + field => 'bug_status', + operator => 'anyexact', + value => '__closed__', + contains => [1, 2, 3, 4] + }, + { + field => 'bug_status', + operator => 'anyexact', + value => '__all__', + contains => [1, 2, 3, 4, 5] + }, + + { + field => 'resolution', + operator => 'anyexact', + value => '---', + contains => [5] + }, + + # email* query parameters. + { + field => 'assigned_to', + operator => 'anyexact', + value => '<1>, <2-reporter>', + contains => [1, 2], + extra_params => {emailreporter1 => 1} + }, + { + field => 'assigned_to', + operator => 'equals', + value => '<1>', + extra_name => 'email2', + contains => [], + extra_params => {email2 => generate_random_password(100), emaillongdesc2 => 1,}, + }, + + # standard pronouns + { + field => 'assigned_to', + operator => 'equals', + value => '%assignee%', + contains => [1, 2, 3, 4, 5] + }, + { + field => 'reporter', + operator => 'equals', + value => '%reporter%', + contains => [1, 2, 3, 4, 5] + }, + { + field => 'qa_contact', + operator => 'equals', + value => '%qacontact%', + contains => [1, 2, 3, 4, 5] + }, + {field => 'cc', operator => 'equals', value => '%user%', contains => [1]}, + + # group pronouns + { + field => 'reporter', + operator => 'equals', + value => '%group.<1-bug_group>%', + contains => [1, 2, 3, 4, 5] + }, + { + field => 'assigned_to', + operator => 'equals', + value => '%group.<1-bug_group>%', + contains => [1, 2, 3, 4, 5] + }, + { + field => 'qa_contact', + operator => 'equals', + value => '%group.<1-bug_group>%', + contains => [1, 2, 3, 4] + }, + { + field => 'cc', + operator => 'equals', + value => '%group.<1-bug_group>%', + contains => [1, 2, 3, 4] + }, + { + field => 'commenter', + operator => 'equals', + value => '%group.<1-bug_group>%', + contains => [1, 2, 3, 4, 5] + }, ); use constant CUSTOM_SEARCH_TESTS => ( - { name => 'OP without CP', contains => [1], - params => [ - { f => 'OP' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - ] - }, - - { name => 'Empty OP/CP pair before criteria', contains => [1], - params => [ - { f => 'OP' }, { f => 'CP' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - ] - }, - - { name => 'Empty OP/CP pair after criteria', contains => [1], - params => [ - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'OP' }, { f => 'CP' }, - ] - }, - - { name => 'empty OP/CP mid criteria', contains => [1], - columns => ['assigned_to'], - params => [ - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'OP' }, { f => 'CP' }, - { f => 'assigned_to', o => 'substr', v => '@' }, - ] - }, - - { name => 'bug_id = 1 AND assigned_to contains @', contains => [1], - columns => ['assigned_to'], - params => [ - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'substr', v => '@' }, - ] - }, - - { name => 'NOT(bug_id = 1) AND NOT(assigned_to = 2)', - contains => [3,4,5], - columns => ['assigned_to'], - params => [ - { n => 1, f => 'bug_id', o => 'equals', v => '<1>' }, - { n => 1, f => 'assigned_to', o => 'equals', v => '<2>' }, - ] - }, - - { name => 'bug_id = 1 OR assigned_to = 2', contains => [1,2], - columns => ['assigned_to'], top_params => { j_top => 'OR' }, - params => [ - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<2>' }, - ] - }, - - { name => 'NOT(bug_id = 1 AND assigned_to = 1)', contains => [2,3,4,5], - columns => ['assigned_to'], - params => [ - { f => 'OP', n => 1 }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<1>' }, - { f => 'CP' }, - ] - }, - - - { name => '(bug_id = 1 AND assigned_to contains @) ' - . ' OR (bug_id = 2 AND assigned_to contains @)', - contains => [1,2], columns => ['assigned_to'], - top_params => { j_top => 'OR' }, - params => [ - { f => 'OP' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'substr', v => '@' }, - { f => 'CP' }, - { f => 'OP' }, - { f => 'bug_id', o => 'equals', v => '<2>' }, - { f => 'assigned_to', o => 'substr', v => '@' }, - { f => 'CP' }, - ] - }, - - { name => '(bug_id = 1 OR assigned_to = 2) ' - . ' AND (bug_id = 2 OR assigned_to = 1)', - contains => [1,2], columns => ['assigned_to'], - params => [ - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<2>' }, - { f => 'CP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<2>' }, - { f => 'assigned_to', o => 'equals', v => '<1>' }, - { f => 'CP' }, - ] - }, - - { name => 'bug_id = 3 OR ( (bug_id = 1 OR assigned_to = 2) ' - . ' AND (bug_id = 2 OR assigned_to = 1) )', - contains => [1,2,3], columns => ['assigned_to'], - top_params => { j_top => 'OR' }, - params => [ - { f => 'bug_id', o => 'equals', v => '<3>' }, - { f => 'OP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<2>' }, - { f => 'CP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<2>' }, - { f => 'assigned_to', o => 'equals', v => '<1>' }, - { f => 'CP' }, - { f => 'CP' }, - ] - }, - - { name => 'bug_id = 3 OR ( (bug_id = 1 OR assigned_to = 2) ' - . ' AND (bug_id = 2 OR assigned_to = 1) ) OR bug_id = 4', - contains => [1,2,3,4], columns => ['assigned_to'], - top_params => { j_top => 'OR' }, - params => [ - { f => 'bug_id', o => 'equals', v => '<3>' }, - { f => 'OP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<1>' }, - { f => 'assigned_to', o => 'equals', v => '<2>' }, - { f => 'CP' }, - { f => 'OP', j => 'OR' }, - { f => 'bug_id', o => 'equals', v => '<2>' }, - { f => 'assigned_to', o => 'equals', v => '<1>' }, - { f => 'CP' }, - { f => 'CP' }, - { f => 'bug_id', o => 'equals', v => '<4>' }, - ] - }, + { + name => 'OP without CP', + contains => [1], + params => [{f => 'OP'}, {f => 'bug_id', o => 'equals', v => '<1>'},] + }, + + { + name => 'Empty OP/CP pair before criteria', + contains => [1], + params => + [{f => 'OP'}, {f => 'CP'}, {f => 'bug_id', o => 'equals', v => '<1>'},] + }, + + { + name => 'Empty OP/CP pair after criteria', + contains => [1], + params => + [{f => 'bug_id', o => 'equals', v => '<1>'}, {f => 'OP'}, {f => 'CP'},] + }, + + { + name => 'empty OP/CP mid criteria', + contains => [1], + columns => ['assigned_to'], + params => [ + {f => 'bug_id', o => 'equals', v => '<1>'}, + {f => 'OP'}, + {f => 'CP'}, + {f => 'assigned_to', o => 'substr', v => '@'}, + ] + }, + + { + name => 'bug_id = 1 AND assigned_to contains @', + contains => [1], + columns => ['assigned_to'], + params => [ + {f => 'bug_id', o => 'equals', v => '<1>'}, + {f => 'assigned_to', o => 'substr', v => '@'}, + ] + }, + + { + name => 'NOT(bug_id = 1) AND NOT(assigned_to = 2)', + contains => [3, 4, 5], + columns => ['assigned_to'], + params => [ + {n => 1, f => 'bug_id', o => 'equals', v => '<1>'}, + {n => 1, f => 'assigned_to', o => 'equals', v => '<2>'}, + ] + }, + + { + name => 'bug_id = 1 OR assigned_to = 2', + contains => [1, 2], + columns => ['assigned_to'], + top_params => {j_top => 'OR'}, + params => [ + {f => 'bug_id', o => 'equals', v => '<1>'}, + {f => 'assigned_to', o => 'equals', v => '<2>'}, + ] + }, + + { + name => 'NOT(bug_id = 1 AND assigned_to = 1)', + contains => [2, 3, 4, 5], + columns => ['assigned_to'], + params => [ + {f => 'OP', n => 1}, + {f => 'bug_id', o => 'equals', v => '<1>'}, + {f => 'assigned_to', o => 'equals', v => '<1>'}, + {f => 'CP'}, + ] + }, + + + { + name => '(bug_id = 1 AND assigned_to contains @) ' + . ' OR (bug_id = 2 AND assigned_to contains @)', + contains => [1, 2], + columns => ['assigned_to'], + top_params => {j_top => 'OR'}, + params => [ + {f => 'OP'}, + {f => 'bug_id', o => 'equals', v => '<1>'}, + {f => 'assigned_to', o => 'substr', v => '@'}, + {f => 'CP'}, + {f => 'OP'}, + {f => 'bug_id', o => 'equals', v => '<2>'}, + {f => 'assigned_to', o => 'substr', v => '@'}, + {f => 'CP'}, + ] + }, + + { + name => '(bug_id = 1 OR assigned_to = 2) ' + . ' AND (bug_id = 2 OR assigned_to = 1)', + contains => [1, 2], + columns => ['assigned_to'], + params => [ + {f => 'OP', j => 'OR'}, + {f => 'bug_id', o => 'equals', v => '<1>'}, + {f => 'assigned_to', o => 'equals', v => '<2>'}, + {f => 'CP'}, + {f => 'OP', j => 'OR'}, + {f => 'bug_id', o => 'equals', v => '<2>'}, + {f => 'assigned_to', o => 'equals', v => '<1>'}, + {f => 'CP'}, + ] + }, + + { + name => 'bug_id = 3 OR ( (bug_id = 1 OR assigned_to = 2) ' + . ' AND (bug_id = 2 OR assigned_to = 1) )', + contains => [1, 2, 3], + columns => ['assigned_to'], + top_params => {j_top => 'OR'}, + params => [ + {f => 'bug_id', o => 'equals', v => '<3>'}, + {f => 'OP'}, + {f => 'OP', j => 'OR'}, + {f => 'bug_id', o => 'equals', v => '<1>'}, + {f => 'assigned_to', o => 'equals', v => '<2>'}, + {f => 'CP'}, + {f => 'OP', j => 'OR'}, + {f => 'bug_id', o => 'equals', v => '<2>'}, + {f => 'assigned_to', o => 'equals', v => '<1>'}, + {f => 'CP'}, + {f => 'CP'}, + ] + }, + + { + name => 'bug_id = 3 OR ( (bug_id = 1 OR assigned_to = 2) ' + . ' AND (bug_id = 2 OR assigned_to = 1) ) OR bug_id = 4', + contains => [1, 2, 3, 4], + columns => ['assigned_to'], + top_params => {j_top => 'OR'}, + params => [ + {f => 'bug_id', o => 'equals', v => '<3>'}, + {f => 'OP'}, + {f => 'OP', j => 'OR'}, + {f => 'bug_id', o => 'equals', v => '<1>'}, + {f => 'assigned_to', o => 'equals', v => '<2>'}, + {f => 'CP'}, + {f => 'OP', j => 'OR'}, + {f => 'bug_id', o => 'equals', v => '<2>'}, + {f => 'assigned_to', o => 'equals', v => '<1>'}, + {f => 'CP'}, + {f => 'CP'}, + {f => 'bug_id', o => 'equals', v => '<4>'}, + ] + }, ); diff --git a/xt/lib/Bugzilla/Test/Search/CustomTest.pm b/xt/lib/Bugzilla/Test/Search/CustomTest.pm index 62a622594..b4f105353 100644 --- a/xt/lib/Bugzilla/Test/Search/CustomTest.pm +++ b/xt/lib/Bugzilla/Test/Search/CustomTest.pm @@ -38,7 +38,7 @@ use Storable qw(dclone); sub new { my ($class, $test, $search_test) = @_; - bless { raw_test => dclone($test), search_test => $search_test }, $class; + bless {raw_test => dclone($test), search_test => $search_test}, $class; } ############# @@ -46,31 +46,33 @@ sub new { ############# sub search_test { return $_[0]->{search_test} } -sub name { return 'Custom: ' . $_[0]->test->{name} } -sub test { return $_[0]->{raw_test} } +sub name { return 'Custom: ' . $_[0]->test->{name} } +sub test { return $_[0]->{raw_test} } sub operator_test { die "unimplemented" } -sub field_object { die "unimplemented" } -sub main_value { die "unimplenmented" } -sub test_value { die "unimplemented" } +sub field_object { die "unimplemented" } +sub main_value { die "unimplenmented" } +sub test_value { die "unimplemented" } + # Custom tests don't use transforms. -sub transformed_value_was_equal { 0 } +sub transformed_value_was_equal {0} + sub debug_value { - my ($self) = @_; - my $string = ''; - my $params = $self->search_params; - foreach my $param (keys %$params) { - $string .= $param . "=" . $params->{$param} . '&'; - } - chop($string); - return $string; + my ($self) = @_; + my $string = ''; + my $params = $self->search_params; + foreach my $param (keys %$params) { + $string .= $param . "=" . $params->{$param} . '&'; + } + chop($string); + return $string; } # The tests we know are broken for this operator/field combination. sub _known_broken { return {} } -sub contains_known_broken { return undef } -sub search_known_broken { return undef } -sub field_not_yet_implemented { return undef } +sub contains_known_broken { return undef } +sub search_known_broken { return undef } +sub field_not_yet_implemented { return undef } sub invalid_field_operator_combination { return undef } ######################################### @@ -80,36 +82,35 @@ sub invalid_field_operator_combination { return undef } # Converts the f, o, v rows into f0, o0, v0, etc. and translates # the values appropriately. sub search_params { - my ($self) = @_; - - my %params = %{ $self->test->{top_params} || {} }; - my $counter = 0; - foreach my $row (@{ $self->test->{params} }) { - $row->{v} = $self->translate_value($row) if exists $row->{v}; - foreach my $key (keys %$row) { - $params{"${key}$counter"} = $row->{$key}; - } - $counter++; + my ($self) = @_; + + my %params = %{$self->test->{top_params} || {}}; + my $counter = 0; + foreach my $row (@{$self->test->{params}}) { + $row->{v} = $self->translate_value($row) if exists $row->{v}; + foreach my $key (keys %$row) { + $params{"${key}$counter"} = $row->{$key}; } + $counter++; + } - return \%params; + return \%params; } sub translate_value { - my ($self, $row) = @_; - my $as_test = { field => $row->{f}, operator => $row->{o}, - value => $row->{v} }; - my $operator_test = new Bugzilla::Test::Search::OperatorTest($row->{o}, - $self->search_test); - my $field = Bugzilla::Field->check($row->{f}); - my $field_test = new Bugzilla::Test::Search::FieldTest($operator_test, - $field, $as_test); - return $field_test->translated_value; + my ($self, $row) = @_; + my $as_test = {field => $row->{f}, operator => $row->{o}, value => $row->{v}}; + my $operator_test + = new Bugzilla::Test::Search::OperatorTest($row->{o}, $self->search_test); + my $field = Bugzilla::Field->check($row->{f}); + my $field_test + = new Bugzilla::Test::Search::FieldTest($operator_test, $field, $as_test); + return $field_test->translated_value; } sub search_columns { - my ($self) = @_; - return ['bug_id', @{ $self->test->{columns} || [] }]; + my ($self) = @_; + return ['bug_id', @{$self->test->{columns} || []}]; } 1; diff --git a/xt/lib/Bugzilla/Test/Search/FieldTest.pm b/xt/lib/Bugzilla/Test/Search/FieldTest.pm index 832c578cc..fc7b107c3 100644 --- a/xt/lib/Bugzilla/Test/Search/FieldTest.pm +++ b/xt/lib/Bugzilla/Test/Search/FieldTest.pm @@ -40,10 +40,12 @@ use Test::Exception; ############### sub new { - my ($class, $operator_test, $field, $test) = @_; - return bless { operator_test => $operator_test, - field_object => $field, - raw_test => $test }, $class; + my ($class, $operator_test, $field, $test) = @_; + return bless { + operator_test => $operator_test, + field_object => $field, + raw_test => $test + }, $class; } ############# @@ -54,144 +56,156 @@ sub num_tests { return TESTS_PER_RUN } # The Bugzilla::Test::Search::OperatorTest that this is a child of. sub operator_test { return $_[0]->{operator_test} } + # The Bugzilla::Field being tested. sub field_object { return $_[0]->{field_object} } + # The name of the field being tested, which we need much more often # than we need the object. sub field { - my ($self) = @_; - $self->{field_name} ||= $self->field_object->name; - return $self->{field_name}; + my ($self) = @_; + $self->{field_name} ||= $self->field_object->name; + return $self->{field_name}; } + # The Bugzilla::Test::Search object that this is a child of. sub search_test { return $_[0]->operator_test->search_test } + # The operator being tested sub operator { return $_[0]->operator_test->operator } + # The bugs currently being tested by Bugzilla::Test::Search. sub bugs { return $_[0]->search_test->bugs } + sub bug { - my $self = shift; - return $self->search_test->bug(@_); + my $self = shift; + return $self->search_test->bug(@_); } + sub number { - my ($self, $id) = @_; - foreach my $number (1..NUM_BUGS) { - return $number if $self->search_test->bug($number)->id == $id; - } - return 0; + my ($self, $id) = @_; + foreach my $number (1 .. NUM_BUGS) { + return $number if $self->search_test->bug($number)->id == $id; + } + return 0; } # The name displayed for this test by Test::More. Used in test descriptions. sub name { - my ($self) = @_; - my $field = $self->field; - my $operator = $self->operator; - my $value = $self->main_value; + my ($self) = @_; + my $field = $self->field; + my $operator = $self->operator; + my $value = $self->main_value; - my $name = "$field-$operator-$value"; - if (my $extra_name = $self->test->{extra_name}) { - $name .= "-$extra_name"; - } - return $name; + my $name = "$field-$operator-$value"; + if (my $extra_name = $self->test->{extra_name}) { + $name .= "-$extra_name"; + } + return $name; } # The appropriate value from the TESTS constant for this test, taking # into account overrides. sub test { - my $self = shift; - return $self->{test} if $self->{test}; + my $self = shift; + return $self->{test} if $self->{test}; - my %test = %{ $self->{raw_test} }; + my %test = %{$self->{raw_test}}; - # We have field name overrides... - my $override = $test{override}->{$self->field}; - # And also field type overrides. - if (!$override) { - $override = $test{override}->{$self->field_object->type} || {}; - } + # We have field name overrides... + my $override = $test{override}->{$self->field}; - foreach my $key (%$override) { - $test{$key} = $override->{$key}; - } + # And also field type overrides. + if (!$override) { + $override = $test{override}->{$self->field_object->type} || {}; + } - $self->{test} = \%test; - return $self->{test}; + foreach my $key (%$override) { + $test{$key} = $override->{$key}; + } + + $self->{test} = \%test; + return $self->{test}; } # All the values for all the bugs for this field. sub _field_values { - my ($self) = @_; - return $self->{field_values} if $self->{field_values}; + my ($self) = @_; + return $self->{field_values} if $self->{field_values}; - my %field_values; - foreach my $number (1..NUM_BUGS) { - $field_values{$number} = $self->_field_values_for_bug($number); - } - $self->{field_values} = \%field_values; - return $self->{field_values}; + my %field_values; + foreach my $number (1 .. NUM_BUGS) { + $field_values{$number} = $self->_field_values_for_bug($number); + } + $self->{field_values} = \%field_values; + return $self->{field_values}; } + # The values for this field for the numbered bug. sub bug_values { - my ($self, $number) = @_; - return @{ $self->_field_values->{$number} }; + my ($self, $number) = @_; + return @{$self->_field_values->{$number}}; } # The untranslated, non-overriden value--used in the name of the test # and other places. sub main_value { return $_[0]->{raw_test}->{value} } + # The untranslated test value, taking into account overrides. -sub test_value { return $_[0]->test->{value} }; +sub test_value { return $_[0]->test->{value} } + # The value translated appropriately for passing to Bugzilla::Search. sub translated_value { - my $self = shift; - if (!exists $self->{translated_value}) { - my $value = $self->search_test->value_translation_cache($self); - if (!defined $value) { - $value = $self->_translate_value(); - $self->search_test->value_translation_cache($self, $value); - } - $self->{translated_value} = $value; + my $self = shift; + if (!exists $self->{translated_value}) { + my $value = $self->search_test->value_translation_cache($self); + if (!defined $value) { + $value = $self->_translate_value(); + $self->search_test->value_translation_cache($self, $value); } - return $self->{translated_value}; + $self->{translated_value} = $value; + } + return $self->{translated_value}; } + # Used in failure diagnostic messages. sub debug_fail { - my ($self, $number, $results, $sql) = @_; - my @expected = @{ $self->test->{contains} }; - my @results = sort - map { $self->number($_) } - map { $_->[0] } - @$results; - return - " Value: '" . $self->translated_value . "'\n" . - "Expected: [" . join(',', @expected) . "]\n" . - " Results: [" . join(',', @results) . "]\n" . - trim($sql) . "\n"; + my ($self, $number, $results, $sql) = @_; + my @expected = @{$self->test->{contains}}; + my @results = sort map { $self->number($_) } map { $_->[0] } @$results; + return + " Value: '" + . $self->translated_value . "'\n" + . "Expected: [" + . join(',', @expected) . "]\n" + . " Results: [" + . join(',', @results) . "]\n" + . trim($sql) . "\n"; } # True for a bug if we ran the "transform" function on it and the # result was equal to its first value. sub transformed_value_was_equal { - my ($self, $number, $value) = @_; - if (@_ > 2) { - $self->{transformed_value_was_equal}->{$number} = $value; - $self->search_test->was_equal_cache($self, $number, $value); - } - my $cached = $self->search_test->was_equal_cache($self, $number); - return $cached if defined $cached; - return $self->{transformed_value_was_equal}->{$number}; + my ($self, $number, $value) = @_; + if (@_ > 2) { + $self->{transformed_value_was_equal}->{$number} = $value; + $self->search_test->was_equal_cache($self, $number, $value); + } + my $cached = $self->search_test->was_equal_cache($self, $number); + return $cached if defined $cached; + return $self->{transformed_value_was_equal}->{$number}; } # True if this test is supposed to contain the numbered bug. sub bug_is_contained { - my ($self, $number) = @_; - my $contains = $self->test->{contains}; - if ($self->transformed_value_was_equal($number) - and !$self->test->{override}->{$self->field}->{contains}) - { - $contains = $self->test->{if_equal}->{contains}; - } - return grep($_ == $number, @$contains) ? 1 : 0; + my ($self, $number) = @_; + my $contains = $self->test->{contains}; + if ($self->transformed_value_was_equal($number) + and !$self->test->{override}->{$self->field}->{contains}) + { + $contains = $self->test->{if_equal}->{contains}; + } + return grep($_ == $number, @$contains) ? 1 : 0; } ################################################### @@ -200,112 +214,114 @@ sub bug_is_contained { # The tests we know are broken for this operator/field combination. sub _known_broken { - my ($self, $constant, $skip_pg_check) = @_; - - $constant ||= KNOWN_BROKEN; - my $field = $self->field; - my $type = $self->field_object->type; - my $operator = $self->operator; - my $value = $self->main_value; - my $value_name = "$operator-$value"; - if (my $extra_name = $self->test->{extra_name}) { - $value_name .= "-$extra_name"; - } - - my $value_broken = $constant->{$value_name}->{$field}; - $value_broken ||= $constant->{$value_name}->{$type}; - return $value_broken if $value_broken; - my $operator_broken = $constant->{$operator}->{$field}; - $operator_broken ||= $constant->{$operator}->{$type}; - return $operator_broken if $operator_broken; - return {}; + my ($self, $constant, $skip_pg_check) = @_; + + $constant ||= KNOWN_BROKEN; + my $field = $self->field; + my $type = $self->field_object->type; + my $operator = $self->operator; + my $value = $self->main_value; + my $value_name = "$operator-$value"; + if (my $extra_name = $self->test->{extra_name}) { + $value_name .= "-$extra_name"; + } + + my $value_broken = $constant->{$value_name}->{$field}; + $value_broken ||= $constant->{$value_name}->{$type}; + return $value_broken if $value_broken; + my $operator_broken = $constant->{$operator}->{$field}; + $operator_broken ||= $constant->{$operator}->{$type}; + return $operator_broken if $operator_broken; + return {}; } # True if the "contains" search for the numbered bug is broken. # That is, either the result is supposed to contain it and doesn't, # or the result is not supposed to contain it and does. sub contains_known_broken { - my ($self, $number) = @_; - my $field = $self->field; - my $operator = $self->operator; + my ($self, $number) = @_; + my $field = $self->field; + my $operator = $self->operator; - my $contains_broken = $self->_known_broken->{contains} || []; - if (grep($_ == $number, @$contains_broken)) { - return "$field $operator contains $number is known to be broken"; - } - return undef; + my $contains_broken = $self->_known_broken->{contains} || []; + if (grep($_ == $number, @$contains_broken)) { + return "$field $operator contains $number is known to be broken"; + } + return undef; } # Used by subclasses. Checks both bug_is_contained and contains_known_broken # to tell you whether or not the bug will *actually* be found by the test. sub will_actually_contain_bug { - my ($self, $number) = @_; - my $is_contained = $self->bug_is_contained($number) ? 1 : 0; - my $is_broken = $self->contains_known_broken($number) ? 1 : 0; + my ($self, $number) = @_; + my $is_contained = $self->bug_is_contained($number) ? 1 : 0; + my $is_broken = $self->contains_known_broken($number) ? 1 : 0; - # If the test is supposed to contain the bug and *isn't* broken, - # then the test will contain the bug. - return 1 if ($is_contained and !$is_broken); - # If this test is *not* supposed to contain the bug, but that test is - # broken, then this test *will* contain the bug. - return 1 if (!$is_contained and $is_broken); + # If the test is supposed to contain the bug and *isn't* broken, + # then the test will contain the bug. + return 1 if ($is_contained and !$is_broken); - return 0; + # If this test is *not* supposed to contain the bug, but that test is + # broken, then this test *will* contain the bug. + return 1 if (!$is_contained and $is_broken); + + return 0; } # Returns a string if creating a Bugzilla::Search object throws an error, # with this field/operator/value combination. sub search_known_broken { - my ($self) = @_; - my $field = $self->field; - my $operator = $self->operator; - if ($self->_known_broken->{search}) { - return "Bugzilla::Search for $field $operator is known to be broken"; - } - return undef; + my ($self) = @_; + my $field = $self->field; + my $operator = $self->operator; + if ($self->_known_broken->{search}) { + return "Bugzilla::Search for $field $operator is known to be broken"; + } + return undef; } # Returns a string if we haven't yet implemented the tests for this field, # but we plan to in the future. sub field_not_yet_implemented { - my ($self) = @_; - my $skip_this_field = grep { $_ eq $self->field } SKIP_FIELDS; - if ($skip_this_field) { - my $field = $self->field; - return "$field testing not yet implemented"; - } - return undef; + my ($self) = @_; + my $skip_this_field = grep { $_ eq $self->field } SKIP_FIELDS; + if ($skip_this_field) { + my $field = $self->field; + return "$field testing not yet implemented"; + } + return undef; } # Returns a message if this field/operator combination can't ever be run. # At no time in the future will this field/operator combination ever work. sub invalid_field_operator_combination { - my ($self) = @_; - my $field = $self->field; - my $operator = $self->operator; + my ($self) = @_; + my $field = $self->field; + my $operator = $self->operator; - if ($field eq 'content' && $operator !~ /matches/) { - return "content field does not support $operator"; - } - elsif ($operator =~ /matches/ && $field ne 'content') { - return "matches operator does not support fields other than content"; - } - return undef; + if ($field eq 'content' && $operator !~ /matches/) { + return "content field does not support $operator"; + } + elsif ($operator =~ /matches/ && $field ne 'content') { + return "matches operator does not support fields other than content"; + } + return undef; } # True if this field is broken in an OR combination. sub join_broken { - my ($self, $or_broken_map) = @_; - my $or_broken = $or_broken_map->{$self->field . '-' . $self->operator}; - if (!$or_broken) { - # See if this is a comment field, and in that case, if there's - # a generic entry for all comment fields. - my $is_comment_field = COMMENT_FIELDS->{$self->field}; - if ($is_comment_field) { - $or_broken = $or_broken_map->{'longdescs.-' . $self->operator}; - } + my ($self, $or_broken_map) = @_; + my $or_broken = $or_broken_map->{$self->field . '-' . $self->operator}; + if (!$or_broken) { + + # See if this is a comment field, and in that case, if there's + # a generic entry for all comment fields. + my $is_comment_field = COMMENT_FIELDS->{$self->field}; + if ($is_comment_field) { + $or_broken = $or_broken_map->{'longdescs.-' . $self->operator}; } - return $or_broken; + } + return $or_broken; } ######################################### @@ -314,28 +330,28 @@ sub join_broken { # The data that will get passed to Bugzilla::Search as its arguments. sub search_params { - my ($self) = @_; - return $self->{search_params} if $self->{search_params}; + my ($self) = @_; + return $self->{search_params} if $self->{search_params}; - my %params = ( - "field0-0-0" => $self->field, - "type0-0-0" => $self->operator, - "value0-0-0" => $self->translated_value, - ); + my %params = ( + "field0-0-0" => $self->field, + "type0-0-0" => $self->operator, + "value0-0-0" => $self->translated_value, + ); - $self->{search_params} = \%params; - return $self->{search_params}; + $self->{search_params} = \%params; + return $self->{search_params}; } sub search_columns { - my ($self) = @_; - my $field = $self->field; - my @search_fields = qw(bug_id); - if ($self->field_object->buglist) { - my $col_name = COLUMN_TRANSLATION->{$field} || $field; - push(@search_fields, $col_name); - } - return \@search_fields; + my ($self) = @_; + my $field = $self->field; + my @search_fields = qw(bug_id); + if ($self->field_object->buglist) { + my $col_name = COLUMN_TRANSLATION->{$field} || $field; + push(@search_fields, $col_name); + } + return \@search_fields; } @@ -344,103 +360,107 @@ sub search_columns { ################ sub _field_values_for_bug { - my ($self, $number) = @_; - my $field = $self->field; - - my @values; - - if ($field =~ /^attach.+\.(.+)$/ ) { - my $attach_field = $1; - $attach_field = ATTACHMENT_FIELDS->{$attach_field} || $attach_field; - @values = $self->_values_for($number, 'attachments', $attach_field); - } - elsif (my $flag_field = FLAG_FIELDS->{$field}) { - @values = $self->_values_for($number, 'flags', $flag_field); - } - elsif (my $translation = COMMENT_FIELDS->{$field}) { - @values = $self->_values_for($number, 'comments', $translation); - # We want the last value to come first, so that single-value - # searches use the last comment. - @values = reverse @values; - } - elsif ($field eq 'longdescs.count') { - @values = scalar(@{ $self->bug($number)->comments }); - } - elsif ($field eq 'work_time') { - @values = $self->_values_for($number, 'actual_time'); - } - elsif ($field eq 'bug_group') { - @values = $self->_values_for($number, 'groups_in', 'name'); - } - elsif ($field eq 'keywords') { - @values = $self->_values_for($number, 'keyword_objects', 'name'); - } - elsif ($field eq 'content') { - @values = $self->_values_for($number, 'short_desc'); - } - elsif ($field eq 'see_also') { - @values = $self->_values_for($number, 'see_also', 'name'); - } - elsif ($field eq 'tag') { - @values = $self->_values_for($number, 'tags'); - } - # Bugzilla::Bug truncates creation_ts, but we need the full value - # from the database. This has no special value for changedfrom, - # because it never changes. - elsif ($field eq 'creation_ts') { - my $bug = $self->bug($number); - my $creation_ts = Bugzilla->dbh->selectrow_array( - 'SELECT creation_ts FROM bugs WHERE bug_id = ?', - undef, $bug->id); - @values = ($creation_ts); - } - else { - @values = $self->_values_for($number, $field); - } - - # We convert user objects to their login name, here, all in one - # block for simplicity. - if (grep { $_ eq $field } USER_FIELDS) { - # requestees.login_name is empty for most bugs (but checking - # blessed(undef) handles that. - # Values that come from %original_values aren't User objects. - @values = map { blessed($_) ? $_->login : $_ } @values; - @values = grep { defined $_ } @values; - } - - return \@values; + my ($self, $number) = @_; + my $field = $self->field; + + my @values; + + if ($field =~ /^attach.+\.(.+)$/) { + my $attach_field = $1; + $attach_field = ATTACHMENT_FIELDS->{$attach_field} || $attach_field; + @values = $self->_values_for($number, 'attachments', $attach_field); + } + elsif (my $flag_field = FLAG_FIELDS->{$field}) { + @values = $self->_values_for($number, 'flags', $flag_field); + } + elsif (my $translation = COMMENT_FIELDS->{$field}) { + @values = $self->_values_for($number, 'comments', $translation); + + # We want the last value to come first, so that single-value + # searches use the last comment. + @values = reverse @values; + } + elsif ($field eq 'longdescs.count') { + @values = scalar(@{$self->bug($number)->comments}); + } + elsif ($field eq 'work_time') { + @values = $self->_values_for($number, 'actual_time'); + } + elsif ($field eq 'bug_group') { + @values = $self->_values_for($number, 'groups_in', 'name'); + } + elsif ($field eq 'keywords') { + @values = $self->_values_for($number, 'keyword_objects', 'name'); + } + elsif ($field eq 'content') { + @values = $self->_values_for($number, 'short_desc'); + } + elsif ($field eq 'see_also') { + @values = $self->_values_for($number, 'see_also', 'name'); + } + elsif ($field eq 'tag') { + @values = $self->_values_for($number, 'tags'); + } + + # Bugzilla::Bug truncates creation_ts, but we need the full value + # from the database. This has no special value for changedfrom, + # because it never changes. + elsif ($field eq 'creation_ts') { + my $bug = $self->bug($number); + my $creation_ts + = Bugzilla->dbh->selectrow_array( + 'SELECT creation_ts FROM bugs WHERE bug_id = ?', + undef, $bug->id); + @values = ($creation_ts); + } + else { + @values = $self->_values_for($number, $field); + } + + # We convert user objects to their login name, here, all in one + # block for simplicity. + if (grep { $_ eq $field } USER_FIELDS) { + + # requestees.login_name is empty for most bugs (but checking + # blessed(undef) handles that. + # Values that come from %original_values aren't User objects. + @values = map { blessed($_) ? $_->login : $_ } @values; + @values = grep { defined $_ } @values; + } + + return \@values; } sub _values_for { - my ($self, $number, $bug_field, $item_field) = @_; + my ($self, $number, $bug_field, $item_field) = @_; - my $item; - if ($self->operator eq 'changedfrom') { - $item = $self->search_test->bug_create_value($number, $bug_field); - } - else { - my $bug = $self->bug($number); - $item = $bug->$bug_field; - } + my $item; + if ($self->operator eq 'changedfrom') { + $item = $self->search_test->bug_create_value($number, $bug_field); + } + else { + my $bug = $self->bug($number); + $item = $bug->$bug_field; + } - if ($item_field) { - if ($bug_field eq 'flags' and $item_field eq 'name') { - return (map { $_->name . $_->status } @$item); - } - return (map { $self->_get_item($_, $item_field) } @$item); + if ($item_field) { + if ($bug_field eq 'flags' and $item_field eq 'name') { + return (map { $_->name . $_->status } @$item); } + return (map { $self->_get_item($_, $item_field) } @$item); + } - return @$item if ref($item) eq 'ARRAY'; - return $item if defined $item; - return (); + return @$item if ref($item) eq 'ARRAY'; + return $item if defined $item; + return (); } sub _get_item { - my ($self, $from, $field) = @_; - if (blessed($from)) { - return $from->$field; - } - return $from->{$field}; + my ($self, $from, $field) = @_; + if (blessed($from)) { + return $from->$field; + } + return $from->{$field}; } ##################### @@ -453,83 +473,85 @@ sub _get_item { # and then we insert it as required into the "value" from TESTS. (For example, # <1> becomes the value for the field from bug 1.) sub _translate_value { - my $self = shift; - my $value = $self->test_value; - foreach my $number (1..NUM_BUGS) { - $value = $self->_translate_value_for_bug($number, $value); - } - # Sanity check to make sure that none of the <> stuff was left in. - if ($value =~ /<\d/) { - die $self->name . ": value untranslated: $value\n"; - } - return $value; + my $self = shift; + my $value = $self->test_value; + foreach my $number (1 .. NUM_BUGS) { + $value = $self->_translate_value_for_bug($number, $value); + } + + # Sanity check to make sure that none of the <> stuff was left in. + if ($value =~ /<\d/) { + die $self->name . ": value untranslated: $value\n"; + } + return $value; } sub _translate_value_for_bug { - my ($self, $number, $value) = @_; - - my $bug = $self->bug($number); - - my $bug_id = $bug->id; - $value =~ s/<$number-id>/$bug_id/g; - my $bug_delta = $bug->delta_ts; - $value =~ s/<$number-delta>/$bug_delta/g; - my $reporter = $bug->reporter->login; - $value =~ s/<$number-reporter>/$reporter/g; - if ($value =~ /<$number-bug_group>/) { - my @bug_groups = map { $_->name } @{ $bug->groups_in }; - @bug_groups = grep { $_ =~ /^\d+-group-/ } @bug_groups; - my $group = $bug_groups[0]; - $value =~ s/<$number-bug_group>/$group/g; - } - - my @bug_values = $self->bug_values($number); - return $value if !@bug_values; - - if ($self->operator =~ /substr/) { - @bug_values = map { $self->_substr_value($_) } @bug_values; - } - - my $string_value = $bug_values[0]; - if ($self->operator =~ /word/) { - $string_value = join(' ', @bug_values); - } - if (my $func = $self->test->{transform}) { - my $transformed = $func->(@bug_values); - my $is_equal = $transformed eq $bug_values[0] ? 1 : 0; - $self->transformed_value_was_equal($number, $is_equal); - $string_value = $transformed; - } - - if ($self->test->{escape}) { - $string_value = quotemeta($string_value); - } - $value =~ s/<$number>/$string_value/g; - - return $value; + my ($self, $number, $value) = @_; + + my $bug = $self->bug($number); + + my $bug_id = $bug->id; + $value =~ s/<$number-id>/$bug_id/g; + my $bug_delta = $bug->delta_ts; + $value =~ s/<$number-delta>/$bug_delta/g; + my $reporter = $bug->reporter->login; + $value =~ s/<$number-reporter>/$reporter/g; + if ($value =~ /<$number-bug_group>/) { + my @bug_groups = map { $_->name } @{$bug->groups_in}; + @bug_groups = grep { $_ =~ /^\d+-group-/ } @bug_groups; + my $group = $bug_groups[0]; + $value =~ s/<$number-bug_group>/$group/g; + } + + my @bug_values = $self->bug_values($number); + return $value if !@bug_values; + + if ($self->operator =~ /substr/) { + @bug_values = map { $self->_substr_value($_) } @bug_values; + } + + my $string_value = $bug_values[0]; + if ($self->operator =~ /word/) { + $string_value = join(' ', @bug_values); + } + if (my $func = $self->test->{transform}) { + my $transformed = $func->(@bug_values); + my $is_equal = $transformed eq $bug_values[0] ? 1 : 0; + $self->transformed_value_was_equal($number, $is_equal); + $string_value = $transformed; + } + + if ($self->test->{escape}) { + $string_value = quotemeta($string_value); + } + $value =~ s/<$number>/$string_value/g; + + return $value; } sub _substr_value { - my ($self, $value) = @_; - my $field = $self->field; - my $type = $self->field_object->type; - my $substr_size = SUBSTR_SIZE; - if (exists FIELD_SUBSTR_SIZE->{$field}) { - $substr_size = FIELD_SUBSTR_SIZE->{$field}; - } - elsif (exists FIELD_SUBSTR_SIZE->{$type}) { - $substr_size = FIELD_SUBSTR_SIZE->{$type}; - } - if ($substr_size > 0) { - # The field name is included in every field value, and if it's - # long, it might take up the whole substring, and we don't want that. - if (!grep { $_ eq $field or $_ eq $type } SUBSTR_NO_FIELD_ADD) { - $substr_size += length($field); - } - my $string = substr($value, 0, $substr_size); - return $string; - } - return substr($value, $substr_size); + my ($self, $value) = @_; + my $field = $self->field; + my $type = $self->field_object->type; + my $substr_size = SUBSTR_SIZE; + if (exists FIELD_SUBSTR_SIZE->{$field}) { + $substr_size = FIELD_SUBSTR_SIZE->{$field}; + } + elsif (exists FIELD_SUBSTR_SIZE->{$type}) { + $substr_size = FIELD_SUBSTR_SIZE->{$type}; + } + if ($substr_size > 0) { + + # The field name is included in every field value, and if it's + # long, it might take up the whole substring, and we don't want that. + if (!grep { $_ eq $field or $_ eq $type } SUBSTR_NO_FIELD_ADD) { + $substr_size += length($field); + } + my $string = substr($value, 0, $substr_size); + return $string; + } + return substr($value, $substr_size); } ##################### @@ -537,95 +559,93 @@ sub _substr_value { ##################### sub run { - my ($self) = @_; + my ($self) = @_; - my $invalid_combination = $self->invalid_field_operator_combination; - my $field_not_implemented = $self->field_not_yet_implemented; + my $invalid_combination = $self->invalid_field_operator_combination; + my $field_not_implemented = $self->field_not_yet_implemented; - SKIP: { - skip($invalid_combination, $self->num_tests) if $invalid_combination; - TODO: { - todo_skip ($field_not_implemented, $self->num_tests) if $field_not_implemented; - $self->do_tests(); - } +SKIP: { + skip($invalid_combination, $self->num_tests) if $invalid_combination; + TODO: { + todo_skip($field_not_implemented, $self->num_tests) if $field_not_implemented; + $self->do_tests(); } + } } sub do_tests { - my ($self) = @_; - my $name = $self->name; + my ($self) = @_; + my $name = $self->name; - my $search_broken = $self->search_known_broken; + my $search_broken = $self->search_known_broken; - my $search = $self->_test_search_object_creation(); + my $search = $self->_test_search_object_creation(); - my $sql; - TODO: { - local $TODO = $search_broken if $search_broken; - lives_ok { $sql = $search->_sql } "$name: generate SQL"; - } + my $sql; +TODO: { + local $TODO = $search_broken if $search_broken; + lives_ok { $sql = $search->_sql } "$name: generate SQL"; + } - my $results; - SKIP: { - skip "Can't run SQL without any SQL", 1 if !defined $sql; - $results = $self->_test_sql($search); - } + my $results; +SKIP: { + skip "Can't run SQL without any SQL", 1 if !defined $sql; + $results = $self->_test_sql($search); + } - $self->_test_content($results, $sql); + $self->_test_content($results, $sql); } sub _test_search_object_creation { - my ($self) = @_; - my $name = $self->name; - my @args = (fields => $self->search_columns, params => $self->search_params); - my $search; - lives_ok { $search = new Bugzilla::Search(@args) } - "$name: create search object"; - return $search; + my ($self) = @_; + my $name = $self->name; + my @args = (fields => $self->search_columns, params => $self->search_params); + my $search; + lives_ok { $search = new Bugzilla::Search(@args) } + "$name: create search object"; + return $search; } sub _test_sql { - my ($self, $search) = @_; - my $name = $self->name; - my $results; - lives_ok { $results = $search->data } "$name: Run SQL Query" - or diag($search->_sql); - return $results; + my ($self, $search) = @_; + my $name = $self->name; + my $results; + lives_ok { $results = $search->data } "$name: Run SQL Query" + or diag($search->_sql); + return $results; } sub _test_content { - my ($self, $results, $sql) = @_; + my ($self, $results, $sql) = @_; - SKIP: { - skip "Without results we can't test them", NUM_BUGS if !$results; - foreach my $number (1..NUM_BUGS) { - $self->_test_content_for_bug($number, $results, $sql); - } +SKIP: { + skip "Without results we can't test them", NUM_BUGS if !$results; + foreach my $number (1 .. NUM_BUGS) { + $self->_test_content_for_bug($number, $results, $sql); } + } } sub _test_content_for_bug { - my ($self, $number, $results, $sql) = @_; - my $name = $self->name; - - my $contains_known_broken = $self->contains_known_broken($number); - - my %result_ids = map { $_->[0] => 1 } @$results; - my $bug_id = $self->bug($number)->id; - - TODO: { - local $TODO = $contains_known_broken if $contains_known_broken; - if ($self->bug_is_contained($number)) { - ok($result_ids{$bug_id}, - "$name: contains bug $number ($bug_id)") - or diag $self->debug_fail($number, $results, $sql); - } - else { - ok(!$result_ids{$bug_id}, - "$name: does not contain bug $number ($bug_id)") - or diag $self->debug_fail($number, $results, $sql); - } + my ($self, $number, $results, $sql) = @_; + my $name = $self->name; + + my $contains_known_broken = $self->contains_known_broken($number); + + my %result_ids = map { $_->[0] => 1 } @$results; + my $bug_id = $self->bug($number)->id; + +TODO: { + local $TODO = $contains_known_broken if $contains_known_broken; + if ($self->bug_is_contained($number)) { + ok($result_ids{$bug_id}, "$name: contains bug $number ($bug_id)") + or diag $self->debug_fail($number, $results, $sql); + } + else { + ok(!$result_ids{$bug_id}, "$name: does not contain bug $number ($bug_id)") + or diag $self->debug_fail($number, $results, $sql); } + } } 1; diff --git a/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm b/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm index 8c3ff19ab..1be251fe9 100644 --- a/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm +++ b/xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm @@ -29,9 +29,9 @@ use base qw(Bugzilla::Test::Search::FieldTest); use Scalar::Util qw(blessed); use constant CH_OPERATOR => { - changedafter => 'chfieldfrom', - changedbefore => 'chfieldto', - changedto => 'chfieldvalue', + changedafter => 'chfieldfrom', + changedbefore => 'chfieldto', + changedto => 'chfieldvalue', }; use constant EMAIL_FIELDS => qw(assigned_to qa_contact cc reporter commenter); @@ -41,78 +41,79 @@ use constant EMAIL_FIELDS => qw(assigned_to qa_contact cc reporter commenter); # sometimes (like in Bugzilla::Test::Search's direct code) we just want # to create a FieldTestNormal. sub new { - my $class = shift; - my ($first_arg) = @_; - if (blessed $first_arg - and $first_arg->isa('Bugzilla::Test::Search::FieldTest')) - { - my $self = { %$first_arg }; - return bless $self, $class; - } - return $class->SUPER::new(@_); + my $class = shift; + my ($first_arg) = @_; + if (blessed $first_arg and $first_arg->isa('Bugzilla::Test::Search::FieldTest')) + { + my $self = {%$first_arg}; + return bless $self, $class; + } + return $class->SUPER::new(@_); } sub name { - my $self = shift; - my $name = $self->SUPER::name(@_); - return "$name (Normal Params)"; + my $self = shift; + my $name = $self->SUPER::name(@_); + return "$name (Normal Params)"; } sub search_columns { - my $self = shift; - my $field = $self->field; - # For the assigned_to, qa_contact, and reporter fields, have the - # "Normal Params" test check that the _realname columns work - # all by themselves. - if (grep($_ eq $field, EMAIL_FIELDS) && $self->field_object->buglist) { - return ['bug_id', "${field}_realname"] - } - return $self->SUPER::search_columns(@_); + my $self = shift; + my $field = $self->field; + + # For the assigned_to, qa_contact, and reporter fields, have the + # "Normal Params" test check that the _realname columns work + # all by themselves. + if (grep($_ eq $field, EMAIL_FIELDS) && $self->field_object->buglist) { + return ['bug_id', "${field}_realname"]; + } + return $self->SUPER::search_columns(@_); } sub search_params { - my ($self) = @_; - my $field = $self->field; - my $operator = $self->operator; - my $value = $self->translated_value; - if ($operator eq 'anyexact') { - $value = [split ',', $value]; - } + my ($self) = @_; + my $field = $self->field; + my $operator = $self->operator; + my $value = $self->translated_value; + if ($operator eq 'anyexact') { + $value = [split ',', $value]; + } - if (my $ch_param = CH_OPERATOR->{$operator}) { - if ($field eq 'creation_ts') { - $field = '[Bug creation]'; - } - return { chfield => $field, $ch_param => $value }; + if (my $ch_param = CH_OPERATOR->{$operator}) { + if ($field eq 'creation_ts') { + $field = '[Bug creation]'; } + return {chfield => $field, $ch_param => $value}; + } - if ($field eq 'delta_ts' and $operator eq 'greaterthaneq') { - return { chfieldfrom => $value }; - } - if ($field eq 'delta_ts' and $operator eq 'lessthaneq') { - return { chfieldto => $value }; - } + if ($field eq 'delta_ts' and $operator eq 'greaterthaneq') { + return {chfieldfrom => $value}; + } + if ($field eq 'delta_ts' and $operator eq 'lessthaneq') { + return {chfieldto => $value}; + } - if ($field eq 'deadline' and $operator eq 'greaterthaneq') { - return { deadlinefrom => $value }; - } - if ($field eq 'deadline' and $operator eq 'lessthaneq') { - return { deadlineto => $value }; - } + if ($field eq 'deadline' and $operator eq 'greaterthaneq') { + return {deadlinefrom => $value}; + } + if ($field eq 'deadline' and $operator eq 'lessthaneq') { + return {deadlineto => $value}; + } - if (grep { $_ eq $field } EMAIL_FIELDS) { - $field = 'longdesc' if $field eq 'commenter'; - return { - email1 => $value, - "email${field}1" => 1, - emailtype1 => $operator, - # Used to do extra tests on special sorts of email* combinations. - %{ $self->test->{extra_params} || {} }, - }; - } + if (grep { $_ eq $field } EMAIL_FIELDS) { + $field = 'longdesc' if $field eq 'commenter'; + return { + email1 => $value, + "email${field}1" => 1, + emailtype1 => $operator, + + # Used to do extra tests on special sorts of email* combinations. + %{$self->test->{extra_params} || {}}, + }; + } - $field =~ s/\./_/g; - return { $field => $value, "${field}_type" => $operator }; + $field =~ s/\./_/g; + return {$field => $value, "${field}_type" => $operator}; } 1; diff --git a/xt/lib/Bugzilla/Test/Search/InjectionTest.pm b/xt/lib/Bugzilla/Test/Search/InjectionTest.pm index 41f5fcdc2..d4654b972 100644 --- a/xt/lib/Bugzilla/Test/Search/InjectionTest.pm +++ b/xt/lib/Bugzilla/Test/Search/InjectionTest.pm @@ -32,60 +32,62 @@ use Test::Exception; sub num_tests { return NUM_SEARCH_TESTS } sub _known_broken { - my ($self) = @_; - my $operator_broken = INJECTION_BROKEN_OPERATOR->{$self->operator}; - # We don't want to auto-vivify $operator_broken and thus make it true. - my @field_ok = $operator_broken ? @{ $operator_broken->{field_ok} || [] } - : (); - $operator_broken = undef if grep { $_ eq $self->field } @field_ok; - - my $field_broken = INJECTION_BROKEN_FIELD->{$self->field} - || INJECTION_BROKEN_FIELD->{$self->field_object->type}; - # We don't want to auto-vivify $field_broken and thus make it true. - my @operator_ok = $field_broken ? @{ $field_broken->{operator_ok} || [] } - : (); - $field_broken = undef if grep { $_ eq $self->operator } @operator_ok; - - return $operator_broken || $field_broken || {}; + my ($self) = @_; + my $operator_broken = INJECTION_BROKEN_OPERATOR->{$self->operator}; + + # We don't want to auto-vivify $operator_broken and thus make it true. + my @field_ok = $operator_broken ? @{$operator_broken->{field_ok} || []} : (); + $operator_broken = undef if grep { $_ eq $self->field } @field_ok; + + my $field_broken = INJECTION_BROKEN_FIELD->{$self->field} + || INJECTION_BROKEN_FIELD->{$self->field_object->type}; + + # We don't want to auto-vivify $field_broken and thus make it true. + my @operator_ok = $field_broken ? @{$field_broken->{operator_ok} || []} : (); + $field_broken = undef if grep { $_ eq $self->operator } @operator_ok; + + return $operator_broken || $field_broken || {}; } sub sql_error_ok { return $_[0]->_known_broken->{sql_error} } # Injection tests only skip fields on certain dbs. sub field_not_yet_implemented { - my ($self) = @_; - # We use the constant directly because we don't want operator_ok - # or field_ok to stop us. - my $broken = INJECTION_BROKEN_FIELD->{$self->field} - || INJECTION_BROKEN_FIELD->{$self->field_object->type}; - my $skip_for_dbs = $broken->{db_skip}; - return undef if !$skip_for_dbs; - my $dbh = Bugzilla->dbh; - if (my ($skip) = grep { $dbh->isa("Bugzilla::DB::$_") } @$skip_for_dbs) { - my $field = $self->field; - return "$field injection testing is not supported with $skip"; - } - return undef; + my ($self) = @_; + + # We use the constant directly because we don't want operator_ok + # or field_ok to stop us. + my $broken = INJECTION_BROKEN_FIELD->{$self->field} + || INJECTION_BROKEN_FIELD->{$self->field_object->type}; + my $skip_for_dbs = $broken->{db_skip}; + return undef if !$skip_for_dbs; + my $dbh = Bugzilla->dbh; + if (my ($skip) = grep { $dbh->isa("Bugzilla::DB::$_") } @$skip_for_dbs) { + my $field = $self->field; + return "$field injection testing is not supported with $skip"; + } + return undef; } + # Injection tests don't do translation. sub translated_value { $_[0]->test_value } sub name { return "injection-" . $_[0]->SUPER::name; } # Injection tests don't check content. -sub _test_content {} +sub _test_content { } sub _test_sql { - my $self = shift; - my ($sql) = @_; - my $dbh = Bugzilla->dbh; - my $name = $self->name; - if (my $error_ok = $self->sql_error_ok) { - throws_ok { $dbh->selectall_arrayref($sql) } $error_ok, - "$name: SQL query dies, as we expect"; - return; - } - return $self->SUPER::_test_sql(@_); + my $self = shift; + my ($sql) = @_; + my $dbh = Bugzilla->dbh; + my $name = $self->name; + if (my $error_ok = $self->sql_error_ok) { + throws_ok { $dbh->selectall_arrayref($sql) } $error_ok, + "$name: SQL query dies, as we expect"; + return; + } + return $self->SUPER::_test_sql(@_); } -1; \ No newline at end of file +1; diff --git a/xt/lib/Bugzilla/Test/Search/NotTest.pm b/xt/lib/Bugzilla/Test/Search/NotTest.pm index 86da4c644..7d11a3652 100644 --- a/xt/lib/Bugzilla/Test/Search/NotTest.pm +++ b/xt/lib/Bugzilla/Test/Search/NotTest.pm @@ -34,9 +34,9 @@ use Bugzilla::Test::Search::Constants; # We just clone a FieldTest because that's the best for performance, # overall--that way we don't have to translate the value again. sub new { - my ($class, $field_test) = @_; - my $self = { %$field_test }; - return bless $self, $class; + my ($class, $field_test) = @_; + my $self = {%$field_test}; + return bless $self, $class; } ############# @@ -44,32 +44,33 @@ sub new { ############# sub name { - my ($self) = @_; - return "NOT(" . $self->SUPER::name . ")"; + my ($self) = @_; + return "NOT(" . $self->SUPER::name . ")"; } # True if this test is supposed to contain the numbered bug. Reversed for # NOT tests. sub bug_is_contained { - my $self = shift; - my ($number) = @_; - # No search ever returns bug 6, because it's protected by security groups - # that the searcher isn't a member of. - return 0 if $number == 6; - return $self->SUPER::bug_is_contained(@_) ? 0 : 1; + my $self = shift; + my ($number) = @_; + + # No search ever returns bug 6, because it's protected by security groups + # that the searcher isn't a member of. + return 0 if $number == 6; + return $self->SUPER::bug_is_contained(@_) ? 0 : 1; } # NOT tests have their own constant for tracking broken-ness. sub _known_broken { - my ($self) = @_; - return $self->SUPER::_known_broken(BROKEN_NOT, 'skip pg check'); + my ($self) = @_; + return $self->SUPER::_known_broken(BROKEN_NOT, 'skip pg check'); } sub search_params { - my ($self) = @_; - my %params = %{ $self->SUPER::search_params() }; - $params{negate0} = 1; - return \%params; + my ($self) = @_; + my %params = %{$self->SUPER::search_params()}; + $params{negate0} = 1; + return \%params; } -1; \ No newline at end of file +1; diff --git a/xt/lib/Bugzilla/Test/Search/OperatorTest.pm b/xt/lib/Bugzilla/Test/Search/OperatorTest.pm index a6d24ffca..86ac01c94 100644 --- a/xt/lib/Bugzilla/Test/Search/OperatorTest.pm +++ b/xt/lib/Bugzilla/Test/Search/OperatorTest.pm @@ -38,10 +38,10 @@ use Bugzilla::Test::Search::NotTest; ############### sub new { - my ($invocant, $operator, $search_test) = @_; - $search_test ||= $invocant->search_test; - my $class = ref($invocant) || $invocant; - return bless { search_test => $search_test, operator => $operator }, $class; + my ($invocant, $operator, $search_test) = @_; + $search_test ||= $invocant->search_test; + my $class = ref($invocant) || $invocant; + return bless {search_test => $search_test, operator => $operator}, $class; } ############# @@ -50,68 +50,69 @@ sub new { # The Bugzilla::Test::Search object that this is a child of. sub search_test { return $_[0]->{search_test} } + # The operator being tested sub operator { return $_[0]->{operator} } + # The tests that we're going to run on this operator. -sub tests { return @{ TESTS->{$_[0]->operator } } } +sub tests { return @{TESTS->{$_[0]->operator}} } + # The fields we're going to test for this operator. sub test_fields { return $_[0]->search_test->all_fields } sub run { - my ($self) = @_; - - foreach my $field ($self->test_fields) { - foreach my $test ($self->tests) { - my $field_test = - new Bugzilla::Test::Search::FieldTest($self, $field, $test); - $field_test->run(); - my $normal_test = - new Bugzilla::Test::Search::FieldTestNormal($field_test); - $normal_test->run(); - my $not_test = new Bugzilla::Test::Search::NotTest($field_test); - $not_test->run(); - - next if !$self->search_test->option('long'); - - # Run the OR tests. This tests every other operator (including - # this operator itself) in combination with every other field, - # in an OR with this operator and field. - foreach my $other_operator ($self->search_test->all_operators) { - $self->run_join_tests($field_test, $other_operator); - } - } - foreach my $test (INJECTION_TESTS) { - my $injection_test = - new Bugzilla::Test::Search::InjectionTest($self, $field, $test); - $injection_test->run(); - } + my ($self) = @_; + + foreach my $field ($self->test_fields) { + foreach my $test ($self->tests) { + my $field_test = new Bugzilla::Test::Search::FieldTest($self, $field, $test); + $field_test->run(); + my $normal_test = new Bugzilla::Test::Search::FieldTestNormal($field_test); + $normal_test->run(); + my $not_test = new Bugzilla::Test::Search::NotTest($field_test); + $not_test->run(); + + next if !$self->search_test->option('long'); + + # Run the OR tests. This tests every other operator (including + # this operator itself) in combination with every other field, + # in an OR with this operator and field. + foreach my $other_operator ($self->search_test->all_operators) { + $self->run_join_tests($field_test, $other_operator); + } } + foreach my $test (INJECTION_TESTS) { + my $injection_test + = new Bugzilla::Test::Search::InjectionTest($self, $field, $test); + $injection_test->run(); + } + } } sub run_join_tests { - my ($self, $field_test, $other_operator) = @_; - - my $other_operator_test = $self->new($other_operator); - foreach my $other_test ($other_operator_test->tests) { - foreach my $other_field ($self->test_fields) { - $self->_run_one_join_test($field_test, $other_operator_test, - $other_field, $other_test); - $self->search_test->clean_test_history(); - } + my ($self, $field_test, $other_operator) = @_; + + my $other_operator_test = $self->new($other_operator); + foreach my $other_test ($other_operator_test->tests) { + foreach my $other_field ($self->test_fields) { + $self->_run_one_join_test($field_test, $other_operator_test, $other_field, + $other_test); + $self->search_test->clean_test_history(); } + } } sub _run_one_join_test { - my ($self, $field_test, $other_operator_test, $other_field, $other_test) = @_; - my $other_field_test = - new Bugzilla::Test::Search::FieldTest($other_operator_test, - $other_field, $other_test); - my $or_test = new Bugzilla::Test::Search::OrTest($field_test, - $other_field_test); - $or_test->run(); - my $and_test = new Bugzilla::Test::Search::AndTest($field_test, - $other_field_test); - $and_test->run(); + my ($self, $field_test, $other_operator_test, $other_field, $other_test) = @_; + my $other_field_test + = new Bugzilla::Test::Search::FieldTest($other_operator_test, $other_field, + $other_test); + my $or_test + = new Bugzilla::Test::Search::OrTest($field_test, $other_field_test); + $or_test->run(); + my $and_test + = new Bugzilla::Test::Search::AndTest($field_test, $other_field_test); + $and_test->run(); } -1; \ No newline at end of file +1; diff --git a/xt/lib/Bugzilla/Test/Search/OrTest.pm b/xt/lib/Bugzilla/Test/Search/OrTest.pm index 57c235fda..7650e1b1a 100644 --- a/xt/lib/Bugzilla/Test/Search/OrTest.pm +++ b/xt/lib/Bugzilla/Test/Search/OrTest.pm @@ -34,36 +34,36 @@ use constant type => 'OR'; ############### sub new { - my $class = shift; - my $self = { field_tests => [@_] }; - return bless $self, $class; + my $class = shift; + my $self = {field_tests => [@_]}; + return bless $self, $class; } ############# # Accessors # ############# -sub field_tests { return @{ $_[0]->{field_tests} } } +sub field_tests { return @{$_[0]->{field_tests}} } sub search_test { ($_[0]->field_tests)[0]->search_test } sub name { - my ($self) = @_; - my @names = map { $_->name } $self->field_tests; - return join('-' . $self->type . '-', @names); + my ($self) = @_; + my @names = map { $_->name } $self->field_tests; + return join('-' . $self->type . '-', @names); } # In an OR test, bugs ARE supposed to be contained if they are contained # by ANY test. sub bug_is_contained { - my ($self, $number) = @_; - return any { $_->bug_is_contained($number) } $self->field_tests; + my ($self, $number) = @_; + return any { $_->bug_is_contained($number) } $self->field_tests; } # Needed only for failure messages sub debug_value { - my ($self) = @_; - my @values = map { $_->field . ' ' . $_->debug_value } $self->field_tests; - return join(' ' . $self->type . ' ', @values); + my ($self) = @_; + my @values = map { $_->field . ' ' . $_->debug_value } $self->field_tests; + return join(' ' . $self->type . ' ', @values); } ######################## @@ -71,61 +71,66 @@ sub debug_value { ######################## sub field_not_yet_implemented { - my ($self) = @_; - return $self->_join_messages('field_not_yet_implemented'); + my ($self) = @_; + return $self->_join_messages('field_not_yet_implemented'); } + sub invalid_field_operator_combination { - my ($self) = @_; - return $self->_join_messages('invalid_field_operator_combination'); + my ($self) = @_; + return $self->_join_messages('invalid_field_operator_combination'); } + sub search_known_broken { - my ($self) = @_; - return $self->_join_messages('search_known_broken'); + my ($self) = @_; + return $self->_join_messages('search_known_broken'); } sub _join_messages { - my ($self, $message_method) = @_; - my @messages = map { $_->$message_method } $self->field_tests; - @messages = grep { $_ } @messages; - return join(' AND ', @messages); + my ($self, $message_method) = @_; + my @messages = map { $_->$message_method } $self->field_tests; + @messages = grep {$_} @messages; + return join(' AND ', @messages); } sub _bug_will_actually_be_contained { - my ($self, $number) = @_; - - foreach my $test ($self->field_tests) { - # Some tests are broken in such a way that they actually - # generate no criteria in the SQL. In this case, the only way - # the test contains the bug is if *another* test contains it. - next if $test->_known_broken->{no_criteria}; - return 1 if $test->will_actually_contain_bug($number); - } - return 0; + my ($self, $number) = @_; + + foreach my $test ($self->field_tests) { + + # Some tests are broken in such a way that they actually + # generate no criteria in the SQL. In this case, the only way + # the test contains the bug is if *another* test contains it. + next if $test->_known_broken->{no_criteria}; + return 1 if $test->will_actually_contain_bug($number); + } + return 0; } sub contains_known_broken { - my ($self, $number) = @_; - - if ( ( $self->bug_is_contained($number) - and !$self->_bug_will_actually_be_contained($number) ) - or ( !$self->bug_is_contained($number) - and $self->_bug_will_actually_be_contained($number) ) ) - { - my @messages = map { $_->contains_known_broken($number) } - $self->field_tests; - @messages = grep { $_ } @messages; - # Sometimes, with things that break because of no_criteria, there won't - # be anything in @messages even though we need to print out a message. - if (!@messages) { - my @no_criteria = grep { $_->_known_broken->{no_criteria} } - $self->field_tests; - @messages = map { "No criteria generated by " . $_->name } - @no_criteria; - } - die "broken test with no message" if !@messages; - return join(' AND ', @messages); + my ($self, $number) = @_; + + if ( + ( + $self->bug_is_contained($number) + and !$self->_bug_will_actually_be_contained($number) + ) + or ( !$self->bug_is_contained($number) + and $self->_bug_will_actually_be_contained($number)) + ) + { + my @messages = map { $_->contains_known_broken($number) } $self->field_tests; + @messages = grep {$_} @messages; + + # Sometimes, with things that break because of no_criteria, there won't + # be anything in @messages even though we need to print out a message. + if (!@messages) { + my @no_criteria = grep { $_->_known_broken->{no_criteria} } $self->field_tests; + @messages = map { "No criteria generated by " . $_->name } @no_criteria; } - return undef; + die "broken test with no message" if !@messages; + return join(' AND ', @messages); + } + return undef; } ############################## @@ -133,23 +138,23 @@ sub contains_known_broken { ############################## sub search_columns { - my ($self) = @_; - my @columns = map { @{ $_->search_columns } } $self->field_tests; - return [uniq @columns]; + my ($self) = @_; + my @columns = map { @{$_->search_columns} } $self->field_tests; + return [uniq @columns]; } sub search_params { - my ($self) = @_; - my @all_params = map { $_->search_params } $self->field_tests; - my %params; - my $chart = 0; - foreach my $item (@all_params) { - $params{"field0-0-$chart"} = $item->{'field0-0-0'}; - $params{"type0-0-$chart"} = $item->{'type0-0-0'}; - $params{"value0-0-$chart"} = $item->{'value0-0-0'}; - $chart++; - } - return \%params; + my ($self) = @_; + my @all_params = map { $_->search_params } $self->field_tests; + my %params; + my $chart = 0; + foreach my $item (@all_params) { + $params{"field0-0-$chart"} = $item->{'field0-0-0'}; + $params{"type0-0-$chart"} = $item->{'type0-0-0'}; + $params{"value0-0-$chart"} = $item->{'value0-0-0'}; + $chart++; + } + return \%params; } 1; diff --git a/xt/search.t b/xt/search.t index bd77f5b20..5ae1a606a 100644 --- a/xt/search.t +++ b/xt/search.t @@ -36,7 +36,8 @@ use Test::More; my %switches; GetOptions(\%switches, 'operators=s', 'top-operators=s', 'long', - 'add-custom-fields', 'help|h') || die $@; + 'add-custom-fields', 'help|h') + || die $@; pod2usage(verbose => 1) if $switches{'help'}; -- cgit v1.2.3-24-g4f1b