aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristian Ruppert <idl0r@gentoo.org>2015-07-11 15:27:33 +0200
committerChristian Ruppert <idl0r@gentoo.org>2015-07-11 15:27:33 +0200
commit4b2ce2725e9a4525e273fb1b08243aad74770a3d (patch)
treec90fb0fef5c9ac923817f908f1c3586efa47ec59
parentBumped version to 4.4.9 (diff)
downloadgentoo-bugzilla-4b2ce2725e9a4525e273fb1b08243aad74770a3d.tar.gz
gentoo-bugzilla-4b2ce2725e9a4525e273fb1b08243aad74770a3d.tar.bz2
gentoo-bugzilla-4b2ce2725e9a4525e273fb1b08243aad74770a3d.zip
Vanilla 4.4.9 to 5.0 without history due to massive merge conflicts
-rw-r--r--.bzrignore22
-rw-r--r--.gitignore22
-rw-r--r--.htaccess8
-rw-r--r--.travis.yml26
-rw-r--r--Bugzilla.pm233
-rw-r--r--Bugzilla/Attachment.pm200
-rw-r--r--Bugzilla/Attachment/PatchReader.pm203
-rw-r--r--Bugzilla/Auth.pm12
-rw-r--r--Bugzilla/Auth/Login.pm3
-rw-r--r--Bugzilla/Auth/Login/APIKey.pm53
-rw-r--r--Bugzilla/Auth/Login/CGI.pm6
-rw-r--r--Bugzilla/Auth/Login/Cookie.pm38
-rw-r--r--Bugzilla/Auth/Login/Env.pm7
-rw-r--r--Bugzilla/Auth/Login/Stack.pm4
-rw-r--r--Bugzilla/Auth/Persist/Cookie.pm7
-rw-r--r--Bugzilla/Auth/Verify.pm12
-rw-r--r--Bugzilla/Auth/Verify/DB.pm37
-rw-r--r--Bugzilla/Auth/Verify/LDAP.pm6
-rw-r--r--Bugzilla/Auth/Verify/RADIUS.pm6
-rw-r--r--Bugzilla/Auth/Verify/Stack.pm4
-rw-r--r--Bugzilla/Bug.pm925
-rw-r--r--Bugzilla/BugMail.pm264
-rw-r--r--Bugzilla/BugUrl.pm21
-rw-r--r--Bugzilla/BugUrl/Bugzilla.pm6
-rw-r--r--Bugzilla/BugUrl/Bugzilla/Local.pm6
-rw-r--r--Bugzilla/BugUrl/Debian.pm6
-rw-r--r--Bugzilla/BugUrl/GitHub.pm10
-rw-r--r--Bugzilla/BugUrl/Google.pm6
-rw-r--r--Bugzilla/BugUrl/JIRA.pm6
-rw-r--r--Bugzilla/BugUrl/Launchpad.pm6
-rw-r--r--Bugzilla/BugUrl/MantisBT.pm6
-rw-r--r--Bugzilla/BugUrl/SourceForge.pm6
-rw-r--r--Bugzilla/BugUrl/Trac.pm6
-rw-r--r--Bugzilla/BugUserLastVisit.pm93
-rw-r--r--Bugzilla/CGI.pm116
-rw-r--r--Bugzilla/Chart.pm32
-rw-r--r--Bugzilla/Classification.pm43
-rw-r--r--Bugzilla/Comment.pm255
-rw-r--r--Bugzilla/Comment/TagWeights.pm78
-rw-r--r--Bugzilla/Component.pm39
-rw-r--r--Bugzilla/Config.pm148
-rw-r--r--Bugzilla/Config/Admin.pm9
-rw-r--r--Bugzilla/Config/Advanced.pm3
-rw-r--r--Bugzilla/Config/Attachment.pm2
-rw-r--r--Bugzilla/Config/Auth.pm19
-rw-r--r--Bugzilla/Config/BugChange.pm2
-rw-r--r--Bugzilla/Config/BugFields.pm10
-rw-r--r--Bugzilla/Config/Common.pm106
-rw-r--r--Bugzilla/Config/Core.pm2
-rw-r--r--Bugzilla/Config/DependencyGraph.pm13
-rw-r--r--Bugzilla/Config/General.pm11
-rw-r--r--Bugzilla/Config/GroupSecurity.pm19
-rw-r--r--Bugzilla/Config/LDAP.pm2
-rw-r--r--Bugzilla/Config/MTA.pm13
-rw-r--r--Bugzilla/Config/Memcached.pm33
-rw-r--r--Bugzilla/Config/PatchViewer.pm51
-rw-r--r--Bugzilla/Config/Query.pm9
-rw-r--r--Bugzilla/Config/RADIUS.pm2
-rw-r--r--Bugzilla/Config/ShadowDB.pm2
-rw-r--r--Bugzilla/Config/UserMatch.pm2
-rw-r--r--Bugzilla/Constants.pm77
-rw-r--r--Bugzilla/DB.pm96
-rw-r--r--Bugzilla/DB/Mysql.pm89
-rw-r--r--Bugzilla/DB/Oracle.pm86
-rw-r--r--Bugzilla/DB/Pg.pm161
-rw-r--r--Bugzilla/DB/Schema.pm164
-rw-r--r--Bugzilla/DB/Schema/Mysql.pm31
-rw-r--r--Bugzilla/DB/Schema/Oracle.pm36
-rw-r--r--Bugzilla/DB/Schema/Pg.pm21
-rw-r--r--Bugzilla/DB/Schema/Sqlite.pm31
-rw-r--r--Bugzilla/DB/Sqlite.pm44
-rw-r--r--Bugzilla/Error.pm37
-rw-r--r--Bugzilla/Extension.pm30
-rw-r--r--Bugzilla/Field.pm58
-rw-r--r--Bugzilla/Field/Choice.pm32
-rw-r--r--Bugzilla/Field/ChoiceInterface.pm13
-rw-r--r--Bugzilla/Flag.pm173
-rw-r--r--Bugzilla/FlagType.pm76
-rw-r--r--Bugzilla/Group.pm64
-rw-r--r--Bugzilla/Hook.pm156
-rw-r--r--Bugzilla/Install.pm22
-rw-r--r--Bugzilla/Install/CPAN.pm27
-rw-r--r--Bugzilla/Install/DB.pm120
-rw-r--r--Bugzilla/Install/Filesystem.pm180
-rw-r--r--Bugzilla/Install/Localconfig.pm24
-rw-r--r--Bugzilla/Install/Requirements.pm133
-rw-r--r--Bugzilla/Install/Util.pm82
-rw-r--r--Bugzilla/Job/BugMail.pm32
-rw-r--r--Bugzilla/Job/Mailer.pm6
-rw-r--r--Bugzilla/JobQueue.pm33
-rw-r--r--Bugzilla/JobQueue/Runner.pm35
-rw-r--r--Bugzilla/Keyword.pm24
-rw-r--r--Bugzilla/Mailer.pm133
-rw-r--r--Bugzilla/Memcached.pm483
-rw-r--r--Bugzilla/Migrate.pm67
-rw-r--r--Bugzilla/Migrate/Gnats.pm26
-rw-r--r--Bugzilla/Milestone.pm20
-rw-r--r--Bugzilla/Object.pm371
-rw-r--r--Bugzilla/Product.pm55
-rw-r--r--Bugzilla/RNG.pm18
-rw-r--r--Bugzilla/Report.pm22
-rw-r--r--Bugzilla/Search.pm423
-rw-r--r--Bugzilla/Search/Clause.pm27
-rw-r--r--Bugzilla/Search/ClauseGroup.pm14
-rw-r--r--Bugzilla/Search/Condition.pm30
-rw-r--r--Bugzilla/Search/Quicksearch.pm64
-rw-r--r--Bugzilla/Search/Recent.pm33
-rw-r--r--Bugzilla/Search/Saved.pm32
-rw-r--r--Bugzilla/Sender/Transport/Sendmail.pm (renamed from Bugzilla/Send/Sendmail.pm)54
-rw-r--r--Bugzilla/Series.pm32
-rw-r--r--Bugzilla/Status.pm55
-rw-r--r--Bugzilla/Template.pm318
-rw-r--r--Bugzilla/Template/Context.pm26
-rw-r--r--Bugzilla/Template/Plugin/Bugzilla.pm4
-rw-r--r--Bugzilla/Template/Plugin/Hook.pm6
-rw-r--r--Bugzilla/Token.pm72
-rw-r--r--Bugzilla/Update.pm2
-rw-r--r--Bugzilla/User.pm905
-rw-r--r--Bugzilla/User/APIKey.pm155
-rw-r--r--Bugzilla/User/Setting.pm46
-rw-r--r--Bugzilla/User/Setting/Lang.pm4
-rw-r--r--Bugzilla/User/Setting/Skin.pm4
-rw-r--r--Bugzilla/User/Setting/Timezone.pm4
-rw-r--r--Bugzilla/UserAgent.pm5
-rw-r--r--Bugzilla/Util.pm97
-rw-r--r--Bugzilla/Version.pm130
-rw-r--r--Bugzilla/WebService.pm111
-rw-r--r--Bugzilla/WebService/Bug.pm1467
-rw-r--r--Bugzilla/WebService/BugUserLastVisit.pm207
-rw-r--r--Bugzilla/WebService/Bugzilla.pm81
-rw-r--r--Bugzilla/WebService/Classification.pm48
-rw-r--r--Bugzilla/WebService/Component.pm153
-rw-r--r--Bugzilla/WebService/Constants.pm142
-rw-r--r--Bugzilla/WebService/FlagType.pm834
-rw-r--r--Bugzilla/WebService/Group.pm332
-rw-r--r--Bugzilla/WebService/Product.pm251
-rw-r--r--Bugzilla/WebService/README2
-rw-r--r--Bugzilla/WebService/Server.pm79
-rw-r--r--Bugzilla/WebService/Server/JSONRPC.pm58
-rw-r--r--Bugzilla/WebService/Server/REST.pm689
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bug.pm179
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/BugUserLastVisit.pm52
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Bugzilla.pm70
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Classification.pm50
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Component.pm48
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/FlagType.pm72
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Group.pm60
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/Product.pm83
-rw-r--r--Bugzilla/WebService/Server/REST/Resources/User.pm81
-rw-r--r--Bugzilla/WebService/Server/XMLRPC.pm92
-rw-r--r--Bugzilla/WebService/User.pm232
-rw-r--r--Bugzilla/WebService/Util.pm190
-rw-r--r--Bugzilla/Whine.pm8
-rw-r--r--Bugzilla/Whine/Query.pm12
-rw-r--r--Bugzilla/Whine/Schedule.pm16
-rwxr-xr-xadmin.cgi15
-rwxr-xr-xattachment.cgi174
-rwxr-xr-xbuglist.cgi150
-rwxr-xr-xchart.cgi9
-rwxr-xr-xchecksetup.pl19
-rwxr-xr-xclean-bug-user-last-visit.pl38
-rwxr-xr-xcolchange.cgi7
-rwxr-xr-xcollectstats.pl63
-rwxr-xr-xconfig.cgi19
-rw-r--r--contrib/Bugzilla.pm2
-rw-r--r--contrib/README4
-rwxr-xr-xcontrib/bz_webservice_demo.pl7
-rwxr-xr-xcontrib/bzdbcopy.pl5
-rwxr-xr-xcontrib/console.pl6
-rwxr-xr-xcontrib/convert-workflow.pl6
-rwxr-xr-xcontrib/cvs-update.pl35
-rwxr-xr-xcontrib/extension-convert.pl10
-rwxr-xr-xcontrib/fixperms.pl4
-rwxr-xr-xcontrib/merge-users.pl28
-rwxr-xr-xcontrib/mysqld-watcher.pl4
-rwxr-xr-xcontrib/recode.pl5
-rwxr-xr-xcontrib/sendbugmail.pl33
-rwxr-xr-xcontrib/sendunsentbugmail.pl31
-rwxr-xr-xcontrib/syncLDAP.pl4
-rwxr-xr-xcreateaccount.cgi6
-rwxr-xr-xdescribecomponents.cgi7
-rwxr-xr-xdescribekeywords.cgi10
-rw-r--r--docs/TODO16
-rw-r--r--docs/bugzilla.ent.tmpl7
-rw-r--r--docs/en/Makefile158
-rw-r--r--docs/en/README.docs42
-rw-r--r--docs/en/images/bugzilla.pngbin0 -> 8892 bytes
-rw-r--r--docs/en/images/callouts/1.gifbin890 -> 0 bytes
-rw-r--r--docs/en/images/callouts/2.gifbin907 -> 0 bytes
-rw-r--r--docs/en/images/callouts/3.gifbin914 -> 0 bytes
-rw-r--r--docs/en/images/caution.gifbin134 -> 0 bytes
-rw-r--r--docs/en/images/note.gifbin226 -> 0 bytes
-rw-r--r--docs/en/images/tip.gifbin1229 -> 0 bytes
-rw-r--r--docs/en/images/warning.gifbin140 -> 0 bytes
-rw-r--r--docs/en/make.bat197
-rw-r--r--docs/en/rel_notes.txt3028
-rw-r--r--docs/en/rst/_static/bugzilla.css21
-rw-r--r--docs/en/rst/about/index.rst128
-rw-r--r--docs/en/rst/administering/categorization.rst416
-rw-r--r--docs/en/rst/administering/custom-fields.rst149
-rw-r--r--docs/en/rst/administering/extensions.rst18
-rw-r--r--docs/en/rst/administering/field-values.rst45
-rw-r--r--docs/en/rst/administering/flags.rst155
-rw-r--r--docs/en/rst/administering/groups.rst191
-rw-r--r--docs/en/rst/administering/index.rst26
-rw-r--r--docs/en/rst/administering/keywords.rst16
-rw-r--r--docs/en/rst/administering/parameters.rst794
-rw-r--r--docs/en/rst/administering/preferences.rst10
-rw-r--r--docs/en/rst/administering/quips.rst38
-rw-r--r--docs/en/rst/administering/users.rst244
-rw-r--r--docs/en/rst/administering/whining.rst145
-rw-r--r--docs/en/rst/administering/workflow.rst34
-rw-r--r--docs/en/rst/api/core/v1/attachment.rst351
-rw-r--r--docs/en/rst/api/core/v1/bug-user-last-visit.rst111
-rw-r--r--docs/en/rst/api/core/v1/bug.rst986
-rw-r--r--docs/en/rst/api/core/v1/bugzilla.rst302
-rw-r--r--docs/en/rst/api/core/v1/classification.rst80
-rw-r--r--docs/en/rst/api/core/v1/comment.rst242
-rw-r--r--docs/en/rst/api/core/v1/component.rst65
-rw-r--r--docs/en/rst/api/core/v1/field.rst240
-rw-r--r--docs/en/rst/api/core/v1/flagtype.rst373
-rw-r--r--docs/en/rst/api/core/v1/general.rst255
-rw-r--r--docs/en/rst/api/core/v1/group.rst278
-rw-r--r--docs/en/rst/api/core/v1/index.rst18
-rw-r--r--docs/en/rst/api/core/v1/product.rst416
-rw-r--r--docs/en/rst/api/core/v1/user.rst380
-rw-r--r--docs/en/rst/api/index.rst13
-rw-r--r--docs/en/rst/conf.py386
-rw-r--r--docs/en/rst/index.rst31
-rw-r--r--docs/en/rst/installing/apache-windows.rst128
-rw-r--r--docs/en/rst/installing/apache.rst137
-rw-r--r--docs/en/rst/installing/backups.rst38
-rw-r--r--docs/en/rst/installing/essential-post-install-config.rst172
-rw-r--r--docs/en/rst/installing/iis.rst109
-rw-r--r--docs/en/rst/installing/index.rst36
-rw-r--r--docs/en/rst/installing/installing-end.inc.rst66
-rw-r--r--docs/en/rst/installing/linux.rst214
-rw-r--r--docs/en/rst/installing/mac-os-x.rst130
-rw-r--r--docs/en/rst/installing/merging-accounts.rst14
-rw-r--r--docs/en/rst/installing/migrating-from-1.inc.rst7
-rw-r--r--docs/en/rst/installing/migrating-from-2.inc.rst149
-rw-r--r--docs/en/rst/installing/migrating-from-a-tarball.rst15
-rw-r--r--docs/en/rst/installing/migrating-from-bzr.rst11
-rw-r--r--docs/en/rst/installing/migrating-from-cvs.rst13
-rw-r--r--docs/en/rst/installing/migrating.rst23
-rw-r--r--docs/en/rst/installing/moving.rst66
-rw-r--r--docs/en/rst/installing/multiple-bugzillas.rst41
-rw-r--r--docs/en/rst/installing/mysql.rst143
-rw-r--r--docs/en/rst/installing/optional-post-install-config.rst154
-rw-r--r--docs/en/rst/installing/oracle.rst69
-rw-r--r--docs/en/rst/installing/postgresql.rst58
-rw-r--r--docs/en/rst/installing/quick-start.rst223
-rw-r--r--docs/en/rst/installing/sanity-check.rst26
-rw-r--r--docs/en/rst/installing/sqlite.rst14
-rw-r--r--docs/en/rst/installing/upgrading-customizations.rst20
-rw-r--r--docs/en/rst/installing/upgrading-with-1.inc.rst41
-rw-r--r--docs/en/rst/installing/upgrading-with-2.inc.rst32
-rw-r--r--docs/en/rst/installing/upgrading-with-a-tarball.rst74
-rw-r--r--docs/en/rst/installing/upgrading-with-git.rst69
-rw-r--r--docs/en/rst/installing/upgrading.rst70
-rw-r--r--docs/en/rst/installing/windows.rst174
-rw-r--r--docs/en/rst/integrating/apis.rst72
-rw-r--r--docs/en/rst/integrating/extensions.rst199
-rw-r--r--docs/en/rst/integrating/faq.rst27
-rw-r--r--docs/en/rst/integrating/index.rst22
-rw-r--r--docs/en/rst/integrating/languages.rst19
-rw-r--r--docs/en/rst/integrating/skins.rst27
-rw-r--r--docs/en/rst/integrating/templates.rst289
-rw-r--r--docs/en/rst/style.rst122
-rw-r--r--docs/en/rst/using/creating-an-account.rst39
-rw-r--r--docs/en/rst/using/editing.rst98
-rw-r--r--docs/en/rst/using/extensions.rst18
-rw-r--r--docs/en/rst/using/filing.rst82
-rw-r--r--docs/en/rst/using/finding.rst259
-rw-r--r--docs/en/rst/using/index.rst19
-rw-r--r--docs/en/rst/using/preferences.rst195
-rw-r--r--docs/en/rst/using/reports-and-charts.rst120
-rw-r--r--docs/en/rst/using/tips.rst65
-rw-r--r--docs/en/rst/using/understanding.rst303
-rw-r--r--docs/en/xml/Bugzilla-Guide.xml134
-rw-r--r--docs/en/xml/about.xml223
-rw-r--r--docs/en/xml/administration.xml3207
-rw-r--r--docs/en/xml/conventions.xml91
-rw-r--r--docs/en/xml/customization.xml612
-rw-r--r--docs/en/xml/gfdl.xml457
-rw-r--r--docs/en/xml/glossary.xml561
-rw-r--r--docs/en/xml/index.xml27
-rw-r--r--docs/en/xml/installation.xml2453
-rw-r--r--docs/en/xml/modules.xml188
-rw-r--r--docs/en/xml/patches.xml143
-rw-r--r--docs/en/xml/security.xml281
-rw-r--r--docs/en/xml/troubleshooting.xml287
-rw-r--r--docs/en/xml/using.xml2072
-rw-r--r--docs/lib/Pod/Simple/HTML/Bugzilla.pm5
-rw-r--r--docs/lib/Pod/Simple/HTMLBatch/Bugzilla.pm5
-rwxr-xr-xdocs/makedocs.pl179
-rw-r--r--docs/style.css4
-rw-r--r--docs/xsl/bugzilla-docs.xsl36
-rw-r--r--docs/xsl/chunks.xsl19
-rw-r--r--docs/xsl/nochunks.xsl16
-rw-r--r--docs/xsl/pdf.xsl42
-rwxr-xr-xduplicates.cgi10
-rwxr-xr-xeditclassifications.cgi19
-rwxr-xr-xeditcomponents.cgi7
-rwxr-xr-xeditfields.cgi5
-rwxr-xr-xeditflagtypes.cgi13
-rwxr-xr-xeditgroups.cgi5
-rwxr-xr-xeditkeywords.cgi5
-rwxr-xr-xeditmilestones.cgi7
-rwxr-xr-xeditparams.cgi5
-rwxr-xr-xeditproducts.cgi7
-rwxr-xr-xeditsettings.cgi6
-rwxr-xr-xeditusers.cgi125
-rwxr-xr-xeditvalues.cgi11
-rwxr-xr-xeditversions.cgi13
-rwxr-xr-xeditwhines.cgi4
-rwxr-xr-xeditworkflow.cgi4
-rwxr-xr-xemail_in.pl6
-rwxr-xr-xenter_bug.cgi36
-rw-r--r--extensions/BmpConvert/Config.pm4
-rw-r--r--extensions/BmpConvert/Extension.pm6
-rw-r--r--extensions/Example/Config.pm4
-rw-r--r--extensions/Example/Extension.pm133
-rw-r--r--extensions/Example/docs/en/rst/api/v1/index.rst4
-rw-r--r--extensions/Example/docs/en/rst/index-admin.rst12
-rw-r--r--extensions/Example/docs/en/rst/index-user.rst11
-rw-r--r--extensions/Example/lib/Auth/Login.pm6
-rw-r--r--extensions/Example/lib/Auth/Verify.pm6
-rw-r--r--extensions/Example/lib/Config.pm2
-rw-r--r--extensions/Example/lib/Util.pm2
-rw-r--r--extensions/Example/lib/WebService.pm4
-rw-r--r--extensions/Example/template/en/default/hook/global/footer-end.html.tmpl2
-rw-r--r--extensions/Example/template/en/default/hook/search/tabs-search_tabs.html.tmpl13
-rw-r--r--extensions/MoreBugUrl/Config.pm3
-rw-r--r--extensions/MoreBugUrl/Extension.pm9
-rw-r--r--extensions/MoreBugUrl/lib/BitBucket.pm41
-rw-r--r--extensions/MoreBugUrl/lib/GetSatisfaction.pm6
-rw-r--r--extensions/MoreBugUrl/lib/PHP.pm6
-rw-r--r--extensions/MoreBugUrl/lib/RT.pm6
-rw-r--r--extensions/MoreBugUrl/lib/Redmine.pm41
-rw-r--r--extensions/MoreBugUrl/lib/ReviewBoard.pm6
-rw-r--r--extensions/MoreBugUrl/lib/Rietveld.pm6
-rw-r--r--extensions/MoreBugUrl/lib/Savane.pm40
-rw-r--r--extensions/MoreBugUrl/template/en/default/hook/global/user-error-bug_url_invalid_tracker.html.tmpl3
-rw-r--r--extensions/OldBugMove/Config.pm4
-rw-r--r--extensions/OldBugMove/Extension.pm6
-rw-r--r--extensions/OldBugMove/lib/Params.pm2
-rw-r--r--extensions/Voting/Config.pm3
-rw-r--r--extensions/Voting/Extension.pm18
-rw-r--r--extensions/Voting/template/en/default/hook/admin/products/edit-common-rows.html.tmpl8
-rw-r--r--extensions/Voting/template/en/default/hook/global/messages-messages.html.tmpl16
-rw-r--r--extensions/Voting/template/en/default/hook/global/user-error-errors.html.tmpl4
-rw-r--r--extensions/Voting/template/en/default/pages/voting/bug.html.tmpl5
-rw-r--r--extensions/Voting/template/en/default/pages/voting/user.html.tmpl24
-rw-r--r--extensions/Voting/web/style.css9
-rwxr-xr-xextensions/create.pl24
-rw-r--r--images/csv.pngbin0 -> 1204 bytes
-rw-r--r--images/ical.pngbin0 -> 241 bytes
-rw-r--r--images/rss.pngbin0 -> 1230 bytes
-rw-r--r--images/rss_small.pngbin0 -> 764 bytes
-rw-r--r--images/xml.pngbin0 -> 1148 bytes
-rwxr-xr-ximportxml.pl18
-rwxr-xr-xindex.cgi27
-rwxr-xr-xinstall-module.pl5
-rwxr-xr-xjobqueue.pl6
-rw-r--r--js/attachment.js5
-rw-r--r--js/bug.js123
-rw-r--r--js/comment-tagging.js387
-rw-r--r--js/comments.js38
-rw-r--r--js/field.js129
-rw-r--r--js/global.js49
-rw-r--r--js/util.js25
-rwxr-xr-xjsonrpc.cgi5
-rwxr-xr-xmigrate.pl5
-rw-r--r--mod_perl.pl24
-rwxr-xr-xpage.cgi4
-rwxr-xr-xpost_bug.cgi24
-rwxr-xr-xprocess_bug.cgi63
-rwxr-xr-xquery.cgi38
-rwxr-xr-xquips.cgi7
-rwxr-xr-xrelogin.cgi5
-rwxr-xr-xreport.cgi96
-rwxr-xr-xreports.cgi62
-rwxr-xr-xrequest.cgi51
-rwxr-xr-xrest.cgi31
-rwxr-xr-xruntests.pl6
-rwxr-xr-xsanitycheck.cgi30
-rwxr-xr-xsanitycheck.pl4
-rwxr-xr-xsearch_plugin.cgi5
-rwxr-xr-xshow_activity.cgi6
-rwxr-xr-xshow_bug.cgi14
-rwxr-xr-xshowdependencygraph.cgi23
-rwxr-xr-xshowdependencytree.cgi4
-rw-r--r--skins/README30
-rw-r--r--skins/contrib/Dusk/global.css10
-rw-r--r--skins/contrib/Dusk/index.css12
-rw-r--r--skins/standard/IE-fixes.css58
-rw-r--r--skins/standard/admin.css203
-rw-r--r--skins/standard/attachment.css236
-rw-r--r--skins/standard/bug.css597
-rw-r--r--skins/standard/buglist.css463
-rw-r--r--skins/standard/dependency-tree.css80
-rw-r--r--skins/standard/duplicates.css37
-rw-r--r--skins/standard/editusers.css64
-rw-r--r--skins/standard/enter_bug.css55
-rw-r--r--skins/standard/global.css534
-rw-r--r--skins/standard/index.css135
-rw-r--r--skins/standard/page.css92
-rw-r--r--skins/standard/params.css69
-rw-r--r--skins/standard/reports.css104
-rw-r--r--skins/standard/search_form.css190
-rw-r--r--skins/standard/show_bug.css123
-rw-r--r--skins/standard/show_multiple.css60
-rw-r--r--skins/standard/summarize-time.css37
-rwxr-xr-xsummarize_time.cgi6
-rw-r--r--t/001compile.t11
-rw-r--r--t/002goodperl.t57
-rw-r--r--t/003safesys.t4
-rw-r--r--t/004template.t5
-rw-r--r--t/005whitespace.t7
-rw-r--r--t/006spellcheck.t56
-rw-r--r--t/007util.t8
-rw-r--r--t/008filter.t12
-rw-r--r--t/009bugwords.t11
-rw-r--r--t/010dependencies.t7
-rw-r--r--t/011pod.t83
-rw-r--r--t/012throwables.t3
-rw-r--r--t/013dbschema.t87
-rw-r--r--t/Support/Files.pm23
-rw-r--r--t/Support/Systemexec.pm14
-rw-r--r--t/Support/Templates.pm37
-rw-r--r--taskgraph.json14
-rw-r--r--template/en/default/account/auth/login-small.html.tmpl88
-rw-r--r--template/en/default/account/auth/login.html.tmpl26
-rw-r--r--template/en/default/account/cancel-token.txt.tmpl4
-rw-r--r--template/en/default/account/create.html.tmpl46
-rw-r--r--template/en/default/account/email/confirm-new.html.tmpl32
-rw-r--r--template/en/default/account/email/confirm.html.tmpl6
-rw-r--r--template/en/default/account/password/forgotten-password.txt.tmpl8
-rw-r--r--template/en/default/account/password/set-forgotten-password.html.tmpl12
-rw-r--r--template/en/default/account/prefs/account.html.tmpl31
-rw-r--r--template/en/default/account/prefs/apikey.html.tmpl84
-rw-r--r--template/en/default/account/prefs/email.html.tmpl202
-rw-r--r--template/en/default/account/prefs/permissions.html.tmpl48
-rw-r--r--template/en/default/account/prefs/prefs.html.tmpl59
-rw-r--r--template/en/default/account/prefs/saved-searches.html.tmpl98
-rw-r--r--template/en/default/account/prefs/settings.html.tmpl4
-rw-r--r--template/en/default/account/profile-activity.html.tmpl3
-rw-r--r--template/en/default/admin/admin.html.tmpl18
-rw-r--r--template/en/default/admin/classifications/add.html.tmpl3
-rw-r--r--template/en/default/admin/classifications/del.html.tmpl48
-rw-r--r--template/en/default/admin/classifications/edit-common.html.tmpl8
-rw-r--r--template/en/default/admin/classifications/edit.html.tmpl19
-rw-r--r--template/en/default/admin/classifications/reclassify.html.tmpl53
-rw-r--r--template/en/default/admin/classifications/select.html.tmpl40
-rw-r--r--template/en/default/admin/components/confirm-delete.html.tmpl168
-rw-r--r--template/en/default/admin/components/create.html.tmpl12
-rw-r--r--template/en/default/admin/components/edit-common.html.tmpl16
-rw-r--r--template/en/default/admin/components/edit.html.tmpl34
-rw-r--r--template/en/default/admin/components/list.html.tmpl3
-rw-r--r--template/en/default/admin/components/select-product.html.tmpl3
-rw-r--r--template/en/default/admin/confirm-action.html.tmpl59
-rw-r--r--template/en/default/admin/custom_fields/confirm-delete.html.tmpl36
-rw-r--r--template/en/default/admin/custom_fields/create.html.tmpl138
-rw-r--r--template/en/default/admin/custom_fields/edit-common.html.tmpl193
-rw-r--r--template/en/default/admin/custom_fields/edit.html.tmpl143
-rw-r--r--template/en/default/admin/custom_fields/list.html.tmpl3
-rw-r--r--template/en/default/admin/fieldvalues/confirm-delete.html.tmpl58
-rw-r--r--template/en/default/admin/fieldvalues/create.html.tmpl15
-rw-r--r--template/en/default/admin/fieldvalues/edit.html.tmpl20
-rw-r--r--template/en/default/admin/fieldvalues/list.html.tmpl1
-rw-r--r--template/en/default/admin/fieldvalues/select-field.html.tmpl1
-rw-r--r--template/en/default/admin/flag-type/confirm-delete.html.tmpl33
-rw-r--r--template/en/default/admin/flag-type/edit.html.tmpl53
-rw-r--r--template/en/default/admin/flag-type/list.html.tmpl81
-rw-r--r--template/en/default/admin/groups/confirm-remove.html.tmpl4
-rw-r--r--template/en/default/admin/groups/create.html.tmpl69
-rw-r--r--template/en/default/admin/groups/delete.html.tmpl27
-rw-r--r--template/en/default/admin/groups/edit.html.tmpl192
-rw-r--r--template/en/default/admin/groups/list.html.tmpl7
-rw-r--r--template/en/default/admin/keywords/confirm-delete.html.tmpl21
-rw-r--r--template/en/default/admin/keywords/create.html.tmpl16
-rw-r--r--template/en/default/admin/keywords/edit.html.tmpl12
-rw-r--r--template/en/default/admin/keywords/list.html.tmpl3
-rw-r--r--template/en/default/admin/milestones/confirm-delete.html.tmpl52
-rw-r--r--template/en/default/admin/milestones/create.html.tmpl13
-rw-r--r--template/en/default/admin/milestones/edit.html.tmpl12
-rw-r--r--template/en/default/admin/milestones/list.html.tmpl3
-rw-r--r--template/en/default/admin/milestones/select-product.html.tmpl3
-rw-r--r--template/en/default/admin/params/admin.html.tmpl7
-rw-r--r--template/en/default/admin/params/advanced.html.tmpl20
-rw-r--r--template/en/default/admin/params/attachment.html.tmpl30
-rw-r--r--template/en/default/admin/params/auth.html.tmpl33
-rw-r--r--template/en/default/admin/params/bugfields.html.tmpl12
-rw-r--r--template/en/default/admin/params/common.html.tmpl10
-rw-r--r--template/en/default/admin/params/core.html.tmpl16
-rw-r--r--template/en/default/admin/params/dependencygraph.html.tmpl16
-rw-r--r--template/en/default/admin/params/editparams.html.tmpl46
-rw-r--r--template/en/default/admin/params/general.html.tmpl27
-rw-r--r--template/en/default/admin/params/groupsecurity.html.tmpl18
-rw-r--r--template/en/default/admin/params/index.html.tmpl42
-rw-r--r--template/en/default/admin/params/ldap.html.tmpl8
-rw-r--r--template/en/default/admin/params/memcached.html.tmpl22
-rw-r--r--template/en/default/admin/params/mta.html.tmpl22
-rw-r--r--template/en/default/admin/params/patchviewer.html.tmpl51
-rw-r--r--template/en/default/admin/params/query.html.tmpl17
-rw-r--r--template/en/default/admin/params/shadowdb.html.tmpl10
-rw-r--r--template/en/default/admin/params/usermatch.html.tmpl2
-rw-r--r--template/en/default/admin/products/confirm-delete.html.tmpl136
-rw-r--r--template/en/default/admin/products/create.html.tmpl9
-rw-r--r--template/en/default/admin/products/edit-common.html.tmpl16
-rw-r--r--template/en/default/admin/products/edit.html.tmpl55
-rw-r--r--template/en/default/admin/products/groupcontrol/confirm-edit.html.tmpl45
-rw-r--r--template/en/default/admin/products/groupcontrol/edit.html.tmpl27
-rw-r--r--template/en/default/admin/products/groupcontrol/updated.html.tmpl26
-rw-r--r--template/en/default/admin/products/list-classifications.html.tmpl3
-rw-r--r--template/en/default/admin/products/list.html.tmpl15
-rw-r--r--template/en/default/admin/products/updated.html.tmpl2
-rw-r--r--template/en/default/admin/sanitycheck/list.html.tmpl2
-rw-r--r--template/en/default/admin/settings/edit.html.tmpl67
-rw-r--r--template/en/default/admin/sudo.html.tmpl8
-rw-r--r--template/en/default/admin/table.html.tmpl14
-rw-r--r--template/en/default/admin/users/confirm-delete.html.tmpl19
-rw-r--r--template/en/default/admin/users/create.html.tmpl33
-rw-r--r--template/en/default/admin/users/edit.html.tmpl28
-rw-r--r--template/en/default/admin/users/list.html.tmpl8
-rw-r--r--template/en/default/admin/users/listselectvars.html.tmpl2
-rw-r--r--template/en/default/admin/users/responsibilities.html.tmpl4
-rw-r--r--template/en/default/admin/users/search.html.tmpl24
-rw-r--r--template/en/default/admin/users/userdata.html.tmpl34
-rw-r--r--template/en/default/admin/versions/confirm-delete.html.tmpl54
-rw-r--r--template/en/default/admin/versions/create.html.tmpl7
-rw-r--r--template/en/default/admin/versions/edit.html.tmpl20
-rw-r--r--template/en/default/admin/versions/list.html.tmpl3
-rw-r--r--template/en/default/admin/versions/select-product.html.tmpl3
-rw-r--r--template/en/default/admin/workflow/comment.html.tmpl17
-rw-r--r--template/en/default/admin/workflow/edit.html.tmpl20
-rw-r--r--template/en/default/attachment/choose.html.tmpl17
-rw-r--r--template/en/default/attachment/confirm-delete.html.tmpl45
-rw-r--r--template/en/default/attachment/create.html.tmpl17
-rw-r--r--template/en/default/attachment/created.html.tmpl4
-rw-r--r--template/en/default/attachment/diff-file.html.tmpl63
-rw-r--r--template/en/default/attachment/diff-header.html.tmpl71
-rw-r--r--template/en/default/attachment/edit.html.tmpl73
-rw-r--r--template/en/default/attachment/list.html.tmpl14
-rw-r--r--template/en/default/attachment/show-multiple.html.tmpl35
-rw-r--r--template/en/default/bug/activity/show.html.tmpl4
-rw-r--r--template/en/default/bug/activity/table.html.tmpl10
-rw-r--r--template/en/default/bug/comment.html.tmpl37
-rw-r--r--template/en/default/bug/comments.html.tmpl99
-rw-r--r--template/en/default/bug/create/create-guided.html.tmpl293
-rw-r--r--template/en/default/bug/create/create.html.tmpl161
-rw-r--r--template/en/default/bug/create/make-template.html.tmpl2
-rw-r--r--template/en/default/bug/dependency-graph.html.tmpl6
-rw-r--r--template/en/default/bug/dependency-tree.html.tmpl37
-rw-r--r--template/en/default/bug/edit.html.tmpl392
-rw-r--r--template/en/default/bug/field-help.none.tmpl10
-rw-r--r--template/en/default/bug/field-label.html.tmpl1
-rw-r--r--template/en/default/bug/field.html.tmpl84
-rw-r--r--template/en/default/bug/link.html.tmpl6
-rw-r--r--template/en/default/bug/navigate.html.tmpl41
-rw-r--r--template/en/default/bug/process/bugmail.html.tmpl24
-rw-r--r--template/en/default/bug/process/confirm-duplicate.html.tmpl2
-rw-r--r--template/en/default/bug/process/header.html.tmpl2
-rw-r--r--template/en/default/bug/process/midair.html.tmpl9
-rw-r--r--template/en/default/bug/process/results.html.tmpl9
-rw-r--r--template/en/default/bug/process/verify-new-product.html.tmpl16
-rw-r--r--template/en/default/bug/show-header.html.tmpl33
-rw-r--r--template/en/default/bug/show-multiple.html.tmpl26
-rw-r--r--template/en/default/bug/summarize-time.html.tmpl232
-rw-r--r--template/en/default/bug/time.html.tmpl8
-rw-r--r--template/en/default/email/bugmail-header.txt.tmpl17
-rw-r--r--template/en/default/email/bugmail.html.tmpl46
-rw-r--r--template/en/default/email/bugmail.txt.tmpl9
-rw-r--r--template/en/default/email/flagmail.txt.tmpl8
-rw-r--r--template/en/default/email/header-common.txt.tmpl24
-rw-r--r--template/en/default/email/new-api-key.txt.tmpl33
-rw-r--r--template/en/default/email/new-user-details.txt.tmpl31
-rw-r--r--template/en/default/extensions/config.pm.tmpl5
-rw-r--r--template/en/default/extensions/extension.pm.tmpl12
-rw-r--r--template/en/default/extensions/hook-readme.txt.tmpl6
-rw-r--r--template/en/default/extensions/index-admin.rst.tmpl23
-rw-r--r--template/en/default/extensions/index-user.rst.tmpl23
-rw-r--r--template/en/default/extensions/name-readme.txt.tmpl6
-rw-r--r--template/en/default/extensions/util.pm.tmpl10
-rw-r--r--template/en/default/filterexceptions.pl19
-rw-r--r--template/en/default/flag/list.html.tmpl80
-rw-r--r--template/en/default/global/choose-classification.html.tmpl24
-rw-r--r--template/en/default/global/choose-product.html.tmpl24
-rw-r--r--template/en/default/global/code-error.html.tmpl49
-rw-r--r--template/en/default/global/common-links.html.tmpl10
-rw-r--r--template/en/default/global/confirm-user-match.html.tmpl39
-rw-r--r--template/en/default/global/docslinks.html.tmpl3
-rw-r--r--template/en/default/global/field-descs.none.tmpl9
-rw-r--r--template/en/default/global/footer.html.tmpl27
-rw-r--r--template/en/default/global/header.html.tmpl195
-rw-r--r--template/en/default/global/messages.html.tmpl97
-rw-r--r--template/en/default/global/product-select.html.tmpl92
-rw-r--r--template/en/default/global/setting-descs.none.tmpl1
-rw-r--r--template/en/default/global/site-navigation.html.tmpl75
-rw-r--r--template/en/default/global/tabs.html.tmpl2
-rw-r--r--template/en/default/global/textarea.html.tmpl4
-rw-r--r--template/en/default/global/useful-links.html.tmpl60
-rw-r--r--template/en/default/global/user-error.html.tmpl181
-rw-r--r--template/en/default/global/userselect.html.tmpl3
-rw-r--r--template/en/default/global/variables.none.tmpl2
-rw-r--r--template/en/default/index.html.tmpl202
-rw-r--r--template/en/default/list/change-columns.html.tmpl2
-rw-r--r--template/en/default/list/edit-multiple.html.tmpl74
-rw-r--r--template/en/default/list/list-simple.html.tmpl3
-rw-r--r--template/en/default/list/list.html.tmpl151
-rw-r--r--template/en/default/list/list.ics.tmpl3
-rw-r--r--template/en/default/list/quips.html.tmpl64
-rw-r--r--template/en/default/list/server-push.html.tmpl7
-rw-r--r--template/en/default/list/table.html.tmpl75
-rw-r--r--template/en/default/pages/bug-writing.html.tmpl8
-rw-r--r--template/en/default/pages/bugzilla.dtd.tmpl6
-rw-r--r--template/en/default/pages/fields.html.tmpl6
-rw-r--r--template/en/default/pages/linked.html.tmpl2
-rw-r--r--template/en/default/pages/linkify.html.tmpl2
-rw-r--r--template/en/default/pages/quicksearch.html.tmpl72
-rw-r--r--template/en/default/pages/release-notes.html.tmpl2520
-rw-r--r--template/en/default/pages/release-notes3.html.tmpl3474
-rw-r--r--template/en/default/pages/sudo.html.tmpl4
-rw-r--r--template/en/default/reports/chart.html.tmpl4
-rw-r--r--template/en/default/reports/chart.png.tmpl31
-rw-r--r--template/en/default/reports/components.html.tmpl30
-rw-r--r--template/en/default/reports/create-chart.html.tmpl60
-rw-r--r--template/en/default/reports/delete-series.html.tmpl2
-rw-r--r--template/en/default/reports/duplicates-simple.html.tmpl5
-rw-r--r--template/en/default/reports/duplicates-table.html.tmpl8
-rw-r--r--template/en/default/reports/duplicates.html.tmpl27
-rw-r--r--template/en/default/reports/edit-series.html.tmpl14
-rw-r--r--template/en/default/reports/keywords.html.tmpl41
-rw-r--r--template/en/default/reports/menu.html.tmpl6
-rw-r--r--template/en/default/reports/old-charts.html.tmpl32
-rw-r--r--template/en/default/reports/report-bar.png.tmpl28
-rw-r--r--template/en/default/reports/report-line.png.tmpl28
-rw-r--r--template/en/default/reports/report-pie.png.tmpl16
-rw-r--r--template/en/default/reports/report-simple.html.tmpl7
-rw-r--r--template/en/default/reports/report-table.html.tmpl45
-rw-r--r--template/en/default/reports/report.html.tmpl74
-rw-r--r--template/en/default/reports/series-common.html.tmpl4
-rw-r--r--template/en/default/reports/series.html.tmpl44
-rw-r--r--template/en/default/request/queue.csv.tmpl46
-rw-r--r--template/en/default/request/queue.html.tmpl37
-rw-r--r--template/en/default/rest.html.tmpl19
-rw-r--r--template/en/default/search/boolean-charts.html.tmpl12
-rw-r--r--template/en/default/search/field.html.tmpl8
-rw-r--r--template/en/default/search/form.html.tmpl26
-rw-r--r--template/en/default/search/search-advanced.html.tmpl4
-rw-r--r--template/en/default/search/search-create-series.html.tmpl4
-rw-r--r--template/en/default/search/search-report-graph.html.tmpl126
-rw-r--r--template/en/default/search/search-report-select.html.tmpl2
-rw-r--r--template/en/default/search/search-report-table.html.tmpl52
-rw-r--r--template/en/default/search/search-specific.html.tmpl58
-rw-r--r--template/en/default/search/tabs.html.tmpl12
-rw-r--r--template/en/default/setup/strings.txt.pl48
-rw-r--r--template/en/default/welcome-admin.html.tmpl7
-rw-r--r--template/en/default/whine/mail.html.tmpl18
-rw-r--r--template/en/default/whine/schedule.html.tmpl100
-rwxr-xr-xtestagent.cgi9
-rwxr-xr-xtestserver.pl5
-rwxr-xr-xtoken.cgi16
-rwxr-xr-xuserprefs.cgi122
-rwxr-xr-xvotes.cgi6
-rwxr-xr-xwhine.pl16
-rwxr-xr-xwhineatnews.pl5
-rwxr-xr-xxmlrpc.cgi5
-rw-r--r--xt/lib/Bugzilla/Test/Search/AndTest.pm2
-rw-r--r--xt/lib/Bugzilla/Test/Search/Constants.pm7
-rw-r--r--xt/lib/Bugzilla/Test/Search/CustomTest.pm2
-rw-r--r--xt/lib/Bugzilla/Test/Search/FieldTestNormal.pm2
-rw-r--r--xt/lib/Bugzilla/Test/Search/InjectionTest.pm2
-rw-r--r--xt/lib/Bugzilla/Test/Search/NotTest.pm2
-rw-r--r--xt/lib/Bugzilla/Test/Search/OrTest.pm2
676 files changed, 33861 insertions, 28113 deletions
diff --git a/.bzrignore b/.bzrignore
index 7ab83e7ad..f30e8f9ae 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -1,10 +1,8 @@
.htaccess
/lib/*
/template/en/custom
-/docs/bugzilla.ent
-/docs/en/xml/bugzilla.ent
-/docs/en/txt
/docs/en/html
+/docs/en/txt
/docs/en/pdf
/skins/custom
/graphs
@@ -12,21 +10,5 @@
/localconfig
/index.html
-/skins/contrib/Dusk/IE-fixes.css
/skins/contrib/Dusk/admin.css
-/skins/contrib/Dusk/attachment.css
-/skins/contrib/Dusk/create_attachment.css
-/skins/contrib/Dusk/dependency-tree.css
-/skins/contrib/Dusk/duplicates.css
-/skins/contrib/Dusk/editusers.css
-/skins/contrib/Dusk/enter_bug.css
-/skins/contrib/Dusk/help.css
-/skins/contrib/Dusk/panel.css
-/skins/contrib/Dusk/page.css
-/skins/contrib/Dusk/params.css
-/skins/contrib/Dusk/reports.css
-/skins/contrib/Dusk/show_bug.css
-/skins/contrib/Dusk/search_form.css
-/skins/contrib/Dusk/show_multiple.css
-/skins/contrib/Dusk/summarize-time.css
-.DS_Store
+/skins/contrib/Dusk/bug.css
diff --git a/.gitignore b/.gitignore
index 7ab83e7ad..f30e8f9ae 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,10 +1,8 @@
.htaccess
/lib/*
/template/en/custom
-/docs/bugzilla.ent
-/docs/en/xml/bugzilla.ent
-/docs/en/txt
/docs/en/html
+/docs/en/txt
/docs/en/pdf
/skins/custom
/graphs
@@ -12,21 +10,5 @@
/localconfig
/index.html
-/skins/contrib/Dusk/IE-fixes.css
/skins/contrib/Dusk/admin.css
-/skins/contrib/Dusk/attachment.css
-/skins/contrib/Dusk/create_attachment.css
-/skins/contrib/Dusk/dependency-tree.css
-/skins/contrib/Dusk/duplicates.css
-/skins/contrib/Dusk/editusers.css
-/skins/contrib/Dusk/enter_bug.css
-/skins/contrib/Dusk/help.css
-/skins/contrib/Dusk/panel.css
-/skins/contrib/Dusk/page.css
-/skins/contrib/Dusk/params.css
-/skins/contrib/Dusk/reports.css
-/skins/contrib/Dusk/show_bug.css
-/skins/contrib/Dusk/search_form.css
-/skins/contrib/Dusk/show_multiple.css
-/skins/contrib/Dusk/summarize-time.css
-.DS_Store
+/skins/contrib/Dusk/bug.css
diff --git a/.htaccess b/.htaccess
index 8b502500b..aec901005 100644
--- a/.htaccess
+++ b/.htaccess
@@ -2,7 +2,7 @@
<FilesMatch (\.pm|\.pl|\.tmpl|localconfig.*)$>
<IfModule mod_version.c>
<IfVersion < 2.4>
- Deny from all
+ Deny from all
</IfVersion>
<IfVersion >= 2.4>
<IfModule mod_perl.c>
@@ -41,3 +41,9 @@ Options -Indexes
</IfModule>
</IfModule>
</IfModule>
+
+<IfModule mod_rewrite.c>
+ RewriteEngine On
+ RewriteOptions inherit
+ RewriteRule ^rest/(.*)$ rest.cgi/$1 [NE]
+</IfModule>
diff --git a/.travis.yml b/.travis.yml
index 1bbf29483..01ecfcfb4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,8 @@ addons:
perl:
- 5.10
- 5.12
+ - 5.14
+ - 5.16
env:
- TEST_SUITE=sanity
@@ -17,19 +19,39 @@ env:
matrix:
exclude:
+ - perl: 5.10
+ env: TEST_SUITE=docs
- perl: 5.12
env: TEST_SUITE=docs
+ - perl: 5.14
+ env: TEST_SUITE=docs
- perl: 5.10
env: TEST_SUITE=webservices DB=mysql
- - perl: 5.12
+ - perl: 5.10
+ env: TEST_SUITE=webservices DB=pg
+ - perl: 5.10
env: TEST_SUITE=selenium DB=mysql
- perl: 5.10
+ env: TEST_SUITE=selenium DB=pg
+ - perl: 5.12
+ env: TEST_SUITE=webservices DB=mysql
+ - perl: 5.12
env: TEST_SUITE=webservices DB=pg
- perl: 5.12
+ env: TEST_SUITE=selenium DB=mysql
+ - perl: 5.12
+ env: TEST_SUITE=selenium DB=pg
+ - perl: 5.14
+ env: TEST_SUITE=webservices DB=mysql
+ - perl: 5.14
+ env: TEST_SUITE=webservices DB=pg
+ - perl: 5.14
+ env: TEST_SUITE=selenium DB=mysql
+ - perl: 5.14
env: TEST_SUITE=selenium DB=pg
before_install:
- - git clone https://github.com/bugzilla/qa.git -b 4.4 qa
+ - git clone https://github.com/bugzilla/qa.git -b 5.0 qa
install: true
diff --git a/Bugzilla.pm b/Bugzilla.pm
index 6e31ba71a..e3184fdb8 100644
--- a/Bugzilla.pm
+++ b/Bugzilla.pm
@@ -7,7 +7,9 @@
package Bugzilla;
+use 5.10.1;
use strict;
+use warnings;
# We want any compile errors to get to the browser, if possible.
BEGIN {
@@ -18,23 +20,24 @@ BEGIN {
}
}
-use Bugzilla::Config;
-use Bugzilla::Constants;
use Bugzilla::Auth;
use Bugzilla::Auth::Persist::Cookie;
use Bugzilla::CGI;
-use Bugzilla::Extension;
+use Bugzilla::Config;
+use Bugzilla::Constants;
use Bugzilla::DB;
+use Bugzilla::Error;
+use Bugzilla::Extension;
+use Bugzilla::Field;
+use Bugzilla::Flag;
use Bugzilla::Install::Localconfig qw(read_localconfig);
-use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES);
+use Bugzilla::Install::Requirements qw(OPTIONAL_MODULES have_vers);
use Bugzilla::Install::Util qw(init_console include_languages);
+use Bugzilla::Memcached;
use Bugzilla::Template;
+use Bugzilla::Token;
use Bugzilla::User;
-use Bugzilla::Error;
use Bugzilla::Util;
-use Bugzilla::Field;
-use Bugzilla::Flag;
-use Bugzilla::Token;
use File::Basename;
use File::Spec::Functions;
@@ -119,8 +122,8 @@ sub init_page {
#
# This code must go here. It cannot go anywhere in Bugzilla::CGI, because
# it uses Template, and that causes various dependency loops.
- if (Bugzilla->params->{"shutdownhtml"}
- && !grep { $_ eq $script } SHUTDOWNHTML_EXEMPT)
+ if (!grep { $_ eq $script } SHUTDOWNHTML_EXEMPT
+ and Bugzilla->params->{'shutdownhtml'})
{
# Allow non-cgi scripts to exit silently (without displaying any
# message), if desired. At this point, no DBI call has been made
@@ -163,10 +166,8 @@ sub init_page {
print Bugzilla->cgi->header(-status => 503,
-retry_after => SHUTDOWNHTML_RETRY_AFTER);
}
- my $t_output;
- $template->process("global/message.$extension.tmpl", $vars, \$t_output)
+ $template->process("global/message.$extension.tmpl", $vars)
|| ThrowTemplateError($template->error);
- print $t_output . "\n";
exit;
}
}
@@ -176,9 +177,7 @@ sub init_page {
#####################################################################
sub template {
- my $class = shift;
- $class->request_cache->{template} ||= Bugzilla::Template->create();
- return $class->request_cache->{template};
+ return $_[0]->request_cache->{template} ||= Bugzilla::Template->create();
}
sub template_inner {
@@ -186,9 +185,7 @@ sub template_inner {
my $cache = $class->request_cache;
my $current_lang = $cache->{template_current_lang}->[0];
$lang ||= $current_lang || '';
- $class->request_cache->{"template_inner_$lang"}
- ||= Bugzilla::Template->create(language => $lang);
- return $class->request_cache->{"template_inner_$lang"};
+ return $cache->{"template_inner_$lang"} ||= Bugzilla::Template->create(language => $lang);
}
our $extension_packages;
@@ -223,7 +220,7 @@ sub feature {
foreach my $package (@{ OPTIONAL_MODULES() }) {
foreach my $f (@{ $package->{feature} }) {
$feature_map->{$f} ||= [];
- push(@{ $feature_map->{$f} }, $package->{module});
+ push(@{ $feature_map->{$f} }, $package);
}
}
$cache->{feature_map} = $feature_map;
@@ -234,22 +231,15 @@ sub feature {
}
my $success = 1;
- foreach my $module (@{ $feature_map->{$feature} }) {
- # We can't use a string eval and "use" here (it kills Template-Toolkit,
- # see https://rt.cpan.org/Public/Bug/Display.html?id=47929), so we have
- # to do a block eval.
- $module =~ s{::}{/}g;
- $module .= ".pm";
- eval { require $module; 1; } or $success = 0;
+ foreach my $package (@{ $feature_map->{$feature} }) {
+ have_vers($package) or $success = 0;
}
$cache->{feature}->{$feature} = $success;
return $success;
}
sub cgi {
- my $class = shift;
- $class->request_cache->{cgi} ||= new Bugzilla::CGI();
- return $class->request_cache->{cgi};
+ return $_[0]->request_cache->{cgi} ||= new Bugzilla::CGI();
}
sub input_params {
@@ -273,15 +263,11 @@ sub localconfig {
}
sub params {
- my $class = shift;
- $class->request_cache->{params} ||= Bugzilla::Config::read_param_file();
- return $class->request_cache->{params};
+ return $_[0]->request_cache->{params} ||= Bugzilla::Config::read_param_file();
}
sub user {
- my $class = shift;
- $class->request_cache->{user} ||= new Bugzilla::User;
- return $class->request_cache->{user};
+ return $_[0]->request_cache->{user} ||= new Bugzilla::User;
}
sub set_user {
@@ -290,8 +276,7 @@ sub set_user {
}
sub sudoer {
- my $class = shift;
- return $class->request_cache->{sudoer};
+ return $_[0]->request_cache->{sudoer};
}
sub sudo_request {
@@ -413,28 +398,20 @@ sub logout_request {
}
sub job_queue {
- my $class = shift;
require Bugzilla::JobQueue;
- $class->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
- return $class->request_cache->{job_queue};
+ return $_[0]->request_cache->{job_queue} ||= Bugzilla::JobQueue->new();
}
sub dbh {
- my $class = shift;
# If we're not connected, then we must want the main db
- $class->request_cache->{dbh} ||= $class->dbh_main;
-
- return $class->request_cache->{dbh};
+ return $_[0]->request_cache->{dbh} ||= $_[0]->dbh_main;
}
sub dbh_main {
- my $class = shift;
- $class->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main();
- return $class->request_cache->{dbh_main};
+ return $_[0]->request_cache->{dbh_main} ||= Bugzilla::DB::connect_main();
}
sub languages {
- my $class = shift;
return Bugzilla::Install::Util::supported_languages();
}
@@ -487,6 +464,9 @@ sub usage_mode {
elsif ($newval == USAGE_MODE_TEST) {
$class->error_mode(ERROR_MODE_TEST);
}
+ elsif ($newval == USAGE_MODE_REST) {
+ $class->error_mode(ERROR_MODE_REST);
+ }
else {
ThrowCodeError('usage_mode_invalid',
{'invalid_usage_mode', $newval});
@@ -550,6 +530,11 @@ sub switch_to_main_db {
return $class->dbh_main;
}
+sub is_shadow_db {
+ my $class = shift;
+ return $class->request_cache->{dbh} != $class->dbh_main;
+}
+
sub fields {
my ($class, $criteria) = @_;
$criteria ||= {};
@@ -618,13 +603,8 @@ sub has_flags {
}
sub local_timezone {
- my $class = shift;
-
- if (!defined $class->process_cache->{local_timezone}) {
- $class->process_cache->{local_timezone} =
- DateTime::TimeZone->new(name => 'local');
- }
- return $class->process_cache->{local_timezone};
+ return $_[0]->process_cache->{local_timezone}
+ ||= DateTime::TimeZone->new(name => 'local');
}
# This creates the request cache for non-mod_perl installations.
@@ -666,18 +646,27 @@ sub process_cache {
return $_process_cache;
}
+# This is a memcached wrapper, which provides cross-process and cross-system
+# caching.
+sub memcached {
+ return $_[0]->process_cache->{memcached} ||= Bugzilla::Memcached->_new();
+}
+
# Private methods
# Per-process cleanup. Note that this is a plain subroutine, not a method,
# so we don't have $class available.
sub _cleanup {
- my $main = Bugzilla->request_cache->{dbh_main};
- my $shadow = Bugzilla->request_cache->{dbh_shadow};
+ my $cache = Bugzilla->request_cache;
+ my $main = $cache->{dbh_main};
+ my $shadow = $cache->{dbh_shadow};
foreach my $dbh ($main, $shadow) {
next if !$dbh;
$dbh->bz_rollback_transaction() if $dbh->bz_in_transaction;
$dbh->disconnect;
}
+ my $smtp = $cache->{smtp};
+ $smtp->disconnect if $smtp;
clear_request_cache();
# These are both set by CGI.pm but need to be undone so that
@@ -769,7 +758,7 @@ If you ever need a L<Bugzilla::Template> object while you're already
processing a template, use this. Also use it if you want to specify
the language to use. If no argument is passed, it uses the last
language set. If the argument is "" (empty string), the language is
-reset to the current one (the one used by Bugzilla->template).
+reset to the current one (the one used by C<Bugzilla-E<gt>template>).
=item C<cgi>
@@ -846,7 +835,7 @@ default), LOGOUT_ALL or LOGOUT_KEEP_CURRENT.
=item C<logout_user($user)>
-Logs out the specified user (invalidating all his sessions), taking a
+Logs out the specified user (invalidating all their sessions), taking a
Bugzilla::User instance.
=item C<logout_by_id($id)>
@@ -887,8 +876,8 @@ specify this argument, all fields will be returned.
=item C<error_mode>
-Call either C<Bugzilla->error_mode(Bugzilla::Constants::ERROR_MODE_DIE)>
-or C<Bugzilla->error_mode(Bugzilla::Constants::ERROR_MODE_DIE_SOAP_FAULT)> to
+Call either C<Bugzilla-E<gt>error_mode(Bugzilla::Constants::ERROR_MODE_DIE)>
+or C<Bugzilla-E<gt>error_mode(Bugzilla::Constants::ERROR_MODE_DIE_SOAP_FAULT)> to
change this flag's default of C<Bugzilla::Constants::ERROR_MODE_WEBPAGE> and to
indicate that errors should be passed to error mode specific error handlers
rather than being sent to a browser and finished with an exit().
@@ -897,24 +886,24 @@ This is useful, for example, to keep C<eval> blocks from producing wild HTML
on errors, making it easier for you to catch them.
(Remember to reset the error mode to its previous value afterwards, though.)
-C<Bugzilla->error_mode> will return the current state of this flag.
+C<Bugzilla-E<gt>error_mode> will return the current state of this flag.
-Note that C<Bugzilla->error_mode> is being called by C<Bugzilla->usage_mode> on
+Note that C<Bugzilla-E<gt>error_mode> is being called by C<Bugzilla-E<gt>usage_mode> on
usage mode changes.
=item C<usage_mode>
-Call either C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_CMDLINE)>
-or C<Bugzilla->usage_mode(Bugzilla::Constants::USAGE_MODE_XMLRPC)> near the
+Call either C<Bugzilla-E<gt>usage_mode(Bugzilla::Constants::USAGE_MODE_CMDLINE)>
+or C<Bugzilla-E<gt>usage_mode(Bugzilla::Constants::USAGE_MODE_XMLRPC)> near the
beginning of your script to change this flag's default of
C<Bugzilla::Constants::USAGE_MODE_BROWSER> and to indicate that Bugzilla is
being called in a non-interactive manner.
This influences error handling because on usage mode changes, C<usage_mode>
-calls C<Bugzilla->error_mode> to set an error mode which makes sense for the
+calls C<Bugzilla-E<gt>error_mode> to set an error mode which makes sense for the
usage mode.
-C<Bugzilla->usage_mode> will return the current state of this flag.
+C<Bugzilla-E<gt>usage_mode> will return the current state of this flag.
=item C<installation_mode>
@@ -951,11 +940,17 @@ Switch from using the main database to using the shadow database.
Change the database object to refer to the main database.
+=item C<is_shadow_db>
+
+Returns true if the currently active database is the shadow database.
+Returns false if a the currently active database is the man database, or if a
+shadow database is not configured or enabled.
+
=item C<params>
-The current Parameters of Bugzilla, as a hashref. If C<data/params>
-does not exist, then we return an empty hashref. If C<data/params>
-is unreadable or is not valid perl, we C<die>.
+The current Parameters of Bugzilla, as a hashref. If C<data/params.json>
+does not exist, then we return an empty hashref. If C<data/params.json>
+is unreadable or is not valid, we C<die>.
=item C<local_timezone>
@@ -975,3 +970,99 @@ Tells you whether or not a specific feature is enabled. For names
of features, see C<OPTIONAL_MODULES> in C<Bugzilla::Install::Requirements>.
=back
+
+=head1 B<CACHING>
+
+Bugzilla has several different caches available which provide different
+capabilities and lifetimes.
+
+The keys of all caches are unregulated; use of prefixes is suggested to avoid
+collisions.
+
+=over
+
+=item B<Request Cache>
+
+The request cache is a hashref which supports caching any perl variable for the
+duration of the current request. At the end of the current request the contents
+of this cache are cleared.
+
+Examples of its use include caching objects to avoid re-fetching the same data
+from the database, and passing data between otherwise unconnected parts of
+Bugzilla.
+
+=over
+
+=item C<request_cache>
+
+Returns a hashref which can be checked and modified to store any perl variable
+for the duration of the current request.
+
+=item C<clear_request_cache>
+
+Removes all entries from the C<request_cache>.
+
+=back
+
+=item B<Process Cache>
+
+The process cache is a hashref which support caching of any perl variable. If
+Bugzilla is configured to run using Apache mod_perl, the contents of this cache
+are persisted across requests for the lifetime of the Apache worker process
+(which varies depending on the SizeLimit configuration in mod_perl.pl).
+
+If Bugzilla isn't running under mod_perl, the process cache's contents are
+cleared at the end of the request.
+
+The process cache is only suitable for items which never change while Bugzilla
+is running (for example the path where Bugzilla is installed).
+
+=over
+
+=item C<process_cache>
+
+Returns a hashref which can be checked and modified to store any perl variable
+for the duration of the current process (mod_perl) or request (mod_cgi).
+
+=back
+
+=item B<Memcached>
+
+If Memcached is installed and configured, Bugzilla can use it to cache data
+across requests and between webheads. Unlike the request and process caches,
+only scalars, hashrefs, and arrayrefs can be stored in Memcached.
+
+Memcached integration is only required for large installations of Bugzilla -- if
+you have multiple webheads then configuring Memcached is recommended.
+
+=over
+
+=item C<memcached>
+
+Returns a C<Bugzilla::Memcached> object. An object is always returned even if
+Memcached is not available.
+
+See the documentation for the C<Bugzilla::Memcached> module for more
+information.
+
+=back
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item init_page
+
+=item extensions
+
+=item logout_user_by_id
+
+=item localconfig
+
+=item active_custom_fields
+
+=item has_flags
+
+=back
diff --git a/Bugzilla/Attachment.pm b/Bugzilla/Attachment.pm
index cd8316a91..932fb6b17 100644
--- a/Bugzilla/Attachment.pm
+++ b/Bugzilla/Attachment.pm
@@ -5,10 +5,12 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Attachment;
+use 5.10.1;
+use strict;
+use warnings;
+
=head1 NAME
Bugzilla::Attachment - Bugzilla attachment class.
@@ -44,8 +46,9 @@ use Bugzilla::Hook;
use File::Copy;
use List::Util qw(max);
+use Storable qw(dclone);
-use base qw(Bugzilla::Object);
+use parent qw(Bugzilla::Object);
###############################
#### Initialization ####
@@ -58,22 +61,19 @@ use constant LIST_ORDER => ID_FIELD;
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
-sub DB_COLUMNS {
- my $dbh = Bugzilla->dbh;
-
- return qw(
- attach_id
- bug_id
- description
- filename
- isobsolete
- ispatch
- isprivate
- mimetype
- modification_time
- submitter_id),
- $dbh->sql_date_format('attachments.creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts';
-}
+use constant DB_COLUMNS => qw(
+ attach_id
+ bug_id
+ creation_ts
+ description
+ filename
+ isobsolete
+ ispatch
+ isprivate
+ mimetype
+ modification_time
+ submitter_id
+);
use constant REQUIRED_FIELD_MAP => {
bug_id => 'bug',
@@ -99,7 +99,8 @@ use constant VALIDATORS => {
};
use constant VALIDATOR_DEPENDENCIES => {
- mimetype => ['ispatch'],
+ content_type => ['ispatch'],
+ mimetype => ['ispatch'],
};
use constant UPDATE_VALIDATORS => {
@@ -125,8 +126,7 @@ the ID of the bug to which the attachment is attached
=cut
sub bug_id {
- my $self = shift;
- return $self->{bug_id};
+ return $_[0]->{bug_id};
}
=over
@@ -140,11 +140,8 @@ the bug object to which the attachment is attached
=cut
sub bug {
- my $self = shift;
-
require Bugzilla::Bug;
- $self->{bug} ||= Bugzilla::Bug->new($self->bug_id);
- return $self->{bug};
+ return $_[0]->{bug} //= Bugzilla::Bug->new({ id => $_[0]->bug_id, cache => 1 });
}
=over
@@ -158,8 +155,7 @@ user-provided text describing the attachment
=cut
sub description {
- my $self = shift;
- return $self->{description};
+ return $_[0]->{description};
}
=over
@@ -173,8 +169,7 @@ the attachment's MIME media type
=cut
sub contenttype {
- my $self = shift;
- return $self->{mimetype};
+ return $_[0]->{mimetype};
}
=over
@@ -188,10 +183,8 @@ the user who attached the attachment
=cut
sub attacher {
- my $self = shift;
- return $self->{attacher} if exists $self->{attacher};
- $self->{attacher} = new Bugzilla::User($self->{submitter_id});
- return $self->{attacher};
+ return $_[0]->{attacher}
+ //= new Bugzilla::User({ id => $_[0]->{submitter_id}, cache => 1 });
}
=over
@@ -205,8 +198,7 @@ the date and time on which the attacher attached the attachment
=cut
sub attached {
- my $self = shift;
- return $self->{creation_ts};
+ return $_[0]->{creation_ts};
}
=over
@@ -220,8 +212,7 @@ the date and time on which the attachment was last modified.
=cut
sub modification_time {
- my $self = shift;
- return $self->{modification_time};
+ return $_[0]->{modification_time};
}
=over
@@ -235,8 +226,7 @@ the name of the file the attacher attached
=cut
sub filename {
- my $self = shift;
- return $self->{filename};
+ return $_[0]->{filename};
}
=over
@@ -250,8 +240,7 @@ whether or not the attachment is a patch
=cut
sub ispatch {
- my $self = shift;
- return $self->{ispatch};
+ return $_[0]->{ispatch};
}
=over
@@ -265,8 +254,7 @@ whether or not the attachment is obsolete
=cut
sub isobsolete {
- my $self = shift;
- return $self->{isobsolete};
+ return $_[0]->{isobsolete};
}
=over
@@ -280,8 +268,7 @@ whether or not the attachment is private
=cut
sub isprivate {
- my $self = shift;
- return $self->{isprivate};
+ return $_[0]->{isprivate};
}
=over
@@ -298,8 +285,7 @@ matches, because this will return a value even if it's matched by the generic
=cut
sub is_viewable {
- my $self = shift;
- my $contenttype = $self->contenttype;
+ my $contenttype = $_[0]->contenttype;
my $cgi = Bugzilla->cgi;
# We assume we can view all text and image types.
@@ -373,7 +359,7 @@ the length (in bytes) of the attachment content
sub datasize {
my $self = shift;
- return $self->{datasize} if exists $self->{datasize};
+ return $self->{datasize} if defined $self->{datasize};
# If we have already retrieved the data, return its size.
return length($self->{data}) if exists $self->{data};
@@ -416,11 +402,8 @@ flags that have been set on the attachment
=cut
sub flags {
- 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};
+ return $_[0]->{flags} = [map { @{$_->{flags}} } @{$_[0]->flag_types}];
}
=over
@@ -443,8 +426,7 @@ sub flag_types {
component_id => $self->bug->component_id,
attach_id => $self->id };
- $self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
- return $self->{flag_types};
+ return $self->{flag_types} = Bugzilla::Flag->_flag_types($vars);
}
###############################
@@ -674,23 +656,27 @@ sub get_attachments_by_bug {
my $attachments = Bugzilla::Attachment->new_from_list($attach_ids);
$_->{bug} = $bug foreach @$attachments;
- # 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.
- # We also load all attachers at once for the same reason.
+ # To avoid $attachment->flags and $attachment->flag_types running SQL queries
+ # themselves for each attachment listed here, we collect all the data at once and
+ # populate $attachment->{flag_types} ourselves. We also load all attachers and
+ # datasizes at once for the same reason.
if ($vars->{preload}) {
- # Preload flags.
- $_->{flags} = [] foreach @$attachments;
- my %att = map { $_->id => $_ } @$attachments;
-
- my $flags = Bugzilla::Flag->match({ bug_id => $bug->id,
- target_type => 'attachment' });
-
- # Exclude flags for private attachments you cannot see.
- @$flags = grep {exists $att{$_->attach_id}} @$flags;
+ # 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);
- push(@{$att{$_->attach_id}->{flags}}, $_) foreach @$flags;
- $attachments = [sort {$a->id <=> $b->id} values %att];
+ 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;
@@ -699,34 +685,44 @@ sub get_attachments_by_bug {
foreach my $attachment (@$attachments) {
$attachment->{attacher} = $user_map{$attachment->{submitter_id}};
}
+
+ # Preload datasizes.
+ my $sizes =
+ $dbh->selectall_hashref('SELECT attach_id, LENGTH(thedata) AS datasize
+ FROM attachments LEFT JOIN attach_data ON attach_id = id
+ WHERE bug_id = ?',
+ 'attach_id', undef, $bug->id);
+
+ # Force the size of attachments not in the DB to be recalculated.
+ $_->{datasize} = $sizes->{$_->id}->{datasize} || undef foreach @$attachments;
}
+
return $attachments;
}
=pod
-=item C<validate_can_edit($attachment, $product_id)>
+=item C<validate_can_edit>
Description: validates if the user is allowed to view and edit the attachment.
Only the submitter or someone with editbugs privs can edit it.
Only the submitter and users in the insider group can view
private attachments.
-Params: $attachment - the attachment object being edited.
- $product_id - the product ID the attachment belongs to.
+Params: none
Returns: 1 on success, 0 otherwise.
=cut
sub validate_can_edit {
- my ($attachment, $product_id) = @_;
+ my $attachment = shift;
my $user = Bugzilla->user;
# The submitter can edit their attachments.
return ($attachment->attacher->id == $user->id
|| ((!$attachment->isprivate || $user->is_insider)
- && $user->in_group('editbugs', $product_id))) ? 1 : 0;
+ && $user->in_group('editbugs', $attachment->bug->product_id))) ? 1 : 0;
}
=item C<validate_obsolete($bug, $attach_ids)>
@@ -734,7 +730,7 @@ sub validate_can_edit {
Description: validates if attachments the user wants to mark as obsolete
really belong to the given bug and are not already obsolete.
Moreover, a user cannot mark an attachment as obsolete if
- he cannot view it (due to restrictions on it).
+ they cannot view it (due to restrictions on it).
Params: $bug - The bug object obsolete attachments should belong to.
$attach_ids - The list of attachments to mark as obsolete.
@@ -763,7 +759,7 @@ sub validate_obsolete {
|| ThrowUserError('invalid_attach_id', $vars);
# Check that the user can view and edit this attachment.
- $attachment->validate_can_edit($bug->product_id)
+ $attachment->validate_can_edit
|| ThrowUserError('illegal_attachment_edit', { attach_id => $attachment->id });
if ($attachment->bug_id != $bug->bug_id) {
@@ -904,10 +900,14 @@ sub update {
}
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));
+ $dbh->do('UPDATE attachments SET modification_time = ? WHERE attach_id = ?',
+ undef, ($timestamp, $self->id));
+ $dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
+ undef, ($timestamp, $self->bug_id));
+ $self->{modification_time} = $timestamp;
+ # because we updated the attachments table after SUPER::update(), we
+ # need to ensure the cache is flushed.
+ Bugzilla->memcached->clear({ table => 'attachments', id => $self->id });
}
return $changes;
@@ -932,7 +932,10 @@ sub remove_from_db {
my $dbh = Bugzilla->dbh;
$dbh->bz_start_transaction();
- $dbh->do('DELETE FROM flags WHERE attach_id = ?', undef, $self->id);
+ my $flag_ids = $dbh->selectcol_arrayref(
+ 'SELECT id FROM flags WHERE attach_id = ?', undef, $self->id);
+ $dbh->do('DELETE FROM flags WHERE ' . $dbh->sql_in('id', $flag_ids))
+ if @$flag_ids;
$dbh->do('DELETE FROM attach_data WHERE id = ?', undef, $self->id);
$dbh->do('UPDATE attachments SET mimetype = ?, ispatch = ?, isobsolete = ?
WHERE attach_id = ?', undef, ('text/plain', 0, 1, $self->id));
@@ -942,6 +945,13 @@ sub remove_from_db {
if (-e $filename) {
unlink $filename or warn "Couldn't unlink $filename: $!";
}
+
+ # As we don't call SUPER->remove_from_db we need to manually clear
+ # memcached here.
+ Bugzilla->memcached->clear({ table => 'attachments', id => $self->id });
+ foreach my $flag_id (@$flag_ids) {
+ Bugzilla->memcached->clear({ table => 'flags', id => $flag_id });
+ }
}
###############################
@@ -985,3 +995,29 @@ sub get_content_type {
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item set_filename
+
+=item set_is_obsolete
+
+=item DB_COLUMNS
+
+=item set_is_private
+
+=item set_content_type
+
+=item set_description
+
+=item get_content_type
+
+=item set_flags
+
+=item set_is_patch
+
+=item update
+
+=back
diff --git a/Bugzilla/Attachment/PatchReader.pm b/Bugzilla/Attachment/PatchReader.pm
index e75a660f2..d0e221220 100644
--- a/Bugzilla/Attachment/PatchReader.pm
+++ b/Bugzilla/Attachment/PatchReader.pm
@@ -5,29 +5,38 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Attachment::PatchReader;
+
+use 5.10.1;
use strict;
+use warnings;
-package Bugzilla::Attachment::PatchReader;
+use Config;
+use IO::Select;
+use IPC::Open3;
+use Symbol 'gensym';
use Bugzilla::Error;
use Bugzilla::Attachment;
use Bugzilla::Util;
+use constant PERLIO_IS_ENABLED => $Config{useperlio};
+
sub process_diff {
- my ($attachment, $format, $context) = @_;
+ my ($attachment, $format) = @_;
my $dbh = Bugzilla->dbh;
my $cgi = Bugzilla->cgi;
my $lc = Bugzilla->localconfig;
my $vars = {};
- my ($reader, $last_reader) = setup_patch_readers(undef, $context);
+ require PatchReader::Raw;
+ my $reader = new PatchReader::Raw;
if ($format eq 'raw') {
require PatchReader::DiffPrinter::raw;
- $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
+ $reader->sends_data_to(new PatchReader::DiffPrinter::raw());
# Actually print out the patch.
- print $cgi->header(-type => 'text/plain',
- -expires => '+3M');
+ print $cgi->header(-type => 'text/plain');
disable_utf8();
$reader->iterate_string('Attachment ' . $attachment->id, $attachment->data);
}
@@ -63,7 +72,7 @@ sub process_diff {
$vars->{'description'} = $attachment->description;
$vars->{'other_patches'} = \@other_patches;
- setup_template_patch_reader($last_reader, $format, $context, $vars);
+ setup_template_patch_reader($reader, $vars);
# The patch is going to be displayed in a HTML page and if the utf8
# param is enabled, we have to encode attachment data as utf8.
if (Bugzilla->params->{'utf8'}) {
@@ -75,11 +84,13 @@ sub process_diff {
}
sub process_interdiff {
- my ($old_attachment, $new_attachment, $format, $context) = @_;
+ my ($old_attachment, $new_attachment, $format) = @_;
my $cgi = Bugzilla->cgi;
my $lc = Bugzilla->localconfig;
my $vars = {};
+ require PatchReader::Raw;
+
# Encode attachment data as utf8 if it's going to be displayed in a HTML
# page using the UTF-8 encoding.
if ($format ne 'raw' && Bugzilla->params->{'utf8'}) {
@@ -98,23 +109,87 @@ sub process_interdiff {
# Send through interdiff, send output directly to template.
# Must hack path so that interdiff will work.
- $ENV{'PATH'} = $lc->{diffpath};
- open my $interdiff_fh, '-|', "$lc->{interdiffbin} $old_filename $new_filename";
- binmode $interdiff_fh;
- my ($reader, $last_reader) = setup_patch_readers("", $context);
+ local $ENV{'PATH'} = $lc->{diffpath};
+
+ # Open the interdiff pipe, reading from both STDOUT and STDERR
+ # To avoid deadlocks, we have to read the entire output from all handles
+ my ($stdout, $stderr) = ('', '');
+ my ($pid, $interdiff_stdout, $interdiff_stderr, $use_select);
+ if ($ENV{MOD_PERL}) {
+ require Apache2::RequestUtil;
+ require Apache2::SubProcess;
+ my $request = Apache2::RequestUtil->request;
+ (undef, $interdiff_stdout, $interdiff_stderr) = $request->spawn_proc_prog(
+ $lc->{interdiffbin}, [$old_filename, $new_filename]
+ );
+ $use_select = !PERLIO_IS_ENABLED;
+ } else {
+ $interdiff_stderr = gensym;
+ $pid = open3(gensym, $interdiff_stdout, $interdiff_stderr,
+ $lc->{interdiffbin}, $old_filename, $new_filename);
+ $use_select = 1;
+ }
+
+ if ($format ne 'raw' && Bugzilla->params->{'utf8'}) {
+ binmode $interdiff_stdout, ':utf8';
+ binmode $interdiff_stderr, ':utf8';
+ } else {
+ binmode $interdiff_stdout;
+ binmode $interdiff_stderr;
+ }
+
+ if ($use_select) {
+ my $select = IO::Select->new();
+ $select->add($interdiff_stdout, $interdiff_stderr);
+ while (my @handles = $select->can_read) {
+ foreach my $handle (@handles) {
+ my $line = <$handle>;
+ if (!defined $line) {
+ $select->remove($handle);
+ next;
+ }
+ if ($handle == $interdiff_stdout) {
+ $stdout .= $line;
+ } else {
+ $stderr .= $line;
+ }
+ }
+ }
+ waitpid($pid, 0) if $pid;
+
+ } else {
+ local $/ = undef;
+ $stdout = <$interdiff_stdout>;
+ $stdout //= '';
+ $stderr = <$interdiff_stderr>;
+ $stderr //= '';
+ }
+
+ close($interdiff_stdout),
+ close($interdiff_stderr);
+
+ # Tidy up
+ unlink($old_filename) or warn "Could not unlink $old_filename: $!";
+ unlink($new_filename) or warn "Could not unlink $new_filename: $!";
+
+ # Any output on STDERR means interdiff failed to full process the patches.
+ # Interdiff's error messages are generic and not useful to end users, so we
+ # show a generic failure message.
+ if ($stderr) {
+ warn($stderr);
+ $warning = 'interdiff3';
+ }
+
+ my $reader = new PatchReader::Raw;
if ($format eq 'raw') {
require PatchReader::DiffPrinter::raw;
- $last_reader->sends_data_to(new PatchReader::DiffPrinter::raw());
+ $reader->sends_data_to(new PatchReader::DiffPrinter::raw());
# Actually print out the patch.
- print $cgi->header(-type => 'text/plain',
- -expires => '+3M');
+ print $cgi->header(-type => 'text/plain');
disable_utf8();
}
else {
- # In case the HTML page is displayed with the UTF-8 encoding.
- binmode $interdiff_fh, ':utf8' if Bugzilla->params->{'utf8'};
-
$vars->{'warning'} = $warning if $warning;
$vars->{'bugid'} = $new_attachment->bug_id;
$vars->{'oldid'} = $old_attachment->id;
@@ -122,16 +197,10 @@ sub process_interdiff {
$vars->{'newid'} = $new_attachment->id;
$vars->{'new_desc'} = $new_attachment->description;
- setup_template_patch_reader($last_reader, $format, $context, $vars);
+ setup_template_patch_reader($reader, $vars);
}
- $reader->iterate_fh($interdiff_fh, 'interdiff #' . $old_attachment->id .
- ' #' . $new_attachment->id);
- close $interdiff_fh;
- $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: $!";
+ $reader->iterate_string('interdiff #' . $old_attachment->id .
+ ' #' . $new_attachment->id, $stdout);
}
######################
@@ -143,7 +212,6 @@ sub get_unified_diff {
# Bring in the modules we need.
require PatchReader::Raw;
- require PatchReader::FixPatchRoot;
require PatchReader::DiffPrinter::raw;
require PatchReader::PatchInfoGrabber;
require File::Temp;
@@ -155,14 +223,6 @@ sub get_unified_diff {
my $reader = new PatchReader::Raw;
my $last_reader = $reader;
- # Fixes patch root (makes canonical if possible).
- if (Bugzilla->params->{'cvsroot'}) {
- my $fix_patch_root =
- new PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'});
- $last_reader->sends_data_to($fix_patch_root);
- $last_reader = $fix_patch_root;
- }
-
# Grabs the patch file info.
my $patch_info_grabber = new PatchReader::PatchInfoGrabber();
$last_reader->sends_data_to($patch_info_grabber);
@@ -209,46 +269,8 @@ sub warn_if_interdiff_might_fail {
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 PatchReader::Raw;
- my $reader = new PatchReader::Raw;
- my $last_reader = $reader;
- # Fix the patch root if we have a cvs root.
- if (Bugzilla->params->{'cvsroot'}) {
- require PatchReader::FixPatchRoot;
- $last_reader->sends_data_to(new PatchReader::FixPatchRoot(Bugzilla->params->{'cvsroot'}));
- $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 PatchReader::AddCVSContext;
- # We need to set $cvsbin as global, because PatchReader::CVSClient
- # needs it in order to find 'cvs'.
- $main::cvsbin = Bugzilla->localconfig->{cvsbin};
- $last_reader->sends_data_to(
- new PatchReader::AddCVSContext($context, Bugzilla->params->{'cvsroot_get'}));
- $last_reader = $last_reader->sends_data_to;
- }
-
- return ($reader, $last_reader);
-}
-
sub setup_template_patch_reader {
- my ($last_reader, $format, $context, $vars) = @_;
+ my ($last_reader, $vars) = @_;
my $cgi = Bugzilla->cgi;
my $template = Bugzilla->template;
@@ -263,24 +285,33 @@ sub setup_template_patch_reader {
}
$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 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'},
- }));
+ 'attachment/diff-header.html.tmpl',
+ 'attachment/diff-file.html.tmpl',
+ 'attachment/diff-footer.html.tmpl',
+ $vars));
}
1;
__END__
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item get_unified_diff
+
+=item process_diff
+
+=item warn_if_interdiff_might_fail
+
+=item setup_template_patch_reader
+
+=item process_interdiff
+
+=back
diff --git a/Bugzilla/Auth.pm b/Bugzilla/Auth.pm
index 09a2c1da4..c830f0506 100644
--- a/Bugzilla/Auth.pm
+++ b/Bugzilla/Auth.pm
@@ -7,7 +7,10 @@
package Bugzilla::Auth;
+use 5.10.1;
use strict;
+use warnings;
+
use fields qw(
_info_getter
_verifier
@@ -29,7 +32,7 @@ sub new {
my $self = fields::new($class);
$params ||= {};
- $params->{Login} ||= Bugzilla->params->{'user_info_class'} . ',Cookie';
+ $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});
@@ -43,7 +46,6 @@ sub new {
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();
@@ -52,7 +54,7 @@ sub login {
return $self->_handle_login_result($login_info, $type);
}
- # Now verify his username and password against the DB, LDAP, etc.
+ # Now verify their username and password against the DB, LDAP, etc.
if ($self->{_info_getter}->{successful}->requires_verification) {
$login_info = $self->{_verifier}->check_credentials($login_info);
if ($login_info->{failure}) {
@@ -177,7 +179,7 @@ sub _handle_login_result {
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",
+ ThrowUserError("invalid_login_or_password",
{ remaining => $remaining_attempts });
}
# The account may be disabled
@@ -296,7 +298,7 @@ An incorrect username or password was given.
The hashref may also contain a C<failure_count> element, which specifies
how many times the account has failed to log in within the lockout
period (see L</AUTH_LOCKOUT>). This is used to warn the user when
-he is getting close to being locked out.
+they are getting close to being locked out.
=head2 C<AUTH_NO_SUCH_USER>
diff --git a/Bugzilla/Auth/Login.pm b/Bugzilla/Auth/Login.pm
index 290cb42ff..a5f089777 100644
--- a/Bugzilla/Auth/Login.pm
+++ b/Bugzilla/Auth/Login.pm
@@ -7,7 +7,10 @@
package Bugzilla::Auth::Login;
+use 5.10.1;
use strict;
+use warnings;
+
use fields qw();
# Determines whether or not a user can logout. It's really a subroutine,
diff --git a/Bugzilla/Auth/Login/APIKey.pm b/Bugzilla/Auth/Login/APIKey.pm
new file mode 100644
index 000000000..63e35578a
--- /dev/null
+++ b/Bugzilla/Auth/Login/APIKey.pm
@@ -0,0 +1,53 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Auth::Login::APIKey;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use base qw(Bugzilla::Auth::Login);
+
+use Bugzilla::Constants;
+use Bugzilla::User::APIKey;
+use Bugzilla::Util;
+use Bugzilla::Error;
+
+use constant requires_persistence => 0;
+use constant requires_verification => 0;
+use constant can_login => 0;
+use constant can_logout => 0;
+
+# This method is only available to web services. An API key can never
+# be used to authenticate a Web request.
+sub get_login_info {
+ my ($self) = @_;
+ my $params = Bugzilla->input_params;
+ my ($user_id, $login_cookie);
+
+ my $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 });
+
+ 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');
+ }
+
+ $api_key->update_last_used();
+
+ return { user_id => $api_key->user_id };
+}
+
+1;
diff --git a/Bugzilla/Auth/Login/CGI.pm b/Bugzilla/Auth/Login/CGI.pm
index f29e8c9c1..6003d62a5 100644
--- a/Bugzilla/Auth/Login/CGI.pm
+++ b/Bugzilla/Auth/Login/CGI.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Login::CGI;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Auth::Login);
+use warnings;
+
+use parent qw(Bugzilla::Auth::Login);
use constant user_can_create_account => 1;
use Bugzilla::Constants;
diff --git a/Bugzilla/Auth/Login/Cookie.pm b/Bugzilla/Auth/Login/Cookie.pm
index b20357307..c09f08d24 100644
--- a/Bugzilla/Auth/Login/Cookie.pm
+++ b/Bugzilla/Auth/Login/Cookie.pm
@@ -7,12 +7,16 @@
package Bugzilla::Auth::Login::Cookie;
+use 5.10.1;
use strict;
+use warnings;
use base qw(Bugzilla::Auth::Login);
use fields qw(_login_token);
use Bugzilla::Constants;
+use Bugzilla::Error;
+use Bugzilla::Token;
use Bugzilla::Util;
use List::Util qw(first);
@@ -47,6 +51,20 @@ sub get_login_info {
@{$cgi->{'Bugzilla_cookie_list'}};
$user_id = $cookie->value if $cookie;
}
+
+ # If the call is for a web service, and an api token is provided, check
+ # it is valid.
+ if (i_am_webservice() && Bugzilla->input_params->{Bugzilla_api_token}) {
+ my $api_token = Bugzilla->input_params->{Bugzilla_api_token};
+ my ($token_user_id, undef, undef, $token_type)
+ = Bugzilla::Token::GetTokenData($api_token);
+ if (!defined $token_type
+ || $token_type ne 'api_token'
+ || $user_id != $token_user_id)
+ {
+ ThrowUserError('auth_invalid_token', { token => $api_token });
+ }
+ }
}
# If no cookies were provided, we also look for a login token
@@ -73,7 +91,9 @@ sub get_login_info {
AND (ipaddr = ? OR ipaddr IS NULL)',
undef, ($login_cookie, $user_id, $ip_addr));
- # If the cookie is valid, return a valid username.
+ # If the cookie or token is valid, return a valid username.
+ # If they were not valid and we are using a webservice, then
+ # throw an error notifying the client.
if (defined $db_cookie && $login_cookie eq $db_cookie) {
# If we logged in successfully, then update the lastused
# time on the login cookie
@@ -81,12 +101,16 @@ sub get_login_info {
WHERE cookie = ?", undef, $login_cookie);
return { user_id => $user_id };
}
+ elsif (i_am_webservice()) {
+ ThrowUserError('invalid_cookies_or_token');
+ }
}
- # Either the he cookie is invalid, or we got no cookie. 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. It should just
- # look like there was no cookie to begin with.
+ # 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 };
}
@@ -97,9 +121,7 @@ sub login_token {
return $self->{'_login_token'} if exists $self->{'_login_token'};
- if ($usage_mode ne USAGE_MODE_XMLRPC
- && $usage_mode ne USAGE_MODE_JSON)
- {
+ if (!i_am_webservice()) {
return $self->{'_login_token'} = undef;
}
diff --git a/Bugzilla/Auth/Login/Env.pm b/Bugzilla/Auth/Login/Env.pm
index 393ac600d..653df2bb3 100644
--- a/Bugzilla/Auth/Login/Env.pm
+++ b/Bugzilla/Auth/Login/Env.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Login::Env;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Auth::Login);
+use warnings;
+
+use parent qw(Bugzilla::Auth::Login);
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -21,7 +25,6 @@ use constant extern_id_used => 1;
sub get_login_info {
my ($self) = @_;
- my $dbh = Bugzilla->dbh;
my $env_id = $ENV{Bugzilla->params->{"auth_env_id"}} || '';
my $env_email = $ENV{Bugzilla->params->{"auth_env_email"}} || '';
diff --git a/Bugzilla/Auth/Login/Stack.pm b/Bugzilla/Auth/Login/Stack.pm
index 17a5855b6..dc35998e4 100644
--- a/Bugzilla/Auth/Login/Stack.pm
+++ b/Bugzilla/Auth/Login/Stack.pm
@@ -6,7 +6,11 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Login::Stack;
+
+use 5.10.1;
use strict;
+use warnings;
+
use base qw(Bugzilla::Auth::Login);
use fields qw(
_stack
diff --git a/Bugzilla/Auth/Persist/Cookie.pm b/Bugzilla/Auth/Persist/Cookie.pm
index b0aeb4f0f..2d1291f3b 100644
--- a/Bugzilla/Auth/Persist/Cookie.pm
+++ b/Bugzilla/Auth/Persist/Cookie.pm
@@ -6,7 +6,11 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Persist::Cookie;
+
+use 5.10.1;
use strict;
+use warnings;
+
use fields qw();
use Bugzilla::Constants;
@@ -108,7 +112,7 @@ sub logout {
if ($cookie) {
push(@login_cookies, $cookie->value);
}
- elsif ($cookie = $cgi->cookie("Bugzilla_logincookie")) {
+ elsif ($cookie = $cgi->cookie('Bugzilla_logincookie')) {
push(@login_cookies, $cookie);
}
@@ -147,6 +151,7 @@ sub logout {
if ($type != LOGOUT_KEEP_CURRENT) {
clear_browser_cookies();
}
+
}
sub clear_browser_cookies {
diff --git a/Bugzilla/Auth/Verify.pm b/Bugzilla/Auth/Verify.pm
index ae256dd8c..e44fb06ae 100644
--- a/Bugzilla/Auth/Verify.pm
+++ b/Bugzilla/Auth/Verify.pm
@@ -7,7 +7,10 @@
package Bugzilla::Auth::Verify;
+use 5.10.1;
use strict;
+use warnings;
+
use fields qw();
use Bugzilla::Constants;
@@ -88,6 +91,7 @@ sub create_or_update_user {
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.
@@ -233,3 +237,11 @@ edit the extern_id for all users.
The default value is C<false>.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item can_change_password
+
+=back
diff --git a/Bugzilla/Auth/Verify/DB.pm b/Bugzilla/Auth/Verify/DB.pm
index 99dc48ddc..28a9310c9 100644
--- a/Bugzilla/Auth/Verify/DB.pm
+++ b/Bugzilla/Auth/Verify/DB.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Verify::DB;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Auth::Verify);
+use warnings;
+
+use parent qw(Bugzilla::Auth::Verify);
use Bugzilla::Constants;
use Bugzilla::Token;
@@ -52,10 +56,19 @@ sub check_credentials {
};
}
- # Force the user to type a longer password if it's too short.
- if (length($password) < USER_PASSWORD_MIN_LENGTH) {
- return { failure => AUTH_ERROR, user_error => 'password_current_too_short',
- details => { locked_user => $user } };
+ # Force the user to change their password if it does not meet the current
+ # criteria. This should usually only happen if the criteria has changed.
+ if (Bugzilla->usage_mode == USAGE_MODE_BROWSER &&
+ Bugzilla->params->{password_check_on_login})
+ {
+ my $check = validate_password_check($password);
+ if ($check) {
+ return {
+ failure => AUTH_ERROR,
+ user_error => $check,
+ details => { locked_user => $user }
+ }
+ }
}
# The user's credentials are okay, so delete any outstanding
@@ -63,11 +76,22 @@ sub check_credentials {
Bugzilla::Token::DeletePasswordTokens($user->id, "user_logged_in");
$user->clear_login_failures();
+ my $update_password = 0;
+
# If their old password was using crypt() or some different hash
# than we're using now, convert the stored password to using
# whatever hashing system we're using now.
my $current_algorithm = PASSWORD_DIGEST_ALGORITHM;
- if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/) {
+ $update_password = 1 if ($real_password_crypted !~ /{\Q$current_algorithm\E}$/);
+
+ # If their old password was using a different length salt than what
+ # we're using now, update the password to use the new salt length.
+ if ($real_password_crypted =~ /^([^,]+),/) {
+ $update_password = 1 if (length($1) != PASSWORD_SALT_LENGTH);
+ }
+
+ # If needed, update the user's password.
+ if ($update_password) {
# We can't call $user->set_password because we don't want the password
# complexity rules to apply here.
$user->{cryptpassword} = bz_crypt($password);
@@ -83,6 +107,7 @@ sub change_password {
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 5704c5848..e37f55793 100644
--- a/Bugzilla/Auth/Verify/LDAP.pm
+++ b/Bugzilla/Auth/Verify/LDAP.pm
@@ -6,7 +6,11 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Verify::LDAP;
+
+use 5.10.1;
use strict;
+use warnings;
+
use base qw(Bugzilla::Auth::Verify);
use fields qw(
ldap
@@ -149,7 +153,7 @@ sub _bind_ldap_for_search {
# We can't just do this in new(), because we're not allowed to throw any
# error from anywhere under Bugzilla::Auth::new -- otherwise we
# could create a situation where the admin couldn't get to editparams
-# to fix his mistake. (Because Bugzilla->login always calls
+# to fix their mistake. (Because Bugzilla->login always calls
# Bugzilla::Auth->new, and almost every page calls Bugzilla->login.)
sub ldap {
my ($self) = @_;
diff --git a/Bugzilla/Auth/Verify/RADIUS.pm b/Bugzilla/Auth/Verify/RADIUS.pm
index d6c4db8e8..283d9b466 100644
--- a/Bugzilla/Auth/Verify/RADIUS.pm
+++ b/Bugzilla/Auth/Verify/RADIUS.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Verify::RADIUS;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Auth::Verify);
+use warnings;
+
+use parent qw(Bugzilla::Auth::Verify);
use Bugzilla::Constants;
use Bugzilla::Error;
diff --git a/Bugzilla/Auth/Verify/Stack.pm b/Bugzilla/Auth/Verify/Stack.pm
index 0930d57ed..3e5db3cec 100644
--- a/Bugzilla/Auth/Verify/Stack.pm
+++ b/Bugzilla/Auth/Verify/Stack.pm
@@ -6,7 +6,11 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Auth::Verify::Stack;
+
+use 5.10.1;
use strict;
+use warnings;
+
use base qw(Bugzilla::Auth::Verify);
use fields qw(
_stack
diff --git a/Bugzilla/Bug.pm b/Bugzilla/Bug.pm
index b390c12d4..bfc2fe0d9 100644
--- a/Bugzilla/Bug.pm
+++ b/Bugzilla/Bug.pm
@@ -7,7 +7,9 @@
package Bugzilla::Bug;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Attachment;
use Bugzilla::Constants;
@@ -27,15 +29,14 @@ use Bugzilla::Group;
use Bugzilla::Status;
use Bugzilla::Comment;
use Bugzilla::BugUrl;
+use Bugzilla::BugUserLastVisit;
use List::MoreUtils qw(firstidx uniq part);
use List::Util qw(min max first);
use Storable qw(dclone);
-use URI;
-use URI::QueryParam;
use Scalar::Util qw(blessed);
-use base qw(Bugzilla::Object Exporter);
+use parent qw(Bugzilla::Object Exporter);
@Bugzilla::Bug::EXPORT = qw(
bug_alias_to_id
LogActivityEntry
@@ -48,11 +49,13 @@ use base qw(Bugzilla::Object Exporter);
use constant DB_TABLE => 'bugs';
use constant ID_FIELD => 'bug_id';
-use constant NAME_FIELD => 'alias';
+use constant NAME_FIELD => 'bug_id';
use constant LIST_ORDER => ID_FIELD;
# Bugs have their own auditing table, bugs_activity.
use constant AUDIT_CREATES => 0;
use constant AUDIT_UPDATES => 0;
+# This will be enabled later
+use constant USE_MEMCACHED => 0;
# This is a sub because it needs to call other subroutines.
sub DB_COLUMNS {
@@ -62,7 +65,6 @@ sub DB_COLUMNS {
my @custom_names = map {$_->name} @custom;
my @columns = (qw(
- alias
assigned_to
bug_file_loc
bug_id
@@ -70,6 +72,7 @@ sub DB_COLUMNS {
bug_status
cclist_accessible
component_id
+ creation_ts
delta_ts
estimated_time
everconfirmed
@@ -88,7 +91,6 @@ sub DB_COLUMNS {
version
),
'reporter AS reporter_id',
- $dbh->sql_date_format('creation_ts', '%Y.%m.%d %H:%i') . ' AS creation_ts',
$dbh->sql_date_format('deadline', '%Y-%m-%d') . ' AS deadline',
@custom_names);
@@ -145,6 +147,9 @@ sub VALIDATORS {
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;
}
@@ -154,6 +159,9 @@ sub VALIDATORS {
elsif ($field->type == FIELD_TYPE_TEXTAREA) {
$validator = \&_check_textarea_field;
}
+ elsif ($field->type == FIELD_TYPE_INTEGER) {
+ $validator = \&_check_integer_field;
+ }
else {
$validator = \&_check_default_field;
}
@@ -199,7 +207,6 @@ sub UPDATE_COLUMNS {
Bugzilla->active_custom_fields;
my @custom_names = map {$_->name} @custom;
my @columns = qw(
- alias
assigned_to
bug_file_loc
bug_severity
@@ -232,7 +239,9 @@ use constant NUMERIC_COLUMNS => qw(
);
sub DATE_COLUMNS {
- my @fields = @{ Bugzilla->fields({ type => FIELD_TYPE_DATETIME }) };
+ my @fields = (@{ Bugzilla->fields({ type => [FIELD_TYPE_DATETIME,
+ FIELD_TYPE_DATE] })
+ });
return map { $_->name } @fields;
}
@@ -263,10 +272,6 @@ use constant FIELD_MAP => {
summary => 'short_desc',
url => 'bug_file_loc',
whiteboard => 'status_whiteboard',
-
- # These are special values for the WebService Bug.search method.
- limit => 'LIMIT',
- offset => 'OFFSET',
};
use constant REQUIRED_FIELD_MAP => {
@@ -294,21 +299,11 @@ use constant EXTRA_REQUIRED_FIELDS => qw(creation_ts target_milestone cc qa_cont
#####################################################################
-# This and "new" catch every single way of creating a bug, so that we
-# can call _create_cf_accessors.
-sub _do_list_select {
- my $invocant = shift;
- $invocant->_create_cf_accessors();
- return $invocant->SUPER::_do_list_select(@_);
-}
-
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
my $param = shift;
- $class->_create_cf_accessors();
-
# Remove leading "#" mark if we've just been passed an id.
if (!ref $param && $param =~ /^#(\d+)$/) {
$param = $1;
@@ -316,9 +311,22 @@ sub new {
# If we get something that looks like a word (not a number),
# make it the "name" param.
- if (!defined $param || (!ref($param) && $param !~ /^\d+$/)) {
+ if (!defined $param
+ || (!ref($param) && $param !~ /^\d+$/)
+ || (ref($param) && $param->{id} !~ /^\d+$/))
+ {
if ($param) {
- $param = { name => $param };
+ my $alias = ref($param) ? $param->{id} : $param;
+ my $bug_id = bug_alias_to_id($alias);
+ if (! $bug_id) {
+ my $error_self = {};
+ bless $error_self, $class;
+ $error_self->{'bug_id'} = $alias;
+ $error_self->{'error'} = 'InvalidBugId';
+ return $error_self;
+ }
+ $param = { id => $bug_id,
+ cache => ref($param) ? $param->{cache} : 0 };
}
else {
# We got something that's not a number.
@@ -352,6 +360,17 @@ sub new {
return $self;
}
+sub initialize {
+ $_[0]->_create_cf_accessors();
+}
+
+sub object_cache_key {
+ my $class = shift;
+ my $key = $class->SUPER::object_cache_key(@_)
+ || return;
+ return $key . ',' . Bugzilla->user->id;
+}
+
sub check {
my $class = shift;
my ($param, $field) = @_;
@@ -486,8 +505,9 @@ sub preload {
# to the more complex method.
my @all_dep_ids;
foreach my $bug (@$bugs) {
- push(@all_dep_ids, @{ $bug->blocked }, @{ $bug->dependson });
- push(@all_dep_ids, @{ $bug->duplicate_ids });
+ push @all_dep_ids, @{ $bug->blocked }, @{ $bug->dependson };
+ push @all_dep_ids, @{ $bug->duplicate_ids };
+ push @all_dep_ids, @{ $bug->_preload_referenced_bugs };
}
@all_dep_ids = uniq @all_dep_ids;
# If we don't do this, can_see_bug will do one call per bug in
@@ -495,6 +515,60 @@ sub preload {
$user->visible_bugs(\@all_dep_ids);
}
+# Helps load up bugs referenced in comments by retrieving them with a single
+# query from the database and injecting bug objects into the object-cache.
+sub _preload_referenced_bugs {
+ my $self = shift;
+
+ # inject current duplicates into the object-cache first
+ foreach my $bug (@{ $self->duplicates }) {
+ $bug->object_cache_set() unless Bugzilla::Bug->object_cache_get($bug->id);
+ }
+
+ # preload bugs from comments
+ my $referenced_bug_ids = _extract_bug_ids($self->comments);
+ my @ref_bug_ids = grep { !Bugzilla::Bug->object_cache_get($_) } @$referenced_bug_ids;
+
+ # inject into object-cache
+ my $referenced_bugs = Bugzilla::Bug->new_from_list(\@ref_bug_ids);
+ $_->object_cache_set() foreach @$referenced_bugs;
+
+ return $referenced_bug_ids
+}
+
+# Extract bug IDs mentioned in comments. This is much faster than calling quoteUrls().
+sub _extract_bug_ids {
+ my $comments = shift;
+ my @bug_ids;
+
+ my $params = Bugzilla->params;
+ my @urlbases = ($params->{'urlbase'});
+ push(@urlbases, $params->{'sslbase'}) if $params->{'sslbase'};
+ my $urlbase_re = '(?:' . join('|', map { qr/$_/ } @urlbases) . ')';
+ my $bug_word = template_var('terms')->{bug};
+ my $bugs_word = template_var('terms')->{bugs};
+
+ foreach my $comment (@$comments) {
+ if ($comment->type == CMT_HAS_DUPE || $comment->type == CMT_DUPE_OF) {
+ push @bug_ids, $comment->extra_data;
+ next;
+ }
+ my $s = $comment->already_wrapped ? qr/\s/ : qr/\h/;
+ my $text = $comment->body;
+ # Full bug links
+ push @bug_ids, $text =~ /\b$urlbase_re\Qshow_bug.cgi?id=\E(\d+)(?:\#c\d+)?/g;
+ # bug X
+ my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
+ push @bug_ids, $text =~ /\b$bug_re/g;
+ # bugs X, Y, Z
+ my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*(\d+)(?:$s*,$s*\#?$s*(\d+))+/i;
+ push @bug_ids, $text =~ /\b$bugs_re/g;
+ # Old duplicate markers
+ push @bug_ids, $text =~ /(?<=^\*\*\*\ This\ bug\ has\ been\ marked\ as\ a\ duplicate\ of\ )(\d+)(?=\ \*\*\*\Z)/;
+ }
+ return [uniq @bug_ids];
+}
+
sub possible_duplicates {
my ($class, $params) = @_;
my $short_desc = $params->{summary};
@@ -635,12 +709,14 @@ sub create {
# These are not a fields in the bugs table, so we don't pass them to
# insert_create_data.
+ my $bug_aliases = delete $params->{alias};
my $cc_ids = delete $params->{cc};
my $groups = delete $params->{groups};
my $depends_on = delete $params->{dependson};
my $blocked = delete $params->{blocked};
my $keywords = delete $params->{keywords};
my $creation_comment = delete $params->{comment};
+ my $see_also = delete $params->{see_also};
# We don't want the bug to appear in the system until it's correctly
# protected by groups.
@@ -704,6 +780,25 @@ sub create {
}
}
+ # 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
@@ -713,6 +808,13 @@ sub create {
# but sometimes it's blank.
Bugzilla::Comment->insert_create_data($creation_comment);
+ # Set up aliases
+ my $sth_aliases = $dbh->prepare('INSERT INTO bugs_aliases (alias, bug_id) VALUES (?, ?)');
+ foreach my $alias (@$bug_aliases) {
+ trick_taint($alias);
+ $sth_aliases->execute($alias, $bug->bug_id);
+ }
+
Bugzilla::Hook::process('bug_end_of_create', { bug => $bug,
timestamp => $timestamp,
});
@@ -776,8 +878,9 @@ sub run_create_validators {
sub update {
my $self = shift;
+ my $dbh = Bugzilla->dbh;
+ my $user = Bugzilla->user;
- my $dbh = Bugzilla->dbh;
# XXX This is just a temporary hack until all updating happens
# inside this function.
my $delta_ts = shift || $dbh->selectrow_array('SELECT LOCALTIMESTAMP(0)');
@@ -831,7 +934,26 @@ sub update {
my $added_names = join(', ', (map {$_->login} @$added_users));
$changes->{cc} = [$removed_names, $added_names];
}
-
+
+ # Aliases
+ my $old_aliases = $old_bug->alias;
+ my $new_aliases = $self->alias;
+ my ($removed_aliases, $added_aliases) = diff_arrays($old_aliases, $new_aliases);
+
+ foreach my $alias (@$removed_aliases) {
+ $dbh->do('DELETE FROM bugs_aliases WHERE bug_id = ? AND alias = ?',
+ undef, $self->id, $alias);
+ }
+ foreach my $alias (@$added_aliases) {
+ trick_taint($alias);
+ $dbh->do('INSERT INTO bugs_aliases (bug_id, alias) VALUES (?,?)',
+ undef, $self->id, $alias);
+ }
+ # If any changes were found, record it in the activity log
+ if (scalar @$removed_aliases || scalar @$added_aliases) {
+ $changes->{alias} = [join(', ', @$removed_aliases), join(', ', @$added_aliases)];
+ }
+
# Keywords
my @old_kw_ids = map { $_->id } @{$old_bug->keyword_objects};
my @new_kw_ids = map { $_->id } @{$self->keyword_objects};
@@ -868,7 +990,7 @@ sub update {
# Add an activity entry for the other bug.
LogActivityEntry($removed_id, $other, $self->id, '',
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
# Update delta_ts on the other bug so that we trigger mid-airs.
$dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
undef, $delta_ts, $removed_id);
@@ -879,7 +1001,7 @@ sub update {
# Add an activity entry for the other bug.
LogActivityEntry($added_id, $other, '', $self->id,
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
# Update delta_ts on the other bug so that we trigger mid-airs.
$dbh->do('UPDATE bugs SET delta_ts = ? WHERE bug_id = ?',
undef, $delta_ts, $added_id);
@@ -921,7 +1043,7 @@ sub update {
$comment = Bugzilla::Comment->insert_create_data($comment);
if ($comment->work_time) {
LogActivityEntry($self->id, "work_time", "", $comment->work_time,
- Bugzilla->user->id, $delta_ts);
+ $user->id, $delta_ts);
}
}
@@ -932,7 +1054,7 @@ sub update {
my ($from, $to)
= $comment->is_private ? (0, 1) : (1, 0);
LogActivityEntry($self->id, "longdescs.isprivate", $from, $to,
- Bugzilla->user->id, $delta_ts, $comment->id);
+ $user->id, $delta_ts, $comment->id);
}
# Clear the cache of comments
@@ -986,8 +1108,8 @@ sub update {
my $change = $changes->{$field};
my $from = defined $change->[0] ? $change->[0] : '';
my $to = defined $change->[1] ? $change->[1] : '';
- LogActivityEntry($self->id, $field, $from, $to, Bugzilla->user->id,
- $delta_ts);
+ LogActivityEntry($self->id, $field, $from, $to,
+ $user->id, $delta_ts);
}
# Check if we have to update the duplicates table and the other bug.
@@ -1001,7 +1123,7 @@ sub update {
$update_dup->update();
}
}
-
+
$changes->{'dup_id'} = [$old_dup || undef, $cur_dup || undef];
}
@@ -1018,6 +1140,37 @@ sub update {
$self->{delta_ts} = $delta_ts;
}
+ # Update last-visited
+ if ($user->is_involved_in_bug($self)) {
+ $self->update_user_last_visit($user, $delta_ts);
+ }
+
+ # If a user is no longer involved, remove their last visit entry
+ my $last_visits =
+ Bugzilla::BugUserLastVisit->match({ bug_id => $self->id });
+ foreach my $lv (@$last_visits) {
+ $lv->remove_from_db() unless $lv->user->is_involved_in_bug($self);
+ }
+
+ # Update bug ignore data if user wants to ignore mail for this bug
+ if (exists $self->{'bug_ignored'}) {
+ my $bug_ignored_changed;
+ if ($self->{'bug_ignored'} && !$user->is_bug_ignored($self->id)) {
+ $dbh->do('INSERT INTO email_bug_ignore
+ (user_id, bug_id) VALUES (?, ?)',
+ undef, $user->id, $self->id);
+ $bug_ignored_changed = 1;
+
+ }
+ elsif (!$self->{'bug_ignored'} && $user->is_bug_ignored($self->id)) {
+ $dbh->do('DELETE FROM email_bug_ignore
+ WHERE user_id = ? AND bug_id = ?',
+ undef, $user->id, $self->id);
+ $bug_ignored_changed = 1;
+ }
+ delete $user->{bugs_ignored} if $bug_ignored_changed;
+ }
+
$dbh->bz_commit_transaction();
# The only problem with this here is that update() is often called
@@ -1035,7 +1188,7 @@ sub update {
# Also flush the visible_bugs cache for this bug as the user's
# relationship with this bug may have changed.
- delete Bugzilla->user->{_visible_bugs_cache}->{$self->id};
+ delete $user->{_visible_bugs_cache}->{$self->id};
return $changes;
}
@@ -1214,32 +1367,38 @@ sub _send_bugmail {
#####################################################################
sub _check_alias {
- my ($invocant, $alias) = @_;
- $alias = trim($alias);
- return undef if (!$alias);
-
- # Make sure the alias isn't too long.
- if (length($alias) > 20) {
- 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, $aliases) = @_;
+ $aliases = ref $aliases ? $aliases : [split(/[\s,]+/, $aliases)];
+
+ # Remove empty aliases
+ @$aliases = grep { $_ } @$aliases;
+
+ foreach my $alias (@$aliases) {
+ $alias = trim($alias);
+
+ # Make sure the alias isn't too long.
+ if (length($alias) > 40) {
+ ThrowUserError("alias_too_long");
+ }
+ # Make sure the alias isn't just a number.
+ if ($alias =~ /^\d+$/) {
+ ThrowUserError("alias_is_numeric", { alias => $alias });
+ }
+ # Make sure the alias has no commas or spaces.
+ if ($alias =~ /[, ]/) {
+ ThrowUserError("alias_has_comma_or_space", { alias => $alias });
+ }
+ # Make sure the alias is unique, or that it's already our alias.
+ my $other_bug = new Bugzilla::Bug($alias);
+ if (!$other_bug->{error}
+ && (!ref $invocant || $other_bug->id != $invocant->id))
+ {
+ ThrowUserError("alias_in_use", { alias => $alias,
+ bug_id => $other_bug->id });
+ }
}
- return $alias;
+ return $aliases;
}
sub _check_assigned_to {
@@ -1591,9 +1750,9 @@ sub _check_dup_id {
}
# Should we add the reporter to the CC list of the new bug?
- # If he can see the bug...
+ # If they 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.
+ # We only add them if they're not the reporter of the other bug.
$self->{_add_dup_cc} = 1
if $dupe_of_bug->reporter->id != $self->reporter->id;
}
@@ -1608,11 +1767,11 @@ sub _check_dup_id {
$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*
+ # Note that here we don't check if the user is already the reporter
+ # of the dupe_of bug, since we already checked if they can *see*
# the bug, above. People might have reporter_accessible turned
# off, but cclist_accessible turned on, so they might want to
- # add the reporter even though he's already the reporter of the
+ # add the reporter even though they're already the reporter of the
# dup_of bug.
my $vars = {};
my $template = Bugzilla->template;
@@ -1680,13 +1839,7 @@ sub _check_keywords {
$keywords_in = trim($keywords_in);
$keyword_array = [split(/[\s,]+/, $keywords_in)];
}
-
- # On creation, only editbugs users can set keywords.
- if (!ref $invocant) {
- my $product = $params->{product};
- return [] if !Bugzilla->user->in_group('editbugs', $product->id);
- }
-
+
my %keywords;
foreach my $keyword (@$keyword_array) {
next unless $keyword;
@@ -1761,7 +1914,7 @@ sub _check_reporter {
}
else {
# On bug creation, the reporter is the logged in user
- # (meaning that he must be logged in first!).
+ # (meaning that they must be logged in first!).
Bugzilla->login(LOGIN_REQUIRED);
$reporter = Bugzilla->user->id;
}
@@ -1992,8 +2145,13 @@ sub _check_field_is_mandatory {
}
}
+sub _check_date_field {
+ my ($invocant, $date) = @_;
+ return $invocant->_check_datetime_field($date, undef, {date_only => 1});
+}
+
sub _check_datetime_field {
- my ($invocant, $date_time) = @_;
+ my ($invocant, $date_time, $field, $params) = @_;
# Empty datetimes are empty strings or strings only containing
# 0's, whitespace, and punctuation.
@@ -2007,6 +2165,10 @@ sub _check_datetime_field {
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' });
@@ -2082,6 +2244,27 @@ sub _check_textarea_field {
return $text;
}
+sub _check_integer_field {
+ my ($invocant, $value, $field) = @_;
+ $value = defined($value) ? trim($value) : '';
+
+ if ($value eq '') {
+ return 0;
+ }
+
+ 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;
+}
+
sub _check_relationship_loop {
# Generates a dependency tree for a given bug. Calls itself recursively
# to generate sub-trees for the bug's dependencies.
@@ -2161,8 +2344,8 @@ sub _set_global_validator {
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);
+ $value = Bugzilla::User->new($value)->login;
+ $current = Bugzilla::User->new($current)->login;
}
ThrowUserError('illegal_change', { field => $field,
oldvalue => $current,
@@ -2255,6 +2438,15 @@ sub set_all {
work_time => $params->{'work_time'} });
}
+ if (exists $params->{alias} && $params->{alias}{set}) {
+ my ($removed_aliases, $added_aliases) = diff_arrays(
+ $self->alias, $params->{alias}{set});
+ $params->{alias} = {
+ add => $added_aliases,
+ remove => $removed_aliases,
+ };
+ }
+
my %normal_set_all;
foreach my $name (keys %$params) {
# These are handled separately below.
@@ -2279,13 +2471,14 @@ sub set_all {
}
$self->_add_remove($params, 'cc');
+ $self->_add_remove($params, 'alias');
# Theoretically you could move a product without ever specifying
# a new assignee or qa_contact, or adding/removing any CCs. So,
# we have to check that the current assignee, qa, and CCs are still
# valid if we've switched products, under strict_isolation. We can only
# do that here, because if they *did* change the assignee, qa, or CC,
- # then we don't want to check the original ones, only the new ones.
+ # then we don't want to check the original ones, only the new ones.
$self->_check_strict_isolation() if $product_changed;
}
@@ -2295,14 +2488,13 @@ sub _add_remove {
my ($self, $params, $name) = @_;
my @add = @{ $params->{$name}->{add} || [] };
my @remove = @{ $params->{$name}->{remove} || [] };
- $name =~ s/s$//;
+ $name =~ s/s$// if $name ne 'alias';
my $add_method = "add_$name";
my $remove_method = "remove_$name";
$self->$add_method($_) foreach @add;
$self->$remove_method($_) foreach @remove;
}
-sub set_alias { $_[0]->set('alias', $_[1]); }
sub set_assigned_to {
my ($self, $value) = @_;
$self->set('assigned_to', $value);
@@ -2315,30 +2507,36 @@ sub reset_assigned_to {
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, $comments, $isprivate) = @_;
+ $self->{comment_isprivate} ||= [];
+ my $is_insider = Bugzilla->user->is_insider;
+
+ $comments = { $comments => $isprivate } unless ref $comments;
+
+ foreach my $comment (@{$self->comments}) {
+ # Skip unmodified comment privacy.
+ next unless exists $comments->{$comment->id};
- # 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);
+ my $isprivate = delete $comments->{$comment->id} ? 1 : 0;
+ if ($isprivate != $comment->is_private) {
+ ThrowUserError('user_not_insider') unless $is_insider;
+ $comment->set_is_private($isprivate);
+ push @{$self->{comment_isprivate}}, $comment;
}
- return;
}
- my ($comment) = grep($comment_id == $_->id, @{ $self->comments });
- ThrowUserError('comment_invalid_isprivate', { id => $comment_id })
- if !$comment;
+ # If there are still entries in $comments, then they are illegal.
+ ThrowUserError('comment_invalid_isprivate', { id => join(', ', keys %$comments) })
+ if scalar keys %$comments;
- $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;
- }
+ # If no comment privacy has been modified, remove this key.
+ delete $self->{comment_isprivate} unless scalar @{$self->{comment_isprivate}};
}
+
sub set_component {
my ($self, $name) = @_;
my $old_comp = $self->component_obj;
@@ -2718,6 +2916,32 @@ sub remove_cc {
@$cc_users = grep { $_->id != $user->id } @$cc_users;
}
+sub add_alias {
+ my ($self, $alias) = @_;
+ return if !$alias;
+ my $aliases = $self->_check_alias($alias);
+ $alias = $aliases->[0];
+ my @new_aliases;
+ my $found = 0;
+ foreach my $old_alias (@{ $self->alias }) {
+ if (lc($old_alias) eq lc($alias)) {
+ push(@new_aliases, $alias);
+ $found = 1;
+ }
+ else {
+ push(@new_aliases, $old_alias);
+ }
+ }
+ push(@new_aliases, $alias) if !$found;
+ $self->{alias} = \@new_aliases;
+}
+
+sub remove_alias {
+ my ($self, $alias) = @_;
+ my $bug_aliases = $self->alias;
+ @$bug_aliases = grep { $_ ne $alias } @$bug_aliases;
+}
+
# $bug->add_comment("comment", {isprivate => 1, work_time => 10.5,
# type => CMT_NORMAL, extra_data => $data});
sub add_comment {
@@ -3045,7 +3269,6 @@ 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} }
@@ -3128,7 +3351,7 @@ sub _resolve_ultimate_dup_id {
# 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.
+ # prevent them from marking this bug as a duplicate.
return $last_dup if exists $dupes{$this_dup};
$dupes{$this_dup} = 1;
$last_dup = $this_dup;
@@ -3155,6 +3378,19 @@ sub actual_time {
return $self->{'actual_time'};
}
+sub alias {
+ my ($self) = @_;
+ return $self->{'alias'} if exists $self->{'alias'};
+ return [] if $self->{'error'};
+
+ my $dbh = Bugzilla->dbh;
+ $self->{'alias'} = $dbh->selectcol_arrayref(
+ q{SELECT alias FROM bugs_aliases WHERE bug_id = ? ORDER BY alias},
+ undef, $self->bug_id);
+
+ return $self->{'alias'};
+}
+
sub any_flags_requesteeble {
my ($self) = @_;
return $self->{'any_flags_requesteeble'}
@@ -3178,6 +3414,7 @@ sub attachments {
$self->{'attachments'} =
Bugzilla::Attachment->get_attachments_by_bug($self, {preload => 1});
+ $_->object_cache_set() foreach @{ $self->{'attachments'} };
return $self->{'attachments'};
}
@@ -3185,7 +3422,7 @@ sub assigned_to {
my ($self) = @_;
return $self->{'assigned_to_obj'} if exists $self->{'assigned_to_obj'};
$self->{'assigned_to'} = 0 if $self->{'error'};
- $self->{'assigned_to_obj'} ||= new Bugzilla::User($self->{'assigned_to'});
+ $self->{'assigned_to_obj'} ||= new Bugzilla::User({ id => $self->{'assigned_to'}, cache => 1 });
return $self->{'assigned_to_obj'};
}
@@ -3248,11 +3485,8 @@ sub cc_users {
sub component {
my ($self) = @_;
- return $self->{component} if exists $self->{component};
return '' if $self->{error};
- ($self->{component}) = Bugzilla->dbh->selectrow_array(
- 'SELECT name FROM components WHERE id = ?',
- undef, $self->{component_id});
+ $self->{component} //= $self->component_obj->name;
return $self->{component};
}
@@ -3261,27 +3495,22 @@ sub component_obj {
my ($self) = @_;
return $self->{component_obj} if defined $self->{component_obj};
return {} if $self->{error};
- $self->{component_obj} = new Bugzilla::Component($self->{component_id});
+ $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});
+ $self->{classification_id} //= $self->product_obj->classification_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);
+ $self->{classification} //= $self->product_obj->classification->name;
return $self->{classification};
}
@@ -3362,7 +3591,10 @@ sub flags {
sub isopened {
my $self = shift;
- return is_open_state($self->{bug_status}) ? 1 : 0;
+ unless (exists $self->{isopened}) {
+ $self->{isopened} = is_open_state($self->{bug_status}) ? 1 : 0;
+ }
+ return $self->{isopened};
}
sub keywords {
@@ -3391,11 +3623,21 @@ sub comments {
if (!defined $self->{'comments'}) {
$self->{'comments'} = Bugzilla::Comment->match({ bug_id => $self->id });
my $count = 0;
+ state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
foreach my $comment (@{ $self->{'comments'} }) {
$comment->{count} = $count++;
$comment->{bug} = $self;
+ # XXX - hack for MySQL. Convert [U+....] back into its Unicode
+ # equivalent for characters above U+FFFF as MySQL older than 5.5.3
+ # cannot store them, see Bugzilla::Comment::_check_thetext().
+ if ($is_mysql) {
+ # Perl 5.13.8 and older complain about non-characters.
+ no warnings 'utf8';
+ $comment->{thetext} =~ s/\x{FDD0}\[U\+((?:[1-9A-F]|10)[0-9A-F]{4})\]\x{FDD1}/chr(hex $1)/eg
+ }
}
- Bugzilla::Comment->preload($self->{'comments'});
+ # Some bugs may have no comments when upgrading old installations.
+ Bugzilla::Comment->preload($self->{'comments'}) if $count;
}
my @comments = @{ $self->{'comments'} };
@@ -3457,11 +3699,8 @@ sub percentage_complete {
sub product {
my ($self) = @_;
- return $self->{product} if exists $self->{product};
return '' if $self->{error};
- ($self->{product}) = Bugzilla->dbh->selectrow_array(
- 'SELECT name FROM products WHERE id = ?',
- undef, $self->{product_id});
+ $self->{product} //= $self->product_obj->name;
return $self->{product};
}
@@ -3469,7 +3708,8 @@ sub product {
sub product_obj {
my $self = shift;
return {} if $self->{error};
- $self->{product_obj} ||= new Bugzilla::Product($self->{product_id});
+ $self->{product_obj} ||=
+ new Bugzilla::Product({ id => $self->{product_id}, cache => 1 });
return $self->{product_obj};
}
@@ -3479,7 +3719,7 @@ sub qa_contact {
return undef if $self->{'error'};
if (Bugzilla->params->{'useqacontact'} && $self->{'qa_contact'}) {
- $self->{'qa_contact_obj'} = new Bugzilla::User($self->{'qa_contact'});
+ $self->{'qa_contact_obj'} = new Bugzilla::User({ id => $self->{'qa_contact'}, cache => 1 });
} else {
$self->{'qa_contact_obj'} = undef;
}
@@ -3490,7 +3730,7 @@ sub reporter {
my ($self) = @_;
return $self->{'reporter'} if exists $self->{'reporter'};
$self->{'reporter_id'} = 0 if $self->{'error'};
- $self->{'reporter'} = new Bugzilla::User($self->{'reporter_id'});
+ $self->{'reporter'} = new Bugzilla::User({ id => $self->{'reporter_id'}, cache => 1 });
return $self->{'reporter'};
}
@@ -3732,6 +3972,11 @@ sub choices {
my @resolutions = grep($_->name, @{ $resolution_field->legal_values });
$choices{'resolution'} = \@resolutions;
+ foreach my $key (keys %choices) {
+ my $value = $self->$key;
+ $choices{$key} = [grep { $_->is_active || $_->name eq $value } @{ $choices{$key} }];
+ }
+
$self->{'choices'} = \%choices;
return $self->{'choices'};
}
@@ -3746,7 +3991,7 @@ sub bug_alias_to_id {
my $dbh = Bugzilla->dbh;
trick_taint($alias);
return $dbh->selectrow_array(
- "SELECT bug_id FROM bugs WHERE alias = ?", undef, $alias);
+ "SELECT bug_id FROM bugs_aliases WHERE alias = ?", undef, $alias);
}
#####################################################################
@@ -3770,9 +4015,7 @@ sub editable_bug_fields {
# Ensure field exists before attempting to remove it.
splice(@fields, $location, 1) if ($location > -1);
}
- # Sorted because the old @::log_columns variable, which this replaces,
- # was sorted.
- return sort(@fields);
+ return @fields;
}
# XXX - When Bug::update() will be implemented, we should make this routine
@@ -3803,16 +4046,48 @@ sub EmitDependList {
# Creates a lot of bug objects in the same order as the input array.
sub _bugs_in_order {
my ($self, $bug_ids) = @_;
- my $bugs = $self->new_from_list($bug_ids);
- my %bug_map = map { $_->id => $_ } @$bugs;
- my @result = map { $bug_map{$_} } @$bug_ids;
- return \@result;
+ return [] unless @$bug_ids;
+
+ my %bug_map;
+ my $dbh = Bugzilla->dbh;
+
+ # there's no need to load bugs from the database if they are already in the
+ # object-cache
+ my @missing_ids;
+ foreach my $bug_id (@$bug_ids) {
+ if (my $bug = Bugzilla::Bug->object_cache_get($bug_id)) {
+ $bug_map{$bug_id} = $bug;
+ }
+ else {
+ push @missing_ids, $bug_id;
+ }
+ }
+ if (@missing_ids) {
+ my $bugs = Bugzilla::Bug->new_from_list(\@missing_ids);
+ $bug_map{$_->id} = $_ foreach @$bugs;
+ }
+
+ # Dependencies are often displayed using their aliases instead of their
+ # bug ID. Load them all at once.
+ my $rows = $dbh->selectall_arrayref(
+ 'SELECT bug_id, alias FROM bugs_aliases WHERE ' .
+ $dbh->sql_in('bug_id', $bug_ids) . ' ORDER BY alias');
+
+ foreach my $row (@$rows) {
+ my ($bug_id, $alias) = @$row;
+ $bug_map{$bug_id}->{alias} ||= [];
+ push @{ $bug_map{$bug_id}->{alias} }, $alias;
+ }
+ # Make sure all bugs have their alias attribute set.
+ $bug_map{$_}->{alias} ||= [] foreach @$bug_ids;
+
+ return [ map { $bug_map{$_} } @$bug_ids ];
}
# Get the activity of a bug, starting from $starttime (if given).
# This routine assumes Bugzilla::Bug->check has been previously called.
sub get_activity {
- my ($self, $attach_id, $starttime) = @_;
+ my ($self, $attach_id, $starttime, $include_comment_tags) = @_;
my $dbh = Bugzilla->dbh;
my $user = Bugzilla->user;
@@ -3824,7 +4099,7 @@ sub get_activity {
if (defined $starttime) {
trick_taint($starttime);
push (@args, $starttime);
- $datepart = "AND bugs_activity.bug_when > ?";
+ $datepart = "AND bug_when > ?";
}
my $attachpart = "";
@@ -3844,7 +4119,7 @@ sub get_activity {
my $query = "SELECT fielddefs.name, bugs_activity.attach_id, " .
$dbh->sql_date_format('bugs_activity.bug_when', '%Y.%m.%d %H:%i:%s') .
- ", bugs_activity.removed, bugs_activity.added, profiles.login_name,
+ " AS bug_when, bugs_activity.removed, bugs_activity.added, profiles.login_name,
bugs_activity.comment_id
FROM bugs_activity
$suppjoins
@@ -3855,8 +4130,42 @@ sub get_activity {
WHERE bugs_activity.bug_id = ?
$datepart
$attachpart
- $suppwhere
- ORDER BY bugs_activity.bug_when, bugs_activity.id";
+ $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
+ ON longdescs.comment_id = longdescs_tags_activity.comment_id";
+ $suppwhere = "AND longdescs.isprivate = 0";
+ }
+
+ $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,
+ longdescs_tags_activity.removed,
+ longdescs_tags_activity.added,
+ profiles.login_name,
+ longdescs_tags_activity.comment_id as comment_id
+ FROM longdescs_tags_activity
+ INNER JOIN profiles ON profiles.userid = longdescs_tags_activity.who
+ $suppjoins
+ WHERE longdescs_tags_activity.bug_id = ?
+ $datepart
+ $suppwhere
+ ";
+ push @args, $self->id;
+ push @args, $starttime if defined $starttime;
+ }
+
+ $query .= "ORDER BY bug_when, comment_id";
my $list = $dbh->selectall_arrayref($query, undef, @args);
@@ -3943,9 +4252,13 @@ sub get_activity {
# Update the bugs_activity table to reflect changes made in bugs.
sub LogActivityEntry {
- my ($i, $col, $removed, $added, $whoid, $timestamp, $comment_id,
+ my ($bug_id, $field, $removed, $added, $user_id, $timestamp, $comment_id,
$attach_id) = @_;
- my $dbh = Bugzilla->dbh;
+ my $sth = Bugzilla->dbh->prepare_cached(
+ 'INSERT INTO bugs_activity
+ (bug_id, who, bug_when, fieldid, removed, added, comment_id, attach_id)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)');
+
# in the case of CCs, deps, and keywords, there's a possibility that someone
# might try to add or remove a lot of them at once, which might take more
# space than the activity table allows. We'll solve this by splitting it
@@ -3968,14 +4281,26 @@ sub LogActivityEntry {
}
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));
+ my $fieldid = get_field_id($field);
+ $sth->execute($bug_id, $user_id, $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 });
}
}
@@ -4059,7 +4384,7 @@ sub check_can_change_field {
return 1;
}
- # If the user isn't allowed to change a field, we must tell him who can.
+ # If the user isn't allowed to change a field, we must tell them who can.
# We store the required permission set into the $PrivilegesRequired
# variable which gets passed to the error template.
#
@@ -4067,9 +4392,10 @@ sub check_can_change_field {
# $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) ) {
+
+ # Only users in the time-tracking group can change time-tracking fields,
+ # including the deadline.
+ if (grep { $_ eq $field } (TIMETRACKING_FIELDS, 'deadline')) {
if (!$user->is_timetracker) {
$$PrivilegesRequired = PRIVILEGES_REQUIRED_EMPOWERED;
return 0;
@@ -4110,7 +4436,7 @@ sub check_can_change_field {
# is not allowed to change.
# The reporter may not:
- # - reassign bugs, unless the bugs are assigned to him;
+ # - reassign bugs, unless the bugs are assigned to them;
# in that case we will have already returned 1 above
# when checking for the assignee of the bug.
if ($field eq 'assigned_to') {
@@ -4127,7 +4453,7 @@ sub check_can_change_field {
$$PrivilegesRequired = PRIVILEGES_REQUIRED_ASSIGNEE;
return 0;
}
- # - change the priority (unless he could have set it originally)
+ # - change the priority (unless they could have set it originally)
if ($field eq 'priority'
&& !Bugzilla->params->{'letsubmitterchoosepriority'})
{
@@ -4197,6 +4523,10 @@ sub ValidateDependencies {
my $dbh = Bugzilla->dbh;
my %deps;
my %deptree;
+ my %sth;
+ $sth{dependson} = $dbh->prepare('SELECT dependson FROM dependencies WHERE blocked = ?');
+ $sth{blocked} = $dbh->prepare('SELECT blocked FROM dependencies WHERE dependson = ?');
+
foreach my $pair (["blocked", "dependson"], ["dependson", "blocked"]) {
my ($me, $target) = @{$pair};
$deptree{$target} = [];
@@ -4221,10 +4551,7 @@ sub ValidateDependencies {
my @stack = @{$deps{$target}};
while (@stack) {
my $i = shift @stack;
- my $dep_list =
- $dbh->selectcol_arrayref("SELECT $target
- FROM dependencies
- WHERE $me = ?", undef, $i);
+ my $dep_list = $dbh->selectcol_arrayref($sth{$target}, undef, $i);
foreach my $t (@$dep_list) {
# ignore any _current_ dependencies involving this bug,
# as they will be overwritten with data from the form.
@@ -4303,3 +4630,295 @@ sub _multi_select_accessor {
}
1;
+
+__END__
+=head1 B<Methods>
+
+=over
+
+=item C<initialize>
+
+Ensures the accessors for custom fields are always created.
+
+=item C<add_alias($alias)>
+
+Adds an alias to the internal respresentation of the bug. You will need to
+call L<update> to make the changes permanent.
+
+=item C<remove_alias($alias)>
+
+Removes an alias from the internal respresentation of the bug. You will need to
+call L<update> to make the changes permanent.
+
+=item C<update_user_last_visit($user, $last_visit)>
+
+Creates or updates a L<Bugzilla::BugUserLastVisit> for this bug and the supplied
+$user, the timestamp given as $last_visit.
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item remove_cc
+
+=item add_see_also
+
+=item choices
+
+=item keywords
+
+=item blocked
+
+=item qa_contact
+
+=item add_comment
+
+=item bug_severity
+
+=item dup_id
+
+=item set_priority
+
+=item any_flags_requesteeble
+
+=item set_bug_status
+
+=item estimated_time
+
+=item set_platform
+
+=item statuses_available
+
+=item set_custom_field
+
+=item remove_see_also
+
+=item remove_from_db
+
+=item product_obj
+
+=item reporter_accessible
+
+=item set_summary
+
+=item LogActivityEntry
+
+=item set_assigned_to
+
+=item add_group
+
+=item bug_file_loc
+
+=item DATE_COLUMNS
+
+=item set_component
+
+=item delta_ts
+
+=item set_resolution
+
+=item version
+
+=item deadline
+
+=item fields
+
+=item dependson
+
+=item check_can_change_field
+
+=item update
+
+=item set_op_sys
+
+=item object_cache_key
+
+=item bug_group
+
+=item comments
+
+=item map_fields
+
+=item assigned_to
+
+=item user
+
+=item ValidateDependencies
+
+=item short_desc
+
+=item duplicate_ids
+
+=item isopened
+
+=item remaining_time
+
+=item set_deadline
+
+=item preload
+
+=item groups_in
+
+=item clear_resolution
+
+=item set_estimated_time
+
+=item in_group
+
+=item status
+
+=item get_activity
+
+=item reporter
+
+=item rep_platform
+
+=item DB_COLUMNS
+
+=item flag_types
+
+=item bug_status
+
+=item attachments
+
+=item flags
+
+=item set_flags
+
+=item actual_time
+
+=item component
+
+=item UPDATE_COLUMNS
+
+=item set_cclist_accessible
+
+=item set_bug_ignored
+
+=item product
+
+=item VALIDATORS
+
+=item show_attachment_flags
+
+=item set_comment_is_private
+
+=item set_severity
+
+=item send_changes
+
+=item add_tag
+
+=item bug_id
+
+=item reset_qa_contact
+
+=item remove_group
+
+=item set_dup_id
+
+=item set_target_milestone
+
+=item cc_users
+
+=item everconfirmed
+
+=item check_is_visible
+
+=item check_for_edit
+
+=item match
+
+=item VALIDATOR_DEPENDENCIES
+
+=item possible_duplicates
+
+=item set_url
+
+=item add_cc
+
+=item blocks_obj
+
+=item set_status_whiteboard
+
+=item product_id
+
+=item error
+
+=item reset_assigned_to
+
+=item status_whiteboard
+
+=item create
+
+=item set_all
+
+=item set_reporter_accessible
+
+=item classification_id
+
+=item tags
+
+=item modify_keywords
+
+=item priority
+
+=item keyword_objects
+
+=item set_dependencies
+
+=item depends_on_obj
+
+=item cclist_accessible
+
+=item cc
+
+=item duplicates
+
+=item component_obj
+
+=item see_also
+
+=item groups
+
+=item default_bug_status
+
+=item related_bugs
+
+=item editable_bug_fields
+
+=item resolution
+
+=item lastdiffed
+
+=item classification
+
+=item alias
+
+=item op_sys
+
+=item remove_tag
+
+=item percentage_complete
+
+=item EmitDependList
+
+=item bug_alias_to_id
+
+=item set_qa_contact
+
+=item creation_ts
+
+=item set_version
+
+=item component_id
+
+=item new_bug_statuses
+
+=item set_remaining_time
+
+=item target_milestone
+
+=back
diff --git a/Bugzilla/BugMail.pm b/Bugzilla/BugMail.pm
index 5a2c9b788..9ebf3ea7d 100644
--- a/Bugzilla/BugMail.pm
+++ b/Bugzilla/BugMail.pm
@@ -5,10 +5,12 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::BugMail;
+use 5.10.1;
+use strict;
+use warnings;
+
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Constants;
@@ -22,6 +24,7 @@ use Date::Parse;
use Date::Format;
use Scalar::Util qw(blessed);
use List::MoreUtils qw(uniq);
+use Storable qw(dclone);
use constant BIT_DIRECT => 1;
use constant BIT_WATCHING => 2;
@@ -35,9 +38,6 @@ sub relationships {
return %relationships;
}
-# This is a bit of a hack, basically keeping the old system()
-# cmd line interface. Should clean this up at some point.
-#
# args: bug_id, and an optional hash ref which may have keys for:
# changer, owner, qa, reporter, cc
# Optional hash contains values of people which will be forced to those
@@ -85,6 +85,8 @@ sub Send {
@diffs = _get_new_bugmail_fields($bug);
}
+ my $comments = [];
+
if ($params->{dep_only}) {
push(@diffs, { field_name => 'bug_status',
old => $params->{changes}->{bug_status}->[0],
@@ -101,11 +103,14 @@ sub Send {
}
else {
push(@diffs, _get_diffs($bug, $end, \%user_cache));
- }
- my $comments = $bug->comments({ after => $start, to => $end });
- # Skip empty comments.
- @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
+ $comments = $bug->comments({ after => $start, to => $end });
+ # Skip empty comments.
+ @$comments = grep { $_->type || $_->body =~ /\S/ } @$comments;
+
+ # If no changes have been made, there is no need to process further.
+ return {'sent' => []} unless scalar(@diffs) || scalar(@$comments);
+ }
###########################################################################
# Start of email filtering code
@@ -114,7 +119,10 @@ sub Send {
# A user_id => roles hash to keep track of people.
my %recipients;
my %watching;
-
+
+ # We also record bugs that are referenced
+ my @referenced_bug_ids = ();
+
# Now we work out all the people involved with this bug, and note all of
# the relationships in a hash. The keys are userids, the values are an
# array of role constants.
@@ -158,8 +166,17 @@ sub Send {
$recipients{$uid}->{+REL_ASSIGNEE} = BIT_DIRECT if $uid;
}
}
+
+ if ($change->{field_name} eq 'dependson' || $change->{field_name} eq 'blocked') {
+ push @referenced_bug_ids, split(/[\s,]+/, $change->{old});
+ push @referenced_bug_ids, split(/[\s,]+/, $change->{new});
+ }
}
+ my $referenced_bugs = scalar(@referenced_bug_ids)
+ ? Bugzilla::Bug->new_from_list([uniq @referenced_bug_ids])
+ : [];
+
# Make sure %user_cache has every user in it so far referenced
foreach my $user_id (keys %recipients) {
$user_cache{$user_id} ||= new Bugzilla::User($user_id);
@@ -169,21 +186,24 @@ sub Send {
{ bug => $bug, recipients => \%recipients,
users => \%user_cache, diffs => \@diffs });
- # Find all those user-watching anyone on the current list, who is not
- # on it already themselves.
- my $involved = join(",", keys %recipients);
-
- 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;
+ # We should not assume %recipients to have any entries.
+ if (scalar keys %recipients) {
+ # Find all those user-watching anyone on the current list, who is not
+ # on it already themselves.
+ my $involved = join(",", keys %recipients);
+
+ my $userwatchers =
+ $dbh->selectall_arrayref("SELECT watcher, watched FROM watch
+ WHERE watched IN ($involved)");
+
+ # Mark these people as having the role of the person they are watching
+ foreach my $watch (@$userwatchers) {
+ while (my ($role, $bits) = each %{$recipients{$watch->[1]}}) {
+ $recipients{$watch->[0]}->{$role} |= BIT_WATCHING
+ if $bits & BIT_DIRECT;
+ }
+ push(@{$watching{$watch->[0]}}, $watch->[1]);
}
- push(@{$watching{$watch->[0]}}, $watch->[1]);
}
# Global watcher
@@ -198,7 +218,6 @@ sub Send {
# 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;
- my @excluded;
# The email client will display the Date: header in the desired timezone,
# so we can always use UTC here.
@@ -207,12 +226,14 @@ sub Send {
foreach my $user_id (keys %recipients) {
my %rels_which_want;
- my $sent_mail = 0;
- $user_cache{$user_id} ||= new Bugzilla::User($user_id);
- my $user = $user_cache{$user_id};
+ my $user = $user_cache{$user_id} ||= new Bugzilla::User($user_id);
# Deleted users must be excluded.
next unless $user;
+ # If email notifications are disabled for this account, or the bug
+ # is ignored, there is no need to do additional checks.
+ next if ($user->email_disabled || $user->is_bug_ignored($id));
+
if ($user->can_see_bug($id)) {
# Go through each role the user has and see if they want mail in
# that role.
@@ -229,7 +250,7 @@ sub Send {
}
}
}
-
+
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?
@@ -242,29 +263,24 @@ sub Send {
$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.
- if ($user->email_enabled && $dep_ok) {
- # OK, OK, if we must. Email the user.
- $sent_mail = sendMail(
- { to => $user,
- bug => $bug,
- comments => $comments,
- date => $date,
- changer => $changer,
- watchers => exists $watching{$user_id} ?
- $watching{$user_id} : undef,
- diffs => \@diffs,
+ # Email the user if the dep check passed.
+ if ($dep_ok) {
+ my $sent_mail = sendMail(
+ { to => $user,
+ bug => $bug,
+ comments => $comments,
+ date => $date,
+ changer => $changer,
+ watchers => exists $watching{$user_id} ?
+ $watching{$user_id} : undef,
+ diffs => \@diffs,
rels_which_want => \%rels_which_want,
+ dep_only => $params->{dep_only},
+ referenced_bugs => $referenced_bugs,
});
+ push(@sent, $user->login) if $sent_mail;
}
}
-
- if ($sent_mail) {
- push(@sent, $user->login);
- }
- else {
- push(@excluded, $user->login);
- }
}
# When sending bugmail about a blocker being reopened or resolved,
@@ -276,27 +292,29 @@ sub Send {
$bug->{lastdiffed} = $end;
}
- return {'sent' => \@sent, 'excluded' => \@excluded};
+ 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 $user = $params->{to};
+ my $bug = $params->{bug};
+ my @send_comments = @{ $params->{comments} };
+ my $date = $params->{date};
+ my $changer = $params->{changer};
+ my $watchingRef = $params->{watchers};
+ my @diffs = @{ $params->{diffs} };
+ my $relRef = $params->{rels_which_want};
+ my $dep_only = $params->{dep_only};
+ my $referenced_bugs = $params->{referenced_bugs};
# Only display changes the user is allowed see.
my @display_diffs;
foreach my $diff (@diffs) {
my $add_diff = 0;
-
+
if (grep { $_ eq $diff->{field_name} } TIMETRACKING_FIELDS) {
$add_diff = 1 if $user->is_timetracker;
}
@@ -326,41 +344,98 @@ sub sendMail {
my @watchingrel = map { $relationships{$_} } @reasons_watch;
push(@headerrel, 'None') unless @headerrel;
push(@watchingrel, 'None') unless @watchingrel;
- push @watchingrel, map { user_id_to_login($_) } @$watchingRef;
+ push @watchingrel, map { Bugzilla::User->new($_)->login } @$watchingRef;
my @changedfields = uniq map { $_->{field_name} } @display_diffs;
-
+
# Add attachments.created to changedfields if one or more
# comments contain information about a new attachment
if (grep($_->type == CMT_ATTACHMENT_CREATED, @send_comments)) {
push(@changedfields, 'attachments.created');
}
+ my $bugmailtype = "changed";
+ $bugmailtype = "new" if !$bug->lastdiffed;
+ $bugmailtype = "dep_changed" if $dep_only;
+
my $vars = {
- date => $date,
- to_user => $user,
- bug => $bug,
- reasons => \@reasons,
- reasons_watch => \@reasons_watch,
- reasonsheader => join(" ", @headerrel),
+ date => $date,
+ to_user => $user,
+ bug => $bug,
+ reasons => \@reasons,
+ reasons_watch => \@reasons_watch,
+ reasonsheader => join(" ", @headerrel),
reasonswatchheader => join(" ", @watchingrel),
- changer => $changer,
- diffs => \@display_diffs,
- changedfields => \@changedfields,
- new_comments => \@send_comments,
- threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed),
+ changer => $changer,
+ diffs => \@display_diffs,
+ changedfields => \@changedfields,
+ referenced_bugs => $user->visible_bugs($referenced_bugs),
+ new_comments => \@send_comments,
+ threadingmarker => build_thread_marker($bug->id, $user->id, !$bug->lastdiffed),
+ bugmailtype => $bugmailtype,
};
- my $msg = _generate_bugmail($user, $vars);
- MessageToMTA($msg);
+ if (Bugzilla->params->{'use_mailer_queue'}) {
+ enqueue($vars);
+ } else {
+ MessageToMTA(_generate_bugmail($vars));
+ }
return 1;
}
+sub enqueue {
+ my ($vars) = @_;
+ # we need to flatten all objects to a hash before pushing to the job queue.
+ # the hashes need to be inflated in the dequeue method.
+ $vars->{bug} = _flatten_object($vars->{bug});
+ $vars->{to_user} = _flatten_object($vars->{to_user});
+ $vars->{changer} = _flatten_object($vars->{changer});
+ $vars->{new_comments} = [ map { _flatten_object($_) } @{ $vars->{new_comments} } ];
+ foreach my $diff (@{ $vars->{diffs} }) {
+ $diff->{who} = _flatten_object($diff->{who});
+ if (exists $diff->{blocker}) {
+ $diff->{blocker} = _flatten_object($diff->{blocker});
+ }
+ }
+ 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});
+ }
+ }
+ # 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;
+}
+
sub _generate_bugmail {
- my ($user, $vars) = @_;
+ 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)
@@ -446,8 +521,8 @@ sub _get_diffs {
&& $diff->{field_name} eq $changes[-1]->{field_name}
&& $diff->{bug_when} eq $changes[-1]->{bug_when}
&& $diff->{who} eq $changes[-1]->{who}
- && ($diff->{attach_id} || 0) == ($changes[-1]->{attach_id} || 0)
- && ($diff->{comment_id} || 0) == ($changes[-1]->{comment_id} || 0)
+ && ($diff->{attach_id} // 0) == ($changes[-1]->{attach_id} // 0)
+ && ($diff->{comment_id} // 0) == ($changes[-1]->{comment_id} // 0)
) {
my $old_change = pop @changes;
$diff->{old} = join_activity_entries($diff->{field_name}, $old_change->{old}, $diff->{old});
@@ -506,3 +581,36 @@ sub _get_new_bugmail_fields {
}
1;
+
+=head1 NAME
+
+BugMail - Routines to generate email notifications when a bug is created or
+modified.
+
+=head1 METHODS
+
+=over 4
+
+=item C<enqueue>
+
+Serialises the variables required to generate bugmail and pushes the result to
+the job-queue for processing by TheSchwartz.
+
+=item C<dequeue>
+
+When given serialised variables from the job-queue, recreates the objects from
+the flattened hashes, generates the bugmail, and sends it.
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item relationships
+
+=item sendMail
+
+=item Send
+
+=back
diff --git a/Bugzilla/BugUrl.pm b/Bugzilla/BugUrl.pm
index 8689e8a35..1e836ca1e 100644
--- a/Bugzilla/BugUrl.pm
+++ b/Bugzilla/BugUrl.pm
@@ -6,14 +6,19 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Object);
+use warnings;
+
+use parent qw(Bugzilla::Object);
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Constants;
use Bugzilla::Hook;
+use URI;
use URI::QueryParam;
###############################
@@ -198,3 +203,17 @@ sub _check_value {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item should_handle
+
+=item class_for
+
+=item class
+
+=item bug_id
+
+=back
diff --git a/Bugzilla/BugUrl/Bugzilla.pm b/Bugzilla/BugUrl/Bugzilla.pm
index 4db37eb7f..402ff1509 100644
--- a/Bugzilla/BugUrl/Bugzilla.pm
+++ b/Bugzilla/BugUrl/Bugzilla.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::Bugzilla;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
use Bugzilla::Error;
use Bugzilla::Util;
diff --git a/Bugzilla/BugUrl/Bugzilla/Local.pm b/Bugzilla/BugUrl/Bugzilla/Local.pm
index 9631716ae..7b9cb6a4f 100644
--- a/Bugzilla/BugUrl/Bugzilla/Local.pm
+++ b/Bugzilla/BugUrl/Bugzilla/Local.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::Bugzilla::Local;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl::Bugzilla);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl::Bugzilla);
use Bugzilla::Error;
use Bugzilla::Util;
diff --git a/Bugzilla/BugUrl/Debian.pm b/Bugzilla/BugUrl/Debian.pm
index cce4c25fd..2b611aa57 100644
--- a/Bugzilla/BugUrl/Debian.pm
+++ b/Bugzilla/BugUrl/Debian.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::Debian;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
###############################
#### Methods ####
diff --git a/Bugzilla/BugUrl/GitHub.pm b/Bugzilla/BugUrl/GitHub.pm
index eeffa2ed6..f14f1d6b0 100644
--- a/Bugzilla/BugUrl/GitHub.pm
+++ b/Bugzilla/BugUrl/GitHub.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::GitHub;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
###############################
#### Methods ####
@@ -18,8 +22,10 @@ sub should_handle {
# 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/\d+$|) ? 1 : 0;
+ and $uri->path =~ m!^/[^/]+/[^/]+/(?:issues|pull)/\d+$!) ? 1 : 0;
}
sub _check_value {
diff --git a/Bugzilla/BugUrl/Google.pm b/Bugzilla/BugUrl/Google.pm
index 9c49f0dcc..71a9c46fb 100644
--- a/Bugzilla/BugUrl/Google.pm
+++ b/Bugzilla/BugUrl/Google.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::Google;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
###############################
#### Methods ####
diff --git a/Bugzilla/BugUrl/JIRA.pm b/Bugzilla/BugUrl/JIRA.pm
index f5f7ee5fa..e9d2a2d2a 100644
--- a/Bugzilla/BugUrl/JIRA.pm
+++ b/Bugzilla/BugUrl/JIRA.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::JIRA;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
###############################
#### Methods ####
diff --git a/Bugzilla/BugUrl/Launchpad.pm b/Bugzilla/BugUrl/Launchpad.pm
index 2ae2c383d..0362747a2 100644
--- a/Bugzilla/BugUrl/Launchpad.pm
+++ b/Bugzilla/BugUrl/Launchpad.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::Launchpad;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
###############################
#### Methods ####
diff --git a/Bugzilla/BugUrl/MantisBT.pm b/Bugzilla/BugUrl/MantisBT.pm
index 3d49ede69..60d3b578e 100644
--- a/Bugzilla/BugUrl/MantisBT.pm
+++ b/Bugzilla/BugUrl/MantisBT.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::MantisBT;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
###############################
#### Methods ####
diff --git a/Bugzilla/BugUrl/SourceForge.pm b/Bugzilla/BugUrl/SourceForge.pm
index fcc720049..acba0df28 100644
--- a/Bugzilla/BugUrl/SourceForge.pm
+++ b/Bugzilla/BugUrl/SourceForge.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::SourceForge;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
###############################
#### Methods ####
diff --git a/Bugzilla/BugUrl/Trac.pm b/Bugzilla/BugUrl/Trac.pm
index 8f6e9cd0e..fe74abf33 100644
--- a/Bugzilla/BugUrl/Trac.pm
+++ b/Bugzilla/BugUrl/Trac.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::BugUrl::Trac;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::BugUrl);
+use warnings;
+
+use parent qw(Bugzilla::BugUrl);
###############################
#### Methods ####
diff --git a/Bugzilla/BugUserLastVisit.pm b/Bugzilla/BugUserLastVisit.pm
new file mode 100644
index 000000000..d043b121a
--- /dev/null
+++ b/Bugzilla/BugUserLastVisit.pm
@@ -0,0 +1,93 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::BugUserLastVisit;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Object);
+
+#####################################################################
+# Overriden Constants that are used as methods
+#####################################################################
+
+use constant DB_TABLE => 'bug_user_last_visit';
+use constant DB_COLUMNS => qw( id user_id bug_id last_visit_ts );
+use constant UPDATE_COLUMNS => qw( last_visit_ts );
+use constant VALIDATORS => {};
+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 };
+
+#####################################################################
+# 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 last_visit_ts { return $_[0]->{last_visit_ts} }
+
+sub user {
+ my $self = shift;
+
+ $self->{user} //= Bugzilla::User->new({ id => $self->user_id, cache => 1 });
+ return $self->{user};
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Bugzilla::BugUserLastVisit - Model for BugUserLastVisit bug search data
+
+=head1 SYNOPSIS
+
+ use Bugzilla::BugUserLastVisit;
+
+ my $lv = Bugzilla::BugUserLastVisit->new($id);
+
+ # Class Functions
+ $user = Bugzilla::BugUserLastVisit->create({
+ bug_id => $bug_id,
+ user_id => $user_id,
+ last_visit_ts => $last_visit_ts
+ });
+
+=head1 DESCRIPTION
+
+This package handles Bugzilla BugUserLastVisit.
+
+C<Bugzilla::BugUserLastVisit> is an implementation of L<Bugzilla::Object>, and
+thus provides all the methods of L<Bugzilla::Object> in addition to the methods
+listed below.
+
+=head1 METHODS
+
+=head2 Accessor Methods
+
+=over
+
+=item C<id>
+
+=item C<bug_id>
+
+=item C<user_id>
+
+=item C<last_visit_ts>
+
+=item C<user>
+
+=back
diff --git a/Bugzilla/CGI.pm b/Bugzilla/CGI.pm
index 7df916b0c..0b8a48697 100644
--- a/Bugzilla/CGI.pm
+++ b/Bugzilla/CGI.pm
@@ -6,12 +6,17 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::CGI;
+
+use 5.10.1;
use strict;
-use base qw(CGI);
+use warnings;
+
+use parent qw(CGI);
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::Util;
+use Bugzilla::Hook;
use Bugzilla::Search::Recent;
use File::Basename;
@@ -53,7 +58,7 @@ sub new {
# the rendering of pages.
my $script = basename($0);
if (my $path_info = $self->path_info) {
- my @whitelist;
+ my @whitelist = ("rest.cgi");
Bugzilla::Hook::process('path_info_whitelist', { whitelist => \@whitelist });
if (!grep($_ eq $script, @whitelist)) {
# IIS includes the full path to the script in PATH_INFO,
@@ -120,7 +125,8 @@ sub canonicalise_query {
my $esc_key = url_quote($key);
foreach my $value ($self->param($key)) {
- if (defined($value)) {
+ # Omit params with an empty value
+ if (defined($value) && $value ne '') {
my $esc_value = url_quote($value);
push(@parameters, "$esc_key=$esc_value");
@@ -233,11 +239,11 @@ sub check_etag {
$possible_etag =~ s/^\"//g;
$possible_etag =~ s/\"$//g;
if ($possible_etag eq $valid_etag or $possible_etag eq '*') {
- print $self->header(-ETag => $possible_etag,
- -status => '304 Not Modified');
- exit;
+ return 1;
}
}
+
+ return 0;
}
# Have to add the cookies in.
@@ -270,28 +276,35 @@ sub multipart_start {
}
sub close_standby_message {
- my ($self, $contenttype, $disposition) = @_;
+ 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,
- -content_disposition => $disposition);
+ print $self->multipart_start(-type => $contenttype);
}
else {
- print $self->header(-type => $contenttype,
- -content_disposition => $disposition);
+ print $self->header($contenttype);
}
}
# 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) {
- # Since we're adding parameters below, we have to name it.
- unshift(@_, '-type' => shift(@_));
+ %headers = ('-type' => shift(@_));
+ }
+ else {
+ %headers = @_;
+ }
+
+ if ($self->{'_content_disp'}) {
+ $headers{'-content_disposition'} = $self->{'_content_disp'};
}
if (!$user->id && $user->authorizer->can_login
@@ -308,7 +321,7 @@ sub header {
# Add the cookies in if we have any
if (scalar(@{$self->{Bugzilla_cookie_list}})) {
- unshift(@_, '-cookie' => $self->{Bugzilla_cookie_list});
+ $headers{'-cookie'} = $self->{Bugzilla_cookie_list};
}
# Add Strict-Transport-Security (STS) header if this response
@@ -322,24 +335,29 @@ sub header {
{
$sts_opts .= '; includeSubDomains';
}
- unshift(@_, '-strict_transport_security' => $sts_opts);
+
+ $headers{'-strict_transport_security'} = $sts_opts;
}
# Add X-Frame-Options header to prevent framing and subsequent
# possible clickjacking problems.
unless ($self->url_is_attachment_base) {
- unshift(@_, '-x_frame_options' => 'SAMEORIGIN');
+ $headers{'-x_frame_options'} = 'SAMEORIGIN';
}
# Add X-XSS-Protection header to prevent simple XSS attacks
# and enforce the blocking (rather than the rewriting) mode.
- unshift(@_, '-x_xss_protection' => '1; mode=block');
+ $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.
- unshift(@_, '-x_content_type_options' => 'nosniff');
+ $headers{'-x_content_type_options'} = 'nosniff';
- return $self->SUPER::header(@_) || "";
+ Bugzilla::Hook::process('cgi_headers',
+ { cgi => $self, headers => \%headers }
+ );
+
+ return $self->SUPER::header(%headers) || "";
}
sub param {
@@ -356,10 +374,7 @@ sub param {
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(@_);
+ @result = $self->url_param(@_);
}
# Fix UTF-8-ness of input parameters.
@@ -384,6 +399,14 @@ sub param {
return $self->SUPER::param(@_);
}
+sub url_param {
+ my $self = shift;
+ # Some servers fail to set the QUERY_STRING parameter, which
+ # causes undef issues
+ $ENV{'QUERY_STRING'} //= '';
+ return $self->SUPER::url_param(@_);
+}
+
sub _fix_utf8 {
my $input = shift;
# The is_utf8 is here in case CGI gets smart about utf8 someday.
@@ -554,6 +577,22 @@ sub url_is_attachment_base {
return ($self->url =~ $regex) ? 1 : 0;
}
+sub set_dated_content_disp {
+ 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";
+
+ $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\"";
+
+ $self->{'_content_disp'} = $disposition;
+}
+
##########################
# Vars TIEHASH Interface #
##########################
@@ -628,7 +667,9 @@ I<Bugzilla::CGI> also includes additional functions.
=item C<canonicalise_query(@exclude)>
-This returns a sorted string of the parameters, suitable for use in a url.
+This returns a sorted string of the parameters whose values are non-empty,
+suitable for use in a url.
+
Values in C<@exclude> are not included in the result.
=item C<send_cookie>
@@ -670,8 +711,35 @@ If not specified, text/html is assumed.
Ends a part of the multipart document, and starts another part.
+=item C<set_dated_content_disp>
+
+Sets an appropriate date-dependent value for the Content Disposition header
+for a downloadable resource.
+
=back
=head1 SEE ALSO
L<CGI|CGI>, L<CGI::Cookie|CGI::Cookie>
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item check_etag
+
+=item clean_search_url
+
+=item url_is_attachment_base
+
+=item should_set
+
+=item redirect_search_url
+
+=item param
+
+=item url_param
+
+=item header
+
+=back
diff --git a/Bugzilla/Chart.pm b/Bugzilla/Chart.pm
index e343a0535..3c69006aa 100644
--- a/Bugzilla/Chart.pm
+++ b/Bugzilla/Chart.pm
@@ -5,8 +5,6 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
# This module represents a chart.
#
# Note that it is perfectly legal for the 'lines' member variable of this
@@ -15,6 +13,10 @@ use strict;
# the same points.
package Bugzilla::Chart;
+use 5.10.1;
+use strict;
+use warnings;
+
use Bugzilla::Error;
use Bugzilla::Util;
use Bugzilla::Series;
@@ -426,3 +428,29 @@ sub dump {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item remove
+
+=item add
+
+=item dump
+
+=item readData
+
+=item getSeriesIDs
+
+=item data
+
+=item init
+
+=item getVisibleSeries
+
+=item generateDateProgression
+
+=item sum
+
+=back
diff --git a/Bugzilla/Classification.pm b/Bugzilla/Classification.pm
index 2b35a8839..09f71baaf 100644
--- a/Bugzilla/Classification.pm
+++ b/Bugzilla/Classification.pm
@@ -5,23 +5,27 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Classification;
+use 5.10.1;
+use strict;
+use warnings;
+
use Bugzilla::Constants;
use Bugzilla::Field;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::Product;
-use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter);
+use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object Exporter);
@Bugzilla::Classification::EXPORT = qw(sort_products_by_classification);
###############################
#### Initialization ####
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_TABLE => 'classifications';
use constant LIST_ORDER => 'sortkey, name';
@@ -47,6 +51,7 @@ use constant VALIDATORS => {
###############################
#### Constructors #####
###############################
+
sub remove_from_db {
my $self = shift;
my $dbh = Bugzilla->dbh;
@@ -54,9 +59,19 @@ sub remove_from_db {
ThrowUserError("classification_not_deletable") if ($self->id == 1);
$dbh->bz_start_transaction();
+
# Reclassify products to the default classification, if needed.
- $dbh->do("UPDATE products SET classification_id = 1
- WHERE classification_id = ?", undef, $self->id);
+ 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();
@@ -259,3 +274,21 @@ A Classification is a higher-level grouping of Products.
=back
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item set_description
+
+=item sortkey
+
+=item set_name
+
+=item description
+
+=item remove_from_db
+
+=item set_sortkey
+
+=back
diff --git a/Bugzilla/Comment.pm b/Bugzilla/Comment.pm
index 30ec1cb82..b036907d7 100644
--- a/Bugzilla/Comment.pm
+++ b/Bugzilla/Comment.pm
@@ -5,18 +5,22 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Comment;
-use base qw(Bugzilla::Object);
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Object);
use Bugzilla::Attachment;
+use Bugzilla::Comment::TagWeights;
use Bugzilla::Constants;
use Bugzilla::Error;
use Bugzilla::User;
use Bugzilla::Util;
+use List::Util qw(first);
use Scalar::Util qw(blessed);
###############################
@@ -78,21 +82,94 @@ use constant VALIDATOR_DEPENDENCIES => {
sub update {
my $self = shift;
- my $changes = $self->SUPER::update(@_);
- $self->bug->_sync_fulltext( update_comments => 1);
+ 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, '');
+ }
+ }
+
return $changes;
}
-# Speeds up displays of comment lists by loading all ->author objects
-# at once for a whole list.
+# Speeds up displays of comment lists by loading all author objects and tags at
+# once for a whole list.
sub preload {
my ($class, $comments) = @_;
+ # Author
my %user_ids = map { $_->{who} => 1 } @$comments;
my $users = Bugzilla::User->new_from_list([keys %user_ids]);
my %user_map = map { $_->id => $_ } @$users;
foreach my $comment (@$comments) {
$comment->{author} = $user_map{$comment->{who}};
}
+ # Tags
+ if (Bugzilla->params->{'comment_taggers_group'}) {
+ my $dbh = Bugzilla->dbh;
+ my @comment_ids = map { $_->id } @$comments;
+ my %comment_map = map { $_->id => $_ } @$comments;
+ my $rows = $dbh->selectall_arrayref(
+ "SELECT comment_id, " . $dbh->sql_group_concat('tag', "','") . "
+ FROM longdescs_tags
+ WHERE " . $dbh->sql_in('comment_id', \@comment_ids) . ' ' .
+ $dbh->sql_group_by('comment_id'));
+ foreach my $row (@$rows) {
+ $comment_map{$row->[0]}->{tags} = [ split(/,/, $row->[1]) ];
+ }
+ # Also sets the 'tags' attribute for comments which have no entry
+ # in the longdescs_tags table, else calling $comment->tags will
+ # trigger another SQL query again.
+ $comment_map{$_}->{tags} ||= [] foreach @comment_ids;
+ }
}
###############################
@@ -112,6 +189,43 @@ sub work_time {
sub type { return $_[0]->{'type'}; }
sub extra_data { return $_[0]->{'extra_data'} }
+sub tags {
+ my ($self) = @_;
+ state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
+ return [] unless $comment_taggers_group;
+ $self->{'tags'} ||= Bugzilla->dbh->selectcol_arrayref(
+ "SELECT tag
+ FROM longdescs_tags
+ WHERE comment_id = ?
+ ORDER BY tag",
+ undef, $self->id);
+ return $self->{'tags'};
+}
+
+sub collapsed {
+ my ($self) = @_;
+ state $comment_taggers_group = Bugzilla->params->{'comment_taggers_group'};
+ return 0 unless $comment_taggers_group;
+ return $self->{collapsed} if exists $self->{collapsed};
+
+ state $collapsed_comment_tags = Bugzilla->params->{'collapsed_comment_tags'};
+ $self->{collapsed} = 0;
+ Bugzilla->request_cache->{comment_tags_collapsed}
+ ||= [ split(/\s*,\s*/, $collapsed_comment_tags) ];
+ my @collapsed_tags = @{ Bugzilla->request_cache->{comment_tags_collapsed} };
+ foreach my $my_tag (@{ $self->tags }) {
+ $my_tag = lc($my_tag);
+ foreach my $collapsed_tag (@collapsed_tags) {
+ if ($my_tag eq lc($collapsed_tag)) {
+ $self->{collapsed} = 1;
+ last;
+ }
+ }
+ last if $self->{collapsed};
+ }
+ return $self->{collapsed};
+}
+
sub bug {
my $self = shift;
require Bugzilla::Bug;
@@ -129,13 +243,15 @@ sub is_about_attachment {
sub attachment {
my ($self) = @_;
return undef if not $self->is_about_attachment;
- $self->{attachment} ||= new Bugzilla::Attachment($self->extra_data);
+ $self->{attachment} ||=
+ new Bugzilla::Attachment({ id => $self->extra_data, cache => 1 });
return $self->{attachment};
}
sub author {
my $self = shift;
- $self->{'author'} ||= new Bugzilla::User($self->{'who'});
+ $self->{'author'}
+ ||= new Bugzilla::User({ id => $self->{'who'}, cache => 1 });
return $self->{'author'};
}
@@ -167,6 +283,26 @@ 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 ];
+}
+
+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);
+}
+
##############
# Validators #
##############
@@ -297,6 +433,18 @@ sub _check_thetext {
$thetext =~ s/\s*$//s;
$thetext =~ s/\r\n?/\n/g; # Get rid of \r.
+ # Characters above U+FFFF cannot be stored by MySQL older than 5.5.3 as they
+ # require the new utf8mb4 character set. Other DB servers are handling them
+ # without any problem. So we need to replace these characters if we use MySQL,
+ # else the comment is truncated.
+ # XXX - Once we use utf8mb4 for comments, this hack for MySQL can go away.
+ state $is_mysql = Bugzilla->dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
+ if ($is_mysql) {
+ # Perl 5.13.8 and older complain about non-characters.
+ no warnings 'utf8';
+ $thetext =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
+ }
+
ThrowUserError('comment_too_long') if length($thetext) > MAX_COMMENT_LENGTH;
return $thetext;
}
@@ -309,6 +457,17 @@ sub _check_isprivate {
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;
+}
+
sub count {
my ($self) = @_;
@@ -323,7 +482,7 @@ sub count {
undef, $self->bug_id, $self->creation_ts);
return --$self->{'count'};
-}
+}
1;
@@ -369,7 +528,7 @@ C<string> Time spent as related to this comment.
=item C<is_private>
-C<boolean> Comment is marked as private
+C<boolean> Comment is marked as private.
=item C<already_wrapped>
@@ -384,6 +543,54 @@ L<Bugzilla::User> who created the comment.
C<int> The position this comment is located in the full list of comments for a bug starting from 0.
+=item C<collapsed>
+
+C<boolean> Comment should be displayed as collapsed by default.
+
+=item C<tags>
+
+C<array of strings> The tags attached to the comment.
+
+=item C<add_tag>
+
+=over
+
+=item B<Description>
+
+Attaches the specified tag to the comment.
+
+=item B<Params>
+
+=over
+
+=item C<tag>
+
+C<string> The tag to attach.
+
+=back
+
+=back
+
+=item C<remove_tag>
+
+=over
+
+=item B<Description>
+
+Detaches the specified tag from the comment.
+
+=item B<Params>
+
+=over
+
+=item C<tag>
+
+C<string> The tag to detach.
+
+=back
+
+=back
+
=item C<body_full>
=over
@@ -417,3 +624,29 @@ A string, the full text of the comment as it would be displayed to an end-user.
=back
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item set_type
+
+=item bug
+
+=item set_extra_data
+
+=item set_is_private
+
+=item attachment
+
+=item is_about_attachment
+
+=item extra_data
+
+=item preload
+
+=item type
+
+=item update
+
+=back
diff --git a/Bugzilla/Comment/TagWeights.pm b/Bugzilla/Comment/TagWeights.pm
new file mode 100644
index 000000000..7dba53e34
--- /dev/null
+++ b/Bugzilla/Comment/TagWeights.pm
@@ -0,0 +1,78 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Comment::TagWeights;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Object);
+
+use Bugzilla::Constants;
+
+# No auditing required
+use constant AUDIT_CREATES => 0;
+use constant AUDIT_UPDATES => 0;
+use constant AUDIT_REMOVES => 0;
+
+use constant DB_COLUMNS => qw(
+ id
+ tag
+ weight
+);
+
+use constant UPDATE_COLUMNS => qw(
+ 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 => { };
+
+# There's no gain to caching these objects
+use constant USE_MEMCACHED => 0;
+
+sub tag { return $_[0]->{'tag'} }
+sub weight { return $_[0]->{'weight'} }
+
+sub set_weight { $_[0]->set('weight', $_[1]); }
+
+1;
+
+=head1 NAME
+
+Comment::TagWeights - Bugzilla comment weighting class.
+
+=head1 DESCRIPTION
+
+TagWeights.pm represents a Comment::TagWeight object. It is an implementation
+of L<Bugzilla::Object>, and thus provides all methods that L<Bugzilla::Object>
+provides.
+
+TagWeights is used to quickly find tags and order by their usage count.
+
+=head1 PROPERTIES
+
+=over
+
+=item C<tag>
+
+C<getter string> The tag
+
+=item C<weight>
+
+C<getter int> The tag's weight. The value returned corresponds to the number of
+comments with this tag attached.
+
+=item C<set_weight>
+
+C<setter int> Set the tag's weight.
+
+=back
diff --git a/Bugzilla/Component.pm b/Bugzilla/Component.pm
index 1ce4e02ea..9bc0a4493 100644
--- a/Bugzilla/Component.pm
+++ b/Bugzilla/Component.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Component;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
+use warnings;
+
+use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Util;
@@ -339,23 +343,16 @@ sub bug_ids {
sub default_assignee {
my $self = shift;
- if (!defined $self->{'default_assignee'}) {
- $self->{'default_assignee'} =
- new Bugzilla::User($self->{'initialowner'});
- }
- return $self->{'default_assignee'};
+ return $self->{'default_assignee'}
+ ||= new Bugzilla::User({ id => $self->{'initialowner'}, cache => 1 });
}
sub default_qa_contact {
my $self = shift;
- return if !$self->{'initialqacontact'};
-
- if (!defined $self->{'default_qa_contact'}) {
- $self->{'default_qa_contact'} =
- new Bugzilla::User($self->{'initialqacontact'});
- }
- return $self->{'default_qa_contact'};
+ return unless $self->{'initialqacontact'};
+ return $self->{'default_qa_contact'}
+ ||= new Bugzilla::User({id => $self->{'initialqacontact'}, cache => 1 });
}
sub flag_types {
@@ -650,3 +647,19 @@ Component.pm represents a Product Component object.
=back
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item is_set_on_bug
+
+=item product_id
+
+=item set_is_active
+
+=item description
+
+=item is_active
+
+=back
diff --git a/Bugzilla/Config.pm b/Bugzilla/Config.pm
index 150996e05..1e22b5239 100644
--- a/Bugzilla/Config.pm
+++ b/Bugzilla/Config.pm
@@ -7,14 +7,21 @@
package Bugzilla::Config;
+use 5.10.1;
use strict;
+use warnings;
+
+use parent qw(Exporter);
+use autodie qw(:default);
-use base qw(Exporter);
use Bugzilla::Constants;
use Bugzilla::Hook;
-use Bugzilla::Install::Filesystem qw(fix_file_permissions);
-use Data::Dumper;
+use Bugzilla::Util qw(trick_taint);
+
+use JSON::XS;
+use File::Slurp;
use File::Temp;
+use File::Basename;
# Don't export localvars by default - people should have to explicitly
# ask for it, as a (probably futile) attempt to stop code using it
@@ -91,8 +98,35 @@ sub SetParam {
sub update_params {
my ($params) = @_;
my $answer = Bugzilla->installation_answers;
+ my $datadir = bz_locations()->{'datadir'};
+ my $param;
+
+ # If the old data/params file using Data::Dumper output still exists,
+ # read it. It will be deleted once the parameters are stored in the new
+ # data/params.json file.
+ my $old_file = "$datadir/params";
+
+ if (-e $old_file) {
+ require Safe;
+ my $s = new Safe;
+
+ $s->rdo($old_file);
+ die "Error reading $old_file: $!" if $!;
+ die "Error evaluating $old_file: $@" if $@;
+
+ # Now read the param back out from the sandbox.
+ $param = \%{ $s->varglob('param') };
+ }
+ else {
+ # Rename params.js to params.json if checksetup.pl
+ # was executed with an earlier version of this change
+ rename "$old_file.js", "$old_file.json"
+ if -e "$old_file.js" && !-e "$old_file.json";
+
+ # Read the new data/params.json file.
+ $param = read_param_file();
+ }
- my $param = read_param_file();
my %new_params;
# If we didn't return any param values, then this is a new installation.
@@ -151,16 +185,19 @@ sub update_params {
}
# 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};
+ my $mta = $param->{'mail_delivery_method'};
+ if ($mta) {
+ if ($mta !~ /[A-Z]/) {
+ my %translation = (
+ 'sendmail' => 'Sendmail',
+ 'smtp' => 'SMTP',
+ 'qmail' => 'Qmail',
+ 'testfile' => 'Test',
+ 'none' => 'None');
+ $param->{'mail_delivery_method'} = $translation{$mta};
+ }
+ # This will force the parameter to be reset to its default value.
+ delete $param->{'mail_delivery_method'} if $param->{'mail_delivery_method'} eq 'Qmail';
}
# Convert the old "ssl" parameter to the new "ssl_redirect" parameter.
@@ -196,6 +233,9 @@ sub update_params {
$param->{'utf8'} = 1 if $new_install;
+ # Bug 452525: OR based groups are on by default for new installations
+ $param->{'or_groups'} = 1 if $new_install;
+
# --- REMOVE OLD PARAMS ---
my %oldparams;
@@ -207,7 +247,6 @@ sub update_params {
}
# Write any old parameters to old-params.txt
- my $datadir = bz_locations()->{'datadir'};
my $old_param_file = "$datadir/old-params.txt";
if (scalar(keys %oldparams)) {
my $op_file = new IO::File($old_param_file, '>>', 0600)
@@ -217,12 +256,9 @@ sub update_params {
" and so have been\nmoved from your parameters file into",
" $old_param_file:\n";
- local $Data::Dumper::Terse = 1;
- local $Data::Dumper::Indent = 0;
-
my $comma = "";
foreach my $item (keys %oldparams) {
- print $op_file "\n\n$item:\n" . Data::Dumper->Dump([$oldparams{$item}]) . "\n";
+ print $op_file "\n\n$item:\n" . $oldparams{$item} . "\n";
print "${comma}$item";
$comma = ", ";
}
@@ -253,6 +289,11 @@ sub update_params {
write_params($param);
+ if (-e $old_file) {
+ unlink $old_file;
+ say "$old_file has been converted into $old_file.json, using the JSON format.";
+ }
+
# Return deleted params and values so that checksetup.pl has a chance
# to convert old params to new data.
return %oldparams;
@@ -261,24 +302,15 @@ sub update_params {
sub write_params {
my ($param_data) = @_;
$param_data ||= Bugzilla->params;
+ my $param_file = bz_locations()->{'datadir'} . '/params.json';
- my $datadir = bz_locations()->{'datadir'};
- my $param_file = "$datadir/params";
-
- local $Data::Dumper::Sortkeys = 1;
-
- my ($fh, $tmpname) = File::Temp::tempfile('params.XXXXX',
- DIR => $datadir );
-
- print $fh (Data::Dumper->Dump([$param_data], ['*param']))
- || die "Can't write param file: $!";
+ my $json_data = JSON::XS->new->canonical->pretty->encode($param_data);
+ write_file($param_file, { binmode => ':utf8', atomic => 1 }, \$json_data);
- close $fh;
-
- rename $tmpname, $param_file
- or die "Can't rename $tmpname to $param_file: $!";
-
- fix_file_permissions($param_file);
+ # It's not common to edit parameters and loading
+ # Bugzilla::Install::Filesystem is slow.
+ require Bugzilla::Install::Filesystem;
+ Bugzilla::Install::Filesystem::fix_file_permissions($param_file);
# And now we have to reset the params cache so that Bugzilla will re-read
# them.
@@ -287,21 +319,25 @@ sub write_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')};
+ my $file = bz_locations()->{'datadir'} . '/params.json';
+
+ if (-e $file) {
+ my $data;
+ read_file($file, binmode => ':utf8', buf_ref => \$data);
+
+ # If params.json has been manually edited and e.g. some quotes are
+ # missing, we don't want JSON::XS to leak the content of the file
+ # to all users in its error message, so we have to eval'uate it.
+ %params = eval { %{JSON::XS->new->decode($data)} };
+ if ($@) {
+ my $error_msg = (basename($0) eq 'checksetup.pl') ?
+ $@ : 'run checksetup.pl to see the details.';
+ die "Error parsing $file: $error_msg";
+ }
+ # JSON::XS doesn't detaint data for us.
+ foreach my $key (keys %params) {
+ trick_taint($params{$key}) if defined $params{$key};
+ }
}
elsif ($ENV{'SERVER_SOFTWARE'}) {
# We're in a CGI, but the params file doesn't exist. We can't
@@ -311,7 +347,7 @@ sub read_param_file {
# so that the user sees the error.
require CGI::Carp;
CGI::Carp->import('fatalsToBrowser');
- die "The $datadir/params file does not exist."
+ die "The $file file does not exist."
. ' You probably need to run checksetup.pl.',
}
return \%params;
@@ -367,7 +403,7 @@ specified.
Description: Writes the parameters to disk.
Params: C<$params> (optional) - A hashref to write to the disk
- instead of C<Bugzilla->params>. Used only by
+ instead of C<Bugzilla-E<gt>params>. Used only by
C<update_params>.
Returns: nothing
@@ -375,11 +411,13 @@ Returns: nothing
=item C<read_param_file()>
Description: Most callers should never need this. This is used
- by C<Bugzilla->params> to directly read C<$datadir/params>
- and load it into memory. Use C<Bugzilla->params> instead.
+ by C<Bugzilla-E<gt>params> to directly read C<$datadir/params.json>
+ and load it into memory. Use C<Bugzilla-E<gt>params> instead.
Params: none
-Returns: A hashref containing the current params in C<$datadir/params>.
+Returns: A hashref containing the current params in C<$datadir/params.json>.
+
+=item C<param_panels()>
=back
diff --git a/Bugzilla/Config/Admin.pm b/Bugzilla/Config/Admin.pm
index 7be3e54d1..41d929298 100644
--- a/Bugzilla/Config/Admin.pm
+++ b/Bugzilla/Config/Admin.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::Admin;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
@@ -32,6 +34,13 @@ sub get_param_list {
name => 'allowuserdeletion',
type => 'b',
default => 0
+ },
+
+ {
+ name => 'last_visit_keep_days',
+ type => 't',
+ default => 10,
+ checker => \&check_numeric
});
return @param_list;
}
diff --git a/Bugzilla/Config/Advanced.pm b/Bugzilla/Config/Advanced.pm
index fa5b7d249..8356c3361 100644
--- a/Bugzilla/Config/Advanced.pm
+++ b/Bugzilla/Config/Advanced.pm
@@ -6,7 +6,10 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Config::Advanced;
+
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
diff --git a/Bugzilla/Config/Attachment.pm b/Bugzilla/Config/Attachment.pm
index ba19f7782..580ec46d9 100644
--- a/Bugzilla/Config/Attachment.pm
+++ b/Bugzilla/Config/Attachment.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::Attachment;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
diff --git a/Bugzilla/Config/Auth.pm b/Bugzilla/Config/Auth.pm
index 19ba59b0c..78d719b15 100644
--- a/Bugzilla/Config/Auth.pm
+++ b/Bugzilla/Config/Auth.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::Auth;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
@@ -73,9 +75,15 @@ sub get_param_list {
},
{
+ name => 'webservice_email_filter',
+ type => 'b',
+ default => 0
+ },
+
+ {
name => 'emailregexp',
type => 't',
- default => q:^[\\w\\.\\+\\-=]+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
+ default => q:^[\\w\\.\\+\\-=']+@[\\w\\.\\-]+\\.[\\w\\-]+$:,
checker => \&check_regexp
},
@@ -106,7 +114,14 @@ sub get_param_list {
'letters_numbers_specialchars' ],
default => 'no_constraints',
checker => \&check_multi
- } );
+ },
+
+ {
+ name => 'password_check_on_login',
+ type => 'b',
+ default => '1'
+ },
+ );
return @param_list;
}
diff --git a/Bugzilla/Config/BugChange.pm b/Bugzilla/Config/BugChange.pm
index 68cfe5676..0acdc0ce4 100644
--- a/Bugzilla/Config/BugChange.pm
+++ b/Bugzilla/Config/BugChange.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::BugChange;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
use Bugzilla::Status;
diff --git a/Bugzilla/Config/BugFields.pm b/Bugzilla/Config/BugFields.pm
index 7ae9ae355..ef2faa64b 100644
--- a/Bugzilla/Config/BugFields.pm
+++ b/Bugzilla/Config/BugFields.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::BugFields;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
use Bugzilla::Field;
@@ -83,7 +85,13 @@ sub get_param_list {
choices => ['', @legal_OS],
default => '',
checker => \&check_opsys
- } );
+ },
+
+ {
+ name => 'collapsed_comment_tags',
+ type => 't',
+ default => 'obsolete, spam',
+ });
return @param_list;
}
diff --git a/Bugzilla/Config/Common.pm b/Bugzilla/Config/Common.pm
index e1c2c8c40..414894773 100644
--- a/Bugzilla/Config/Common.pm
+++ b/Bugzilla/Config/Common.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::Common;
+use 5.10.1;
use strict;
+use warnings;
use Email::Address;
use Socket;
@@ -18,15 +20,16 @@ use Bugzilla::Field;
use Bugzilla::Group;
use Bugzilla::Status;
-use base qw(Exporter);
+use parent qw(Exporter);
@Bugzilla::Config::Common::EXPORT =
qw(check_multi check_numeric check_regexp check_url check_group
check_sslbase check_priority check_severity check_platform
check_opsys check_shadowdb check_urlbase check_webdotbase
- check_user_verify_class check_ip check_smtp_server
+ check_user_verify_class check_ip check_font_file
check_mail_delivery_method check_notification check_utf8
check_bug_status check_smtp_auth check_theschwartz_available
check_maxattachmentsize check_email check_smtp_ssl
+ check_comment_taggers_group check_smtp_server
);
# Checking functions for the various values
@@ -97,7 +100,7 @@ sub check_sslbase {
my $iaddr = inet_aton($host) || return "The host $host cannot be resolved";
my $sin = sockaddr_in($port, $iaddr);
if (!connect(SOCK, $sin)) {
- return "Failed to connect to $host:$port; unable to enable SSL";
+ return "Failed to connect to $host:$port ($!); unable to enable SSL";
}
close(SOCK);
}
@@ -241,6 +244,20 @@ sub check_webdotbase {
return "";
}
+sub check_font_file {
+ my ($font) = @_;
+ $font = trim($font);
+ return '' unless $font;
+
+ if ($font !~ /\.ttf$/) {
+ return "The file must point to a TrueType font file (its extension must be .ttf)"
+ }
+ if (! -f $font) {
+ return "The file '$font' cannot be found. Make sure you typed the full path to the file"
+ }
+ return '';
+}
+
sub check_user_verify_class {
# doeditparams traverses the list of params, and for each one it checks,
# then updates. This means that if one param checker wants to look at
@@ -335,7 +352,21 @@ sub check_smtp_server {
return "Invalid port. It must be an integer (typically 25, 465 or 587)";
}
}
- return "";
+ trick_taint($host);
+ # Let's first try to connect using SSL. If this fails, we fall back to
+ # an unencrypted connection.
+ foreach my $method (['Net::SMTP::SSL', 465], ['Net::SMTP', 25]) {
+ my ($class, $default_port) = @$method;
+ next if $class eq 'Net::SMTP::SSL' && !Bugzilla->feature('smtp_ssl');
+ eval "require $class";
+ my $smtp = $class->new($host, Port => $port || $default_port, Timeout => 5);
+ if ($smtp) {
+ # The connection works!
+ $smtp->quit;
+ return '';
+ }
+ }
+ return "Cannot connect to $host" . ($port ? " using port $port" : "");
}
sub check_smtp_auth {
@@ -365,6 +396,14 @@ sub check_theschwartz_available {
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);
+}
+
# OK, here are the parameter definitions themselves.
#
# Each definition is a hash with keys:
@@ -463,4 +502,63 @@ Checks that the value is a valid number
Checks that the value is a valid regexp
+=item C<check_comment_taggers_group>
+
+Checks that the required modules for comment tagging are installed, and that a
+valid group is provided.
+
+=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item check_notification
+
+=item check_priority
+
+=item check_ip
+
+=item check_user_verify_class
+
+=item check_bug_status
+
+=item check_shadowdb
+
+=item check_smtp_server
+
+=item check_smtp_auth
+
+=item check_url
+
+=item check_urlbase
+
+=item check_email
+
+=item check_webdotbase
+
+=item check_font_file
+
+=item get_param_list
+
+=item check_maxattachmentsize
+
+=item check_utf8
+
+=item check_group
+
+=item check_opsys
+
+=item check_platform
+
+=item check_severity
+
+=item check_sslbase
+
+=item check_mail_delivery_method
+
+=item check_theschwartz_available
+
+=item check_smtp_ssl
+
=back
diff --git a/Bugzilla/Config/Core.pm b/Bugzilla/Config/Core.pm
index f2510d2b2..654e569ba 100644
--- a/Bugzilla/Config/Core.pm
+++ b/Bugzilla/Config/Core.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::Core;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
diff --git a/Bugzilla/Config/DependencyGraph.pm b/Bugzilla/Config/DependencyGraph.pm
index cc61e3588..c815822f3 100644
--- a/Bugzilla/Config/DependencyGraph.pm
+++ b/Bugzilla/Config/DependencyGraph.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::DependencyGraph;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
@@ -19,9 +21,16 @@ sub get_param_list {
{
name => 'webdotbase',
type => 't',
- default => 'http://www.research.att.com/~north/cgi-bin/webdot.cgi/%urlbase%',
+ default => '',
checker => \&check_webdotbase
- } );
+ },
+
+ {
+ name => 'font_file',
+ type => 't',
+ default => '',
+ checker => \&check_font_file
+ });
return @param_list;
}
diff --git a/Bugzilla/Config/General.pm b/Bugzilla/Config/General.pm
index 6e00b202a..380680590 100644
--- a/Bugzilla/Config/General.pm
+++ b/Bugzilla/Config/General.pm
@@ -6,7 +6,11 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Config::General;
+
+use 5.10.1;
use strict;
+use warnings;
+
use Bugzilla::Config::Common;
our $sortkey = 150;
@@ -21,13 +25,6 @@ use constant get_param_list => (
},
{
- name => 'docs_urlbase',
- type => 't',
- default => 'docs/%lang%/html/',
- checker => \&check_url
- },
-
- {
name => 'utf8',
type => 'b',
default => '0',
diff --git a/Bugzilla/Config/GroupSecurity.pm b/Bugzilla/Config/GroupSecurity.pm
index f0b4ac808..e827834a0 100644
--- a/Bugzilla/Config/GroupSecurity.pm
+++ b/Bugzilla/Config/GroupSecurity.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::GroupSecurity;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
use Bugzilla::Group;
@@ -55,7 +57,15 @@ sub get_param_list {
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',
@@ -74,6 +84,12 @@ sub get_param_list {
name => 'strict_isolation',
type => 'b',
default => 0
+ },
+
+ {
+ name => 'or_groups',
+ type => 'b',
+ default => 0
} );
return @param_list;
}
@@ -83,4 +99,5 @@ sub _get_all_group_names {
unshift(@group_names, '');
return \@group_names;
}
+
1;
diff --git a/Bugzilla/Config/LDAP.pm b/Bugzilla/Config/LDAP.pm
index daa7f72bb..0bc8240df 100644
--- a/Bugzilla/Config/LDAP.pm
+++ b/Bugzilla/Config/LDAP.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::LDAP;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
diff --git a/Bugzilla/Config/MTA.pm b/Bugzilla/Config/MTA.pm
index e6e9505a3..467bdab3f 100644
--- a/Bugzilla/Config/MTA.pm
+++ b/Bugzilla/Config/MTA.pm
@@ -7,16 +7,11 @@
package Bugzilla::Config::MTA;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
-# Return::Value 1.666002 pollutes the error log with warnings about this
-# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
-# to disable these warnings.
-BEGIN {
- $Return::Value::NO_CLUCK = 1;
-}
-use Email::Send;
our $sortkey = 1200;
@@ -26,9 +21,7 @@ sub get_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'],
+ choices => ['Sendmail', 'SMTP', 'Test', 'None'],
default => 'Sendmail',
checker => \&check_mail_delivery_method
},
diff --git a/Bugzilla/Config/Memcached.pm b/Bugzilla/Config/Memcached.pm
new file mode 100644
index 000000000..292803d86
--- /dev/null
+++ b/Bugzilla/Config/Memcached.pm
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Config::Memcached;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::Config::Common;
+
+our $sortkey = 1550;
+
+sub get_param_list {
+ return (
+ {
+ name => 'memcached_servers',
+ type => 't',
+ default => ''
+ },
+ {
+ name => 'memcached_namespace',
+ type => 't',
+ default => 'bugzilla:',
+ },
+ );
+}
+
+1;
diff --git a/Bugzilla/Config/PatchViewer.pm b/Bugzilla/Config/PatchViewer.pm
deleted file mode 100644
index 08e8028f3..000000000
--- a/Bugzilla/Config/PatchViewer.pm
+++ /dev/null
@@ -1,51 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-#
-# This Source Code Form is "Incompatible With Secondary Licenses", as
-# defined by the Mozilla Public License, v. 2.0.
-
-package Bugzilla::Config::PatchViewer;
-
-use strict;
-
-use Bugzilla::Config::Common;
-
-our $sortkey = 1300;
-
-sub get_param_list {
- my $class = shift;
- my @param_list = (
- {
- name => 'cvsroot',
- type => 't',
- default => '',
- },
-
- {
- name => 'cvsroot_get',
- type => 't',
- default => '',
- },
-
- {
- name => 'bonsai_url',
- type => 't',
- default => ''
- },
-
- {
- name => 'lxr_url',
- type => 't',
- default => ''
- },
-
- {
- name => 'lxr_root',
- type => 't',
- default => '',
- } );
- return @param_list;
-}
-
-1;
diff --git a/Bugzilla/Config/Query.pm b/Bugzilla/Config/Query.pm
index fe54d67ab..f18bb90df 100644
--- a/Bugzilla/Config/Query.pm
+++ b/Bugzilla/Config/Query.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::Query;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
@@ -25,13 +27,6 @@ sub get_param_list {
},
{
- name => 'mostfreqthreshold',
- type => 't',
- default => '2',
- checker => \&check_numeric
- },
-
- {
name => 'mybugstemplate',
type => 't',
default => 'buglist.cgi?resolution=---&amp;emailassigned_to1=1&amp;emailreporter1=1&amp;emailtype1=exact&amp;email1=%userid%'
diff --git a/Bugzilla/Config/RADIUS.pm b/Bugzilla/Config/RADIUS.pm
index 312be1ded..8e30b07a9 100644
--- a/Bugzilla/Config/RADIUS.pm
+++ b/Bugzilla/Config/RADIUS.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::RADIUS;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
diff --git a/Bugzilla/Config/ShadowDB.pm b/Bugzilla/Config/ShadowDB.pm
index c0ff59096..5dbbb5202 100644
--- a/Bugzilla/Config/ShadowDB.pm
+++ b/Bugzilla/Config/ShadowDB.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::ShadowDB;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
diff --git a/Bugzilla/Config/UserMatch.pm b/Bugzilla/Config/UserMatch.pm
index 5b98a25ea..3f74a7c44 100644
--- a/Bugzilla/Config/UserMatch.pm
+++ b/Bugzilla/Config/UserMatch.pm
@@ -7,7 +7,9 @@
package Bugzilla::Config::UserMatch;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Config::Common;
diff --git a/Bugzilla/Constants.pm b/Bugzilla/Constants.pm
index 179b765f2..837ed895d 100644
--- a/Bugzilla/Constants.pm
+++ b/Bugzilla/Constants.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Constants;
+
+use 5.10.1;
use strict;
-use base qw(Exporter);
+use warnings;
+
+use parent qw(Exporter);
# For bz_locations
use File::Basename;
@@ -15,12 +19,15 @@ use Memoize;
@Bugzilla::Constants::EXPORT = qw(
BUGZILLA_VERSION
+ REST_DOC
REMOTE_FILE
LOCAL_FILE
bz_locations
+ CONCATENATE_ASSETS
+
IS_NULL
NOT_NULL
@@ -66,6 +73,9 @@ use Memoize;
COMMENT_COLS
MAX_COMMENT_LENGTH
+ MIN_COMMENT_TAG_LENGTH
+ MAX_COMMENT_TAG_LENGTH
+
CMT_NORMAL
CMT_DUPE_OF
CMT_HAS_DUPE
@@ -101,9 +111,12 @@ use Memoize;
FIELD_TYPE_MULTI_SELECT
FIELD_TYPE_TEXTAREA
FIELD_TYPE_DATETIME
+ FIELD_TYPE_DATE
FIELD_TYPE_BUG_ID
FIELD_TYPE_BUG_URLS
FIELD_TYPE_KEYWORDS
+ FIELD_TYPE_INTEGER
+ FIELD_TYPE_HIGHEST_PLUS_ONE
EMPTY_DATETIME_REGEX
@@ -117,12 +130,14 @@ use Memoize;
USAGE_MODE_EMAIL
USAGE_MODE_JSON
USAGE_MODE_TEST
+ USAGE_MODE_REST
ERROR_MODE_WEBPAGE
ERROR_MODE_DIE
ERROR_MODE_DIE_SOAP_FAULT
ERROR_MODE_JSON_RPC
ERROR_MODE_TEST
+ ERROR_MODE_REST
COLOR_ERROR
COLOR_SUCCESS
@@ -162,6 +177,7 @@ use Memoize;
MAX_POSSIBLE_DUPLICATES
MAX_ATTACH_FILENAME_LENGTH
MAX_QUIP_LENGTH
+ MAX_WEBDOT_BUGS
PASSWORD_DIGEST_ALGORITHM
PASSWORD_SALT_LENGTH
@@ -175,6 +191,8 @@ use Memoize;
AUDIT_CREATE
AUDIT_REMOVE
+
+ MOST_FREQUENT_THRESHOLD
);
@Bugzilla::Constants::EXPORT_OK = qw(contenttypes);
@@ -182,12 +200,21 @@ use Memoize;
# CONSTANTS
#
# Bugzilla version
-use constant BUGZILLA_VERSION => "4.4.9";
+use constant BUGZILLA_VERSION => "5.0";
+
+# A base link to the current REST Documentation. We place it here
+# as it will need to be updated to whatever the current release is.
+use constant REST_DOC => "http://www.bugzilla.org/docs/tip/en/html/api/";
# Location of the remote and local XML files to track new releases.
use constant REMOTE_FILE => 'http://updates.bugzilla.org/bugzilla-update.xml';
use constant LOCAL_FILE => 'bugzilla-update.xml'; # Relative to datadir.
+# When true CSS and JavaScript assets will be concatanted and minified at
+# run-time, to reduce the number of requests required to render a page.
+# Setting this to a false value can help debugging.
+use constant CONCATENATE_ASSETS => 1;
+
# 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
# and end with spaces because most Bugzilla stuff has trim() called on it,
@@ -283,6 +310,10 @@ use constant COMMENT_COLS => 80;
# Used in _check_comment(). Gives the max length allowed for a comment.
use constant MAX_COMMENT_LENGTH => 65535;
+# The minimum and maximum length of comment tags.
+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;
@@ -382,6 +413,11 @@ 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;
+# Add new field types above this line, and change the below value in the
+# obvious fashion
+use constant FIELD_TYPE_HIGHEST_PLUS_ONE => 11;
use constant EMPTY_DATETIME_REGEX => qr/^[0\-:\sA-Za-z]+$/;
@@ -396,8 +432,7 @@ use constant ABNORMAL_SELECTS => {
# 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);
# The maximum number of days a token will remain valid.
use constant MAX_TOKEN_AGE => 3;
@@ -412,8 +447,8 @@ use constant MAX_LOGIN_ATTEMPTS => 5;
# account is locked.
use constant LOGIN_LOCKOUT_INTERVAL => 30;
-# The time in minutes a user must wait before he can request another email to
-# create a new account or change his password.
+# The time in minutes a user must wait before they can request another email to
+# create a new account or change their password.
use constant ACCOUNT_CHANGE_INTERVAL => 10;
# The maximum number of seconds the Strict-Transport-Security header
@@ -450,6 +485,7 @@ use constant USAGE_MODE_XMLRPC => 2;
use constant USAGE_MODE_EMAIL => 3;
use constant USAGE_MODE_JSON => 4;
use constant USAGE_MODE_TEST => 5;
+use constant USAGE_MODE_REST => 6;
# Error modes. Default set by Bugzilla->usage_mode (so ERROR_MODE_WEBPAGE
# usually). Use with Bugzilla->error_mode.
@@ -458,6 +494,7 @@ use constant ERROR_MODE_DIE => 1;
use constant ERROR_MODE_DIE_SOAP_FAULT => 2;
use constant ERROR_MODE_JSON_RPC => 3;
use constant ERROR_MODE_TEST => 4;
+use constant ERROR_MODE_REST => 5;
# The ANSI colors of messages that command-line scripts use
use constant COLOR_ERROR => 'red';
@@ -562,13 +599,16 @@ use constant MAX_ATTACH_FILENAME_LENGTH => 255;
# Maximum length of a quip.
use constant MAX_QUIP_LENGTH => 512;
+# Maximum number of bugs to display in a dependency graph
+use constant MAX_WEBDOT_BUGS => 2000;
+
# This is the name of the algorithm used to hash passwords before storing
# them in the database. This can be any string that is valid to pass to
# Perl's "Digest" module. Note that if you change this, it won't take
-# effect until a user changes his password.
+# effect until a user logs in or changes their password.
use constant PASSWORD_DIGEST_ALGORITHM => 'SHA-256';
-# How long of a salt should we use? Note that if you change this, none
-# of your users will be able to log in until they reset their passwords.
+# How long of a salt should we use? Note that if you change this, it
+# won't take effect until a user logs in or changes their password.
use constant PASSWORD_SALT_LENGTH => 8;
# Certain scripts redirect to GET even if the form was submitted originally
@@ -577,7 +617,7 @@ use constant PASSWORD_SALT_LENGTH => 8;
# See http://support.microsoft.com/kb/208427 for why MSIE is different
use constant CGI_URI_LIMIT => ($ENV{'HTTP_USER_AGENT'} || '') =~ /MSIE/ ? 2083 : 8000;
-# If the user isn't allowed to change a field, we must tell him who can.
+# If the user isn't allowed to change a field, we must tell them who can.
# We store the required permission set into the $PrivilegesRequired
# variable which gets passed to the error template.
@@ -591,6 +631,10 @@ use constant PRIVILEGES_REQUIRED_EMPOWERED => 3;
use constant AUDIT_CREATE => '__create__';
use constant AUDIT_REMOVE => '__remove__';
+# The minimum number of duplicates a bug needs to show up
+# on the "Most frequently reported bugs" page.
+use constant MOST_FREQUENT_THRESHOLD => 2;
+
sub bz_locations {
# Force memoize() to re-compute data per project, to avoid
# sharing the same data across different installations.
@@ -651,6 +695,7 @@ sub _bz_locations {
# The script should really generate these graphs directly...
'webdotdir' => "$datadir/webdot",
'extensionsdir' => "$libpath/extensions",
+ 'assetsdir' => "$datadir/assets",
};
}
@@ -659,3 +704,15 @@ sub _bz_locations {
BEGIN { memoize('_bz_locations') };
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item DB_MODULE
+
+=item contenttypes
+
+=item bz_locations
+
+=back
diff --git a/Bugzilla/DB.pm b/Bugzilla/DB.pm
index 248312e12..c9eaf6e9e 100644
--- a/Bugzilla/DB.pm
+++ b/Bugzilla/DB.pm
@@ -7,20 +7,24 @@
package Bugzilla::DB;
+use 5.10.1;
use strict;
+use warnings;
use DBI;
# Inherit the DB class from DBI::db.
-use base qw(DBI::db);
+use parent -norequire, qw(DBI::db);
use Bugzilla::Constants;
+use Bugzilla::Mailer;
use Bugzilla::Install::Requirements;
-use Bugzilla::Install::Util qw(vers_cmp install_string);
+use Bugzilla::Install::Util qw(install_string);
use Bugzilla::Install::Localconfig;
use Bugzilla::Util;
use Bugzilla::Error;
use Bugzilla::DB::Schema;
+use Bugzilla::Version;
use List::Util qw(max);
use Storable qw(dclone);
@@ -1210,12 +1214,13 @@ sub bz_start_transaction {
sub bz_commit_transaction {
my ($self) = @_;
-
+
if ($self->{private_bz_transaction_count} > 1) {
$self->{private_bz_transaction_count}--;
} elsif ($self->bz_in_transaction) {
$self->commit();
$self->{private_bz_transaction_count} = 0;
+ Bugzilla::Mailer->send_staged_mail();
} else {
ThrowCodeError('not_in_transaction');
}
@@ -1251,11 +1256,9 @@ sub db_new {
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?)
+ # See https://rt.perl.org/rt3/Public/Bug/Display.html?id=30933
+ # for the reason to use NAME instead of NAME_lc (bug 253696).
+ FetchHashKeyName => 'NAME',
};
if ($override_attrs) {
@@ -1365,14 +1368,19 @@ sub _bz_real_schema {
my ($self) = @_;
return $self->{private_real_schema} if exists $self->{private_real_schema};
- my ($data, $version) = $self->selectrow_array(
- "SELECT schema_data, version FROM 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 });
+ }
(die "_bz_real_schema tried to read the bz_schema table but it's empty!")
- if !$data;
+ if !$bz_schema;
- $self->{private_real_schema} =
- $self->_bz_schema->deserialize_abstract($data, $version);
+ $self->{private_real_schema} =
+ $self->_bz_schema->deserialize_abstract($bz_schema->[0], $bz_schema->[1]);
return $self->{private_real_schema};
}
@@ -1414,6 +1422,8 @@ sub _bz_store_real_schema {
$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
@@ -2685,3 +2695,63 @@ our check for implementation of C<new> by derived class useless.
L<DBI>
L<Bugzilla::Constants/DB_MODULE>
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item bz_add_fks
+
+=item bz_add_fk
+
+=item bz_drop_index_raw
+
+=item bz_table_info
+
+=item bz_add_index_raw
+
+=item bz_get_related_fks
+
+=item quote
+
+=item bz_drop_fk
+
+=item bz_drop_field_tables
+
+=item bz_drop_related_fks
+
+=item bz_table_columns
+
+=item bz_drop_foreign_keys
+
+=item bz_alter_column_raw
+
+=item bz_table_list_real
+
+=item bz_fk_info
+
+=item bz_setup_database
+
+=item bz_setup_foreign_keys
+
+=item bz_table_indexes
+
+=item bz_check_regexp
+
+=item bz_enum_initial_values
+
+=item bz_alter_fk
+
+=item bz_set_next_serial_value
+
+=item bz_table_list
+
+=item bz_table_columns_real
+
+=item bz_check_server_version
+
+=item bz_server_version
+
+=item bz_add_field_tables
+
+=back
diff --git a/Bugzilla/DB/Mysql.pm b/Bugzilla/DB/Mysql.pm
index dc93b7406..d0915f1e6 100644
--- a/Bugzilla/DB/Mysql.pm
+++ b/Bugzilla/DB/Mysql.pm
@@ -20,8 +20,12 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
=cut
package Bugzilla::DB::Mysql;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::DB);
+use warnings;
+
+use parent qw(Bugzilla::DB);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(install_string);
@@ -55,6 +59,18 @@ sub new {
mysql_auto_reconnect => 1,
);
+ # MySQL SSL options
+ my ($ssl_ca_file, $ssl_ca_path, $ssl_cert, $ssl_key) =
+ @$params{qw(db_mysql_ssl_ca_file db_mysql_ssl_ca_path
+ db_mysql_ssl_client_cert db_mysql_ssl_client_key)};
+ if ($ssl_ca_file || $ssl_ca_path || $ssl_cert || $ssl_key) {
+ $attrs{'mysql_ssl'} = 1;
+ $attrs{'mysql_ssl_ca_file'} = $ssl_ca_file if $ssl_ca_file;
+ $attrs{'mysql_ssl_ca_path'} = $ssl_ca_path if $ssl_ca_path;
+ $attrs{'mysql_ssl_client_cert'} = $ssl_cert if $ssl_cert;
+ $attrs{'mysql_ssl_client_key'} = $ssl_key if $ssl_key;
+ }
+
my $self = $class->db_new({ dsn => $dsn, user => $user,
pass => $pass, attrs => \%attrs });
@@ -111,10 +127,13 @@ sub bz_last_key {
}
sub sql_group_concat {
- my ($self, $column, $separator, $sort) = @_;
+ my ($self, $column, $separator, $sort, $order_by) = @_;
$separator = $self->quote(', ') if !defined $separator;
$sort = 1 if !defined $sort;
- if ($sort) {
+ if ($order_by) {
+ $column .= " ORDER BY $order_by";
+ }
+ elsif ($sort) {
my $sort_order = $column;
$sort_order =~ s/^DISTINCT\s+//i;
$column = "$column ORDER BY $sort_order";
@@ -165,15 +184,19 @@ sub sql_fulltext_search {
if ($text =~ /(?:^|\W)[+\-<>~"()]/ || $text =~ /[()"*](?:$|\W)/) {
$mode = 'IN BOOLEAN MODE';
- # quote un-quoted compound words
- my @words = quotewords('[\s()]+', 'delimiters', $text);
- 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 . '"';
+ my @terms = split(quotemeta(FULLTEXT_OR), $text);
+ foreach my $term (@terms) {
+ # quote un-quoted compound words
+ my @words = quotewords('[\s()]+', 'delimiters', $term);
+ foreach my $word (@words) {
+ # match words that have non-word chars in the middle of them
+ if ($word =~ /\w\W+\w/ && $word !~ m/"/) {
+ $word = '"' . $word . '"';
+ }
}
+ $term = join('', @words);
}
- $text = join('', @words);
+ $text = join(FULLTEXT_OR, @terms);
}
# quote the text for use in the MATCH AGAINST expression
@@ -1035,3 +1058,49 @@ sub _bz_build_schema_from_disk {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item sql_date_format
+
+=item bz_explain
+
+=item bz_last_key
+
+=item sql_position
+
+=item sql_fulltext_search
+
+=item sql_iposition
+
+=item bz_enum_initial_values
+
+=item sql_group_by
+
+=item sql_limit
+
+=item sql_not_regexp
+
+=item sql_string_concat
+
+=item sql_date_math
+
+=item sql_to_days
+
+=item bz_check_server_version
+
+=item sql_from_days
+
+=item sql_regexp
+
+=item sql_istring
+
+=item sql_group_concat
+
+=item bz_setup_database
+
+=item bz_db_is_utf8
+
+=back
diff --git a/Bugzilla/DB/Oracle.pm b/Bugzilla/DB/Oracle.pm
index 6511242dc..7424019ac 100644
--- a/Bugzilla/DB/Oracle.pm
+++ b/Bugzilla/DB/Oracle.pm
@@ -20,8 +20,12 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
=cut
package Bugzilla::DB::Oracle;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::DB);
+use warnings;
+
+use parent qw(Bugzilla::DB);
use DBD::Oracle;
use DBD::Oracle qw(:ora_types);
@@ -41,8 +45,6 @@ use constant BLOB_TYPE => { ora_type => ORA_BLOB };
use constant MIN_LONG_READ_LEN => 32 * 1024;
use constant FULLTEXT_OR => ' OR ';
-our $fulltext_label = 0;
-
sub new {
my ($class, $params) = @_;
my ($user, $pass, $host, $dbname, $port) =
@@ -161,10 +163,11 @@ sub sql_from_days{
sub sql_fulltext_search {
my ($self, $column, $text) = @_;
+ state $label = 0;
$text = $self->quote($text);
trick_taint($text);
- $fulltext_label++;
- return "CONTAINS($column,$text,$fulltext_label) > 0", "SCORE($fulltext_label)";
+ $label++;
+ return "CONTAINS($column,$text,$label) > 0", "SCORE($label)";
}
sub sql_date_format {
@@ -716,7 +719,12 @@ sub _get_create_trigger_ddl {
############################################################################
package Bugzilla::DB::Oracle::st;
-use base qw(DBI::st);
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent -norequire, qw(DBI::st);
sub fetchrow_arrayref {
my $self = shift;
@@ -781,3 +789,69 @@ sub fetch {
return $row;
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item adjust_statement
+
+=item bz_check_regexp
+
+=item bz_drop_table
+
+=item bz_explain
+
+=item bz_last_key
+
+=item bz_setup_database
+
+=item bz_table_columns_real
+
+=item bz_table_list_real
+
+=item do
+
+=item prepare
+
+=item prepare_cached
+
+=item quote_identifier
+
+=item selectall_arrayref
+
+=item selectall_hashref
+
+=item selectcol_arrayref
+
+=item selectrow_array
+
+=item selectrow_arrayref
+
+=item selectrow_hashref
+
+=item sql_date_format
+
+=item sql_date_math
+
+=item sql_from_days
+
+=item sql_fulltext_search
+
+=item sql_group_concat
+
+=item sql_in
+
+=item sql_limit
+
+=item sql_not_regexp
+
+=item sql_position
+
+=item sql_regexp
+
+=item sql_string_concat
+
+=item sql_to_days
+
+=back
diff --git a/Bugzilla/DB/Pg.pm b/Bugzilla/DB/Pg.pm
index cd5fed581..a950c575f 100644
--- a/Bugzilla/DB/Pg.pm
+++ b/Bugzilla/DB/Pg.pm
@@ -21,13 +21,16 @@ For interface details see L<Bugzilla::DB> and L<DBI>.
package Bugzilla::DB::Pg;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Error;
+use Bugzilla::Version;
use DBD::Pg;
# This module extends the DB interface via inheritance
-use base qw(Bugzilla::DB);
+use parent qw(Bugzilla::DB);
use constant BLOB_TYPE => { pg_type => DBD::Pg::PG_BYTEA };
@@ -78,14 +81,37 @@ sub bz_last_key {
}
sub sql_group_concat {
- my ($self, $text, $separator, $sort) = @_;
+ my ($self, $text, $separator, $sort, $order_by) = @_;
$sort = 1 if !defined $sort;
$separator = $self->quote(', ') if !defined $separator;
- my $sql = "array_accum($text)";
- if ($sort) {
- $sql = "array_sort($sql)";
+
+ # PostgreSQL 8.x doesn't support STRING_AGG
+ if (vers_cmp($self->bz_server_version, 9) < 0) {
+ my $sql = "ARRAY_ACCUM($text)";
+ if ($sort) {
+ $sql = "ARRAY_SORT($sql)";
+ }
+ return "ARRAY_TO_STRING($sql, $separator)";
+ }
+
+ if ($order_by && $text =~ /^DISTINCT\s*(.+)$/i) {
+ # Since Postgres (quite rightly) doesn't support "SELECT DISTINCT x
+ # ORDER BY y", we need to sort the list, and then get the unique
+ # values
+ return "ARRAY_TO_STRING(ANYARRAY_UNIQ(ARRAY_AGG($1 ORDER BY $order_by)), $separator)";
}
- return "array_to_string($sql, $separator)";
+
+ # Determine the ORDER BY clause (if any)
+ if ($order_by) {
+ $order_by = " ORDER BY $order_by";
+ }
+ elsif ($sort) {
+ # We don't include the DISTINCT keyword in an order by
+ $text =~ /^(?:DISTINCT\s*)?(.+)$/i;
+ $order_by = " ORDER BY $1";
+ }
+
+ return "STRING_AGG(${text}::text, $separator${order_by}::text)"
}
sub sql_istring {
@@ -209,21 +235,25 @@ 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 (
- SFUNC = array_append,
- BASETYPE = anyelement,
- STYPE = anyarray,
- INITCOND = '{}'
- )");
- }
+ my ($has_plpgsql) = $self->selectrow_array("SELECT COUNT(*) FROM pg_language WHERE lanname = 'plpgsql'");
+ $self->do('CREATE LANGUAGE plpgsql') unless $has_plpgsql;
+
+ if (vers_cmp($self->bz_server_version, 9) < 0) {
+ # Custom Functions for Postgres 8
+ my $function = 'array_accum';
+ my $array_accum = $self->selectrow_array(
+ 'SELECT 1 FROM pg_proc WHERE proname = ?', undef, $function);
+ if (!$array_accum) {
+ print "Creating function $function...\n";
+ $self->do("CREATE AGGREGATE array_accum (
+ 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
@@ -236,6 +266,57 @@ SELECT ARRAY(
);
$$;
END
+ }
+ else {
+ # Custom functions for Postgres 9.0+
+
+ # -Copyright © 2013 Joshua D. Burns (JDBurnZ) and Message In Action LLC
+ # JDBurnZ: https://github.com/JDBurnZ
+ # Message In Action: https://www.messageinaction.com
+ #
+ #Permission is hereby granted, free of charge, to any person obtaining a copy of
+ #this software and associated documentation files (the "Software"), to deal in
+ #the Software without restriction, including without limitation the rights to
+ #use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+ #the Software, and to permit persons to whom the Software is furnished to do so,
+ #subject to the following conditions:
+ #
+ #The above copyright notice and this permission notice shall be included in all
+ #copies or substantial portions of the Software.
+ #
+ #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+ #FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+ #COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+ #IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ #CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ $self->do(q|
+ DROP FUNCTION IF EXISTS anyarray_uniq(anyarray);
+ CREATE OR REPLACE FUNCTION anyarray_uniq(with_array anyarray)
+ RETURNS anyarray AS $BODY$
+ DECLARE
+ -- The variable used to track iteration over "with_array".
+ loop_offset integer;
+
+ -- The array to be returned by this function.
+ return_array with_array%TYPE := '{}';
+ BEGIN
+ IF with_array IS NULL THEN
+ return NULL;
+ END IF;
+
+ -- Iterate over each element in "concat_array".
+ FOR loop_offset IN ARRAY_LOWER(with_array, 1)..ARRAY_UPPER(with_array, 1) LOOP
+ IF NOT with_array[loop_offset] = ANY(return_array) THEN
+ return_array = ARRAY_APPEND(return_array, with_array[loop_offset]);
+ END IF;
+ END LOOP;
+
+ RETURN return_array;
+ END;
+ $BODY$ LANGUAGE plpgsql;
+ |);
+ }
# PostgreSQL doesn't like having *any* index on the thetext
# field, because it can't have index data longer than 2770
@@ -366,3 +447,43 @@ sub bz_table_list_real {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item sql_date_format
+
+=item bz_explain
+
+=item bz_sequence_exists
+
+=item bz_last_key
+
+=item sql_position
+
+=item sql_limit
+
+=item sql_not_regexp
+
+=item sql_string_concat
+
+=item sql_date_math
+
+=item sql_to_days
+
+=item bz_check_server_version
+
+=item sql_from_days
+
+=item bz_table_list_real
+
+=item sql_regexp
+
+=item sql_istring
+
+=item sql_group_concat
+
+=item bz_setup_database
+
+=back
diff --git a/Bugzilla/DB/Schema.pm b/Bugzilla/DB/Schema.pm
index 80c57a3df..d1c1dc7e9 100644
--- a/Bugzilla/DB/Schema.pm
+++ b/Bugzilla/DB/Schema.pm
@@ -15,7 +15,10 @@ package Bugzilla::DB::Schema;
#
###########################################################################
+use 5.10.1;
use strict;
+use warnings;
+
use Bugzilla::Error;
use Bugzilla::Hook;
use Bugzilla::Util;
@@ -252,7 +255,7 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid'}},
version => {TYPE => 'varchar(64)', NOTNULL => 1},
- component_id => {TYPE => 'INT2', NOTNULL => 1,
+ component_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'components',
COLUMN => 'id'}},
resolution => {TYPE => 'varchar(64)',
@@ -275,11 +278,8 @@ use constant ABSTRACT_SCHEMA => {
remaining_time => {TYPE => 'decimal(7,2)',
NOTNULL => 1, DEFAULT => '0'},
deadline => {TYPE => 'DATETIME'},
- alias => {TYPE => 'varchar(20)'},
],
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'],
@@ -356,6 +356,21 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ bugs_aliases => {
+ FIELDS => [
+ alias => {TYPE => 'varchar(40)', NOTNULL => 1},
+ bug_id => {TYPE => 'INT3',
+ REFERENCES => {TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'}},
+ ],
+ INDEXES => [
+ bugs_aliases_bug_id_idx => ['bug_id'],
+ bugs_aliases_alias_idx => {FIELDS => ['alias'],
+ TYPE => 'UNIQUE'},
+ ],
+ },
+
cc => {
FIELDS => [
bug_id => {TYPE => 'INT3', NOTNULL => 1,
@@ -404,6 +419,54 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ 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,
@@ -632,7 +695,7 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
- component_id => {TYPE => 'INT2',
+ component_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'components',
COLUMN => 'id',
DELETE => 'CASCADE'}},
@@ -653,7 +716,7 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'products',
COLUMN => 'id',
DELETE => 'CASCADE'}},
- component_id => {TYPE => 'INT2',
+ component_id => {TYPE => 'INT3',
REFERENCES => {TABLE => 'components',
COLUMN => 'id',
DELETE => 'CASCADE'}},
@@ -943,6 +1006,23 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ 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,
@@ -1050,7 +1130,7 @@ use constant ABSTRACT_SCHEMA => {
REFERENCES => {TABLE => 'profiles',
COLUMN => 'userid',
DELETE => 'CASCADE'}},
- component_id => {TYPE => 'INT2', NOTNULL => 1,
+ component_id => {TYPE => 'INT3', NOTNULL => 1,
REFERENCES => {TABLE => 'components',
COLUMN => 'id',
DELETE => 'CASCADE'}},
@@ -1110,7 +1190,7 @@ use constant ABSTRACT_SCHEMA => {
issuedate => {TYPE => 'DATETIME', NOTNULL => 1} ,
token => {TYPE => 'varchar(16)', NOTNULL => 1,
PRIMARYKEY => 1},
- tokentype => {TYPE => 'varchar(8)', NOTNULL => 1} ,
+ tokentype => {TYPE => 'varchar(16)', NOTNULL => 1} ,
eventdata => {TYPE => 'TINYTEXT'},
],
INDEXES => [
@@ -1328,7 +1408,7 @@ use constant ABSTRACT_SCHEMA => {
components => {
FIELDS => [
- id => {TYPE => 'SMALLSERIAL', NOTNULL => 1,
+ id => {TYPE => 'MEDIUMSERIAL', NOTNULL => 1,
PRIMARYKEY => 1},
name => {TYPE => 'varchar(64)', NOTNULL => 1},
product_id => {TYPE => 'INT2', NOTNULL => 1,
@@ -1549,6 +1629,16 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ # BUGMAIL
+ # -------
+
+ mail_staging => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', PRIMARYKEY => 1, NOTNULL => 1},
+ message => {TYPE => 'LONGBLOB', NOTNULL => 1},
+ ],
+ },
+
# THESCHWARTZ TABLES
# ------------------
# Note: In the standard TheSchwartz schema, most integers are unsigned,
@@ -1646,6 +1736,46 @@ use constant ABSTRACT_SCHEMA => {
],
},
+ bug_user_last_visit => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ bug_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'bugs',
+ COLUMN => 'bug_id',
+ DELETE => 'CASCADE'}},
+ last_visit_ts => {TYPE => 'DATETIME', NOTNULL => 1},
+ ],
+ INDEXES => [
+ bug_user_last_visit_idx => {FIELDS => ['user_id', 'bug_id'],
+ TYPE => 'UNIQUE'},
+ bug_user_last_visit_last_visit_ts_idx => ['last_visit_ts'],
+ ],
+ },
+
+ user_api_keys => {
+ FIELDS => [
+ id => {TYPE => 'INTSERIAL', NOTNULL => 1,
+ PRIMARYKEY => 1},
+ user_id => {TYPE => 'INT3', NOTNULL => 1,
+ REFERENCES => {TABLE => 'profiles',
+ COLUMN => 'userid',
+ DELETE => 'CASCADE'}},
+ api_key => {TYPE => 'VARCHAR(40)', NOTNULL => 1},
+ description => {TYPE => 'VARCHAR(255)'},
+ revoked => {TYPE => 'BOOLEAN', NOTNULL => 1,
+ DEFAULT => 'FALSE'},
+ last_used => {TYPE => 'DATETIME'},
+ ],
+ INDEXES => [
+ user_api_keys_api_key_idx => {FIELDS => ['api_key'], TYPE => 'UNIQUE'},
+ user_api_keys_user_id_idx => ['user_id'],
+ ],
+ },
};
# Foreign Keys are added in Bugzilla::DB::bz_add_field_tables
@@ -3011,3 +3141,19 @@ L<Bugzilla::DB>
L<http://www.bugzilla.org/docs/developer.html#sql-schema>
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item get_table_indexes_abstract
+
+=item get_create_database_sql
+
+=item get_add_fks_sql
+
+=item get_fk_ddl
+
+=item get_drop_fk_sql
+
+=back
diff --git a/Bugzilla/DB/Schema/Mysql.pm b/Bugzilla/DB/Schema/Mysql.pm
index 020549a89..0195fcb06 100644
--- a/Bugzilla/DB/Schema/Mysql.pm
+++ b/Bugzilla/DB/Schema/Mysql.pm
@@ -13,10 +13,13 @@ package Bugzilla::DB::Schema::Mysql;
#
###############################################################################
+use 5.10.1;
use strict;
+use warnings;
+
use Bugzilla::Error;
-use base qw(Bugzilla::DB::Schema);
+use parent qw(Bugzilla::DB::Schema);
# This is for column_info_to_column, to know when a tinyint is a
# boolean and when it's really a tinyint. This only has to be accurate
@@ -104,7 +107,7 @@ sub _initialize {
LONGBLOB => 'longblob',
DATETIME => 'datetime',
-
+ DATE => 'date',
};
$self->_adjust_schema;
@@ -381,3 +384,27 @@ sub get_rename_column_ddl {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item get_rename_column_ddl
+
+=item get_create_database_sql
+
+=item get_drop_index_ddl
+
+=item get_set_serial_sql
+
+=item get_rename_indexes_ddl
+
+=item get_drop_fk_sql
+
+=item MYISAM_TABLES
+
+=item column_info_to_column
+
+=item get_alter_column_ddl
+
+=back
diff --git a/Bugzilla/DB/Schema/Oracle.pm b/Bugzilla/DB/Schema/Oracle.pm
index a97929726..8fb5479b1 100644
--- a/Bugzilla/DB/Schema/Oracle.pm
+++ b/Bugzilla/DB/Schema/Oracle.pm
@@ -13,9 +13,11 @@ package Bugzilla::DB::Schema::Oracle;
#
###############################################################################
+use 5.10.1;
use strict;
+use warnings;
-use base qw(Bugzilla::DB::Schema);
+use parent qw(Bugzilla::DB::Schema);
use Carp qw(confess);
use Bugzilla::Util;
@@ -55,7 +57,7 @@ sub _initialize {
LONGBLOB => 'blob',
DATETIME => 'date',
-
+ DATE => 'date',
};
$self->_adjust_schema;
@@ -503,3 +505,33 @@ sub get_set_serial_sql {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item get_rename_column_ddl
+
+=item get_add_fks_sql
+
+=item get_drop_index_ddl
+
+=item get_rename_table_sql
+
+=item get_add_column_ddl
+
+=item get_set_serial_sql
+
+=item get_drop_column_ddl
+
+=item get_drop_table_ddl
+
+=item get_drop_fk_sql
+
+=item get_table_ddl
+
+=item get_alter_column_ddl
+
+=item get_fk_ddl
+
+=back
diff --git a/Bugzilla/DB/Schema/Pg.pm b/Bugzilla/DB/Schema/Pg.pm
index 3dc0fe85e..55a932272 100644
--- a/Bugzilla/DB/Schema/Pg.pm
+++ b/Bugzilla/DB/Schema/Pg.pm
@@ -13,8 +13,11 @@ package Bugzilla::DB::Schema::Pg;
#
###############################################################################
+use 5.10.1;
use strict;
-use base qw(Bugzilla::DB::Schema);
+use warnings;
+
+use parent qw(Bugzilla::DB::Schema);
use Storable qw(dclone);
#------------------------------------------------------------------------------
@@ -64,7 +67,7 @@ sub _initialize {
LONGBLOB => 'bytea',
DATETIME => 'timestamp(0) without time zone',
-
+ DATE => 'date',
};
$self->_adjust_schema;
@@ -186,3 +189,17 @@ sub _get_alter_type_sql {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item get_rename_column_ddl
+
+=item get_rename_table_sql
+
+=item get_create_database_sql
+
+=item get_set_serial_sql
+
+=back
diff --git a/Bugzilla/DB/Schema/Sqlite.pm b/Bugzilla/DB/Schema/Sqlite.pm
index 4cfc2b052..ccdbfd8aa 100644
--- a/Bugzilla/DB/Schema/Sqlite.pm
+++ b/Bugzilla/DB/Schema/Sqlite.pm
@@ -5,9 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
package Bugzilla::DB::Schema::Sqlite;
-use base qw(Bugzilla::DB::Schema);
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::DB::Schema);
use Bugzilla::Error;
use Bugzilla::Util qw(generate_random_password);
@@ -43,6 +47,7 @@ sub _initialize {
LONGBLOB => 'blob',
DATETIME => 'DATETIME',
+ DATE => 'DATETIME',
};
$self->_adjust_schema;
@@ -296,3 +301,25 @@ sub get_drop_fk_sql {
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item get_rename_column_ddl
+
+=item get_add_fks_sql
+
+=item get_drop_fk_sql
+
+=item get_create_database_sql
+
+=item get_alter_column_ddl
+
+=item get_add_column_ddl
+
+=item get_type_ddl
+
+=item get_drop_column_ddl
+
+=back
diff --git a/Bugzilla/DB/Sqlite.pm b/Bugzilla/DB/Sqlite.pm
index 47cb0cd25..ddafc1696 100644
--- a/Bugzilla/DB/Sqlite.pm
+++ b/Bugzilla/DB/Sqlite.pm
@@ -5,9 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
package Bugzilla::DB::Sqlite;
-use base qw(Bugzilla::DB);
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::DB);
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -296,3 +300,39 @@ SQLite-specific implementation. It is instantiated by the Bugzilla::DB module
and should never be used directly.
For interface details see L<Bugzilla::DB> and L<DBI>.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item sql_date_format
+
+=item bz_explain
+
+=item sql_position
+
+=item sql_iposition
+
+=item sql_group_by
+
+=item sql_not_regexp
+
+=item sql_limit
+
+=item sql_date_math
+
+=item sql_to_days
+
+=item sql_from_days
+
+=item bz_table_list_real
+
+=item sql_regexp
+
+=item sql_group_concat
+
+=item sql_istring
+
+=item bz_setup_database
+
+=back
diff --git a/Bugzilla/Error.pm b/Bugzilla/Error.pm
index 32c7715b4..ef6320d15 100644
--- a/Bugzilla/Error.pm
+++ b/Bugzilla/Error.pm
@@ -7,8 +7,11 @@
package Bugzilla::Error;
+use 5.10.1;
use strict;
-use base qw(Exporter);
+use warnings;
+
+use parent qw(Exporter);
@Bugzilla::Error::EXPORT = qw(ThrowCodeError ThrowTemplateError ThrowUserError);
@@ -93,7 +96,7 @@ sub _throw_error {
if (Bugzilla->error_mode == ERROR_MODE_WEBPAGE) {
my $cgi = Bugzilla->cgi;
- $cgi->close_standby_message('text/html', 'inline');
+ $cgi->close_standby_message('text/html', 'inline', 'error', 'html');
print $message;
print $cgi->multipart_final() if $cgi->{_multipart_in_progress};
}
@@ -104,7 +107,8 @@ sub _throw_error {
die("$message\n");
}
elsif (Bugzilla->error_mode == ERROR_MODE_DIE_SOAP_FAULT
- || Bugzilla->error_mode == ERROR_MODE_JSON_RPC)
+ || 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() };
@@ -121,13 +125,20 @@ sub _throw_error {
}
else {
my $server = Bugzilla->_json_server;
+
+ my $status_code = 0;
+ if (Bugzilla->error_mode == ERROR_MODE_REST) {
+ my %status_code_map = %{ REST_STATUS_CODE_MAP() };
+ $status_code = $status_code_map{$code} || $status_code_map{'_default'};
+ }
# Technically JSON-RPC isn't allowed to have error numbers
# higher than 999, but we do this to avoid conflicts with
# the internal JSON::RPC error codes.
- $server->raise_error(code => 100000 + $code,
- message => $message,
- id => $server->{_bz_request_id},
- version => $server->version);
+ $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
@@ -182,24 +193,18 @@ sub ThrowTemplateError {
my $maintainer = Bugzilla->params->{'maintainer'};
my $error = html_quote($vars->{'template_error_msg'});
my $error2 = html_quote($template->error());
+ my $url = html_quote(Bugzilla->cgi->self_url);
+
print <<END;
- <tt>
<p>
Bugzilla has suffered an internal error. Please save this page and
send it to $maintainer with details of what you were doing at the
time this message appeared.
</p>
- <script type="text/javascript"> <!--
- document.write("<p>URL: " +
- document.location.href.replace(/&/g,"&amp;")
- .replace(/</g,"&lt;")
- .replace(/>/g,"&gt;") + "</p>");
- // -->
- </script>
+ <p>URL: $url</p>
<p>Template->process() failed twice.<br>
First error: $error<br>
Second error: $error2</p>
- </tt>
END
}
exit;
diff --git a/Bugzilla/Extension.pm b/Bugzilla/Extension.pm
index 49422eca3..e24ceb9eb 100644
--- a/Bugzilla/Extension.pm
+++ b/Bugzilla/Extension.pm
@@ -6,7 +6,10 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Extension;
+
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -238,7 +241,7 @@ F<extensions/Foo.pm>:
package Bugzilla::Extension::Foo
use strict;
- use base qw(Bugzilla::Extension);
+ use parent qw(Bugzilla::Extension);
our $VERSION = '0.02';
use constant NAME => 'Foo';
@@ -648,6 +651,21 @@ So, for example, if you had a CSS file called F<style.css> and your
extension was called F<Foo>, your file would go into
F<extensions/Foo/web/style.css>.
+=head2 Documenting Extensions
+
+Documentation goes in F<extensions/Foo/docs/en/rst/>, if it's in English, or
+change "en" to something else if it's not. The user documentation index file
+must be called index-user.rst; the admin documentation must be called
+index-admin.rst. These will end up in the User Guide and the Administration
+Guide respectively. Both documentation types are optional. You can use various
+Sphinx constructs such as :toctree: or :include: to include further reST files
+if you need more than one page of docs.
+
+When documenting extensions to the Bugzilla API, if your extension provides
+them, the index file would be F<extensions/Foo/docs/en/rst/api/v1/index.rst>.
+When and if your API has more than one version, increment the version number.
+These docs will get included in the WebService API Reference.
+
=head2 Disabling Your Extension
If you want your extension to be totally ignored by Bugzilla (it will
@@ -807,3 +825,13 @@ package name of the loaded extension.
Calls L</load> for every enabled extension installed into Bugzilla,
and returns an arrayref of all the package names that were loaded.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item modify_inc
+
+=item my_inc
+
+=back
diff --git a/Bugzilla/Field.pm b/Bugzilla/Field.pm
index 0c9da9b56..761f7b94e 100644
--- a/Bugzilla/Field.pm
+++ b/Bugzilla/Field.pm
@@ -57,9 +57,11 @@ in addition to what is documented here.
package Bugzilla::Field;
+use 5.10.1;
use strict;
+use warnings;
-use base qw(Exporter Bugzilla::Object);
+use parent qw(Exporter Bugzilla::Object);
@Bugzilla::Field::EXPORT = qw(check_field get_field_id get_legal_field_values);
use Bugzilla::Constants;
@@ -73,6 +75,8 @@ use Scalar::Util qw(blessed);
#### Initialization ####
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_TABLE => 'fielddefs';
use constant LIST_ORDER => 'sortkey, name';
@@ -150,7 +154,9 @@ use constant SQL_DEFINITIONS => {
FIELD_TYPE_TEXTAREA, { TYPE => 'MEDIUMTEXT',
NOTNULL => 1, DEFAULT => "''"},
FIELD_TYPE_DATETIME, { TYPE => 'DATETIME' },
+ FIELD_TYPE_DATE, { TYPE => 'DATE' },
FIELD_TYPE_BUG_ID, { TYPE => 'INT3' },
+ FIELD_TYPE_INTEGER, { TYPE => 'INT4', NOTNULL => 1, DEFAULT => 0 },
};
# Field definitions for the fields that ship with Bugzilla.
@@ -204,9 +210,9 @@ use constant DEFAULT_FIELDS => (
in_new_bugmail => 0, buglist => 1},
{name => 'cc', desc => 'CC', in_new_bugmail => 1},
{name => 'dependson', desc => 'Depends on', in_new_bugmail => 1,
- is_numeric => 1},
+ is_numeric => 1, buglist => 1},
{name => 'blocked', desc => 'Blocks', in_new_bugmail => 1,
- is_numeric => 1},
+ is_numeric => 1, buglist => 1},
{name => 'attachments.description', desc => 'Attachment description'},
{name => 'attachments.filename', desc => 'Attachment filename'},
@@ -220,7 +226,7 @@ use constant DEFAULT_FIELDS => (
{name => 'attachments.submitter', desc => 'Attachment creator'},
{name => 'target_milestone', desc => 'Target Milestone',
- buglist => 1},
+ in_new_bugmail => 1, buglist => 1},
{name => 'creation_ts', desc => 'Creation date',
buglist => 1},
{name => 'delta_ts', desc => 'Last changed date',
@@ -257,8 +263,11 @@ use constant DEFAULT_FIELDS => (
{name => "owner_idle_time", desc => "Time Since Assignee Touched"},
{name => 'see_also', desc => "See Also",
type => FIELD_TYPE_BUG_URLS},
- {name => 'tag', desc => 'Tags', buglist => 1,
+ {name => 'tag', desc => 'Personal Tags', buglist => 1,
type => FIELD_TYPE_KEYWORDS},
+ {name => 'last_visit_ts', desc => 'Last Visit', buglist => 1,
+ type => FIELD_TYPE_DATETIME},
+ {name => 'comment_tag', desc => 'Comment Tag'},
);
################
@@ -354,9 +363,7 @@ sub _check_sortkey {
sub _check_type {
my ($invocant, $type, undef, $params) = @_;
my $saved_type = $type;
- # The constant here should be updated every time a new,
- # higher field type is added.
- (detaint_natural($type) && $type <= FIELD_TYPE_KEYWORDS)
+ (detaint_natural($type) && $type < FIELD_TYPE_HIGHEST_PLUS_ONE)
|| ThrowCodeError('invalid_customfield_type', { type => $saved_type });
my $custom = blessed($invocant) ? $invocant->custom : $params->{custom};
@@ -957,7 +964,10 @@ sub remove_from_db {
}
else {
$bugs_query = "SELECT COUNT(*) FROM bugs WHERE $name IS NOT NULL";
- if ($self->type != FIELD_TYPE_BUG_ID && $self->type != FIELD_TYPE_DATETIME) {
+ 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
@@ -1055,6 +1065,7 @@ sub create {
$field->_update_visibility_values();
$dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
if ($field->custom) {
my $name = $field->name;
@@ -1077,6 +1088,9 @@ sub create {
# Restore the original obsolete state of the custom field.
$dbh->do('UPDATE fielddefs SET obsolete = 0 WHERE id = ?', undef, $field->id)
unless $is_obsolete;
+
+ Bugzilla->memcached->clear({ table => 'fielddefs', id => $field->id });
+ Bugzilla->memcached->clear_config();
}
return $field;
@@ -1090,6 +1104,7 @@ sub update {
$dbh->do("UPDATE " . $self->name . " SET visibility_value_id = NULL");
}
$self->_update_visibility_values();
+ Bugzilla->memcached->clear_config();
return $changes;
}
@@ -1327,7 +1342,7 @@ sub check_field {
Description: Returns the ID of the specified field name and throws
an error if this field does not exist.
-Params: $name - a field name
+Params: $fieldname - a field name
Returns: the corresponding field ID or an error if the field name
does not exist.
@@ -1337,17 +1352,24 @@ Returns: the corresponding field ID or an error if the field name
=cut
sub get_field_id {
- my ($name) = @_;
- my $dbh = Bugzilla->dbh;
+ my $field = Bugzilla->fields({ by_name => 1 })->{$_[0]}
+ or ThrowCodeError('invalid_field_name', {field => $_[0]});
- 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
+ return $field->id;
}
1;
__END__
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item match
+
+=item set_is_numeric
+
+=item update
+
+=back
diff --git a/Bugzilla/Field/Choice.pm b/Bugzilla/Field/Choice.pm
index a292185c4..a66f69cee 100644
--- a/Bugzilla/Field/Choice.pm
+++ b/Bugzilla/Field/Choice.pm
@@ -5,11 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Field::Choice;
-use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
use Bugzilla::Config qw(SetParam write_params);
use Bugzilla::Constants;
@@ -23,6 +25,8 @@ use Scalar::Util qw(blessed);
# Initialization #
##################
+use constant IS_CONFIG => 1;
+
use constant DB_COLUMNS => qw(
id
value
@@ -94,7 +98,7 @@ sub type {
if (!defined *{"${package}::DB_TABLE"}) {
eval <<EOC;
package $package;
- use base qw(Bugzilla::Field::Choice);
+ use parent qw(Bugzilla::Field::Choice);
use constant DB_TABLE => '$field_name';
EOC
}
@@ -333,3 +337,23 @@ must call C<type> to get a class you can call methods on.
This class implements mutators for all of the settable accessors in
L<Bugzilla::Field::ChoiceInterface>.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item create
+
+=item remove_from_db
+
+=item set_is_active
+
+=item set_sortkey
+
+=item set_name
+
+=item update
+
+=item set_visibility_value
+
+=back
diff --git a/Bugzilla/Field/ChoiceInterface.pm b/Bugzilla/Field/ChoiceInterface.pm
index b2f1bffd2..634d36ad1 100644
--- a/Bugzilla/Field/ChoiceInterface.pm
+++ b/Bugzilla/Field/ChoiceInterface.pm
@@ -6,7 +6,10 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Field::ChoiceInterface;
+
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -269,3 +272,13 @@ I<any> of the currently selected values are this value.
Returns C<0> otherwise.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item FIELD_NAME
+
+=item controlled_values_array
+
+=back
diff --git a/Bugzilla/Flag.pm b/Bugzilla/Flag.pm
index affeaee68..50474b885 100644
--- a/Bugzilla/Flag.pm
+++ b/Bugzilla/Flag.pm
@@ -5,10 +5,12 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Flag;
+use 5.10.1;
+use strict;
+use warnings;
+
=head1 NAME
Bugzilla::Flag - A module to deal with Bugzilla flag values.
@@ -49,7 +51,7 @@ use Bugzilla::Mailer;
use Bugzilla::Constants;
use Bugzilla::Field;
-use base qw(Bugzilla::Object Exporter);
+use parent qw(Bugzilla::Object Exporter);
@Bugzilla::Flag::EXPORT = qw(SKIP_REQUESTEE_ON_ERROR);
###############################
@@ -180,22 +182,20 @@ is an attachment flag, else undefined.
sub type {
my $self = shift;
- $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
- return $self->{'type'};
+ return $self->{'type'} ||= new Bugzilla::FlagType($self->{'type_id'});
}
sub setter {
my $self = shift;
- $self->{'setter'} ||= new Bugzilla::User($self->{'setter_id'});
- return $self->{'setter'};
+ return $self->{'setter'} ||= new Bugzilla::User({ id => $self->{'setter_id'}, cache => 1 });
}
sub requestee {
my $self = shift;
if (!defined $self->{'requestee'} && $self->{'requestee_id'}) {
- $self->{'requestee'} = new Bugzilla::User($self->{'requestee_id'});
+ $self->{'requestee'} = new Bugzilla::User({ id => $self->{'requestee_id'}, cache => 1 });
}
return $self->{'requestee'};
}
@@ -205,16 +205,15 @@ sub attachment {
return undef unless $self->attach_id;
require Bugzilla::Attachment;
- $self->{'attachment'} ||= new Bugzilla::Attachment($self->attach_id);
- return $self->{'attachment'};
+ return $self->{'attachment'}
+ ||= new Bugzilla::Attachment({ id => $self->attach_id, cache => 1 });
}
sub bug {
my $self = shift;
require Bugzilla::Bug;
- $self->{'bug'} ||= new Bugzilla::Bug($self->bug_id);
- return $self->{'bug'};
+ return $self->{'bug'} ||= new Bugzilla::Bug({ id => $self->bug_id, cache => 1 });
}
################################
@@ -464,6 +463,7 @@ sub update {
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;
}
@@ -610,6 +610,7 @@ sub force_retarget {
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.
@@ -670,10 +671,15 @@ sub _check_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.
- ThrowUserError('flag_requestee_disabled', { type => $self->type })
+ # user can either remove them or leave them alone.
+ ThrowUserError('flag_type_requestee_disabled', { type => $self->type })
if !$self->type->is_requesteeble;
+ # You can't ask a disabled account, as they don't have the ability to
+ # set the flag.
+ ThrowUserError('flag_requestee_disabled', { requestee => $requestee })
+ if !$requestee->is_enabled;
+
# Make sure the requestee can see the bug.
# Note that can_see_bug() will query the DB, so if the bug
# is being added/removed from some groups and these changes
@@ -821,7 +827,7 @@ sub extract_flags_from_cgi {
# Extract a list of existing flag IDs.
my @flag_ids = map(/^flag-(\d+)$/ ? $1 : (), $cgi->param());
- return () if (!scalar(@flagtype_ids) && !scalar(@flag_ids));
+ return ([], []) unless (scalar(@flagtype_ids) || scalar(@flag_ids));
my (@new_flags, @flags);
foreach my $flag_id (@flag_ids) {
@@ -927,6 +933,117 @@ sub extract_flags_from_cgi {
=over
+=item C<multi_extract_flags_from_cgi($bug, $hr_vars)>
+
+Checks whether or not there are new flags to create and returns an
+array of hashes. This array is then passed to Flag::create(). This differs
+from the previous sub-routine as it is called for changing multiple bugs
+
+=back
+
+=cut
+
+sub multi_extract_flags_from_cgi {
+ my ($class, $bug, $vars, $skip) = @_;
+ my $cgi = Bugzilla->cgi;
+
+ my $match_status = Bugzilla::User::match_field({
+ '^requestee(_type)?-(\d+)$' => { 'type' => 'multi' },
+ }, undef, $skip);
+
+ $vars->{'match_field'} = 'requestee';
+ if ($match_status == USER_MATCH_FAILED) {
+ $vars->{'message'} = 'user_match_failed';
+ }
+ elsif ($match_status == USER_MATCH_MULTIPLE) {
+ $vars->{'message'} = 'user_match_multiple';
+ }
+
+ # Extract a list of flag type IDs from field names.
+ my @flagtype_ids = map(/^flag_type-(\d+)$/ ? $1 : (), $cgi->param());
+
+ my (@new_flags, @flags);
+
+ # Get a list of active flag types available for this product/component.
+ my $flag_types = Bugzilla::FlagType::match(
+ { 'product_id' => $bug->{'product_id'},
+ 'component_id' => $bug->{'component_id'},
+ 'is_active' => 1 });
+
+ foreach my $flagtype_id (@flagtype_ids) {
+ # Checks if there are unexpected flags for the product/component.
+ if (!scalar(grep { $_->id == $flagtype_id } @$flag_types)) {
+ $vars->{'message'} = 'unexpected_flag_types';
+ last;
+ }
+ }
+
+ foreach my $flag_type (@$flag_types) {
+ my $type_id = $flag_type->id;
+
+ # Bug flags are only valid for bugs
+ next unless ($flag_type->target_type eq 'bug');
+
+ # We are only interested in flags the user tries to create.
+ next unless scalar(grep { $_ == $type_id } @flagtype_ids);
+
+ # Get the flags of this type already set for this bug.
+ my $current_flags = $class->match(
+ { 'type_id' => $type_id,
+ 'target_type' => 'bug',
+ 'bug_id' => $bug->bug_id });
+
+ # We will update existing flags (instead of creating new ones)
+ # if the flag exists and the user has not chosen the 'always add'
+ # option
+ my $update = scalar(@$current_flags) && ! $cgi->param("flags_add-$type_id");
+
+ my $status = $cgi->param("flag_type-$type_id");
+ trick_taint($status);
+
+ my @logins = $cgi->param("requestee_type-$type_id");
+ if ($status eq "?" && scalar(@logins)) {
+ foreach my $login (@logins) {
+ if ($update) {
+ foreach my $current_flag (@$current_flags) {
+ push (@flags, { id => $current_flag->id,
+ status => $status,
+ requestee => $login,
+ skip_roe => $skip });
+ }
+ }
+ else {
+ push (@new_flags, { type_id => $type_id,
+ status => $status,
+ requestee => $login,
+ skip_roe => $skip });
+ }
+
+ last unless $flag_type->is_multiplicable;
+ }
+ }
+ else {
+ if ($update) {
+ foreach my $current_flag (@$current_flags) {
+ push (@flags, { id => $current_flag->id,
+ status => $status });
+ }
+ }
+ else {
+ push (@new_flags, { type_id => $type_id,
+ status => $status });
+ }
+ }
+ }
+
+ # Return the list of flags to update and/or to create.
+ return (\@flags, \@new_flags);
+}
+
+=pod
+
+=over
+
=item C<notify($flag, $old_flag, $object, $timestamp)>
Sends an email notification about a flag being created, fulfilled
@@ -1087,3 +1204,29 @@ sub _flag_types {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item update_activity
+
+=item setter_id
+
+=item bug
+
+=item requestee_id
+
+=item DB_COLUMNS
+
+=item set_flag
+
+=item type_id
+
+=item snapshot
+
+=item update_flags
+
+=item update
+
+=back
diff --git a/Bugzilla/FlagType.pm b/Bugzilla/FlagType.pm
index 9e7ab09de..72b3f64c1 100644
--- a/Bugzilla/FlagType.pm
+++ b/Bugzilla/FlagType.pm
@@ -5,10 +5,12 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::FlagType;
+use 5.10.1;
+use strict;
+use warnings;
+
=head1 NAME
Bugzilla::FlagType - A module to deal with Bugzilla flag types.
@@ -41,7 +43,7 @@ use Bugzilla::Group;
use Email::Address;
use List::MoreUtils qw(uniq);
-use base qw(Bugzilla::Object);
+use parent qw(Bugzilla::Object);
###############################
#### Initialization ####
@@ -185,8 +187,16 @@ sub update {
# Silently remove requestees from flags which are no longer
# specifically requestable.
if (!$self->is_requesteeble) {
- $dbh->do('UPDATE flags SET requestee_id = NULL WHERE type_id = ?',
- undef, $self->id);
+ my $ids = $dbh->selectcol_arrayref(
+ 'SELECT id FROM flags WHERE type_id = ? AND requestee_id IS NOT NULL',
+ undef, $self->id);
+
+ if (@$ids) {
+ $dbh->do('UPDATE flags SET requestee_id = NULL WHERE ' . $dbh->sql_in('id', $ids));
+ foreach my $id (@$ids) {
+ Bugzilla->memcached->clear({ table => 'flags', id => $id });
+ }
+ }
}
$dbh->bz_commit_transaction();
@@ -650,9 +660,19 @@ sub sqlify_criteria {
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 (ref($criteria->{name}) eq 'ARRAY') {
+ my @names = map { $dbh->quote($_) } @{$criteria->{name}};
+ # Detaint data as we have quoted it.
+ foreach my $name (@names) {
+ trick_taint($name);
+ }
+ push @criteria, $dbh->sql_in('flagtypes.name', \@names);
+ }
+ else {
+ my $name = $dbh->quote($criteria->{name});
+ trick_taint($name); # Detaint data as we have quoted it.
+ push(@criteria, "flagtypes.name = $name");
+ }
}
if ($criteria->{target_type}) {
# The target type is stored in the database as a one-character string
@@ -716,3 +736,43 @@ sub sqlify_criteria {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item exclusions_as_hash
+
+=item request_group_id
+
+=item set_is_active
+
+=item set_is_multiplicable
+
+=item inclusions_as_hash
+
+=item set_sortkey
+
+=item grant_group_id
+
+=item set_cc_list
+
+=item set_request_group
+
+=item set_name
+
+=item set_is_specifically_requestable
+
+=item set_grant_group
+
+=item create
+
+=item set_clusions
+
+=item set_description
+
+=item set_is_requestable
+
+=item update
+
+=back
diff --git a/Bugzilla/Group.pm b/Bugzilla/Group.pm
index 5404dec7e..07b78e366 100644
--- a/Bugzilla/Group.pm
+++ b/Bugzilla/Group.pm
@@ -5,11 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Group;
-use base qw(Bugzilla::Object);
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Util;
@@ -20,6 +22,8 @@ use Bugzilla::Config qw(:admin);
##### Module Initialization ###
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_COLUMNS => qw(
groups.id
groups.name
@@ -52,8 +56,10 @@ use constant UPDATE_COLUMNS => qw(
);
# Parameters that are lists of groups.
-use constant GROUP_PARAMS => qw(chartgroup insidergroup timetrackinggroup
- querysharegroup debug_group);
+use constant GROUP_PARAMS => qw(
+ chartgroup comment_taggers_group debug_group insidergroup
+ querysharegroup timetrackinggroup
+);
###############################
#### Accessors ######
@@ -92,7 +98,8 @@ sub 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 $grant_clause = defined($grant_type) ? "AND grant_type = $grant_type"
+ : "";
my $user_ids = $dbh->selectcol_arrayref(
"SELECT DISTINCT user_id
FROM user_group_map
@@ -213,6 +220,7 @@ sub update {
Bugzilla::Hook::process('group_end_of_update',
{ group => $self, changes => $changes });
$dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
return $changes;
}
@@ -647,3 +655,47 @@ user_group_map for any user with DIRECT or REGEXP membership IN() the list
of groups returned.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item icon_url
+
+=item set_name
+
+=item bugs
+
+=item granted_by_direct
+
+=item set_user_regexp
+
+=item flag_types
+
+=item products
+
+=item set_icon_url
+
+=item set_description
+
+=item set_is_active
+
+=item user_regexp
+
+=item members_direct
+
+=item is_bug_group
+
+=item grant_direct
+
+=item description
+
+=item is_active
+
+=item remove_from_db
+
+=item is_active_bug_group
+
+=item update
+
+=back
diff --git a/Bugzilla/Hook.pm b/Bugzilla/Hook.pm
index b75b329d5..d6ba5e1d0 100644
--- a/Bugzilla/Hook.pm
+++ b/Bugzilla/Hook.pm
@@ -6,7 +6,10 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Hook;
+
+use 5.10.1;
use strict;
+use warnings;
sub process {
my ($name, $args) = @_;
@@ -585,7 +588,7 @@ about, and the value should always be C<1>. The "relationships"
are described by the various C<REL_> constants in L<Bugzilla::Constants>.
Here's an example of adding userid C<123> to the recipient list
-as though he were on the CC list:
+as though they were on the CC list:
$recipients->{123}->{+REL_CC} = 1
@@ -639,6 +642,33 @@ spaces.
=back
+=head2 cgi_headers
+
+This allows you to modify the HTTP headers sent out on every Bugzilla
+response.
+
+Params:
+
+=over
+
+=item C<headers>
+
+A hashref, where the keys are header names and the values are header
+values. Keys need to be lower-case, and begin with a "-". If you use
+the "_" character it will be converted to "-", and the library will
+also fix the casing to Camel-Case.
+
+You can delete (some) headers that Bugzilla adds by deleting entries
+from the hash.
+
+=item C<cgi>
+
+The CGI object, which may tell you useful things about the response on
+which to base a decision of whether or not to add a header.
+
+=back
+
+
=head2 config_add_panels
If you want to add new panels to the Parameters administrative interface,
@@ -1003,9 +1033,6 @@ Params:
=item C<email> - The C<Email::MIME> object that's about to be sent.
-=item C<mailer_args> - An arrayref that's passed as C<mailer_args> to
-L<Email::Send/new>.
-
=back
=head2 object_before_create
@@ -1499,6 +1526,24 @@ name), you can get it from here.
=back
+=head2 user_check_account_creation
+
+This hook permits you to do extra checks before the creation of a new user
+account. This hook is called after email address validation has been done.
+Note that this hook can also access the IP address of the requester thanks
+to the C<remote_ip()> subroutine exported by C<Bugzilla::Util>.
+
+Params:
+
+=over
+
+=item C<login>
+
+The login of the new account. This is usually an email address, unless the
+C<emailsuffix> parameter is not empty.
+
+=back
+
=head2 user_preferences
This hook allows you to add additional panels to the User Preferences page,
@@ -1594,6 +1639,109 @@ See L<Bugzilla::WebService::Constants/WS_ERROR_CODE> for an example.
=back
+=head2 webservice_fix_credentials
+
+This hook allows for altering the credential parameters provided by the client
+before authentication actually occurs. For example, this can be used to allow mapping
+of custom parameters to the standard Bugzilla_login and Bugzilla_password parameters.
+
+Params:
+
+=over
+
+=item C<params>
+
+A hash ref containing the parameters passed into the webservice after
+they have been obtained from the URL or body of the request.
+
+=back
+
+=head2 webservice_rest_request
+
+This hook allows for altering any of the parameters provided by the client
+after authentication has occured. You are able to change things like renaming
+of keys, removing values, or adding additional information.
+
+Params:
+
+=over
+
+=item C<params>
+
+A hash ref containing the parameters passed into the webservice after
+they have been obtained from the URL or body of the request.
+
+=item C<rpc>
+
+The current JSONRPC, XMLRPC, or REST object.
+
+=back
+
+=head2 webservice_rest_resources
+
+This hook allows for altering of the REST resources data allowing you to
+add additional paths to perform additional operations or to override the
+resources already provided by the webservice modules.
+
+Params:
+
+=over
+
+=item C<resources>
+
+A hash returned from each module loaded that is used to determine
+which code handler to use based on a regex match of the CGI path.
+
+=item C<rpc>
+
+The current JSONRPC, XMLRPC, or REST object.
+
+=back
+
+=head2 webservice_rest_response
+
+This hook allows for altering the result data or response object
+that is being returned by the current REST webservice call.
+
+Params:
+
+=over
+
+=item C<response>
+
+The HTTP response object generated by JSON-RPC library. You can use this
+to add headers, etc.
+
+=item C<result>
+
+A reference to a hash that contains the result data.
+
+=item C<rpc>
+
+The current JSONRPC, XMLRPC, or REST object.
+
+=back
+
+=head2 webservice_status_code_map
+
+This hook allows an extension to change the status codes returned by
+specific webservice errors. The valid internal error codes that Bugzilla
+generates, and the status codes they map to by default, are defined in the
+C<WS_ERROR_CODE> constant in C<Bugzilla::WebService::Constants>. When
+remapping an error, you may wish to use an existing status code constant.
+Such constants are also in C<Bugzilla::WebService::Constants> and start
+with C<STATUS_*> such as C<STATUS_BAD_REQUEST>.
+
+Params:
+
+=over
+
+=item C<status_code_map>
+
+A hash reference containing the current status code mapping.
+
+=back
+
=head1 SEE ALSO
L<Bugzilla::Extension>
diff --git a/Bugzilla/Install.pm b/Bugzilla/Install.pm
index 4954dbfd0..07bc9d6c3 100644
--- a/Bugzilla/Install.pm
+++ b/Bugzilla/Install.pm
@@ -15,7 +15,9 @@ package Bugzilla::Install;
# make those assumptions, then it should go into one of the
# packages under the Bugzilla::Install namespace.
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Component;
use Bugzilla::Config qw(:admin);
@@ -25,7 +27,7 @@ use Bugzilla::Group;
use Bugzilla::Product;
use Bugzilla::User;
use Bugzilla::User::Setting;
-use Bugzilla::Util qw(get_text say);
+use Bugzilla::Util qw(get_text);
use Bugzilla::Version;
use constant STATUS_WORKFLOW => (
@@ -86,6 +88,8 @@ sub SETTINGS {
requestee_cc => { options => ['on', 'off'], default => 'on' },
# 2012-04-30 glob@mozilla.com -- Bug 663747
bugmail_new_prefix => { options => ['on', 'off'], default => 'on' },
+ # 2013-07-26 joshi_sunil@in.com -- Bug 669535
+ possible_duplicates => { options => ['on', 'off'], default => 'on' },
}
};
@@ -492,3 +496,19 @@ Params: none
Returns: nothing
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item update_system_groups
+
+=item reset_password
+
+=item make_admin
+
+=item create_admin
+
+=item init_workflow
+
+=back
diff --git a/Bugzilla/Install/CPAN.pm b/Bugzilla/Install/CPAN.pm
index 8a880df80..094784e1a 100644
--- a/Bugzilla/Install/CPAN.pm
+++ b/Bugzilla/Install/CPAN.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Install::CPAN;
+
+use 5.10.1;
use strict;
-use base qw(Exporter);
+use warnings;
+
+use parent qw(Exporter);
our @EXPORT = qw(
BZ_LIB
@@ -161,20 +165,9 @@ sub install_module {
if (!$module) {
die install_string('no_such_module', { module => $name }) . "\n";
}
- my $version = $module->cpan_version;
- my $module_name = $name;
-
- if ($name eq 'LWP::UserAgent' && $^V lt v5.8.8) {
- # LWP 6.x requires Perl 5.8.8 or newer.
- # As PAUSE only indexes the very last version of each module,
- # we have to specify the path to the tarball ourselves.
- $name = 'GAAS/libwww-perl-5.837.tar.gz';
- # This tarball contains LWP::UserAgent 5.835.
- $version = '5.835';
- }
print install_string('install_module',
- { module => $module_name, version => $version }) . "\n";
+ { module => $name, version => $module->cpan_version }) . "\n";
if ($test) {
CPAN::Shell->force('install', $name);
@@ -336,3 +329,11 @@ Note that calling this function prints a B<lot> of information to
STDOUT and STDERR.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item check_cpan_requirements
+
+=back
diff --git a/Bugzilla/Install/DB.pm b/Bugzilla/Install/DB.pm
index 023ef7523..96f14ec0f 100644
--- a/Bugzilla/Install/DB.pm
+++ b/Bugzilla/Install/DB.pm
@@ -10,7 +10,9 @@ package Bugzilla::Install::DB;
# NOTE: This package may "use" any modules that it likes,
# localconfig is available, and params are up to date.
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Constants;
use Bugzilla::Hook;
@@ -23,6 +25,7 @@ use Bugzilla::Field;
use Date::Parse;
use Date::Format;
+use Digest;
use IO::File;
use List::MoreUtils qw(uniq);
use URI;
@@ -270,10 +273,6 @@ sub update_table_definitions {
$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");
@@ -700,12 +699,36 @@ sub update_table_definitions {
# 2012-08-01 koosha.khajeh@gmail.com - Bug 187753
_shorten_long_quips();
- # 2012-12-23 LpSolit@gmail.com - Bug 824361
+ # 2012-12-29 reed@reedloden.com - Bug 785283
+ _add_password_salt_separator();
+
+ # 2013-01-02 LpSolit@gmail.com - Bug 824361
_fix_longdescs_indexes();
# 2013-02-04 dkl@mozilla.com - Bug 824346
_fix_flagclusions_indexes();
+ # 2013-08-26 sgreen@redhat.com - Bug 903895
+ _fix_components_primary_key();
+
+ # 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-27 LpSolit@gmail.com - Bug 1044561
+ _fix_user_api_keys_indexes();
+
+ # 2014-08-11 sgreen@redhat.com - Bug 1012506
+ _update_alias();
+
+ # 2014-11-10 dkl@mozilla.com - Bug 1093928
+ $dbh->bz_drop_column('longdescs', 'is_markdown');
+
################################################################
# New --TABLE-- changes should go *** A B O V E *** this point #
################################################################
@@ -863,8 +886,8 @@ sub _populate_longdescs {
if (!$who) {
# This username doesn't exist. Maybe someone
- # renamed him or something. Invent a new profile
- # entry disabled, just to represent him.
+ # renamed them or something. Invent a new profile
+ # entry disabled, just to represent them.
$dbh->do("INSERT INTO profiles (login_name,
cryptpassword, disabledtext)
VALUES (?,?,?)", undef, $name, '*',
@@ -1424,9 +1447,9 @@ sub _use_ids_for_products_and_components {
print "Updating the database to use component IDs.\n";
$dbh->bz_add_column("components", "id",
- {TYPE => 'SMALLSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
$dbh->bz_add_column("bugs", "component_id",
- {TYPE => 'INT2', NOTNULL => 1}, 0);
+ {TYPE => 'INT3', NOTNULL => 1}, 0);
my %components;
$sth = $dbh->prepare("SELECT id, value, product_id FROM components");
@@ -2537,7 +2560,7 @@ sub _fix_whine_queries_title_and_op_sys_value {
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.
+ # make $datadir/params.json 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.)
@@ -3785,6 +3808,39 @@ sub _shorten_long_quips {
$dbh->bz_alter_column('quips', 'quip', { TYPE => 'varchar(512)', NOTNULL => 1});
}
+sub _add_password_salt_separator {
+ my $dbh = Bugzilla->dbh;
+
+ $dbh->bz_start_transaction();
+
+ my $profiles = $dbh->selectall_arrayref("SELECT userid, cryptpassword FROM profiles WHERE ("
+ . $dbh->sql_regexp("cryptpassword", "'^[^,]+{'") . ")");
+
+ if (@$profiles) {
+ say "Adding salt separator to password hashes...";
+
+ my $query = $dbh->prepare("UPDATE profiles SET cryptpassword = ? WHERE userid = ?");
+ my %algo_sizes;
+
+ foreach my $profile (@$profiles) {
+ my ($userid, $hash) = @$profile;
+ my ($algorithm) = $hash =~ /{([^}]+)}$/;
+
+ $algo_sizes{$algorithm} ||= length(Digest->new($algorithm)->b64digest);
+
+ # Calculate the salt length by taking the stored hash and
+ # subtracting the combined lengths of the hash size, the
+ # algorithm name, and 2 for the {} surrounding the name.
+ my $not_salt_len = $algo_sizes{$algorithm} + length($algorithm) + 2;
+ my $salt_len = length($hash) - $not_salt_len;
+
+ substr($hash, $salt_len, 0, ',');
+ $query->execute($hash, $userid);
+ }
+ }
+ $dbh->bz_commit_transaction();
+}
+
sub _fix_flagclusions_indexes {
my $dbh = Bugzilla->dbh;
foreach my $table ('flaginclusions', 'flagexclusions') {
@@ -3814,6 +3870,50 @@ sub _fix_flagclusions_indexes {
}
}
+sub _fix_components_primary_key {
+ my $dbh = Bugzilla->dbh;
+ if ($dbh->bz_column_info('components', 'id')->{TYPE} ne 'MEDIUMSERIAL') {
+ $dbh->bz_drop_related_fks('components', 'id');
+ $dbh->bz_alter_column("components", "id",
+ {TYPE => 'MEDIUMSERIAL', NOTNULL => 1, PRIMARYKEY => 1});
+ $dbh->bz_alter_column("flaginclusions", "component_id",
+ {TYPE => 'INT3'});
+ $dbh->bz_alter_column("flagexclusions", "component_id",
+ {TYPE => 'INT3'});
+ $dbh->bz_alter_column("bugs", "component_id",
+ {TYPE => 'INT3', NOTNULL => 1});
+ $dbh->bz_alter_column("component_cc", "component_id",
+ {TYPE => 'INT3', NOTNULL => 1});
+ }
+}
+
+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']);
+ }
+}
+
+sub _update_alias {
+ my $dbh = Bugzilla->dbh;
+ return unless $dbh->bz_column_info('bugs', 'alias');
+
+ # We need to move the aliases from the bugs table to the bugs_aliases table
+ $dbh->do(q{
+ INSERT INTO bugs_aliases (bug_id, alias)
+ SELECT bug_id, alias FROM bugs WHERE alias IS NOT NULL
+ });
+
+ $dbh->bz_drop_column('bugs', 'alias');
+}
+
1;
__END__
diff --git a/Bugzilla/Install/Filesystem.pm b/Bugzilla/Install/Filesystem.pm
index 457f66f9a..cf0a5b0ca 100644
--- a/Bugzilla/Install/Filesystem.pm
+++ b/Bugzilla/Install/Filesystem.pm
@@ -15,7 +15,9 @@ package Bugzilla::Install::Filesystem;
# * Files do not have the correct permissions.
# * The database does not exist.
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -28,10 +30,12 @@ use File::Find;
use File::Path;
use File::Basename;
use File::Copy qw(move);
+use File::Spec;
+use File::Slurp;
use IO::File;
use POSIX ();
-use base qw(Exporter);
+use parent qw(Exporter);
our @EXPORT = qw(
update_filesystem
create_htaccess
@@ -57,7 +61,7 @@ use constant HT_DEFAULT_DENY => <<EOT;
</IfVersion>
</IfModule>
<IfModule !mod_version.c>
- Deny from all
+ Deny from all
</IfModule>
EOT
@@ -130,6 +134,7 @@ sub FILESYSTEM {
my $localconfig = bz_locations()->{'localconfig'};
my $template_cache = bz_locations()->{'template_cache'};
my $graphsdir = bz_locations()->{'graphsdir'};
+ my $assetsdir = bz_locations()->{'assetsdir'};
# We want to set the permissions the same for all localconfig files
# across all PROJECTs, so we do something special with $localconfig,
@@ -165,6 +170,7 @@ sub FILESYSTEM {
'jobqueue.pl' => { perms => OWNER_EXECUTE },
'migrate.pl' => { perms => OWNER_EXECUTE },
'install-module.pl' => { perms => OWNER_EXECUTE },
+ 'clean-bug-user-last-visit.pl' => { perms => WS_EXECUTE },
'Bugzilla.pm' => { perms => CGI_READ },
"$localconfig*" => { perms => CGI_READ },
@@ -181,7 +187,7 @@ sub FILESYSTEM {
'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/params.json" => { perms => CGI_WRITE },
"$datadir/old-params.txt" => { perms => OWNER_WRITE },
"$extensionsdir/create.pl" => { perms => OWNER_EXECUTE },
"$extensionsdir/*/*.pl" => { perms => WS_EXECUTE },
@@ -211,6 +217,8 @@ sub FILESYSTEM {
dirs => DIR_CGI_WRITE | DIR_ALSO_WS_SERVE },
"$datadir/db" => { files => CGI_WRITE,
dirs => DIR_CGI_WRITE },
+ $assetsdir => { files => WS_SERVE,
+ dirs => DIR_CGI_OVERWRITE | DIR_ALSO_WS_SERVE },
# Readable directories
"$datadir/mining" => { files => CGI_READ,
@@ -270,7 +278,8 @@ sub FILESYSTEM {
# 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.
+ # 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,
@@ -281,6 +290,7 @@ sub FILESYSTEM {
$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,
# Directories that contain content served directly by the web server.
"$skinsdir/custom" => DIR_WS_SERVE,
"$skinsdir/contrib" => DIR_WS_SERVE,
@@ -409,8 +419,8 @@ EOT
</IfModule>
</FilesMatch>
-# Allow access to .png files created by a local copy of 'dot'
-<FilesMatch \\.png\$>
+ # Allow access to .png files created by a local copy of 'dot'
+ <FilesMatch \\.png\$>
<IfModule mod_version.c>
<IfVersion < 2.4>
Allow from all
@@ -448,6 +458,48 @@ EOT
</IfModule>
EOT
},
+
+ "$assetsdir/.htaccess" => { perms => WS_SERVE, contents => <<EOT
+# Allow access to .css files
+<FilesMatch \\.(css|js)\$>
+ <IfModule mod_version.c>
+ <IfVersion < 2.4>
+ Allow from all
+ </IfVersion>
+ <IfVersion >= 2.4>
+ <IfModule mod_perl.c>
+ Allow from all
+ </IfModule>
+ <IfModule !mod_perl.c>
+ Require all granted
+ </IfModule>
+ </IfVersion>
+ </IfModule>
+ <IfModule !mod_version.c>
+ Allow from all
+ </IfModule>
+</FilesMatch>
+
+# And no directory listings, either.
+<IfModule mod_version.c>
+ <IfVersion < 2.4>
+ Deny from all
+ </IfVersion>
+ <IfVersion >= 2.4>
+ <IfModule mod_perl.c>
+ Deny from all
+ </IfModule>
+ <IfModule !mod_perl.c>
+ Require all denied
+ </IfModule>
+ </IfVersion>
+</IfModule>
+<IfModule !mod_version.c>
+ Deny from all
+</IfModule>
+EOT
+ },
+
);
Bugzilla::Hook::process('install_filesystem', {
@@ -482,6 +534,7 @@ sub update_filesystem {
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.
@@ -489,6 +542,13 @@ sub update_filesystem {
_update_old_charts($datadir);
}
+ # If there is a file named '-All-' in $datadir/mining, then we're still
+ # having mining files named by product name, and we need to convert them to
+ # files named by product ID.
+ if (-e File::Spec->catfile($datadir, 'mining', '-All-')) {
+ _update_old_mining_filenames(File::Spec->catdir($datadir, 'mining'));
+ }
+
# By sorting the dirs, we assure that shorter-named directories
# (meaning parent directories) are always created before their
# child directories.
@@ -515,6 +575,13 @@ sub update_filesystem {
_rename_file($oldparamsfile, "$datadir/$oldparamsfile");
}
+ # Remove old assets htaccess file to force recreation with correct values.
+ if (-e "$assetsdir/.htaccess") {
+ if (read_file("$assetsdir/.htaccess") =~ /<FilesMatch \\\.css\$>/) {
+ unlink("$assetsdir/.htaccess");
+ }
+ }
+
_create_files(%files);
if ($params->{index_html}) {
_create_files(%{$fs->{index_html}});
@@ -558,6 +625,7 @@ EOT
_remove_empty_css_files();
_convert_single_file_skins();
+ _remove_dynamic_assets();
}
sub _remove_empty_css_files {
@@ -602,6 +670,27 @@ sub _convert_single_file_skins {
}
}
+# delete all automatically generated css/js files to force recreation at the
+# next request.
+sub _remove_dynamic_assets {
+ my @files = (
+ glob(bz_locations()->{assetsdir} . '/*.css'),
+ glob(bz_locations()->{assetsdir} . '/*.js'),
+ );
+ foreach my $file (@files) {
+ unlink($file);
+ }
+
+ # remove old skins/assets directory
+ my $old_path = bz_locations()->{skinsdir} . '/assets';
+ if (-d $old_path) {
+ foreach my $file (glob("$old_path/*.css")) {
+ unlink($file);
+ }
+ rmdir($old_path);
+ }
+}
+
sub create_htaccess {
_create_files(%{FILESYSTEM()->{htaccess}});
@@ -730,6 +819,59 @@ sub _update_old_charts {
}
}
+# The old naming scheme has product names as mining file names; we rename them
+# to product IDs.
+sub _update_old_mining_filenames {
+ my ($miningdir) = @_;
+ my @conversion_errors;
+
+ require Bugzilla::Product;
+
+ # We use a dummy product instance with ID 0, representing all products
+ my $product_all = {id => 0, name => '-All-'};
+ bless($product_all, 'Bugzilla::Product');
+
+ print "Updating old charting data file names...";
+ my @products = Bugzilla::Product->get_all();
+ push(@products, $product_all);
+ foreach my $product (@products) {
+ if (-e File::Spec->catfile($miningdir, $product->id)) {
+ push(@conversion_errors,
+ { product => $product,
+ message => 'A file named "' . $product->id .
+ '" already exists.' });
+ }
+ }
+
+ if (! @conversion_errors) {
+ # Renaming mining files should work now without a hitch.
+ foreach my $product (@products) {
+ if (! rename(File::Spec->catfile($miningdir, $product->name),
+ File::Spec->catfile($miningdir, $product->id))) {
+ push(@conversion_errors,
+ { product => $product,
+ message => $! });
+ }
+ }
+ }
+
+ # Error reporting
+ if (! @conversion_errors) {
+ print " done.\n";
+ }
+ else {
+ print " FAILED:\n";
+ foreach my $error (@conversion_errors) {
+ printf "Cannot rename charting data file for product %d (%s): %s\n",
+ $error->{product}->id, $error->{product}->name,
+ $error->{message};
+ }
+ print "You need to empty the \"$miningdir\" directory, then run\n",
+ " collectstats.pl --regenerate\n",
+ "in order to clean this up.\n";
+ }
+}
+
sub fix_dir_permissions {
my ($dir) = @_;
return if ON_WINDOWS;
@@ -961,3 +1103,29 @@ how they are supposed to be set in Bugzilla's current configuration.
If it fails to set the permissions, a warning will be printed to STDERR.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item CGI_WRITE
+
+=item DIR_WS_SERVE
+
+=item DIR_ALSO_WS_SERVE
+
+=item WS_SERVE
+
+=item FILESYSTEM
+
+=item WS_EXECUTE
+
+=item CGI_READ
+
+=item DIR_CGI_READ
+
+=item DIR_CGI_WRITE
+
+=item DIR_CGI_OVERWRITE
+
+=back
diff --git a/Bugzilla/Install/Localconfig.pm b/Bugzilla/Install/Localconfig.pm
index 881f6c956..7f473cc77 100644
--- a/Bugzilla/Install/Localconfig.pm
+++ b/Bugzilla/Install/Localconfig.pm
@@ -15,7 +15,9 @@ package Bugzilla::Install::Localconfig;
# * Files do not have the correct permissions
# * The database is not up to date
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(bin_loc install_string);
@@ -26,7 +28,7 @@ use File::Basename qw(dirname);
use Safe;
use Term::ANSIColor;
-use base qw(Exporter);
+use parent qw(Exporter);
our @EXPORT_OK = qw(
read_localconfig
@@ -79,12 +81,24 @@ use constant LOCALCONFIG_VARS => (
default => 1,
},
{
- name => 'index_html',
- default => 0,
+ name => 'db_mysql_ssl_ca_file',
+ default => '',
+ },
+ {
+ name => 'db_mysql_ssl_ca_path',
+ default => '',
+ },
+ {
+ name => 'db_mysql_ssl_client_cert',
+ default => '',
},
{
- name => 'cvsbin',
- default => sub { bin_loc('cvs') },
+ name => 'db_mysql_ssl_client_key',
+ default => '',
+ },
+ {
+ name => 'index_html',
+ default => 0,
},
{
name => 'interdiffbin',
diff --git a/Bugzilla/Install/Requirements.pm b/Bugzilla/Install/Requirements.pm
index fbd7d7882..f723543b8 100644
--- a/Bugzilla/Install/Requirements.pm
+++ b/Bugzilla/Install/Requirements.pm
@@ -13,23 +13,17 @@ package Bugzilla::Install::Requirements;
# Subroutines may "require" and "import" from modules, but they
# MUST NOT "use."
+use 5.10.1;
use strict;
-use version;
+use warnings;
use Bugzilla::Constants;
-use Bugzilla::Install::Util qw(vers_cmp install_string bin_loc
+use Bugzilla::Install::Util qw(install_string bin_loc
extension_requirement_packages);
use List::Util qw(max);
use Term::ANSIColor;
-# Return::Value 1.666002 pollutes the error log with warnings about this
-# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
-# in have_vers() to disable these warnings.
-BEGIN {
- $Return::Value::NO_CLUCK = 1;
-}
-
-use base qw(Exporter);
+use parent qw(Exporter);
our @EXPORT = qw(
REQUIRED_MODULES
OPTIONAL_MODULES
@@ -55,6 +49,8 @@ use constant APACHE_MODULES => {
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
@@ -86,7 +82,6 @@ use constant APACHE_PATH => [qw(
# are 'blacklisted'--that is, even if the version is high enough, Bugzilla
# will refuse to say that it's OK to run with that version.
sub REQUIRED_MODULES {
- my $perl_ver = sprintf('%vd', $^V);
my @modules = (
{
package => 'CGI.pm',
@@ -106,40 +101,36 @@ sub REQUIRED_MODULES {
module => 'Date::Format',
version => '2.23'
},
- # 0.28 fixed some important bugs in DateTime.
+ # 0.75 fixes a warning thrown with Perl 5.17 and newer.
{
package => 'DateTime',
module => 'DateTime',
- version => '0.28'
+ version => '0.75'
},
- # 0.79 is required to work on Windows Vista and Windows Server 2008.
- # As correctly detecting the flavor of Windows is not easy,
- # we require this version for all Windows installations.
- # 0.71 fixes a major bug affecting all platforms.
+ # 1.64 fixes a taint issue preventing the local timezone from
+ # being determined on some systems.
{
package => 'DateTime-TimeZone',
module => 'DateTime::TimeZone',
- version => ON_WINDOWS ? '0.79' : '0.71'
+ version => '1.64'
},
# 1.54 is required for Perl 5.10+. It also makes DBD::Oracle happy.
{
package => 'DBI',
module => 'DBI',
- version => (vers_cmp($perl_ver, '5.13.3') > -1) ? '1.614' : '1.54'
+ version => ($^V >= v5.13.3) ? '1.614' : '1.54'
},
- # 2.22 fixes various problems related to UTF8 strings in hash keys,
- # as well as line endings on Windows.
+ # 2.24 contains several useful text virtual methods.
{
package => 'Template-Toolkit',
module => 'Template',
- version => '2.22'
+ version => '2.24'
},
- # 2.04 implement the "Test" method (to write to data/mailer.testfile).
+ # 1.300011 has a debug mode for SMTP and automatically pass -i to sendmail.
{
- package => 'Email-Send',
- module => 'Email::Send',
- version => ON_WINDOWS ? '2.16' : '2.04',
- blacklist => ['^2\.196$']
+ package => 'Email-Sender',
+ module => 'Email::Sender',
+ version => '1.300011',
},
{
package => 'Email-MIME',
@@ -150,9 +141,8 @@ sub REQUIRED_MODULES {
{
package => 'URI',
module => 'URI',
- # This version properly handles a semicolon as the delimiter
- # in a URL query string.
- version => '1.37',
+ # Follows RFC 3986 to escape characters in URI::Escape.
+ version => '1.55',
},
# 0.32 fixes several memory leaks in the XS version of some functions.
{
@@ -165,10 +155,22 @@ sub REQUIRED_MODULES {
module => 'Math::Random::ISAAC',
version => '1.0.1',
},
+ {
+ package => 'File-Slurp',
+ module => 'File::Slurp',
+ version => '9999.13',
+ },
+ {
+ package => 'JSON-XS',
+ module => 'JSON::XS',
+ # 2.0 is the first version that will work with JSON::RPC.
+ version => '2.01',
+ },
);
if (ON_WINDOWS) {
- push(@modules, {
+ push(@modules,
+ {
package => 'Win32',
module => 'Win32',
# 0.35 fixes a memory leak in GetOSVersion, which we use.
@@ -179,7 +181,14 @@ sub REQUIRED_MODULES {
module => 'Win32::API',
# 0.55 fixes a bug with char* that might affect Bugzilla::RNG.
version => '0.55',
- });
+ },
+ {
+ package => 'DateTime-TimeZone-Local-Win32',
+ module => 'DateTime::TimeZone::Local::Win32',
+ # We require DateTime::TimeZone 1.64, so this version must match.
+ version => '1.64',
+ }
+ );
}
my $extra_modules = _get_extension_requirements('REQUIRED_MODULES');
@@ -188,7 +197,6 @@ sub REQUIRED_MODULES {
};
sub OPTIONAL_MODULES {
- my $perl_ver = sprintf('%vd', $^V);
my @modules = (
{
package => 'GD',
@@ -199,10 +207,9 @@ sub OPTIONAL_MODULES {
{
package => 'Chart',
module => 'Chart::Lines',
- # Versions below 2.1 cannot be detected accurately.
- # There is no 2.1.0 release (it was 2.1), but .0 is required to fix
+ # Versions below 2.4.1 cannot be compared accurately, see
# https://rt.cpan.org/Public/Bug/Display.html?id=28218.
- version => '2.1.0',
+ version => '2.4.1',
feature => [qw(new_charts old_charts)],
},
{
@@ -283,6 +290,8 @@ sub OPTIONAL_MODULES {
# Fixes various bugs, including 542931 and 552353 + stops
# throwing warnings with Perl 5.12.
version => '0.712',
+ # SOAP::Transport::HTTP 1.12 is bogus.
+ blacklist => ['^1\.12$'],
feature => ['xmlrpc'],
},
# Since SOAP::Lite 1.0, XMLRPC::Lite is no longer included
@@ -297,26 +306,20 @@ sub OPTIONAL_MODULES {
package => 'JSON-RPC',
module => 'JSON::RPC',
version => 0,
- feature => ['jsonrpc'],
- },
- {
- package => 'JSON-XS',
- module => 'JSON::XS',
- # 2.0 is the first version that will work with JSON::RPC.
- version => '2.0',
- feature => ['jsonrpc_faster'],
+ feature => ['jsonrpc', 'rest'],
},
{
package => 'Test-Taint',
module => 'Test::Taint',
- version => 0,
- feature => ['jsonrpc', 'xmlrpc'],
+ # 1.06 no longer throws warnings with Perl 5.10+.
+ version => 1.06,
+ feature => ['jsonrpc', 'xmlrpc', 'rest'],
},
{
# We need the 'utf8_mode' method of HTML::Parser, for HTML::Scrubber.
package => 'HTML-Parser',
module => 'HTML::Parser',
- version => (vers_cmp($perl_ver, '5.13.3') > -1) ? '3.67' : '3.40',
+ version => ($^V >= v5.13.3) ? '3.67' : '3.40',
feature => ['html_desc'],
},
{
@@ -368,12 +371,6 @@ sub OPTIONAL_MODULES {
version => 0,
feature => ['jobqueue'],
},
- {
- package => 'File-Slurp',
- module => 'File::Slurp',
- version => '9999.13',
- feature => ['jobqueue'],
- },
# mod_perl
{
@@ -403,6 +400,22 @@ sub OPTIONAL_MODULES {
version => '0',
feature => ['typesniffer'],
},
+
+ # memcached
+ {
+ package => 'Cache-Memcached',
+ module => 'Cache::Memcached',
+ version => '0',
+ feature => ['memcached'],
+ },
+
+ # Documentation
+ {
+ package => 'File-Copy-Recursive',
+ module => 'File::Copy::Recursive',
+ version => 0,
+ feature => ['documentation'],
+ }
);
my $extra_modules = _get_extension_requirements('OPTIONAL_MODULES');
@@ -416,14 +429,18 @@ 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/WebService/Server/REST.pm', 'rest.cgi',
+ 'Bugzilla/WebService/Server/REST/Resources/*.pm'],
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'],
+ memcached => ['Bugzilla/Memcache.pm'],
);
# This implements the REQUIRED_MODULES and OPTIONAL_MODULES stuff
@@ -506,7 +523,7 @@ sub _missing_apache_modules {
return [];
}
my @missing;
- foreach my $module (keys %$modules) {
+ foreach my $module (sort keys %$modules) {
my $ok = _check_apache_module($module, $modules->{$module},
$cmd_info, $output);
push(@missing, $module) if !$ok;
@@ -940,3 +957,11 @@ Returns a hashref where file names are the keys and the value is the feature
that must be enabled in order to compile that file.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item print_module_instructions
+
+=back
diff --git a/Bugzilla/Install/Util.pm b/Bugzilla/Install/Util.pm
index cbc41db32..c05037061 100644
--- a/Bugzilla/Install/Util.pm
+++ b/Bugzilla/Install/Util.pm
@@ -11,7 +11,9 @@ package Bugzilla::Install::Util;
# module may require *only* Bugzilla::Constants and built-in
# perl modules.
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Constants;
@@ -23,7 +25,7 @@ use Scalar::Util qw(tainted);
use Term::ANSIColor qw(colored);
use PerlIO;
-use base qw(Exporter);
+use parent qw(Exporter);
our @EXPORT_OK = qw(
bin_loc
get_version_and_os
@@ -37,7 +39,6 @@ our @EXPORT_OK = qw(
include_languages
success
template_include_path
- vers_cmp
init_console
);
@@ -475,49 +476,6 @@ sub template_include_path {
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;
- }
- }
- @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');
@@ -893,26 +851,36 @@ Used by L<Bugzilla::Template> to determine the languages' list which
are compiled with the browser's I<Accept-Language> and the languages
of installed templates.
-=item C<vers_cmp>
+=back
+
+=head1 B<Methods in need of POD>
=over
-=item B<Description>
+=item supported_languages
-This is a comparison function, like you would use in C<sort>, except that
-it compares two version numbers. So, for example, 2.10 would be greater
-than 2.2.
+=item extension_template_directory
-It's based on versioncmp from L<Sort::Versions>, with some Bugzilla-specific
-fixes.
+=item extension_code_files
-=item B<Params>: C<$a> and C<$b> - The versions you want to compare.
+=item extension_web_directory
-=item B<Returns>
+=item trick_taint
-C<-1> if C<$a> is less than C<$b>, C<0> if they are equal, or C<1> if C<$a>
-is greater than C<$b>.
+=item success
-=back
+=item trim
+
+=item extension_package_directory
+
+=item set_output_encoding
+
+=item extension_requirement_packages
+
+=item prevent_windows_dialog_boxes
+
+=item sortQvalue
+
+=item no_checksetup_from_cgi
=back
diff --git a/Bugzilla/Job/BugMail.pm b/Bugzilla/Job/BugMail.pm
new file mode 100644
index 000000000..e0b7f5448
--- /dev/null
+++ b/Bugzilla/Job/BugMail.pm
@@ -0,0 +1,32 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Job::BugMail;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::BugMail;
+BEGIN { eval "use parent qw(Bugzilla::Job::Mailer)"; }
+
+sub work {
+ my ($class, $job) = @_;
+ my $success = eval {
+ Bugzilla::BugMail::dequeue($job->arg->{vars});
+ 1;
+ };
+ if (!$success) {
+ $job->failed($@);
+ undef $@;
+ }
+ else {
+ $job->completed;
+ }
+}
+
+1;
diff --git a/Bugzilla/Job/Mailer.pm b/Bugzilla/Job/Mailer.pm
index 958089e45..cd1c23445 100644
--- a/Bugzilla/Job/Mailer.pm
+++ b/Bugzilla/Job/Mailer.pm
@@ -6,9 +6,13 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Job::Mailer;
+
+use 5.10.1;
use strict;
+use warnings;
+
use Bugzilla::Mailer;
-BEGIN { eval "use base qw(TheSchwartz::Worker)"; }
+BEGIN { eval "use parent qw(TheSchwartz::Worker)"; }
# The longest we expect a job to possibly take, in seconds.
use constant grab_for => 300;
diff --git a/Bugzilla/JobQueue.pm b/Bugzilla/JobQueue.pm
index 2c3d8a773..d5ceda8e9 100644
--- a/Bugzilla/JobQueue.pm
+++ b/Bugzilla/JobQueue.pm
@@ -7,7 +7,9 @@
package Bugzilla::JobQueue;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -21,6 +23,7 @@ use fields qw(_worker_pidfile);
# 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',
};
# Without a driver cache TheSchwartz opens a new database connection
@@ -28,6 +31,10 @@ use constant JOB_MAP => {
# across requests.
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 => 1000;
+
sub job_map {
if (!defined(Bugzilla->request_cache->{job_map})) {
my $job_map = JOB_MAP;
@@ -153,6 +160,16 @@ sub work_once {
return $self->SUPER::work_once(@_);
}
+# 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;
+ }
+}
+
1;
__END__
@@ -178,3 +195,19 @@ Bugzilla to use some sort of service to schedule jobs to happen asyncronously.
See the synopsis above for an easy to follow example on how to insert a
job into the queue. Give it a name and some arguments and the job will
be sent away to be done later.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item insert
+
+=item bz_databases
+
+=item job_map
+
+=item set_pidfile
+
+=item kill_worker
+
+=back
diff --git a/Bugzilla/JobQueue/Runner.pm b/Bugzilla/JobQueue/Runner.pm
index 8b5cf9b6b..104a97b0b 100644
--- a/Bugzilla/JobQueue/Runner.pm
+++ b/Bugzilla/JobQueue/Runner.pm
@@ -11,7 +11,10 @@
package Bugzilla::JobQueue::Runner;
+use 5.10.1;
use strict;
+use warnings;
+
use Cwd qw(abs_path);
use File::Basename;
use File::Copy;
@@ -20,7 +23,7 @@ use Pod::Usage;
use Bugzilla::Constants;
use Bugzilla::JobQueue;
use Bugzilla::Util qw(get_text);
-BEGIN { eval "use base qw(Daemon::Generic)"; }
+BEGIN { eval "use parent qw(Daemon::Generic)"; }
our $VERSION = BUGZILLA_VERSION;
@@ -235,3 +238,33 @@ job queue.
This is a subclass of L<Daemon::Generic> that is used by L<jobqueue>
to run the Bugzilla job queue.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item gd_check
+
+=item gd_run
+
+=item gd_can_install
+
+=item gd_quit_event
+
+=item gd_other_cmd
+
+=item gd_more_opt
+
+=item gd_postconfig
+
+=item gd_usage
+
+=item gd_getopt
+
+=item gd_preconfig
+
+=item gd_can_uninstall
+
+=item gd_setup_signals
+
+=back
diff --git a/Bugzilla/Keyword.pm b/Bugzilla/Keyword.pm
index 3f3213be4..afa93e1e9 100644
--- a/Bugzilla/Keyword.pm
+++ b/Bugzilla/Keyword.pm
@@ -5,11 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Keyword;
-use base qw(Bugzilla::Object);
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Object);
use Bugzilla::Error;
use Bugzilla::Util;
@@ -18,6 +20,8 @@ use Bugzilla::Util;
#### Initialization ####
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_COLUMNS => qw(
keyworddefs.id
keyworddefs.name
@@ -165,3 +169,17 @@ implements.
=back
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item set_description
+
+=item bug_count
+
+=item set_name
+
+=item description
+
+=back
diff --git a/Bugzilla/Mailer.pm b/Bugzilla/Mailer.pm
index 64640150b..0b82ded41 100644
--- a/Bugzilla/Mailer.pm
+++ b/Bugzilla/Mailer.pm
@@ -7,9 +7,11 @@
package Bugzilla::Mailer;
+use 5.10.1;
use strict;
+use warnings;
-use base qw(Exporter);
+use parent qw(Exporter);
@Bugzilla::Mailer::EXPORT = qw(MessageToMTA build_thread_marker);
use Bugzilla::Constants;
@@ -21,22 +23,20 @@ use Date::Format qw(time2str);
use Encode qw(encode);
use Encode::MIME::Header;
-use Email::Address;
use Email::MIME;
-# Return::Value 1.666002 pollutes the error log with warnings about this
-# deprecated module. We have to set NO_CLUCK = 1 before loading Email::Send
-# to disable these warnings.
-BEGIN {
- $Return::Value::NO_CLUCK = 1;
-}
-use Email::Send;
+use Email::Sender::Simple qw(sendmail);
+use Email::Sender::Transport::SMTP::Persistent;
+use Bugzilla::Sender::Transport::Sendmail;
sub MessageToMTA {
my ($msg, $send_now) = (@_);
my $method = Bugzilla->params->{'mail_delivery_method'};
return if $method eq 'None';
- if (Bugzilla->params->{'use_mailer_queue'} and !$send_now) {
+ if (Bugzilla->params->{'use_mailer_queue'}
+ && ! $send_now
+ && ! Bugzilla->dbh->bz_in_transaction()
+ ) {
Bugzilla->job_queue->insert('send_mail', { msg => $msg });
return;
}
@@ -50,12 +50,22 @@ sub MessageToMTA {
# Email::MIME doesn't do this for us. 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
$msg =~ s/(?:\015+)?\012/\015\012/msg;
$email = new Email::MIME($msg);
}
+ # If we're called from within a transaction, we don't want to send the
+ # email immediately, in case the transaction is rolled back. Instead we
+ # insert it into the mail_staging table, and bz_commit_transaction calls
+ # send_staged_mail() after the transaction is committed.
+ if (! $send_now && Bugzilla->dbh->bz_in_transaction()) {
+ # The e-mail string may contain tainted values.
+ my $string = $email->as_string;
+ trick_taint($string);
+ Bugzilla->dbh->do("INSERT INTO mail_staging (message) VALUES(?)", undef, $string);
+ return;
+ }
+
# We add this header to uniquely identify all email that we
# send as coming from this Bugzilla installation.
#
@@ -63,7 +73,7 @@ sub MessageToMTA {
# *always* be the same for this Bugzilla, in every email,
# even if the admin changes the "ssl_redirect" parameter some day.
$email->header_set('X-Bugzilla-URL', Bugzilla->params->{'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');
@@ -91,21 +101,14 @@ sub MessageToMTA {
my $from = $email->header('From');
- my ($hostname, @args);
- my $mailer_class = $method;
+ my $hostname;
+ my $transport;
if ($method eq "Sendmail") {
- $mailer_class = 'Bugzilla::Send::Sendmail';
if (ON_WINDOWS) {
- $Email::Send::Sendmail::SENDMAIL = SENDMAIL_EXE;
+ $transport = Bugzilla::Sender::Transport::Sendmail->new({ 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 {
+ $transport = Bugzilla::Sender::Transport::Sendmail->new();
}
}
else {
@@ -113,7 +116,7 @@ sub MessageToMTA {
# address, but other mailers won't.
my $urlbase = Bugzilla->params->{'urlbase'};
$urlbase =~ m|//([^:/]+)[:/]?|;
- $hostname = $1;
+ $hostname = $1 || 'localhost';
$from .= "\@$hostname" if $from !~ /@/;
$email->header_set('From', $from);
@@ -124,16 +127,21 @@ sub MessageToMTA {
}
if ($method eq "SMTP") {
- push @args, Host => Bugzilla->params->{"smtpserver"},
- username => Bugzilla->params->{"smtp_username"},
- password => Bugzilla->params->{"smtp_password"},
- Hello => $hostname,
- ssl => Bugzilla->params->{'smtp_ssl'},
- Debug => Bugzilla->params->{'smtp_debug'};
+ my ($host, $port) = split(/:/, Bugzilla->params->{'smtpserver'}, 2);
+ $transport = Bugzilla->request_cache->{smtp} //=
+ Email::Sender::Transport::SMTP::Persistent->new({
+ host => $host,
+ defined($port) ? (port => $port) : (),
+ sasl_username => Bugzilla->params->{'smtp_username'},
+ sasl_password => Bugzilla->params->{'smtp_password'},
+ helo => $hostname,
+ ssl => Bugzilla->params->{'smtp_ssl'},
+ debug => Bugzilla->params->{'smtp_debug'} });
}
- Bugzilla::Hook::process('mailer_before_send',
- { email => $email, mailer_args => \@args });
+ Bugzilla::Hook::process('mailer_before_send', { email => $email });
+
+ return if $email->header('to') eq '';
$email->walk_parts(sub {
my ($part) = @_;
@@ -166,13 +174,12 @@ sub MessageToMTA {
close TESTFILE;
}
else {
- # This is useful for both Sendmail and Qmail, so we put it out here.
+ # This is useful for Sendmail, 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;
+ eval { sendmail($email, { transport => $transport }) };
+ if ($@) {
+ ThrowCodeError('mail_send_error', { msg => $@->message, mail => $email });
+ }
}
}
@@ -205,4 +212,50 @@ sub build_thread_marker {
return $threadingmarker;
}
+sub send_staged_mail {
+ my $dbh = Bugzilla->dbh;
+ my @ids;
+ my $emails
+ = $dbh->selectall_arrayref("SELECT id, message FROM mail_staging");
+
+ foreach my $row (@$emails) {
+ MessageToMTA($row->[1]);
+ push(@ids, $row->[0]);
+ }
+
+ if (@ids) {
+ $dbh->do("DELETE FROM mail_staging WHERE " . $dbh->sql_in('id', \@ids));
+ }
+}
+
1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Mailer - Provides methods for sending email
+
+=head1 METHODS
+
+=over
+
+=item C<MessageToMTA>
+
+Sends the passed message to the mail transfer agent.
+
+The actual behaviour depends on a number of factors: if called from within a
+database transaction, the message will be staged and sent when the transaction
+is committed. If email queueing is enabled, the message will be sent to
+TheSchwartz job queue where it will be processed by the jobqueue daemon, else
+the message is sent immediately.
+
+=item C<build_thread_marker>
+
+Builds header suitable for use as a threading marker in email notifications.
+
+=item C<send_staged_mail>
+
+Sends all staged messages -- called after a database transaction is committed.
+
+=back
diff --git a/Bugzilla/Memcached.pm b/Bugzilla/Memcached.pm
new file mode 100644
index 000000000..df90fef93
--- /dev/null
+++ b/Bugzilla/Memcached.pm
@@ -0,0 +1,483 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# This Source Code Form is "Incompatible With Secondary Licenses", as
+# defined by the Mozilla Public License, v. 2.0.
+
+package Bugzilla::Memcached;
+
+use 5.10.1;
+use strict;
+use warnings;
+
+use Bugzilla::Error;
+use Bugzilla::Util qw(trick_taint);
+use Scalar::Util qw(blessed);
+use URI::Escape;
+
+# memcached keys have a maximum length of 250 bytes
+use constant MAX_KEY_LENGTH => 250;
+
+sub _new {
+ my $invocant = shift;
+ my $class = ref($invocant) || $invocant;
+ my $self = {};
+
+ # always return an object to simplify calling code when memcached is
+ # disabled.
+ if (Bugzilla->feature('memcached')
+ && Bugzilla->params->{memcached_servers})
+ {
+ require Cache::Memcached;
+ $self->{namespace} = Bugzilla->params->{memcached_namespace} || '';
+ $self->{memcached} =
+ Cache::Memcached->new({
+ servers => [ split(/[, ]+/, Bugzilla->params->{memcached_servers}) ],
+ namespace => $self->{namespace},
+ });
+ }
+ return bless($self, $class);
+}
+
+sub enabled {
+ 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);
+ }
+ }
+
+ 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' ] });
+ }
+}
+
+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' ] });
+ }
+}
+
+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' ] });
+ }
+}
+
+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' ] });
+ }
+}
+
+sub clear_all {
+ 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");
+ }
+}
+
+# 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};
+}
+
+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"};
+}
+
+sub _global_prefix {
+ return $_[0]->_prefix("global");
+}
+
+sub _config_prefix {
+ return $_[0]->_prefix("config");
+}
+
+sub _encode_key {
+ my ($self, $key) = @_;
+ $key = $self->_global_prefix . '.' . uri_escape_utf8($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" });
+ }
+
+ $key = $self->_encode_key($key)
+ or return;
+ return $self->{memcached}->set($key, $value);
+}
+
+sub _get {
+ my ($self, $key) = @_;
+
+ $key = $self->_encode_key($key)
+ or return;
+ my $value = $self->{memcached}->get($key);
+ return unless defined $value;
+
+ # detaint returned values
+ # hashes and arrays are detainted just one level deep
+ if (ref($value) eq 'HASH') {
+ _detaint_hashref($value);
+ }
+ elsif (ref($value) eq 'ARRAY') {
+ foreach my $value (@$value) {
+ next unless defined $value;
+ # arrays of hashes and arrays are common
+ if (ref($value) eq 'HASH') {
+ _detaint_hashref($value);
+ }
+ elsif (ref($value) eq 'ARRAY') {
+ _detaint_arrayref($value);
+ }
+ elsif (!ref($value)) {
+ trick_taint($value);
+ }
+ }
+ }
+ elsif (!ref($value)) {
+ trick_taint($value);
+ }
+ return $value;
+}
+
+sub _detaint_hashref {
+ my ($hashref) = @_;
+ foreach my $value (values %$hashref) {
+ if (defined($value) && !ref($value)) {
+ trick_taint($value);
+ }
+ }
+}
+
+sub _detaint_arrayref {
+ my ($arrayref) = @_;
+ foreach my $value (@$arrayref) {
+ if (defined($value) && !ref($value)) {
+ trick_taint($value);
+ }
+ }
+}
+
+sub _delete {
+ my ($self, $key) = @_;
+ $key = $self->_encode_key($key)
+ or return;
+ return $self->{memcached}->delete($key);
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Bugzilla::Memcached - Interface between Bugzilla and Memcached.
+
+=head1 SYNOPSIS
+
+ use Bugzilla;
+
+ my $memcached = Bugzilla->memcached;
+
+ # grab data from the cache. there is no need to check if memcached is
+ # available or enabled.
+ my $data = $memcached->get({ key => 'data_key' });
+ if (!defined $data) {
+ # not in cache, generate the data and populate the cache for next time
+ $data = some_long_process();
+ $memcached->set({ key => 'data_key', value => $data });
+ }
+ # do something with $data
+
+ # updating the profiles table directly shouldn't be attempted unless you know
+ # what you're doing. if you do update a table directly, you need to clear that
+ # object from memcached.
+ $dbh->do("UPDATE profiles SET request_count=10 WHERE login_name=?", undef, $login);
+ $memcached->clear({ table => 'profiles', name => $login });
+
+=head1 DESCRIPTION
+
+If Memcached is installed and configured, Bugzilla can use it to cache data
+across requests and between webheads. Unlike the request and process caches,
+only scalars, hashrefs, and arrayrefs can be stored in Memcached.
+
+Memcached integration is only required for large installations of Bugzilla --
+if you have multiple webheads then configuring Memcache is recommended.
+
+L<Bugzilla::Memcached> provides an interface to a Memcached server/servers, with
+the ability to get, set, or clear entries from the cache.
+
+The stored value must be an unblessed hashref, unblessed array ref, or a
+scalar. Currently nested data structures are supported but require manual
+de-tainting after reading from Memcached (flat data structures are automatically
+de-tainted).
+
+All values are stored in the Memcached systems using the prefix configured with
+the C<memcached_namespace> parameter, as well as an additional prefix managed
+by this class to allow all values to be cleared when C<checksetup.pl> is
+executed.
+
+Do not create an instance of this object directly, instead use
+L<Bugzilla-E<gt>memcached()|Bugzilla/memcached>.
+
+=head1 METHODS
+
+=over
+
+=item C<enabled>
+
+Returns true if Memcached support is available and enabled.
+
+=back
+
+=head2 Setting
+
+Adds a value to Memcached.
+
+=over
+
+=item C<set({ key =E<gt> $key, value =E<gt> $value })>
+
+Adds the C<value> using the specific C<key>.
+
+=item C<set({ table =E<gt> $table, id =E<gt> $id, name =E<gt> $name, data =E<gt> $data })>
+
+Adds the C<data> using a keys generated from the C<table>, C<id>, and C<name>.
+All three parameters must be provided, however C<name> can be provided but set
+to C<undef>.
+
+This is a convenience method which allows cached data to be later retrieved by
+specifying the C<table> and either the C<id> or C<name>.
+
+=item C<set_config({ key =E<gt> $key, data =E<gt> $data })>
+
+Adds the C<data> using the C<key> while identifying the data as part of
+Bugzilla's configuration (such as fields, products, components, groups, etc).
+Values set with C<set_config> are automatically cleared when changes are made
+to Bugzilla's configuration.
+
+=back
+
+=head2 Getting
+
+Retrieves a value from Memcached. Returns C<undef> if no matching values were
+found in the cache.
+
+=over
+
+=item C<get({ key =E<gt> $key })>
+
+Return C<value> with the specified C<key>.
+
+=item C<get({ table =E<gt> $table, id =E<gt> $id })>
+
+Return C<value> with the specified C<table> and C<id>.
+
+=item C<get({ table =E<gt> $table, name =E<gt> $name })>
+
+Return C<value> with the specified C<table> and C<name>.
+
+=item C<get_config({ key =E<gt> $key })>
+
+Return C<value> with the specified C<key> from the configuration cache. See
+C<set_config> for more information.
+
+=back
+
+=head2 Clearing
+
+Removes the matching value from Memcached.
+
+=over
+
+=item C<clear({ key =E<gt> $key })>
+
+Removes C<value> with the specified C<key>.
+
+=item C<clear({ table =E<gt> $table, id =E<gt> $id })>
+
+Removes C<value> with the specified C<table> and C<id>, as well as the
+corresponding C<table> and C<name> entry.
+
+=item C<clear({ table =E<gt> $table, name =E<gt> $name })>
+
+Removes C<value> with the specified C<table> and C<name>, as well as the
+corresponding C<table> and C<id> entry.
+
+=item C<clear_config({ key =E<gt> $key })>
+
+Remove C<value> with the specified C<key> from the configuration cache. See
+C<set_config> for more information.
+
+=item C<clear_config>
+
+Removes all configuration related values from the cache. See C<set_config> for
+more information.
+
+=item C<clear_all>
+
+Removes all values from the cache.
+
+=back
+
+=head1 Bugzilla::Object CACHE
+
+The main driver for Memcached integration is to allow L<Bugzilla::Object> based
+objects to be automatically cached in Memcache. This is enabled on a
+per-package basis by setting the C<USE_MEMCACHED> constant to any true value.
+
+The current implementation is an opt-in (USE_MEMCACHED is false by default),
+however this will change to opt-out once further testing has been completed
+(USE_MEMCACHED will be true by default).
+
+=head1 DIRECT DATABASE UPDATES
+
+If an object is cached and the database is updated directly (instead of via
+C<$object-E<gt>update()>), then it's possible for the data in the cache to be
+out of sync with the database.
+
+As an example let's consider an extension which adds a timestamp field
+C<last_activitiy_ts> to the profiles table and user object which contains the
+user's last activity. If the extension were to call C<$user-E<gt>update()>,
+then an audit entry would be created for each change to the C<last_activity_ts>
+field, which is undesirable.
+
+To remedy this, the extension updates the table directly. It's critical with
+Memcached that it then clears the cache:
+
+ $dbh->do("UPDATE profiles SET last_activity_ts=? WHERE userid=?",
+ undef, $timestamp, $user_id);
+ Bugzilla->memcached->clear({ table => 'profiles', id => $user_id });
+
diff --git a/Bugzilla/Migrate.pm b/Bugzilla/Migrate.pm
index 75552c203..0731d4fed 100644
--- a/Bugzilla/Migrate.pm
+++ b/Bugzilla/Migrate.pm
@@ -6,7 +6,10 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Migrate;
+
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Attachment;
use Bugzilla::Bug qw(LogActivityEntry);
@@ -16,7 +19,7 @@ use Bugzilla::Error;
use Bugzilla::Install::Requirements ();
use Bugzilla::Install::Util qw(indicate_progress);
use Bugzilla::Product;
-use Bugzilla::Util qw(get_text trim generate_random_password say);
+use Bugzilla::Util qw(get_text trim generate_random_password);
use Bugzilla::User ();
use Bugzilla::Status ();
use Bugzilla::Version;
@@ -151,6 +154,7 @@ sub do_migration {
}
$dbh->bz_start_transaction();
+ $self->before_read();
# Read Other Database
my $users = $self->users;
my $products = $self->products;
@@ -445,8 +449,11 @@ sub translate_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))
+ 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;
@@ -539,6 +546,7 @@ sub write_config {
sub after_insert {}
sub before_insert {}
sub after_read {}
+sub before_read {}
#############
# Inserters #
@@ -749,7 +757,7 @@ sub insert_bugs {
# 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
+ # Allow the user to file a bug in any product, no matter their current
# permissions.
$reporter->{groups} = $super_user->groups;
Bugzilla->set_user($reporter);
@@ -1142,6 +1150,11 @@ and yet shouldn't be added to the initial description of the bug when
translating bugs, then they should be listed here. See L</translate_bug> for
more detail.
+=head2 before_read
+
+This is called before any data is read from the "other bug-tracker".
+The default implementation does nothing.
+
=head2 after_read
This is run after all data is read from the other bug-tracker, but
@@ -1158,3 +1171,49 @@ or any custom fields are created. The default implementation does nothing.
This is run after all data is inserted into Bugzilla. The default
implementation does nothing.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item do_migration
+
+=item verbose
+
+=item bug_fields
+
+=item insert_users
+
+=item users
+
+=item check_requirements
+
+=item bugs
+
+=item map_value
+
+=item insert_products
+
+=item products
+
+=item translate_all_bugs
+
+=item config_file_name
+
+=item dry_run
+
+=item name
+
+=item create_custom_fields
+
+=item reset_serial_values
+
+=item read_config
+
+=item write_config
+
+=item insert_bugs
+
+=item create_legal_values
+
+=back
diff --git a/Bugzilla/Migrate/Gnats.pm b/Bugzilla/Migrate/Gnats.pm
index 05d3b3739..5feda4b8d 100644
--- a/Bugzilla/Migrate/Gnats.pm
+++ b/Bugzilla/Migrate/Gnats.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Migrate::Gnats;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Migrate);
+use warnings;
+
+use parent qw(Bugzilla::Migrate);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(indicate_progress);
@@ -696,3 +700,23 @@ sub translate_value {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item user_map
+
+=item user_to_email
+
+=item add_user
+
+=item translate_value
+
+=item before_insert
+
+=item translate_bug
+
+=item CONFIG_VARS
+
+=back
diff --git a/Bugzilla/Milestone.pm b/Bugzilla/Milestone.pm
index b4ddaeafe..cf7e3e35f 100644
--- a/Bugzilla/Milestone.pm
+++ b/Bugzilla/Milestone.pm
@@ -5,11 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Milestone;
-use base qw(Bugzilla::Object);
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Util;
@@ -112,8 +114,10 @@ sub update {
$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;
}
@@ -376,3 +380,13 @@ Milestone.pm represents a Product Milestone object.
Returns: A Bugzilla::Milestone object.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item set_is_active
+
+=item is_active
+
+=back
diff --git a/Bugzilla/Object.pm b/Bugzilla/Object.pm
index 47b935664..8f25e2b20 100644
--- a/Bugzilla/Object.pm
+++ b/Bugzilla/Object.pm
@@ -5,10 +5,12 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Object;
+use 5.10.1;
+use strict;
+use warnings;
+
use Bugzilla::Constants;
use Bugzilla::Hook;
use Bugzilla::Util;
@@ -33,6 +35,15 @@ 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.
+use constant USE_MEMCACHED => 1;
+
+# When IS_CONFIG is true, the class is used to track seldom changed
+# configuration objects. This includes, but is not limited to, fields, field
+# values, keywords, products, classifications, priorities, severities, etc.
+use constant IS_CONFIG => 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.
@@ -45,17 +56,52 @@ sub TO_JSON { return { %{ $_[0] } }; }
sub new {
my $invocant = shift;
my $class = ref($invocant) || $invocant;
- my $object = $class->_init(@_);
- bless($object, $class) if $object;
+ 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;
+ }
+ $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;
}
-
# Note: Because this uses sql_istrcmp, if you make a new object use
# Bugzilla::Object, make sure that you modify bz_setup_database
# in Bugzilla::DB::Pg appropriately, to add the right LOWER
# index. You can see examples already there.
-sub _init {
+sub _load_from_db {
my $class = shift;
my ($param) = @_;
my $dbh = Bugzilla->dbh;
@@ -68,19 +114,19 @@ sub _init {
if (ref $param eq 'HASH') {
$id = $param->{id};
}
- my $object;
+ 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 . '::_init'});
+ {function => $class . '::_load_from_db'});
# Too large integers make PostgreSQL crash.
return if $id > MAX_INT_32;
- $object = $dbh->selectrow_hashref(qq{
+ $object_data = $dbh->selectrow_hashref(qq{
SELECT $columns FROM $table
WHERE $id_field = ?}, undef, $id);
} else {
@@ -107,11 +153,110 @@ sub _init {
}
map { trick_taint($_) } @values;
- $object = $dbh->selectrow_hashref(
+ $object_data = $dbh->selectrow_hashref(
"SELECT $columns FROM $table WHERE $condition", undef, @values);
}
+ return $object_data;
+}
- return $object;
+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);
+ }
+
+ # 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.
+ return match($invocant, { $id_field => \@detainted_ids });
+}
+
+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;
+}
+
+sub initialize {
+ # abstract
+}
+
+# Provides a mechanism for objects to be cached in the request_cache
+
+sub object_cache_get {
+ 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
+ );
+}
+
+sub _object_cache_get {
+ 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;
+}
+
+sub _object_cache_remove {
+ my $class = shift;
+ my ($param) = @_;
+ $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;
+ }
+}
+
+# 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} };
}
sub check {
@@ -147,28 +292,6 @@ sub check {
return $obj;
}
-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);
- }
- # 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.
- return match($invocant, { $id_field => \@detainted_ids });
-}
-
# 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.
@@ -252,23 +375,46 @@ sub _do_list_select {
my $cols = join(',', $class->_get_db_columns);
my $order = $class->LIST_ORDER;
- my $sql = "SELECT $cols FROM $table";
- if (defined $where) {
- $sql .= " WHERE $where ";
+ # 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;
}
- $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;
- my $objects = $dbh->selectall_arrayref($sql, {Slice=>{}}, @untainted);
- bless ($_, $class) foreach @$objects;
- return $objects
+
+ 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;
+ }
+
+ 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;
}
###############################
@@ -392,6 +538,13 @@ sub update {
$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);
@@ -410,6 +563,13 @@ sub remove_from_db {
$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;
}
@@ -444,6 +604,13 @@ sub audit_log {
}
}
+sub flatten_to_hash {
+ my $self = shift;
+ my $class = blessed($self);
+ my %hash = map { $_ => $self->{$_} } $class->_serialisation_keys;
+ return \%hash;
+}
+
###############################
#### Subroutines ######
###############################
@@ -467,6 +634,13 @@ sub create {
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();
+ }
+
return $object;
}
@@ -562,7 +736,7 @@ sub insert_create_data {
sub get_all {
my $class = shift;
- return @{$class->_do_list_select()};
+ return @{ $class->_do_list_select() };
}
###############################
@@ -971,6 +1145,17 @@ database matching the parameters you passed in.
=back
+=item C<initialize>
+
+=over
+
+=item B<Description>
+
+Abstract method to allow subclasses to perform initialization tasks after an
+object has been created.
+
+=back
+
=item C<check>
=over
@@ -1010,6 +1195,13 @@ template.
Returns: A reference to an array of objects.
+=item C<new_from_hash($hashref)>
+
+ Description: Create an object from the given hash.
+
+ Params: $hashref - A reference to a hash which was created by
+ flatten_to_hash.
+
=item C<match>
=over
@@ -1247,6 +1439,17 @@ that should be passed to the C<set_> function that is called.
=back
+=head2 Simple Methods
+
+=over
+
+=item C<flatten_to_hash>
+
+Returns a hashref suitable for serialisation and re-inflation with C<new_from_hash>.
+
+=back
+
+
=head2 Simple Validators
You can use these in your subclass L</VALIDATORS> or L</UPDATE_VALIDATORS>.
@@ -1261,6 +1464,58 @@ Returns C<1> if the passed-in value is true, C<0> otherwise.
=back
+=head2 CACHE FUNCTIONS
+
+=over
+
+=item C<object_cache_get>
+
+=over
+
+=item B<Description>
+
+Class function which returns an object from the object-cache for the provided
+C<$id>.
+
+=item B<Params>
+
+Takes an integer C<$id> of the object to retrieve.
+
+=item B<Returns>
+
+Returns the object from the cache if found, otherwise returns C<undef>.
+
+=item B<Example>
+
+my $bug_from_cache = Bugzilla::Bug->object_cache_get(35);
+
+=back
+
+=item C<object_cache_set>
+
+=over
+
+=item B<Description>
+
+Object function which injects the object into the object-cache, using the
+object's C<id> as the key.
+
+=item B<Params>
+
+(none)
+
+=item B<Returns>
+
+(nothing)
+
+=item B<Example>
+
+$bug->object_cache_set();
+
+=back
+
+=back
+
=head1 CLASS FUNCTIONS
=over
@@ -1285,3 +1540,19 @@ C<0> otherwise.
=back
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item object_cache_key
+
+=item check_time
+
+=item id
+
+=item TO_JSON
+
+=item audit_log
+
+=back
diff --git a/Bugzilla/Product.pm b/Bugzilla/Product.pm
index fdebc6b00..30ebc7c6c 100644
--- a/Bugzilla/Product.pm
+++ b/Bugzilla/Product.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Product;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
+use warnings;
+
+use parent qw(Bugzilla::Field::ChoiceInterface Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Util;
@@ -31,6 +35,8 @@ use constant DEFAULT_CLASSIFICATION_ID => 1;
#### Initialization ####
###############################
+use constant IS_CONFIG => 1;
+
use constant DB_TABLE => 'products';
use constant DB_COLUMNS => qw(
@@ -97,6 +103,7 @@ sub create {
Bugzilla::Hook::process('product_end_of_create', { product => $product });
$dbh->bz_commit_transaction();
+ Bugzilla->memcached->clear_config();
return $product;
}
@@ -253,6 +260,7 @@ sub update {
# Changes have been committed.
delete $self->{check_group_controls};
Bugzilla->user->clear_product_cache();
+ Bugzilla->memcached->clear_config();
return $changes;
}
@@ -270,8 +278,8 @@ sub remove_from_db {
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.
+ # Note that we allow the user to delete bugs they can't see,
+ # which is okay, because they're deleting the whole Product.
my $bug = new Bugzilla::Bug($bug_id);
$bug->remove_from_db();
}
@@ -311,6 +319,7 @@ sub remove_from_db {
$self->SUPER::remove_from_db();
$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.
@@ -807,8 +816,8 @@ sub flag_types {
sub classification {
my $self = shift;
- $self->{'classification'} ||=
- new Bugzilla::Classification($self->classification_id);
+ $self->{'classification'} ||=
+ new Bugzilla::Classification({ id => $self->classification_id, cache => 1 });
return $self->{'classification'};
}
@@ -1022,7 +1031,7 @@ a group is valid in a particular product.)
Params: C<$user> - A Bugzilla::User object.
- Returns C<1> If this user's groups allow him C<entry> access to
+ Returns C<1> If this user's groups allow them C<entry> access to
this Product, C<0> otherwise.
=item C<flag_types()>
@@ -1067,3 +1076,37 @@ C<Bugzilla::Product::preload($products)>.
L<Bugzilla::Object>
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item set_allows_unconfirmed
+
+=item allows_unconfirmed
+
+=item set_name
+
+=item set_default_milestone
+
+=item set_group_controls
+
+=item create
+
+=item set_description
+
+=item set_is_active
+
+=item classification_id
+
+=item description
+
+=item default_milestone
+
+=item remove_from_db
+
+=item is_active
+
+=item update
+
+=back
diff --git a/Bugzilla/RNG.pm b/Bugzilla/RNG.pm
index 457d2cae0..96e442fa0 100644
--- a/Bugzilla/RNG.pm
+++ b/Bugzilla/RNG.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::RNG;
+
+use 5.10.1;
use strict;
-use base qw(Exporter);
+use warnings;
+
+use parent qw(Exporter);
use Bugzilla::Constants qw(ON_WINDOWS);
use Math::Random::ISAAC;
@@ -216,3 +220,15 @@ sub _win2k_seed {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item srand
+
+=item rand
+
+=item irand
+
+=back
diff --git a/Bugzilla/Report.pm b/Bugzilla/Report.pm
index 4c9f33226..10af2ea9e 100644
--- a/Bugzilla/Report.pm
+++ b/Bugzilla/Report.pm
@@ -5,11 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Report;
-use base qw(Bugzilla::Object);
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Object);
use Bugzilla::CGI;
use Bugzilla::Constants;
@@ -132,3 +134,17 @@ of L<Bugzilla::Object>, and thus provides all methods that
L<Bugzilla::Object> provides.
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item create
+
+=item query
+
+=item set_query
+
+=item set_name
+
+=back
diff --git a/Bugzilla/Search.pm b/Bugzilla/Search.pm
index d67df03dd..8097d5fb8 100644
--- a/Bugzilla/Search.pm
+++ b/Bugzilla/Search.pm
@@ -5,10 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
+package Bugzilla::Search;
+
+use 5.10.1;
use strict;
+use warnings;
-package Bugzilla::Search;
-use base qw(Exporter);
+use parent qw(Exporter);
@Bugzilla::Search::EXPORT = qw(
IsValidQueryType
split_order_term
@@ -107,6 +110,7 @@ use Time::HiRes qw(gettimeofday tv_interval);
# When doing searches, NULL datetimes are treated as this date.
use constant EMPTY_DATETIME => '1970-01-01 00:00:00';
+use constant EMPTY_DATE => '1970-01-01';
# This is the regex for real numbers from Regexp::Common, modified to be
# more readable.
@@ -157,6 +161,8 @@ use constant OPERATORS => {
changedfrom => \&_changedfrom_changedto,
changedto => \&_changedfrom_changedto,
changedby => \&_changedby,
+ isempty => \&_isempty,
+ isnotempty => \&_isnotempty,
};
# Some operators are really just standard SQL operators, and are
@@ -183,6 +189,8 @@ use constant OPERATOR_REVERSE => {
lessthaneq => 'greaterthan',
greaterthan => 'lessthaneq',
greaterthaneq => 'lessthan',
+ isempty => 'isnotempty',
+ isnotempty => 'isempty',
# The following don't currently have reversals:
# casesubstring, anyexact, allwords, allwordssubstr
};
@@ -198,6 +206,12 @@ use constant NON_NUMERIC_OPERATORS => qw(
notregexp
);
+# These operators ignore the entered value
+use constant NO_VALUE_OPERATORS => qw(
+ isempty
+ isnotempty
+);
+
use constant MULTI_SELECT_OVERRIDE => {
notequals => \&_multiselect_negative,
notregexp => \&_multiselect_negative,
@@ -251,7 +265,7 @@ use constant OPERATOR_FIELD_OVERRIDE => {
},
# General Bug Fields
- alias => { _non_changed => \&_nullable },
+ alias => { _non_changed => \&_alias_nonchanged },
'attach_data.thedata' => MULTI_SELECT_OVERRIDE,
# We check all attachment fields against this.
attachments => MULTI_SELECT_OVERRIDE,
@@ -303,7 +317,8 @@ use constant OPERATOR_FIELD_OVERRIDE => {
_non_changed => \&_product_nonchanged,
},
tag => MULTI_SELECT_OVERRIDE,
-
+ comment_tag => MULTI_SELECT_OVERRIDE,
+
# Timetracking Fields
deadline => { _non_changed => \&_deadline },
percentage_complete => {
@@ -315,11 +330,16 @@ use constant OPERATOR_FIELD_OVERRIDE => {
changedafter => \&_work_time_changedbefore_after,
_default => \&_work_time,
},
+ last_visit_ts => {
+ _non_changed => \&_last_visit_ts,
+ _default => \&_last_visit_ts_invalid_operator,
+ },
# 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,
@@ -340,13 +360,19 @@ sub SPECIAL_PARSING {
'requestees.login_name' => \&_contact_pronoun,
# Date Fields that accept the 1d, 1w, 1m, 1y, etc. format.
- creation_ts => \&_timestamp_translate,
- deadline => \&_timestamp_translate,
- delta_ts => \&_timestamp_translate,
+ 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,
};
foreach my $field (Bugzilla->active_custom_fields) {
if ($field->type == FIELD_TYPE_DATETIME) {
- $map->{$field->name} = \&_timestamp_translate;
+ $map->{$field->name} = \&_datetime_translate;
+ } elsif ($field->type == FIELD_TYPE_DATE) {
+ $map->{$field->name} = \&_date_translate;
}
}
return $map;
@@ -391,6 +417,7 @@ use constant FIELD_MAP => {
bugidtype => 'bug_id_type',
changedin => 'days_elapsed',
long_desc => 'longdesc',
+ tags => 'tag',
};
# Some fields are not sorted on themselves, but on other fields.
@@ -429,6 +456,10 @@ sub COLUMN_JOINS {
. ' FROM longdescs GROUP BY bug_id)',
join => 'INNER',
},
+ alias => {
+ table => 'bugs_aliases',
+ as => 'map_alias',
+ },
assigned_to => {
from => 'assigned_to',
to => 'userid',
@@ -484,6 +515,14 @@ sub COLUMN_JOINS {
to => 'id',
},
},
+ blocked => {
+ table => 'dependencies',
+ to => 'dependson',
+ },
+ dependson => {
+ table => 'dependencies',
+ to => 'blocked',
+ },
'longdescs.count' => {
table => 'longdescs',
join => 'INNER',
@@ -498,7 +537,14 @@ sub COLUMN_JOINS {
from => 'map_bug_tag.tag_id',
to => 'id',
},
- }
+ },
+ 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',
+ },
};
return $joins;
};
@@ -544,6 +590,7 @@ sub COLUMNS {
# like "bugs.bug_id".
my $total_time = "(map_actual_time.total + bugs.remaining_time)";
my %special_sql = (
+ alias => $dbh->sql_group_concat('DISTINCT map_alias.alias'),
deadline => $dbh->sql_date_format('bugs.deadline', '%Y-%m-%d'),
actual_time => 'map_actual_time.total',
@@ -558,13 +605,18 @@ sub COLUMNS {
. " END)",
'flagtypes.name' => $dbh->sql_group_concat('DISTINCT '
- . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status')),
+ . $dbh->sql_string_concat('map_flagtypes.name', 'map_flags.status'),
+ undef, undef, 'map_flagtypes.sortkey, map_flagtypes.name'),
'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)',
tag => $dbh->sql_group_concat('DISTINCT map_tag.name'),
+ last_visit_ts => 'bug_user_last_visit.last_visit_ts',
);
# Backward-compatibility for old field names. Goes new_name => old_name.
@@ -635,12 +687,7 @@ sub REPORT_COLUMNS {
# 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);
+ flagtypes.name relevance);
# If you're not a time-tracker, you can't use time-tracking
# columns.
@@ -658,7 +705,10 @@ sub REPORT_COLUMNS {
# 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(
+ alias
+ blocked
bug_id
+ dependson
flagtypes.name
keywords
longdescs.count
@@ -712,7 +762,7 @@ sub data {
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+$/;
+ next if ($self->COLUMNS->{$field}->{name} // $field) =~ /^bugs\.\w+$/;
$self->{fields} = ['bug_id'];
$all_in_bugs_table = 0;
last;
@@ -964,10 +1014,16 @@ 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";
+ my $sql = $self->COLUMNS->{$column}->{name} // '';
+ if ($sql) {
+ my $alias = $column;
+ # Aliases cannot contain dots in them. We convert them to underscores.
+ $alias =~ tr/./_/;
+ $sql .= " AS $alias";
+ }
+ else {
+ $sql = $column;
+ }
push(@sql_fields, $sql);
}
return @sql_fields;
@@ -1210,9 +1266,12 @@ sub _standard_joins {
push(@joins, $security_join);
if ($user->id) {
- $security_join->{extra} =
- ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"];
-
+ # See also _standard_joins for the other half of the below statement
+ if (!Bugzilla->params->{'or_groups'}) {
+ $security_join->{extra} =
+ ["NOT (" . $user->groups_in_sql('security_map.group_id') . ")"];
+ }
+
my $security_cc_join = {
table => 'cc',
as => 'security_cc',
@@ -1286,10 +1345,17 @@ sub _standard_where {
# 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;
+ my $security_term = '';
+ # See also _standard_joins for the other half of the below statement
+ if (Bugzilla->params->{'or_groups'}) {
+ $security_term .= " (security_map.group_id IS NULL OR security_map.group_id IN (" . $user->groups_as_string . "))";
+ }
+ else {
+ $security_term = 'security_map.group_id IS NULL';
+ }
+
if ($user->id) {
my $userid = $user->id;
# This indentation makes the resulting SQL more readable.
@@ -1334,7 +1400,7 @@ sub _sql_group_by {
my @extra_group_by;
foreach my $column ($self->_select_columns) {
next if $self->_skip_group_by->{$column};
- my $sql = $self->COLUMNS->{$column}->{name};
+ my $sql = $self->COLUMNS->{$column}->{name} // $column;
push(@extra_group_by, $sql);
}
@@ -1536,9 +1602,8 @@ sub _special_parse_chfield {
sub _special_parse_deadline {
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);
@@ -1680,6 +1745,8 @@ sub _boolean_charts {
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);
@@ -1726,6 +1793,8 @@ sub _custom_search {
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);
@@ -1755,20 +1824,30 @@ sub _handle_chart {
my ($field, $operator, $value) = $condition->fov;
return if (!defined $field or !defined $operator or !defined $value);
$field = FIELD_MAP->{$field} || $field;
-
- my $string_value;
+
+ my ($string_value, $orig_value);
+ state $is_mysql = $dbh->isa('Bugzilla::DB::Mysql') ? 1 : 0;
+
if (ref $value eq 'ARRAY') {
# Trim input and ignore blank values.
@$value = map { trim($_) } @$value;
@$value = grep { defined $_ and $_ ne '' } @$value;
return if !@$value;
+ $orig_value = join(',', @$value);
+ if ($field eq 'longdesc' && $is_mysql) {
+ @$value = map { _convert_unicode_characters($_) } @$value;
+ }
$string_value = join(',', @$value);
}
else {
return if $value eq '';
+ $orig_value = $value;
+ if ($field eq 'longdesc' && $is_mysql) {
+ $value = _convert_unicode_characters($value);
+ }
$string_value = $value;
}
-
+
$self->_chart_fields->{$field}
or ThrowCodeError("invalid_field_name", { field => $field });
trick_taint($field);
@@ -1812,7 +1891,7 @@ sub _handle_chart {
# do_search_function modified them.
$self->search_description({
field => $field, type => $operator,
- value => $string_value, term => $search_args{term},
+ value => $orig_value, term => $search_args{term},
});
foreach my $join (@{ $search_args{joins} }) {
@@ -1823,6 +1902,18 @@ sub _handle_chart {
$condition->translated(\%search_args);
}
+# XXX - This is a hack for MySQL which doesn't understand Unicode characters
+# above U+FFFF, see Bugzilla::Comment::_check_thetext(). This hack can go away
+# once we require MySQL 5.5.3 and use utf8mb4.
+sub _convert_unicode_characters {
+ my $string = shift;
+
+ # Perl 5.13.8 and older complain about non-characters.
+ no warnings 'utf8';
+ $string =~ s/([\x{10000}-\x{10FFFF}])/"\x{FDD0}[" . uc(sprintf('U+%04x', ord($1))) . "]\x{FDD1}"/eg;
+ return $string;
+}
+
##################################
# do_search_function And Helpers #
##################################
@@ -2075,22 +2166,44 @@ sub _word_terms {
#####################################
sub _timestamp_translate {
- my ($self, $args) = @_;
+ my ($self, $ignore_time, $args) = @_;
my $value = $args->{value};
my $dbh = Bugzilla->dbh;
return if $value !~ /^(?:[\+\-]?\d+[hdwmy]s?|now)$/i;
- # By default, the time is appended to the date, which we don't want
- # for deadlines.
$value = SqlifyDate($value);
- if ($args->{field} eq 'deadline') {
+ # 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, @_);
+}
+
+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';
+ }
+ }
+}
+
+
+sub _date_translate {
+ return shift->_timestamp_translate(1, @_);
+}
+
sub SqlifyDate {
my ($str) = @_;
my $fmt = "%Y-%m-%d %H:%M:%S";
@@ -2178,7 +2291,8 @@ sub pronoun {
if ($noun eq "%qacontact%") {
return "COALESCE(bugs.qa_contact,0)";
}
- return 0;
+
+ ThrowUserError('illegal_pronoun', { pronoun => $noun });
}
sub _contact_pronoun {
@@ -2354,7 +2468,7 @@ sub _user_nonchanged {
# 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/ ? 1 : 0;
+ my $is_negative = $operator =~ /^(?:no|isempty)/ ? 1 : 0;
if ($is_negative) {
$args->{operator} = $self->_reverse_operator($operator);
}
@@ -2442,6 +2556,11 @@ 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";
@@ -2583,6 +2702,21 @@ sub _percentage_complete {
$self->_add_extra_column('actual_time');
}
+sub _last_visit_ts {
+ my ($self, $args) = @_;
+
+ $args->{full_field} = $self->COLUMNS->{last_visit_ts}->{name};
+ $self->_add_extra_column('last_visit_ts');
+}
+
+sub _last_visit_ts_invalid_operator {
+ my ($self, $args) = @_;
+
+ ThrowUserError('search_field_operator_invalid',
+ { field => $args->{field},
+ operator => $args->{operator} });
+}
+
sub _days_elapsed {
my ($self, $args) = @_;
my $dbh = Bugzilla->dbh;
@@ -2612,6 +2746,15 @@ sub _product_nonchanged {
"products.id", "products", $term);
}
+sub _alias_nonchanged {
+ my ($self, $args) = @_;
+
+ $args->{full_field} = "bugs_aliases.alias";
+ $self->_do_operator_function($args);
+ $args->{term} = build_subselect("bugs.bug_id",
+ "bugs_aliases.bug_id", "bugs_aliases", $args->{term});
+}
+
sub _classification_nonchanged {
my ($self, $args) = @_;
my $joins = $args->{joins};
@@ -2646,6 +2789,13 @@ sub _nullable_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)";
+}
+
sub _deadline {
my ($self, $args) = @_;
my $field = $args->{full_field};
@@ -2734,6 +2884,12 @@ 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.
@@ -2838,10 +2994,12 @@ sub _multiselect_table {
return "attachments INNER JOIN attach_data "
. " ON attachments.attach_id = attach_data.id"
}
- 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";
@@ -2850,6 +3008,11 @@ sub _multiselect_table {
sub _multiselect_term {
my ($self, $args, $not) = @_;
+ my ($operator) = $args->{operator};
+ my $value = $args->{value} || '';
+ # 'empty' operators require special handling
+ return $self->_multiselect_isempty($args, $not)
+ if ($operator =~ /^is(not)?empty$/ || $value eq '---');
my $table = $self->_multiselect_table($args);
$self->_do_operator_function($args);
my $term = $args->{term};
@@ -2858,6 +3021,125 @@ sub _multiselect_term {
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 'attach_data.thedata') {
+ push @$joins, {
+ table => 'attachments',
+ as => "attachments_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ extra => [ $self->_user->is_insider ? '' : "attachments_$chart_id.isprivate = 0" ],
+ };
+ push @$joins, {
+ table => 'attach_data',
+ as => "attach_data_$chart_id",
+ from => "attachments_$chart_id.attach_id",
+ to => 'id',
+ };
+ return "attach_data_$chart_id.thedata IS $not NULL";
+ }
+ 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";
+ }
+ elsif ($self->_multi_select_fields->{$field}) {
+ push @$joins, {
+ table => "bug_$field",
+ as => "bug_${field}_$chart_id",
+ from => 'bug_id',
+ to => 'bug_id',
+ };
+ return "bug_${field}_$chart_id.bug_id IS $not NULL";
+ }
+}
+
###############################
# Standard Operator Functions #
###############################
@@ -3074,6 +3356,27 @@ sub _changed_security_check {
}
}
+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});
+}
+
+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});
+}
+
+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 "''";
+}
+
######################
# Public Subroutines #
######################
@@ -3177,7 +3480,7 @@ value for this field. At least one search criteria must be defined if the
=item C<sharer>
-When a saved search is shared by a user, this is his user ID.
+When a saved search is shared by a user, this is their user ID.
=item C<user>
@@ -3228,3 +3531,35 @@ two hashes if two SQL queries have been executed sequentially to get all the
required data.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item invalid_order_columns
+
+=item COLUMN_JOINS
+
+=item split_order_term
+
+=item SqlifyDate
+
+=item REPORT_COLUMNS
+
+=item pronoun
+
+=item COLUMNS
+
+=item order
+
+=item search_description
+
+=item IsValidQueryType
+
+=item build_subselect
+
+=item do_search_function
+
+=item boolean_charts_to_custom_search
+
+=back
diff --git a/Bugzilla/Search/Clause.pm b/Bugzilla/Search/Clause.pm
index 6214ab197..1d7872c78 100644
--- a/Bugzilla/Search/Clause.pm
+++ b/Bugzilla/Search/Clause.pm
@@ -6,7 +6,10 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Search::Clause;
+
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Error;
use Bugzilla::Search::Condition qw(condition);
@@ -131,3 +134,27 @@ sub as_params {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item has_translated_conditions
+
+=item as_string
+
+=item add
+
+=item children
+
+=item negate
+
+=item update_search_args
+
+=item walk_conditions
+
+=item joiner
+
+=item as_params
+
+=back
diff --git a/Bugzilla/Search/ClauseGroup.pm b/Bugzilla/Search/ClauseGroup.pm
index 83961e12b..590c737fa 100644
--- a/Bugzilla/Search/ClauseGroup.pm
+++ b/Bugzilla/Search/ClauseGroup.pm
@@ -7,9 +7,11 @@
package Bugzilla::Search::ClauseGroup;
+use 5.10.1;
use strict;
+use warnings;
-use base qw(Bugzilla::Search::Clause);
+use parent qw(Bugzilla::Search::Clause);
use Bugzilla::Error;
use Bugzilla::Search::Condition qw(condition);
@@ -97,3 +99,13 @@ sub update_search_args {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item add
+
+=item update_search_args
+
+=back
diff --git a/Bugzilla/Search/Condition.pm b/Bugzilla/Search/Condition.pm
index fb899afbc..306a63eed 100644
--- a/Bugzilla/Search/Condition.pm
+++ b/Bugzilla/Search/Condition.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Search::Condition;
+
+use 5.10.1;
use strict;
-use base qw(Exporter);
+use warnings;
+
+use parent qw(Exporter);
our @EXPORT_OK = qw(condition);
sub new {
@@ -73,3 +77,27 @@ sub condition {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item as_string
+
+=item fov
+
+=item value
+
+=item negate
+
+=item translated
+
+=item operator
+
+=item as_params
+
+=item condition
+
+=item field
+
+=back
diff --git a/Bugzilla/Search/Quicksearch.pm b/Bugzilla/Search/Quicksearch.pm
index 3e8340c86..830177f8b 100644
--- a/Bugzilla/Search/Quicksearch.pm
+++ b/Bugzilla/Search/Quicksearch.pm
@@ -7,8 +7,9 @@
package Bugzilla::Search::Quicksearch;
-# Make it harder for us to do dangerous things in Perl.
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Error;
use Bugzilla::Constants;
@@ -21,7 +22,7 @@ use List::Util qw(min max);
use List::MoreUtils qw(firstidx);
use Text::ParseWords qw(parse_line);
-use base qw(Exporter);
+use parent qw(Exporter);
@Bugzilla::Search::Quicksearch::EXPORT = qw(quicksearch);
# Custom mappings for some fields.
@@ -103,6 +104,17 @@ use constant FIELD_OPERATOR => {
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',
+};
+
# We might want to put this into localconfig or somewhere
use constant PRODUCT_EXCEPTIONS => (
'row', # [Browser]
@@ -196,6 +208,7 @@ sub quicksearch {
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);
@@ -262,6 +275,8 @@ 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
@@ -276,7 +291,7 @@ sub _parse_line {
my @words = parse_line($delim, $keep, $line);
foreach my $word (@words) {
- $word =~ tr/\000/'/;
+ $word =~ tr/\000/'/ if defined $word;
}
return @words;
}
@@ -287,7 +302,7 @@ sub _bug_numbers_only {
# Allow separation by comma or whitespace.
$searchstring =~ s/[,\s]+/,/g;
- if ($searchstring !~ /,/) {
+ if ($searchstring !~ /,/ && !i_am_webservice()) {
# Single bug number; shortcut to show_bug.cgi.
print $cgi->redirect(
-uri => correct_urlbase() . "show_bug.cgi?id=$searchstring");
@@ -307,9 +322,10 @@ sub _handle_alias {
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, do not resolve its alias.
- if ($bug_id && Bugzilla->user->can_see_bug($bug_id)) {
+ q{SELECT bug_id FROM bugs_aliases 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 => correct_urlbase() . "show_bug.cgi?id=$alias");
@@ -348,6 +364,7 @@ sub _handle_status_and_resolution {
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);
@@ -386,8 +403,13 @@ sub _handle_field_names {
# Generic field1,field2,field3:value1,value2 notation.
# We have to correctly ignore commas and colons in quotes.
- my @field_values = _parse_line(':', 1, $or_operand);
- if (scalar @field_values == 2) {
+ # 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) {
@@ -406,7 +428,9 @@ sub _handle_field_names {
$bug_status_set = 1;
}
foreach my $value (@values) {
- my $operator = FIELD_OPERATOR->{$translated} || 'substring';
+ 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.
@@ -448,7 +472,7 @@ sub _handle_flags {
# are unable to run queries of the form (a AND b) OR c. In our case:
# (flag name is foo AND requestee is bar) OR (any other criteria).
# But this has never been possible, so this is not a regression. If one
- # needs to run such queries, he must use the Custom Search section of
+ # needs to run such queries, they must use the Custom Search section of
# the Advanced Search page.
$chart++;
$and = $or = 0;
@@ -654,3 +678,21 @@ sub makeChart {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item FIELD_MAP
+
+=item quicksearch
+
+=item negateComparisonType
+
+=item makeChart
+
+=item addChart
+
+=item matchPrefixes
+
+=back
diff --git a/Bugzilla/Search/Recent.pm b/Bugzilla/Search/Recent.pm
index 1ff56c7e3..e774c7fe0 100644
--- a/Bugzilla/Search/Recent.pm
+++ b/Bugzilla/Search/Recent.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Search::Recent;
+
+use 5.10.1;
use strict;
-use base qw(Bugzilla::Object);
+use warnings;
+
+use parent qw(Bugzilla::Object);
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -39,6 +43,9 @@ use constant VALIDATORS => {
use constant UPDATE_COLUMNS => qw(bug_list list_order);
+# There's no gain to caching these objects
+use constant USE_MEMCACHED => 0;
+
###################
# DB Manipulation #
###################
@@ -157,3 +164,27 @@ Bugzilla::Search::Recent - A search recently run by a logged-in user.
This is an implementation of L<Bugzilla::Object>, and so has all the
same methods available as L<Bugzilla::Object>, in addition to what is
documented below.
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item create
+
+=item list_order
+
+=item check_quietly
+
+=item new_from_cookie
+
+=item create_placeholder
+
+=item bug_list
+
+=item set_bug_list
+
+=item user_id
+
+=item set_list_order
+
+=back
diff --git a/Bugzilla/Search/Saved.pm b/Bugzilla/Search/Saved.pm
index c9885c9ce..50a9cdd67 100644
--- a/Bugzilla/Search/Saved.pm
+++ b/Bugzilla/Search/Saved.pm
@@ -5,11 +5,13 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Search::Saved;
-use base qw(Bugzilla::Object);
+use 5.10.1;
+use strict;
+use warnings;
+
+use parent qw(Bugzilla::Object);
use Bugzilla::CGI;
use Bugzilla::Constants;
@@ -186,6 +188,7 @@ sub rename_field_value {
}
$dbh->do("UPDATE $table SET query = ? WHERE $id_field = ?",
undef, $query, $id);
+ Bugzilla->memcached->clear({ table => $table, id => $id });
}
$dbh->bz_commit_transaction();
@@ -288,9 +291,8 @@ sub url { return $_[0]->{'query'}; }
sub user {
my ($self) = @_;
- return $self->{user} if defined $self->{user};
- $self->{user} = new Bugzilla::User($self->{userid});
- return $self->{user};
+ return $self->{user} ||=
+ Bugzilla::User->new({ id => $self->{userid}, cache => 1 });
}
############
@@ -385,3 +387,21 @@ Returns how many users (besides the author of the saved search) are
using the saved search, i.e. have it displayed in their footer.
=back
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item create
+
+=item set_name
+
+=item set_url
+
+=item rename_field_value
+
+=item user
+
+=item used_in_whine
+
+=back
diff --git a/Bugzilla/Send/Sendmail.pm b/Bugzilla/Sender/Transport/Sendmail.pm
index 012cd6f28..49f00777f 100644
--- a/Bugzilla/Send/Sendmail.pm
+++ b/Bugzilla/Sender/Transport/Sendmail.pm
@@ -5,56 +5,49 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-package Bugzilla::Send::Sendmail;
+package Bugzilla::Sender::Transport::Sendmail;
+use 5.10.1;
use strict;
+use warnings;
-use base qw(Email::Send::Sendmail);
+use parent qw(Email::Sender::Transport::Sendmail);
-use Return::Value;
-use Symbol qw(gensym);
+use Email::Sender::Failure;
-sub send {
- my ($class, $message, @args) = @_;
- my $mailer = $class->_find_sendmail;
+sub send_email {
+ my ($self, $email, $envelope) = @_;
- return failure "Couldn't find 'sendmail' executable in your PATH"
- ." and Email::Send::Sendmail::SENDMAIL is not set"
- unless $mailer;
+ my $pipe = $self->_sendmail_pipe($envelope);
- return failure "Found $mailer but cannot execute it"
- unless -x $mailer;
-
- local $SIG{'CHLD'} = 'DEFAULT';
+ my $string = $email->as_string;
+ $string =~ s/\x0D\x0A/\x0A/g unless $^O eq 'MSWin32';
- my $pipe = gensym;
+ print $pipe $string
+ or Email::Sender::Failure->throw("couldn't send message to sendmail: $!");
- 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 $!;
+ Email::Sender::Failure->throw("error when closing pipe to sendmail: $!") if $!;
my ($error_message, $is_transient) = _map_exitcode($? >> 8);
if (Bugzilla->params->{'use_mailer_queue'}) {
# Return success for errors which are fatal so Bugzilla knows to
- # remove them from the queue
+ # remove them from the queue.
if ($is_transient) {
- return failure "error when closing pipe to $mailer: $error_message";
+ Email::Sender::Failure->throw("error when closing pipe to sendmail: $error_message");
} else {
- warn "error when closing pipe to $mailer: $error_message\n";
- return success;
+ warn "error when closing pipe to sendmail: $error_message\n";
+ return $self->success;
}
} else {
- return failure "error when closing pipe to $mailer: $error_message";
+ Email::Sender::Failure->throw("error when closing pipe to sendmail: $error_message");
}
}
- return success;
+ return $self->success;
}
sub _map_exitcode {
# Returns (error message, is_transient)
- # from the sendmail source (sendmail/sysexit.h)
+ # from the sendmail source (sendmail/sysexits.h)
my $code = shift;
if ($code == 64) {
return ("Command line usage error (EX_USAGE)", 1);
@@ -93,3 +86,10 @@ sub _map_exitcode {
1;
+=head1 B<Methods in need of POD>
+
+=over
+
+=item send_email
+
+=back
diff --git a/Bugzilla/Series.pm b/Bugzilla/Series.pm
index f2ed9a4af..22202c6f1 100644
--- a/Bugzilla/Series.pm
+++ b/Bugzilla/Series.pm
@@ -5,8 +5,6 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
# This module implements a series - a set of data to be plotted on a chart.
#
# This Series is in the database if and only if self->{'series_id'} is defined.
@@ -16,6 +14,10 @@ use strict;
package Bugzilla::Series;
+use 5.10.1;
+use strict;
+use warnings;
+
use Bugzilla::Error;
use Bugzilla::Util;
@@ -269,3 +271,29 @@ sub remove_from_db {
}
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item creator
+
+=item existsInDatabase
+
+=item name
+
+=item getCategoryID
+
+=item initFromParameters
+
+=item initFromCGI
+
+=item initFromDatabase
+
+=item remove_from_db
+
+=item writeToDatabase
+
+=item id
+
+=back
diff --git a/Bugzilla/Status.pm b/Bugzilla/Status.pm
index 2821f7c6e..275510216 100644
--- a/Bugzilla/Status.pm
+++ b/Bugzilla/Status.pm
@@ -5,16 +5,17 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-use strict;
-
package Bugzilla::Status;
-use Bugzilla::Error;
+use 5.10.1;
+use strict;
+use warnings;
+
# This subclasses Bugzilla::Field::Choice instead of implementing
# ChoiceInterface, because a bug status literally is a special type
# of Field::Choice, not just an object that happens to have the same
# methods.
-use base qw(Bugzilla::Field::Choice Exporter);
+use parent qw(Bugzilla::Field::Choice Exporter);
@Bugzilla::Status::EXPORT = qw(
BUG_STATE_OPEN
SPECIAL_STATUS_WORKFLOW_ACTIONS
@@ -23,6 +24,8 @@ use base qw(Bugzilla::Field::Choice Exporter);
closed_bug_statuses
);
+use Bugzilla::Error;
+
################################
##### Initialization #####
################################
@@ -106,11 +109,21 @@ sub _check_value {
sub BUG_STATE_OPEN {
my $dbh = Bugzilla->dbh;
- my $cache = Bugzilla->request_cache;
- $cache->{status_bug_state_open} ||=
- $dbh->selectcol_arrayref('SELECT value FROM bug_status
- WHERE is_open = 1');
- return @{ $cache->{status_bug_state_open} };
+ 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.
@@ -295,3 +308,27 @@ C<1> if a comment is required on this change, C<0> if not.
=back
=cut
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item create
+
+=item BUG_STATE_OPEN
+
+=item is_static
+
+=item is_open_state
+
+=item is_active
+
+=item remove_from_db
+
+=item DB_COLUMNS
+
+=item is_open
+
+=item VALIDATORS
+
+=back
diff --git a/Bugzilla/Template.pm b/Bugzilla/Template.pm
index acfc5a50f..6ac36f783 100644
--- a/Bugzilla/Template.pm
+++ b/Bugzilla/Template.pm
@@ -8,7 +8,9 @@
package Bugzilla::Template;
+use 5.10.1;
use strict;
+use warnings;
use Bugzilla::Constants;
use Bugzilla::WebService::Constants;
@@ -16,6 +18,7 @@ use Bugzilla::Hook;
use Bugzilla::Install::Requirements;
use Bugzilla::Install::Util qw(install_string template_include_path
include_languages);
+use Bugzilla::Classification;
use Bugzilla::Keyword;
use Bugzilla::Util;
use Bugzilla::Error;
@@ -25,15 +28,17 @@ use Bugzilla::Token;
use Cwd qw(abs_path);
use MIME::Base64;
use Date::Format ();
+use Digest::MD5 qw(md5_hex);
use File::Basename qw(basename dirname);
use File::Find;
use File::Path qw(rmtree mkpath);
+use File::Slurp;
use File::Spec;
use IO::Dir;
use List::MoreUtils qw(firstidx);
use Scalar::Util qw(blessed);
-use base qw(Template);
+use parent qw(Template);
use constant FORMAT_TRIPLE => '%19s|%-28s|%-28s';
use constant FORMAT_3_SIZE => [19,28,28];
@@ -95,8 +100,8 @@ sub get_format {
my $self = shift;
my ($template, $format, $ctype) = @_;
- $ctype ||= 'html';
- $format ||= '';
+ $ctype //= 'html';
+ $format //= '';
# ctype and format can have letters and a hyphen only.
if ($ctype =~ /[^a-zA-Z\-]/ || $format =~ /[^a-zA-Z\-]/) {
@@ -157,6 +162,10 @@ sub quoteUrls {
# 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.
@@ -223,7 +232,7 @@ sub quoteUrls {
~<a href=\"mailto:$2\">$1$2</a>~igx;
# attachment links
- $text =~ s~\b(attachment\s*\#?\s*(\d+)(?:\s+\[details\])?)
+ $text =~ s~\b(attachment$s*\#?$s*(\d+)(?:$s+\[details\])?)
~($things[$count++] = get_attachment_link($2, $1, $user)) &&
("\x{FDD2}" . ($count-1) . "\x{FDD3}")
~egmxi;
@@ -236,14 +245,40 @@ sub quoteUrls {
# 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)
+ my $bug_re = qr/\Q$bug_word\E$s*\#?$s*(\d+)/i;
+ my $comment_word = template_var('terms')->{comment};
+ my $comment_re = qr/(?:\Q$comment_word\E|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) ? get_bug_link($2, $1, { comment_num => $3, user => $user }) :
"<a href=\"$current_bugurl#c$4\">$1</a>")
- ~egox;
+ ~egx;
+
+ # Handle a list of bug ids: bugs 1, #2, 3, 4
+ # Currently, the only delimiter supported is comma.
+ # Concluding "and" and "or" are not supported.
+ my $bugs_word = template_var('terms')->{bugs};
+
+ my $bugs_re = qr/\Q$bugs_word\E$s*\#?$s*
+ \d+(?:$s*,$s*\#?$s*\d+)+/ix;
+
+ $text =~ s{($bugs_re)}{
+ my $match = $1;
+ $match =~ s/((?:#$s*)?(\d+))/get_bug_link($2, $1);/eg;
+ $match;
+ }eg;
+
+ my $comments_word = template_var('terms')->{comments};
+
+ my $comments_re = qr/(?:comments|\Q$comments_word\E)$s*\#?$s*
+ \d+(?:$s*,$s*\#?$s*\d+)+/ix;
+
+ $text =~ s{($comments_re)}{
+ my $match = $1;
+ $match =~ s|((?:#$s*)?(\d+))|<a href="$current_bugurl#c$2">$1</a>|g;
+ $match;
+ }eg;
# Old duplicate markers. These don't use $bug_word because they are old
# and were never customizable.
@@ -264,10 +299,9 @@ sub quoteUrls {
# 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($attachid);
+ my $attachment = new Bugzilla::Attachment({ id => $attachid, cache => 1 });
if ($attachment) {
my $title = "";
@@ -315,12 +349,11 @@ sub get_bug_link {
my ($bug, $link_text, $options) = @_;
$options ||= {};
$options->{user} ||= Bugzilla->user;
- my $dbh = Bugzilla->dbh;
- if (defined $bug) {
+ if (defined $bug && $bug ne '') {
if (!blessed($bug)) {
require Bugzilla::Bug;
- $bug = new Bugzilla::Bug($bug);
+ $bug = new Bugzilla::Bug({ id => $bug, cache => 1 });
}
return $link_text if $bug->{error};
}
@@ -386,18 +419,18 @@ sub mtime_filter {
# Set up the skin CSS cascade:
#
-# 1. YUI CSS
-# 2. Standard Bugzilla stylesheet set (persistent)
-# 3. Third-party "skin" stylesheet set, per user prefs (persistent)
-# 4. Page-specific styles
-# 5. Custom Bugzilla stylesheet set (persistent)
+# 1. standard/global.css
+# 2. YUI CSS
+# 3. Standard Bugzilla stylesheet set
+# 4. Third-party "skin" stylesheet set, per user prefs
+# 5. Inline css passed to global/header.html.tmpl
+# 6. Custom Bugzilla stylesheet set
sub css_files {
my ($style_urls, $yui, $yui_css) = @_;
-
- # global.css goes on every page, and so does IE-fixes.css.
- my @requested_css = ('skins/standard/global.css', @$style_urls,
- 'skins/standard/IE-fixes.css');
+
+ # global.css goes on every page.
+ my @requested_css = ('skins/standard/global.css', @$style_urls);
my @yui_required_css;
foreach my $yui_name (@$yui) {
@@ -414,7 +447,12 @@ sub css_files {
push(@{ $by_type{$key} }, $set->{$key});
}
}
-
+
+ # build unified
+ $by_type{unified_standard_skin} = _concatenate_css($by_type{standard},
+ $by_type{skin});
+ $by_type{unified_custom} = _concatenate_css($by_type{custom});
+
return \%by_type;
}
@@ -422,30 +460,137 @@ sub _css_link_set {
my ($file_name) = @_;
my %set = (standard => mtime_filter($file_name));
-
- # We use (^|/) to allow Extensions to use the skins system if they
- # want.
- if ($file_name !~ m{(^|/)skins/standard/}) {
+
+ # 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/};
+ $skin_file_name =~ s{(?:^|/)skins/standard/}{skins/contrib/$skin/};
if (my $mtime = _mtime("$cgi_path/$skin_file_name")) {
$set{skin} = mtime_filter($skin_file_name, $mtime);
}
my $custom_file_name = $file_name;
- $custom_file_name =~ s{(^|/)skins/standard/}{skins/custom/};
+ $custom_file_name =~ s{(?:^|/)skins/standard/}{skins/custom/};
if (my $custom_mtime = _mtime("$cgi_path/$custom_file_name")) {
$set{custom} = mtime_filter($custom_file_name, $custom_mtime);
}
-
+
return \%set;
}
+sub _concatenate_css {
+ my @sources = map { @$_ } @_;
+ return unless @sources;
+
+ my %files =
+ map {
+ (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
+ $_ => $file;
+ } @sources;
+
+ my $cgi_path = bz_locations()->{cgi_path};
+ my $skins_path = bz_locations()->{assetsdir};
+
+ # build minified files
+ my @minified;
+ foreach my $source (@sources) {
+ next unless -e "$cgi_path/$files{$source}";
+ my $file = $skins_path . '/' . md5_hex($source) . '.css';
+ if (!-e $file) {
+ my $content = read_file("$cgi_path/$files{$source}");
+
+ # minify
+ $content =~ s{/\*.*?\*/}{}sg; # comments
+ $content =~ s{(^\s+|\s+$)}{}mg; # leading/trailing whitespace
+ $content =~ s{\n}{}g; # single line
+
+ # rewrite urls
+ $content =~ s{url\(([^\)]+)\)}{_css_url_rewrite($source, $1)}eig;
+
+ write_file($file, "/* $files{$source} */\n" . $content . "\n");
+ }
+ push @minified, $file;
+ }
+
+ # concat files
+ my $file = $skins_path . '/' . md5_hex(join(' ', @sources)) . '.css';
+ if (!-e $file) {
+ my $content = '';
+ foreach my $source (@minified) {
+ $content .= read_file($source);
+ }
+ write_file($file, $content);
+ }
+
+ $file =~ s/^\Q$cgi_path\E\///o;
+ return mtime_filter($file);
+}
+
+sub _css_url_rewrite {
+ my ($source, $url) = @_;
+ # rewrite relative urls as the unified stylesheet lives in a different
+ # directory from the source
+ $url =~ s/(^['"]|['"]$)//g;
+ if (substr($url, 0, 1) eq '/' || substr($url, 0, 5) eq 'data:') {
+ return 'url(' . $url . ')';
+ }
+ return 'url(../../' . ($ENV{'PROJECT'} ? '../' : '') . dirname($source) . '/' . $url . ')';
+}
+
+sub _concatenate_js {
+ return @_ unless CONCATENATE_ASSETS;
+ my ($sources) = @_;
+ return [] unless $sources;
+ $sources = ref($sources) ? $sources : [ $sources ];
+
+ my %files =
+ map {
+ (my $file = $_) =~ s/(^[^\?]+)\?.+/$1/;
+ $_ => $file;
+ } @$sources;
+
+ my $cgi_path = bz_locations()->{cgi_path};
+ my $skins_path = bz_locations()->{assetsdir};
+
+ # build minified files
+ my @minified;
+ foreach my $source (@$sources) {
+ next unless -e "$cgi_path/$files{$source}";
+ my $file = $skins_path . '/' . md5_hex($source) . '.js';
+ if (!-e $file) {
+ my $content = read_file("$cgi_path/$files{$source}");
+
+ # minimal minification
+ $content =~ s#/\*.*?\*/##sg; # block comments
+ $content =~ s#(^ +| +$)##gm; # leading/trailing spaces
+ $content =~ s#^//.+$##gm; # single line comments
+ $content =~ s#\n{2,}#\n#g; # blank lines
+ $content =~ s#(^\s+|\s+$)##g; # whitespace at the start/end of file
+
+ write_file($file, ";/* $files{$source} */\n" . $content . "\n");
+ }
+ push @minified, $file;
+ }
+
+ # concat files
+ my $file = $skins_path . '/' . md5_hex(join(' ', @$sources)) . '.js';
+ if (!-e $file) {
+ my $content = '';
+ foreach my $source (@minified) {
+ $content .= read_file($source);
+ }
+ write_file($file, $content);
+ }
+
+ $file =~ s/^\Q$cgi_path\E\///o;
+ return [ $file ];
+}
+
# YUI dependency resolution
sub yui_resolve_deps {
my ($yui, $yui_deps) = @_;
@@ -510,6 +655,21 @@ $Template::Stash::LIST_OPS->{ clone } =
return [@$list];
};
+# Allow us to sort the list of fields correctly
+$Template::Stash::LIST_OPS->{ sort_by_field_name } =
+ sub {
+ sub field_name {
+ if ($_[0] eq 'noop') {
+ # Sort --- first
+ return '';
+ }
+ # Otherwise sort by field_desc or description
+ return $_[1]{$_[0]} || $_[0];
+ }
+ my ($list, $field_desc) = @_;
+ return [ sort { lc field_name($a, $field_desc) cmp lc field_name($b, $field_desc) } @$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 } =
@@ -522,10 +682,9 @@ $Template::Stash::SCALAR_OPS->{ 0 } =
$Template::Stash::SCALAR_OPS->{ truncate } =
sub {
my ($string, $length, $ellipsis) = @_;
- $ellipsis ||= "";
-
return $string if !$length || length($string) <= $length;
-
+
+ $ellipsis ||= '';
my $strlen = $length - length($ellipsis);
my $newstr = substr($string, 0, $strlen) . $ellipsis;
return $newstr;
@@ -631,6 +790,8 @@ sub create {
$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/</\\x3c/g;
$var =~ s/>/\\x3e/g;
@@ -646,9 +807,10 @@ sub create {
# Strips out control characters excepting whitespace
strip_control_chars => sub {
my ($data) = @_;
+ state $use_utf8 = Bugzilla->params->{'utf8'};
# Only run for utf8 to avoid issues with other multibyte encodings
# that may be reassigning meaning to ascii characters.
- if (Bugzilla->params->{'utf8'}) {
+ if ($use_utf8) {
$data =~ s/(?![\t\r\n])[[:cntrl:]]//g;
}
return $data;
@@ -668,14 +830,6 @@ sub create {
return $var;
},
- # Prevents line break on hyphens and whitespaces.
- no_break => sub {
- my ($var) = @_;
- $var =~ s/ /\&nbsp;/g;
- $var =~ s/-/\&#8209;/g;
- return $var;
- },
-
xml => \&Bugzilla::Util::xml_quote ,
# This filter is similar to url_quote but used a \ instead of a %
@@ -812,9 +966,7 @@ sub create {
# (Wrapping the message in the WebService is unnecessary
# and causes awkward things like \n's appearing in error
# messages in JSON-RPC.)
- unless (Bugzilla->usage_mode == USAGE_MODE_JSON
- or Bugzilla->usage_mode == USAGE_MODE_XMLRPC)
- {
+ unless (i_am_webservice()) {
$var = wrap_comment($var, 72);
}
$var =~ s/\&nbsp;/ /g;
@@ -872,14 +1024,42 @@ sub create {
# started the session.
'sudoer' => sub { return Bugzilla->sudoer; },
- # Allow templates to access the "corect" URLBase value
+ # Allow templates to access the "correct" URLBase value
'urlbase' => sub { return Bugzilla::Util::correct_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/;
+ # We fall back to English if documentation in the preferred
+ # language is not available
+ 'docs_urlbase' => sub {
+ my $docs_urlbase;
+ my $lang = Bugzilla->current_language;
+ # Translations currently available on readthedocs.org
+ my @rtd_translations = ('en', 'fr');
+
+ if ($lang ne 'en' && -f "docs/$lang/html/index.html") {
+ $docs_urlbase = "docs/$lang/html/";
+ }
+ elsif (-f "docs/en/html/index.html") {
+ $docs_urlbase = "docs/en/html/";
+ }
+ else {
+ if (!grep { $_ eq $lang } @rtd_translations) {
+ $lang = "en";
+ }
+
+ my $version = BUGZILLA_VERSION;
+ $version =~ /^(\d+)\.(\d+)/;
+ if ($2 % 2 == 1) {
+ # second number is odd; development version
+ $version = 'latest';
+ }
+ else {
+ $version = "$1.$2";
+ }
+
+ $docs_urlbase = "https://bugzilla.readthedocs.org/$lang/$version/";
+ }
+
return $docs_urlbase;
},
@@ -904,6 +1084,12 @@ sub create {
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;
@@ -922,6 +1108,12 @@ sub create {
'css_files' => \&css_files,
yui_resolve_deps => \&yui_resolve_deps,
+ concatenate_js => \&_concatenate_js,
+
+ # All classifications (sorted by sortkey, name)
+ 'all_classifications' => sub {
+ return [map { $_->name } Bugzilla::Classification->get_all()];
+ },
# Whether or not keywords are enabled, in this Bugzilla.
'use_keywords' => sub { return Bugzilla::Keyword->any_exist; },
@@ -1160,3 +1352,29 @@ Returns: nothing
=head1 SEE ALSO
L<Bugzilla>, L<Template>
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item multiline_sprintf
+
+=item create
+
+=item css_files
+
+=item mtime_filter
+
+=item yui_resolve_deps
+
+=item process
+
+=item get_bug_link
+
+=item quoteUrls
+
+=item get_attachment_link
+
+=item SAFE_URL_REGEXP
+
+=back
diff --git a/Bugzilla/Template/Context.pm b/Bugzilla/Template/Context.pm
index 9dc1f08f9..470e6a9ee 100644
--- a/Bugzilla/Template/Context.pm
+++ b/Bugzilla/Template/Context.pm
@@ -7,8 +7,12 @@
# This exists to implement the template-before_process hook.
package Bugzilla::Template::Context;
+
+use 5.10.1;
use strict;
-use base qw(Template::Context);
+use warnings;
+
+use parent qw(Template::Context);
use Bugzilla::Hook;
use Scalar::Util qw(blessed);
@@ -81,6 +85,14 @@ sub 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);
+}
+
# We need a DESTROY sub for the same reason that Bugzilla::CGI does.
sub DESTROY {
my $self = shift;
@@ -88,3 +100,15 @@ sub DESTROY {
};
1;
+
+=head1 B<Methods in need of POD>
+
+=over
+
+=item stash
+
+=item filter
+
+=item process
+
+=back
diff --git a/Bugzilla/Template/Plugin/Bugzilla.pm b/Bugzilla/Template/Plugin/Bugzilla.pm
index e2b59c7a8..806dd903b 100644
--- a/Bugzilla/Template/Plugin/Bugzilla.pm
+++ b/Bugzilla/Template/Plugin/Bugzilla.pm
@@ -7,9 +7,11 @@
package Bugzilla::Template::Plugin::Bugzilla;
+use 5.10.1;
use strict;
+use warnings;
-use base qw(Template::Plugin);
+use parent qw(Template::Plugin);
use Bugzilla;
diff --git a/Bugzilla/Template/Plugin/Hook.pm b/Bugzilla/Template/Plugin/Hook.pm
index f1de7a602..669c77614 100644
--- a/Bugzilla/Template/Plugin/Hook.pm
+++ b/Bugzilla/Template/Plugin/Hook.pm
@@ -6,8 +6,12 @@
# defined by the Mozilla Public License, v. 2.0.
package Bugzilla::Template::Plugin::Hook;
+
+use 5.10.1;
use strict;
-use base qw(Template::Plugin);
+use warnings;
+
+use parent qw(Template::Plugin);
use Bugzilla::Constants;
use Bugzilla::Install::Util qw(template_include_path);
diff --git a/Bugzilla/Token.pm b/Bugzilla/Token.pm
index c7e9f645f..84d86b8c6 100644
--- a/Bugzilla/Token.pm
+++ b/Bugzilla/Token.pm
@@ -5,15 +5,11 @@
# This Source Code Form is "Incompatible With Secondary Licenses", as
# defined by the Mozilla Public License, v. 2.0.
-################################################################################
-# Module Initialization
-################################################################################
+package Bugzilla::Token;
-# Make it harder for us to do dangerous things in Perl.
+use 5.10.1;
use strict;
-
-# Bundle the functions in this file together into the "Bugzilla::Token" package.
-package Bugzilla::Token;
+use warnings;
use Bugzilla::Constants;
use Bugzilla::Error;
@@ -26,15 +22,30 @@ use Date::Parse;
use File::Basename;
use Digest::SHA qw(hmac_sha256_base64);
-use base qw(Exporter);
+use parent qw(Exporter);
-@Bugzilla::Token::EXPORT = qw(issue_session_token check_token_data delete_token
+@Bugzilla::Token::EXPORT = qw(issue_api_token issue_session_token
+ check_token_data delete_token
issue_hash_token check_hash_token);
################################################################################
# Public Functions
################################################################################
+# 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("
+ 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', '');
+}
+
# 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 {
@@ -44,7 +55,7 @@ sub issue_new_user_account_token {
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.
+ # an error because the user may have lost their email with the token inside.
# But to prevent using this way to mailbomb an email address, make sure
# the last request is old enough before sending a new email (default: 10 minutes).
@@ -71,7 +82,7 @@ sub issue_new_user_account_token {
# 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
+ # language used to view the "Create a New Account" page (we cannot use their
# user prefs as the user has no account yet!).
MessageToMTA($message);
}
@@ -127,16 +138,18 @@ sub IssuePasswordToken {
ThrowUserError('too_soon_for_new_token', {'type' => 'password'}) if $too_soon;
- my ($token, $token_ts) = _create_token($user->id, 'password', remote_ip());
+ my $ip_addr = remote_ip();
+ my ($token, $token_ts) = _create_token($user->id, 'password', $ip_addr);
# 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->{'ip_addr'} = $ip_addr;
$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).
+ # The user is not logged in (else they wouldn't request a new password).
# So we have to pass this information to the template.
$vars->{'timezone'} = $user->timezone;
@@ -377,7 +390,7 @@ sub check_token_data {
{
# 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!
+ # In this case, we want to know their name!
require Bugzilla::User;
my $vars = {};
@@ -419,6 +432,9 @@ sub _create_token {
trick_taint($tokentype);
trick_taint($eventdata);
+ my $is_shadow = Bugzilla->is_shadow_db;
+ $dbh = Bugzilla->switch_to_main_db() if $is_shadow;
+
$dbh->bz_start_transaction();
my $token = GenerateUniqueToken();
@@ -431,8 +447,10 @@ sub _create_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;
}
}
@@ -469,12 +487,20 @@ Bugzilla::Token - Provides different routines to manage tokens.
=over
+=item C<issue_api_token($login_name)>
+
+ Description: Creates a token that can be used for API calls on the web page.
+
+ Params: None.
+
+ Returns: The token.
+
=item C<issue_new_user_account_token($login_name)>
Description: Creates and sends a token per email to the email address
requesting a new user account. It doesn't check whether
the user account already exists. The user will have to
- use this token to confirm the creation of his user account.
+ use this token to confirm the creation of their user account.
Params: $login_name - The new login name requested by the user.
@@ -497,7 +523,7 @@ Bugzilla::Token - Provides different routines to manage tokens.
Description: Sends a token per email to the given user. This token
can be used to change the password (e.g. in case the user
- cannot remember his password and wishes to enter a new one).
+ cannot remember their password and wishes to enter a new one).
Params: $user - User object of the user requesting a new password.
@@ -527,7 +553,7 @@ Bugzilla::Token - Provides different routines to manage tokens.
Description: Invalidates an existing token, generally when the token is used
for an action which is not the one expected. An email is sent
- to the user who originally requested this token to inform him
+ to the user who originally requested this token to inform them
that this token has been invalidated (e.g. because an hacker
tried to use this token for some malicious action).
@@ -540,7 +566,7 @@ Bugzilla::Token - Provides different routines to manage tokens.
=item C<DeletePasswordTokens($user_id, $reason)>
Description: Cancels all password tokens for the given user. Emails are sent
- to the user to inform him about this action.
+ to the user to inform them about this action.