commit 4eb6a5cc4dfda874c8fe5de01f579d586283eb58 Author: Ralf Corsépius <corsepiu@xxxxxxxxxxxxxxxxx> Date: Tue Mar 24 09:56:39 2015 +0100 Update patches. - R: perl(Time::ParseDate). - Add docs symlink. - Add %license. - Spec cleanup. .gitignore | 1 - 0001-Remove-configure-time-generated-files.patch | 16731 +++++++++++++++++++ ...-usr-bin-perl-instead-of-usr-bin-env-perl.patch | 57 +- 0006-Fix-permissions.patch | 76 +- 0007-Fix-translation.patch | 12 +- 0008-Work-around-testsuite-failure.patch | 51 + rt.spec | 71 +- 7 files changed, 16851 insertions(+), 148 deletions(-) --- diff --git a/.gitignore b/.gitignore index 3ea9f93..c04fef0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1 @@ -/rt-4.2.9.tar.gz /rt-4.2.10.tar.gz diff --git a/0001-Remove-configure-time-generated-files.patch b/0001-Remove-configure-time-generated-files.patch new file mode 100644 index 0000000..f99d38d --- /dev/null +++ b/0001-Remove-configure-time-generated-files.patch @@ -0,0 +1,16731 @@ +From 70ba2dc6ef4e1eb97d77972872128160ae8b1039 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ralf=20Cors=C3=A9pius?= <corsepiu@xxxxxxxxxxxxxxxxx> +Date: Tue, 18 Mar 2014 07:23:24 +0100 +Subject: [PATCH 1/8] Remove configure time generated files. + +--- + Makefile | 583 ----- + bin/rt | 2682 --------------------- + bin/rt-crontool | 456 ---- + bin/rt-mailgate | 512 ---- + etc/RT_Config.pm | 3037 ------------------------ + etc/upgrade/3.8-ical-extension | 96 - + etc/upgrade/4.0-customfield-checkbox-extension | 87 - + etc/upgrade/generate-rtaddressregexp | 108 - + etc/upgrade/split-out-cf-categories | 170 -- + etc/upgrade/switch-templates-to | 147 -- + etc/upgrade/upgrade-articles | 264 -- + etc/upgrade/vulnerable-passwords | 141 -- + lib/RT/Generated.pm | 84 - + sbin/rt-attributes-viewer | 115 - + sbin/rt-clean-sessions | 183 -- + sbin/rt-dump-metadata | 336 --- + sbin/rt-email-dashboards | 165 -- + sbin/rt-email-digest | 374 --- + sbin/rt-email-group-admin | 520 ---- + sbin/rt-fulltext-indexer | 416 ---- + sbin/rt-importer | 283 --- + sbin/rt-preferences-viewer | 144 -- + sbin/rt-serializer | 399 ---- + sbin/rt-server | 181 -- + sbin/rt-server.fcgi | 181 -- + sbin/rt-session-viewer | 114 - + sbin/rt-setup-database | 795 ------- + sbin/rt-setup-fulltext-index | 763 ------ + sbin/rt-shredder | 317 --- + sbin/rt-test-dependencies | 652 ----- + sbin/rt-validate-aliases | 373 --- + sbin/rt-validator | 1465 ------------ + sbin/standalone_httpd | 181 -- + t/data/configs/apache2.2+fastcgi.conf | 49 - + t/data/configs/apache2.2+mod_perl.conf | 67 - + 35 files changed, 16440 deletions(-) + delete mode 100644 Makefile + delete mode 100755 bin/rt + delete mode 100755 bin/rt-crontool + delete mode 100755 bin/rt-mailgate + delete mode 100644 etc/RT_Config.pm + delete mode 100755 etc/upgrade/3.8-ical-extension + delete mode 100755 etc/upgrade/4.0-customfield-checkbox-extension + delete mode 100755 etc/upgrade/generate-rtaddressregexp + delete mode 100755 etc/upgrade/split-out-cf-categories + delete mode 100755 etc/upgrade/switch-templates-to + delete mode 100755 etc/upgrade/upgrade-articles + delete mode 100755 etc/upgrade/vulnerable-passwords + delete mode 100644 lib/RT/Generated.pm + delete mode 100755 sbin/rt-attributes-viewer + delete mode 100755 sbin/rt-clean-sessions + delete mode 100755 sbin/rt-dump-metadata + delete mode 100755 sbin/rt-email-dashboards + delete mode 100755 sbin/rt-email-digest + delete mode 100755 sbin/rt-email-group-admin + delete mode 100755 sbin/rt-fulltext-indexer + delete mode 100755 sbin/rt-importer + delete mode 100755 sbin/rt-preferences-viewer + delete mode 100755 sbin/rt-serializer + delete mode 100755 sbin/rt-server + delete mode 100755 sbin/rt-server.fcgi + delete mode 100755 sbin/rt-session-viewer + delete mode 100755 sbin/rt-setup-database + delete mode 100755 sbin/rt-setup-fulltext-index + delete mode 100755 sbin/rt-shredder + delete mode 100755 sbin/rt-test-dependencies + delete mode 100755 sbin/rt-validate-aliases + delete mode 100755 sbin/rt-validator + delete mode 100755 sbin/standalone_httpd + delete mode 100644 t/data/configs/apache2.2+fastcgi.conf + delete mode 100644 t/data/configs/apache2.2+mod_perl.conf + +diff --git a/Makefile b/Makefile +deleted file mode 100644 +index c16f4dd..0000000 +--- a/Makefile ++++ /dev/null +@@ -1,583 +0,0 @@ +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-# +-# DO NOT HAND-EDIT the file named 'Makefile'. This file is autogenerated. +-# Have a look at "configure" and "Makefile.in" instead +-# +- +- +-PERL = /usr/bin/perl +-INSTALL = ./install-sh +-CC = @CC@ +- +-RT_LAYOUT = relative +- +-CONFIG_FILE_PATH = /opt/rt4/etc +-CONFIG_FILE = $(CONFIG_FILE_PATH)/RT_Config.pm +-SITE_CONFIG_FILE = $(CONFIG_FILE_PATH)/RT_SiteConfig.pm +- +- +-RT_VERSION_MAJOR = 4 +-RT_VERSION_MINOR = 2 +-RT_VERSION_PATCH = 10 +- +-RT_VERSION = $(RT_VERSION_MAJOR).$(RT_VERSION_MINOR).$(RT_VERSION_PATCH) +-TAG = rt-$(RT_VERSION_MAJOR)-$(RT_VERSION_MINOR)-$(RT_VERSION_PATCH) +- +- +-# This is the group that all of the installed files will be chgrp'ed to. +-RTGROUP = www-data +- +- +-# User which should own rt binaries. +-BIN_OWNER = root +- +-# User that should own all of RT's libraries, generally root. +-LIBS_OWNER = root +- +-# Group that should own all of RT's libraries, generally root. +-LIBS_GROUP = bin +- +-WEB_USER = www-data +-WEB_GROUP = www-data +- +-# DESTDIR allows you to specify that RT be installed somewhere other than +-# where it will eventually reside. DESTDIR _must_ have a trailing slash +-# if it's defined. +- +-DESTDIR = +- +- +- +-RT_PATH = /opt/rt4 +-RT_ETC_PATH = /opt/rt4/etc +-RT_BIN_PATH = /opt/rt4/bin +-RT_SBIN_PATH = /opt/rt4/sbin +-RT_LIB_PATH = /opt/rt4/lib +-RT_MAN_PATH = /opt/rt4/man +-RT_VAR_PATH = /opt/rt4/var +-RT_DOC_PATH = /opt/rt4/docs +-RT_FONT_PATH = /opt/rt4/share/fonts +-RT_LEXICON_PATH = /opt/rt4/share/po +-RT_STATIC_PATH = /opt/rt4/share/static +-RT_LOCAL_PATH = /opt/rt4/local +-LOCAL_PLUGIN_PATH = /opt/rt4/local/plugins +-LOCAL_ETC_PATH = /opt/rt4/local/etc +-LOCAL_LIB_PATH = /opt/rt4/local/lib +-LOCAL_LEXICON_PATH = /opt/rt4/local/po +-LOCAL_STATIC_PATH = /opt/rt4/local/static +-MASON_HTML_PATH = /opt/rt4/share/html +-MASON_LOCAL_HTML_PATH = /opt/rt4/local/html +-MASON_DATA_PATH = /opt/rt4/var/mason_data +-MASON_SESSION_PATH = /opt/rt4/var/session_data +-RT_LOG_PATH = /opt/rt4/var/log +- +-# RT_READABLE_DIR_MODE is the mode of directories that are generally meant +-# to be accessable +-RT_READABLE_DIR_MODE = 0755 +- +- +- +- +- +-# RT's CLI +-RT_CLI_BIN = rt +-# RT's mail gateway +-RT_MAILGATE_BIN = rt-mailgate +-# RT's cron tool +-RT_CRON_BIN = rt-crontool +- +- +- +-BINARIES = $(RT_MAILGATE_BIN) \ +- $(RT_CLI_BIN) \ +- $(RT_CRON_BIN) +- +-SYSTEM_BINARIES = rt-attributes-viewer \ +- rt-clean-sessions \ +- rt-dump-metadata \ +- rt-email-dashboards \ +- rt-email-digest \ +- rt-email-group-admin \ +- rt-fulltext-indexer \ +- rt-importer \ +- rt-preferences-viewer \ +- rt-serializer \ +- rt-server \ +- rt-server.fcgi \ +- rt-session-viewer \ +- rt-setup-database \ +- rt-setup-fulltext-index \ +- rt-shredder \ +- rt-test-dependencies \ +- rt-validator \ +- rt-validate-aliases \ +- standalone_httpd +- +- +-ETC_FILES = acl.Pg \ +- acl.Oracle \ +- acl.mysql \ +- schema.Pg \ +- schema.Oracle \ +- schema.mysql \ +- schema.SQLite \ +- initialdata +- +- +- +-WEB_HANDLER = standalone +- +- +- +-# +-# DB_TYPE defines what sort of database RT trys to talk to +-# "mysql", "Oracle", "Pg", and "SQLite" are known to work. +- +-DB_TYPE = SQLite +- +-# Set DBA to the name of a unix account with the proper permissions and +-# environment to run your commandline SQL sbin +- +-# Set DB_DBA to the name of a DB user with permission to create new databases +- +-# For mysql, you probably want 'root' +-# For Pg, you probably want 'postgres' +-# For Oracle, you want 'system' +- +-DB_DBA = root +- +-DB_HOST = localhost +- +-# If you're not running your database server on its default port, +-# specifiy the port the database server is running on below. +-# It's generally safe to leave this blank +- +-DB_PORT = +- +- +- +- +-# +-# Set this to the canonical name of the interface RT will be talking to the +-# database on. If you said that the RT_DB_HOST above was "localhost," this +-# should be too. This value will be used to grant rt access to the database. +-# If you want to access the RT database from multiple hosts, you'll need +-# to grant those database rights by hand. +-# +- +-DB_RT_HOST = localhost +- +-# set this to the name you want to give to the RT database in +-# your database server. For Oracle, this should be the name of your sid +- +-DB_DATABASE = rt4 +-DB_RT_USER = rt_user +-DB_RT_PASS = rt_pass +- +- +- +-TEST_FILES = t/*.t t/*/*.t t/*/*/*.t +-TEST_VERBOSE = 0 +- +-RT_TEST_PARALLEL_NUM ?= 5 +- +- +-#################################################################### +- +-all: default +- +-default: +- @echo "Please read RT's README before beginning your installation." +- +- +- +-instruct: +- @echo "Congratulations. RT is now installed." +- @echo "" +- @echo "" +- @echo "You must now configure RT by editing $(SITE_CONFIG_FILE)." +- @echo "" +- @echo "(You will definitely need to set RT's database password in " +- @echo "$(SITE_CONFIG_FILE) before continuing. Not doing so could be " +- @echo "very dangerous. Note that you do not have to manually add a " +- @echo "database user or set up a database for RT. These actions will be " +- @echo "taken care of in the next step.)" +- @echo "" +- @echo "After that, you need to initialize RT's database by running" +- @echo " 'make initialize-database'" +- +- +-upgrade-instruct: +- @echo "Congratulations. RT has been upgraded. You should now check over" +- @echo "$(CONFIG_FILE) for any necessary site customization. Additionally," +- @echo "you should update RT's system database objects by running " +- @echo " make upgrade-database" +- +- +-upgrade: testdeps config-install dirs files-install fixperms upgrade-instruct +- +-my_with_web_handlers= $(shell $(PERL) -e 'print join " ", map "--with-$$_", grep defined && length, split /,/, "$(WEB_HANDLER)"') +-testdeps: +- $(PERL) ./sbin/rt-test-dependencies --verbose --with-$(DB_TYPE) $(my_with_web_handlers) +- +-depends: fixdeps +- +-fixdeps: +- $(PERL) ./sbin/rt-test-dependencies --verbose --install --with-$(DB_TYPE) $(my_with_web_handlers) +- +-#}}} +- +-fixperms: +- # Make the libraries readable +- chmod $(RT_READABLE_DIR_MODE) $(DESTDIR)$(RT_PATH) +- chown -R $(LIBS_OWNER) $(DESTDIR)$(RT_LIB_PATH) +- chgrp -R $(LIBS_GROUP) $(DESTDIR)$(RT_LIB_PATH) +- chmod -R u+rwX,go-w,go+rX $(DESTDIR)$(RT_LIB_PATH) +- +- +- chmod $(RT_READABLE_DIR_MODE) $(DESTDIR)$(RT_BIN_PATH) +- +- chmod 0755 $(DESTDIR)$(RT_ETC_PATH) +- cd $(DESTDIR)$(RT_ETC_PATH) && chmod 0400 $(ETC_FILES) +- +- #TODO: the config file should probably be able to have its +- # owner set separately from the binaries. +- chown -R $(BIN_OWNER) $(DESTDIR)$(RT_ETC_PATH) +- chgrp -R $(RTGROUP) $(DESTDIR)$(RT_ETC_PATH) +- +- chmod 0440 $(DESTDIR)$(CONFIG_FILE) +- chmod 0640 $(DESTDIR)$(SITE_CONFIG_FILE) +- +- # Make the system binaries +- cd $(DESTDIR)$(RT_BIN_PATH) && ( chmod 0755 $(BINARIES) ; chown $(BIN_OWNER) $(BINARIES); chgrp $(RTGROUP) $(BINARIES)) +- +- # Make the system binaries executable also +- cd $(DESTDIR)$(RT_SBIN_PATH) && ( chmod 0755 $(SYSTEM_BINARIES) ; chown $(BIN_OWNER) $(SYSTEM_BINARIES); chgrp $(RTGROUP) $(SYSTEM_BINARIES)) +- +- # Make upgrade scripts executable if they are in the source. +- # +- # Note that we use the deprecated (by GNU/POSIX find) -perm +0NNN syntax +- # instead of -perm /0NNN since BSD find doesn't support the latter. +- ( cd etc/upgrade && find . -type f -not -name '*.in' -perm +0111 -print ) | while read file ; do \ +- chmod a+x "$(DESTDIR)$(RT_ETC_PATH)/upgrade/$$file" ; \ +- done +- +- # Make the web ui readable by all. +- chmod -R u+rwX,go-w,go+rX $(DESTDIR)$(MASON_HTML_PATH) \ +- $(DESTDIR)$(MASON_LOCAL_HTML_PATH) \ +- $(DESTDIR)$(RT_LEXICON_PATH) \ +- $(DESTDIR)$(LOCAL_LEXICON_PATH) \ +- $(DESTDIR)$(RT_STATIC_PATH) \ +- $(DESTDIR)$(LOCAL_STATIC_PATH) +- chown -R $(LIBS_OWNER) $(DESTDIR)$(MASON_HTML_PATH) \ +- $(DESTDIR)$(MASON_LOCAL_HTML_PATH) \ +- $(DESTDIR)$(RT_LEXICON_PATH) \ +- $(DESTDIR)$(LOCAL_LEXICON_PATH) \ +- $(DESTDIR)$(RT_STATIC_PATH) \ +- $(DESTDIR)$(LOCAL_STATIC_PATH) +- chgrp -R $(LIBS_GROUP) $(DESTDIR)$(MASON_HTML_PATH) \ +- $(DESTDIR)$(MASON_LOCAL_HTML_PATH) \ +- $(DESTDIR)$(RT_LEXICON_PATH) \ +- $(DESTDIR)$(LOCAL_LEXICON_PATH) \ +- $(DESTDIR)$(RT_STATIC_PATH) \ +- $(DESTDIR)$(LOCAL_STATIC_PATH) +- +- # Make the web ui's data dir writable +- chmod 0770 $(DESTDIR)$(MASON_DATA_PATH) \ +- $(DESTDIR)$(MASON_SESSION_PATH) +- chown -R $(WEB_USER) $(DESTDIR)$(MASON_DATA_PATH) \ +- $(DESTDIR)$(MASON_SESSION_PATH) +- chgrp -R $(WEB_GROUP) $(DESTDIR)$(MASON_DATA_PATH) \ +- $(DESTDIR)$(MASON_SESSION_PATH) +- +-dirs: +- $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_LOG_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_FONT_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_LEXICON_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_STATIC_PATH) +- $(INSTALL) -m 0770 -d $(DESTDIR)$(MASON_DATA_PATH) +- $(INSTALL) -m 0770 -d $(DESTDIR)$(MASON_DATA_PATH)/cache +- $(INSTALL) -m 0770 -d $(DESTDIR)$(MASON_DATA_PATH)/etc +- $(INSTALL) -m 0770 -d $(DESTDIR)$(MASON_DATA_PATH)/obj +- $(INSTALL) -m 0770 -d $(DESTDIR)$(MASON_SESSION_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(MASON_HTML_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(MASON_LOCAL_HTML_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(LOCAL_ETC_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(LOCAL_LIB_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(LOCAL_PLUGIN_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(LOCAL_LEXICON_PATH) +- $(INSTALL) -m 0755 -d $(DESTDIR)$(LOCAL_STATIC_PATH) +- +-clean-mason-cache: +- rm -rf $(DESTDIR)$(MASON_DATA_PATH)/cache/* +- rm -rf $(DESTDIR)$(MASON_DATA_PATH)/etc/* +- rm -rf $(DESTDIR)$(MASON_DATA_PATH)/obj/* +- +-install: testdeps config-install dirs files-install fixperms instruct +- +-files-install: libs-install etc-install config-install bin-install sbin-install html-install doc-install font-install po-install static-install +- +-config-install: +- $(INSTALL) -m 0755 -o $(BIN_OWNER) -g $(RTGROUP) -d $(DESTDIR)$(CONFIG_FILE_PATH) +- -$(INSTALL) -m 0440 -o $(BIN_OWNER) -g $(RTGROUP) etc/RT_Config.pm $(DESTDIR)$(CONFIG_FILE) +- [ -f $(DESTDIR)$(SITE_CONFIG_FILE) ] || $(INSTALL) -m 0640 -o $(BIN_OWNER) -g $(RTGROUP) etc/RT_SiteConfig.pm $(DESTDIR)$(SITE_CONFIG_FILE) +- @echo "Installed configuration. About to install RT in $(RT_PATH)" +- +-test: +- $(PERL) "-MExtUtils::Command::MM" -e "test_harness($(TEST_VERBOSE), 'lib')" $(TEST_FILES) +- +-parallel-test: test-parallel +- +-test-parallel: +- RT_TEST_PARALLEL=1 $(PERL) "-MApp::Prove" -e 'my $$p = App::Prove->new(); $$p->process_args("-wlrj$(RT_TEST_PARALLEL_NUM)","--state=slow,save", "t"); exit( $$p->run() ? 0 : 1 )' +- +-regression-reset-db: force-dropdb +- $(PERL) -I$(LOCAL_LIB_PATH) -I$(RT_LIB_PATH) sbin/rt-setup-database --action init --dba-password '' +- +-initdb :: initialize-database +- +-initialize-database: +- $(PERL) -I$(LOCAL_LIB_PATH) -I$(RT_LIB_PATH) sbin/rt-setup-database --action init --prompt-for-dba-password +- +-upgrade-database: +- $(PERL) -I$(LOCAL_LIB_PATH) -I$(RT_LIB_PATH) sbin/rt-setup-database --action upgrade --prompt-for-dba-password +- +-dropdb: +- $(PERL) -I$(LOCAL_LIB_PATH) -I$(RT_LIB_PATH) sbin/rt-setup-database --action drop --prompt-for-dba-password +- +-force-dropdb: +- $(PERL) -I$(LOCAL_LIB_PATH) -I$(RT_LIB_PATH) sbin/rt-setup-database --action drop --dba-password '' --force +- +-critic: +- perlcritic --quiet sbin bin lib +- +-libs-install: +- [ -d $(DESTDIR)$(RT_LIB_PATH) ] || $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_LIB_PATH) +- -( cd lib && find . -type d -print ) | while read dir ; do \ +- $(INSTALL) -m 0755 -d "$(DESTDIR)$(RT_LIB_PATH)/$$dir" ; \ +- done +- -( cd lib && find . -type f -print ) | while read file ; do \ +- $(INSTALL) -m 0644 "lib/$$file" "$(DESTDIR)$(RT_LIB_PATH)/$$file" ; \ +- done +- +-html-install: +- [ -d $(DESTDIR)$(MASON_HTML_PATH) ] || $(INSTALL) -m 0755 -d $(DESTDIR)$(MASON_HTML_PATH) +- -( cd share/html && find . -type d -print ) | while read dir ; do \ +- $(INSTALL) -m 0755 -d "$(DESTDIR)$(MASON_HTML_PATH)/$$dir" ; \ +- done +- -( cd share/html && find . -type f -print ) | while read file ; do \ +- $(INSTALL) -m 0644 "share/html/$$file" "$(DESTDIR)$(MASON_HTML_PATH)/$$file" ; \ +- done +- $(MAKE) clean-mason-cache +- +-font-install: +- [ -d $(DESTDIR)$(RT_FONT_PATH) ] || $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_FONT_PATH) +- -( cd share/fonts && find . -type f -print ) | while read file ; do \ +- $(INSTALL) -m 0644 "share/fonts/$$file" "$(DESTDIR)$(RT_FONT_PATH)/$$file" ; \ +- done +- +- +-po-install: +- [ -d $(DESTDIR)$(RT_LEXICON_PATH) ] || $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_LEXICON_PATH) +- -( cd share/po && find . -type f -print ) | while read file ; do \ +- $(INSTALL) -m 0644 "share/po/$$file" "$(DESTDIR)$(RT_LEXICON_PATH)/$$file" ; \ +- done +- +-static-install: +- [ -d $(DESTDIR)$(RT_STATIC_PATH) ] || $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_STATIC_PATH) +- -( cd share/static && find . -type d -print ) | while read dir ; do \ +- $(INSTALL) -m 0755 -d "$(DESTDIR)$(RT_STATIC_PATH)/$$dir" ; \ +- done +- -( cd share/static && find . -type f -print ) | while read file ; do \ +- $(INSTALL) -m 0644 "share/static/$$file" "$(DESTDIR)$(RT_STATIC_PATH)/$$file" ; \ +- done +- +- +-doc-install: +- # RT 3.0.0 - RT 3.0.2 would accidentally create a file instead of a dir +- -[ -f $(DESTDIR)$(RT_DOC_PATH) ] && rm $(DESTDIR)$(RT_DOC_PATH) +- [ -d $(DESTDIR)$(RT_DOC_PATH) ] || $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_DOC_PATH) +- -( cd docs && find . -type d -print ) | while read dir ; do \ +- $(INSTALL) -m 0755 -d "$(DESTDIR)$(RT_DOC_PATH)/$$dir" ; \ +- done +- -( cd docs && find . -type f -print ) | while read file ; do \ +- $(INSTALL) -m 0644 "docs/$$file" "$(DESTDIR)$(RT_DOC_PATH)/$$file" ; \ +- done +- -$(INSTALL) -m 0644 ./README $(DESTDIR)$(RT_DOC_PATH)/ +- +- +-etc-install: +- [ -d $(DESTDIR)$(RT_ETC_PATH) ] || $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_ETC_PATH) +- for file in $(ETC_FILES) ; do \ +- $(INSTALL) -m 0644 "etc/$$file" "$(DESTDIR)$(RT_ETC_PATH)/" ; \ +- done +- [ -d $(DESTDIR)$(RT_ETC_PATH)/upgrade ] || $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_ETC_PATH)/upgrade +- -( cd etc/upgrade && find . -type d -print ) | while read dir ; do \ +- $(INSTALL) -m 0755 -d "$(DESTDIR)$(RT_ETC_PATH)/upgrade/$$dir" ; \ +- done +- -( cd etc/upgrade && find . -type f -not -name '*.in' -print ) | while read file ; do \ +- $(INSTALL) -m 0644 "etc/upgrade/$$file" "$(DESTDIR)$(RT_ETC_PATH)/upgrade/$$file" ; \ +- done +- +- +-sbin-install: +- $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_SBIN_PATH) +- for file in $(SYSTEM_BINARIES) ; do \ +- $(INSTALL) -o $(BIN_OWNER) -g $(RTGROUP) -m 0755 "sbin/$$file" "$(DESTDIR)$(RT_SBIN_PATH)/" ; \ +- done +- +- +- +-bin-install: +- $(INSTALL) -m 0755 -d $(DESTDIR)$(RT_BIN_PATH) +- for file in $(BINARIES) ; do \ +- $(INSTALL) -o $(BIN_OWNER) -g $(RTGROUP) -m 0755 "bin/$$file" "$(DESTDIR)$(RT_BIN_PATH)/" ; \ +- done +- +- +- +-regenerate-catalogs: +- $(PERL) devel/tools/extract-message-catalog +- +-license-tag: +- $(PERL) devel/tools/license_tag +- +-start-httpd: +- $(PERL) sbin/standalone_httpd & +- +-start-server: +- $(PERL) sbin/rt-server & +- +- +-SNAPSHOT=$(shell git describe --tags) +-THIRD_PARTY=devel/third-party/ +-snapshot: build-snapshot build-third-party clearsign-snapshot clearsign-third-party snapshot-shasums +- +-build-snapshot: +- git archive --prefix "$(SNAPSHOT)/" HEAD | tar -xf - +- ( cd $(SNAPSHOT) && \ +- echo "$(SNAPSHOT)" > .tag && \ +- autoconf && \ +- INSTALL=./install-sh PERL=/usr/bin/perl ./configure \ +- --with-db-type=SQLite \ +- --enable-layout=relative \ +- --with-web-handler=standalone && \ +- rm -rf autom4te.cache \ +- config.status config.log config.pld \ +- ) +- tar -czf "$(SNAPSHOT).tar.gz" "$(SNAPSHOT)/" +- rm -fr "$(SNAPSHOT)/" +- +-clearsign-snapshot: +- gpg --armor --detach-sign "$(SNAPSHOT).tar.gz" +- +-build-third-party: +- git archive --prefix "$(SNAPSHOT)/$(THIRD_PARTY)" HEAD:$(THIRD_PARTY) \ +- | gzip > "$(SNAPSHOT)-third-party-source.tar.gz" +- rm -rf "$(SNAPSHOT)/$(THIRD_PARTY)" +- +-clearsign-third-party: +- gpg --armor --detach-sign "$(SNAPSHOT)-third-party-source.tar.gz" +- +-snapshot-shasums: +- sha1sum $(SNAPSHOT)*.tar.gz* +- +-vessel-import: build-snapshot +- [ -d $(VESSEL) ] || (echo "VESSEL isn't a path to your shipwright vessel" && exit -1) +- cp $(VESSEL)/scripts/RT/build.pl /tmp/build.pl +- ./sbin/rt-test-dependencies --with-standalone --with-fastcgi --with-sqlite --list > /tmp/rt.yml +- shipwright import file:$(SNAPSHOT).tar.gz \ +- --require-yml /tmp/rt.yml \ +- --build-script /tmp/build.pl \ +- --name RT \ +- --repository fs:$(VESSEL) \ +- --log-level=info \ +- --skip cpan-capitalization,cpan-mod_perl,cpan-Encode,cpan-PPI,cpan-Test-Exception-LessClever,cpan-Test-Manifest,cpan-Test-Object,cpan-Test-Pod,cpan-Test-Requires,cpan-Test-SubCalls,cpan-Test-cpan-Tester,cpan-Test-Warn --skip-all-recommends +- mv $(VESSEL)/scripts/RT/build $(VESSEL)/scripts/RT/build.pl +- +-JSMIN_URL = http://download.bestpractical.com/mirror/jsmin-2013-03-29.c +-JSMIN_SHA = 67dc8d73a8878f88cdaeb1a86775872eae5c3077 +- +-jsmin: jsmin-checkcc jsmin-fetch jsmin-verify jsmin-confirm jsmin-build jsmin-install +- @echo "" +- @echo "To configure RT to use jsmin, add the following line to $(DESTDIR)$(RT_ETC_PATH)/RT_SiteConfig.pm:" +- @echo "" +- @echo " Set(\$$JSMinPath, '$(DESTDIR)$(RT_BIN_PATH)/jsmin');" +- @echo "" +- +-jsmin-checkcc: +- @[ -n "$(CC)" ] || (echo "You don't appear to have a C compiler, please set CC and re-run configure" && exit 1) +- +-jsmin-confirm: +- @echo "jsmin is distributed under a slightly unusual license and can't be shipped" +- @echo "with RT. Before configuring RT to use jsmin, please read jsmin's license" +- @echo "below:" +- @echo "" +- @$(PERL) -pe 'print && exit if /^\*\// or /^#include/' jsmin.c +- @echo "" +- @echo "Press Enter to accept the license, or Ctrl-C to stop now." +- @$(PERL) -e '<STDIN>' +- +-jsmin-fetch: +- @echo "" +- @echo "Downloading jsmin.c from $(JSMIN_URL)" +- @echo "" +- @$(PERL) -MLWP::Simple -e 'exit not is_success(getstore("$(JSMIN_URL)", "jsmin.c"))' \ +- || (echo "Failed to download $(JSMIN_URL)" && exit 1) +- +-jsmin-verify: +- @$(PERL) -MDigest::SHA -e \ +- 'exit not Digest::SHA->new(1)->addfile("jsmin.c")->hexdigest eq "$(JSMIN_SHA)"' \ +- || (echo "Verification of jsmin.c failed! Possible man in the middle?" && exit 1) +- +-jsmin-build: +- $(CC) -o jsmin jsmin.c +- +-jsmin-install: +- $(INSTALL) -o $(BIN_OWNER) -g $(RTGROUP) -m 0755 "jsmin" "$(DESTDIR)$(RT_BIN_PATH)/" +diff --git a/bin/rt b/bin/rt +deleted file mode 100755 +index 8df2d8a..0000000 +--- a/bin/rt ++++ /dev/null +@@ -1,2682 +0,0 @@ +-#!/usr/bin/perl -w +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-# Designed and implemented for Best Practical Solutions, LLC by +-# Abhijit Menon-Sen <ams@xxxxxxx> +- +-use strict; +-use warnings; +- +-if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) { +- require Pod::Usage; +- print Pod::Usage::pod2usage( { verbose => 2 } ); +- exit; +-} +- +-# This program is intentionally written to have as few non-core module +-# dependencies as possible. It should stay that way. +- +-use Cwd; +-use LWP; +-use Text::ParseWords; +-use HTTP::Request::Common; +-use HTTP::Headers; +-use Term::ReadLine; +-use Time::Local; # used in prettyshow +-use File::Temp; +- +-# strong (GSSAPI based) authentication is supported if the server does provide +-# it and the perl modules GSSAPI and LWP::Authen::Negotiate are installed +-# it can be suppressed by setting externalauth=0 (default is undef) +-eval { require GSSAPI }; +-my $no_strong_auth = 'missing perl module GSSAPI'; +-if ( ! $@ ) { +- eval {require LWP::Authen::Negotiate}; +- $no_strong_auth = $@ ? 'missing perl module LWP::Authen::Negotiate' : 0; +-} +- +-# We derive configuration information from hardwired defaults, dotfiles, +-# and the RT* environment variables (in increasing order of precedence). +-# Session information is stored in ~/.rt_sessions. +- +-my $VERSION = 0.02; +-my $HOME = eval{(getpwuid($<))[7]} +- || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH} +- || "."; +-my %config = ( +- ( +- debug => 0, +- user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME}, +- passwd => undef, +- server => 'http://localhost/', +- query => "Status!='resolved' and Status!='rejected'", +- orderby => 'id', +- queue => undef, +-# to protect against unlimited searches a better choice would be +-# queue => 'Unknown_Queue', +-# setting externalauth => undef will try GSSAPI auth if the corresponding perl +-# modules are installed, externalauth => 0 is the backward compatible choice +- externalauth => 0, +- ), +- config_from_file($ENV{RTCONFIG} || ".rtrc"), +- config_from_env() +-); +-my $session = Session->new("$HOME/.rt_sessions"); +-my $REST = "$config{server}/REST/1.0"; +-$no_strong_auth = 'switched off by externalauth=0' +- if defined $config{externalauth}; +- +- +-my $prompt = 'rt> '; +- +-sub whine; +-sub DEBUG { warn @_ if $config{debug} >= shift } +- +-# These regexes are used by command handlers to parse arguments. +-# (XXX: Ask Autrijus how i18n changes these definitions.) +- +-my $name = '[\w.-]+'; +-my $CF_name = '[^,]+?'; +-my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})'; +-my $label = '[^,\\/]+'; +-my $labels = "(?:$label,)*$label"; +-my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+'; +- +-# Our command line looks like this: +-# +-# rt <action> [options] [arguments] +-# +-# We'll parse just enough of it to decide upon an action to perform, and +-# leave the rest to per-action handlers to interpret appropriately. +- +-my %handlers = ( +-# handler => [ ...aliases... ], +- version => ["version", "ver"], +- shell => ["shell"], +- logout => ["logout"], +- help => ["help", "man"], +- show => ["show", "cat"], +- edit => ["create", "edit", "new", "ed"], +- list => ["search", "list", "ls"], +- comment => ["comment", "correspond"], +- link => ["link", "ln"], +- merge => ["merge"], +- grant => ["grant", "revoke"], +- take => ["take", "steal", "untake"], +- quit => ["quit", "exit"], +- setcommand => ["del", "delete", "give", "res", "resolve", +- "subject"], +-); +- +-my %actions; +-foreach my $fn (keys %handlers) { +- foreach my $alias (@{ $handlers{$fn} }) { +- $actions{$alias} = \&{"$fn"}; +- } +-} +- +-# Once we find and call an appropriate handler, we're done. +- +-sub handler { +- my $action; +- +- push @ARGV, 'shell' if (!@ARGV); # default to shell mode +- shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt' +- if (@ARGV && exists $actions{$ARGV[0]}) { +- $action = shift @ARGV; +- return $actions{$action}->($action); +- } +- else { +- print STDERR "rt: Unknown command '@ARGV'.\n"; +- print STDERR "rt: For help, run 'rt help'.\n"; +- return 1; +- } +-} +- +-exit handler(); +- +-# Handler functions. +-# ------------------ +-# +-# The following subs are handlers for each entry in %actions. +- +-sub shell { +- $|=1; +- my $term = Term::ReadLine->new('RT CLI'); +- while ( defined ($_ = $term->readline($prompt)) ) { +- next if /^#/ || /^\s*$/; +- +- @ARGV = shellwords($_); +- handler(); +- } +-} +- +-sub version { +- print "rt $VERSION\n"; +- return 0; +-} +- +-sub logout { +- submit("$REST/logout") if defined $session->cookie; +- return 0; +-} +- +-sub quit { +- logout(); +- exit; +-} +- +-my %help; +-sub help { +- my ($action, $type, $rv) = @_; +- $rv = defined $rv ? $rv : 0; +- my $key; +- +- # What help topics do we know about? +- if (!%help) { +- local $/ = undef; +- foreach my $item (@{ Form::parse(<DATA>) }) { +- my $title = $item->[2]{Title}; +- my @titles = ref $title eq 'ARRAY' ? @$title : $title; +- +- foreach $title (grep $_, @titles) { +- $help{$title} = $item->[2]{Text}; +- } +- } +- } +- +- # What does the user want help with? +- undef $action if ($action && $actions{$action} eq \&help); +- unless ($action || $type) { +- # If we don't know, we'll look for clues in @ARGV. +- foreach (@ARGV) { +- if (exists $help{$_}) { $key = $_; last; } +- } +- unless ($key) { +- # Tolerate possibly plural words. +- foreach (@ARGV) { +- if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; } +- } +- } +- } +- +- if ($type && $action) { +- $key = "$type.$action"; +- } +- $key ||= $type || $action || "introduction"; +- +- # Find a suitable topic to display. +- while (!exists $help{$key}) { +- if ($type && $action) { +- if ($key eq "$type.$action") { $key = $action; } +- elsif ($key eq $action) { $key = $type; } +- else { $key = "introduction"; } +- } +- else { +- $key = "introduction"; +- } +- } +- +- print STDERR $help{$key}, "\n\n"; +- return $rv; +-} +- +-# Displays a list of objects that match some specified condition. +- +-sub list { +- my ($q, $type, %data); +- my $orderby = $config{orderby}; +- +- if ($config{orderby}) { +- $data{orderby} = $config{orderby}; +- } +- my $bad = 0; +- my $rawprint = 0; +- my $reverse_sort = 0; +- my $queue = $config{queue}; +- +- while (@ARGV) { +- $_ = shift @ARGV; +- +- if (/^-t$/) { +- $bad = 1, last unless defined($type = get_type_argument()); +- } +- elsif (/^-S$/) { +- $bad = 1, last unless get_var_argument(\%data); +- } +- elsif (/^-o$/) { +- $data{'orderby'} = shift @ARGV; +- } +- elsif (/^-([isl])$/) { +- $data{format} = $1; +- $rawprint = 1; +- } +- elsif (/^-q$/) { +- $queue = shift @ARGV; +- } +- elsif (/^-r$/) { +- $reverse_sort = 1; +- } +- elsif (/^-f$/) { +- if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) { +- whine "No valid field list in '-f $ARGV[0]'."; +- $bad = 1; last; +- } +- $data{fields} = shift @ARGV; +- $data{format} = 's' if ! $data{format}; +- $rawprint = 1; +- } +- elsif (!defined $q && !/^-/) { +- $q = $_; +- } +- else { +- my $datum = /^-/ ? "option" : "argument"; +- whine "Unrecognised $datum '$_'."; +- $bad = 1; last; +- } +- } +- if ( ! $rawprint and ! exists $data{format} ) { +- $data{format} = 'l'; +- $data{fields} = 'subject,status,queue,created,told,owner,requestors'; +- } +- if ( $reverse_sort and $data{orderby} =~ /^-/ ) { +- $data{orderby} =~ s/^-/+/; +- } elsif ($reverse_sort) { +- $data{orderby} =~ s/^\+?(.*)/-$1/; +- } +- +- $type ||= "ticket"; +- +- if (!defined $q ) { +- if ( $type eq 'ticket' ) { +- $q = $config{query}; +- } +- else { +- $q = ''; +- } +- } +- +- if ( $type ne 'ticket' ) { +- $rawprint = 1; +- } +- +- unless (defined $q) { +- my $item = $type ? "query string" : "object type"; +- whine "No $item specified."; +- $bad = 1; +- } +- +- $q =~ s/^#//; # get rid of leading hash +- if ( $type eq 'ticket' ) { +- if ( $q =~ /^\d+$/ ) { +- +- # only digits, must be an id, formulate a correct query +- $q = "id=$q" if $q =~ /^\d+$/; +- } +- else { +- +- # a string only, take it as an owner or requestor (quoting done later) +- $q = "(Owner=$q or Requestor like $q) and $config{query}" +- if $q =~ /^[\w\-]+$/; +- +- # always add a query for a specific queue or (comma separated) queues +- $queue =~ s/,/ or Queue=/g if $queue; +- $q .= " and (Queue=$queue)" +- if $queue +- and $q +- and $q !~ /Queue\s*=/i +- and $q !~ /id\s*=/i; +- } +- +- # correctly quote strings in a query +- $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g; +- } +- +- #return help("list", $type) if $bad; +- return suggest_help("list", $type, $bad) if $bad; +- +- print "Query:$q\n" if ! $rawprint; +- my $r = submit("$REST/search/$type", { query => $q, %data }); +- if ( $rawprint ) { +- print $r->content; +- } else { +- my $forms = Form::parse($r->content); +- prettylist ($forms); +- } +- return 0; +-} +- +-# Displays selected information about a single object. +- +-sub show { +- my ($type, @objects, %data); +- my $slurped = 0; +- my $bad = 0; +- my $rawprint = 0; +- my $histspec; +- +- while (@ARGV) { +- $_ = shift @ARGV; +- s/^#// if /^#\d+/; # get rid of leading hash +- if (/^-t$/) { +- $bad = 1, last unless defined($type = get_type_argument()); +- } +- elsif (/^-S$/) { +- $bad = 1, last unless get_var_argument(\%data); +- } +- elsif (/^-([isl])$/) { +- $data{format} = $1; +- $rawprint = 1; +- } +- elsif (/^-$/ && !$slurped) { +- chomp(my @lines = <STDIN>); +- foreach (@lines) { +- unless (is_object_spec($_, $type)) { +- whine "Invalid object on STDIN: '$_'."; +- $bad = 1; last; +- } +- push @objects, $_; +- } +- $slurped = 1; +- } +- elsif (/^-f$/) { +- if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) { +- whine "No valid field list in '-f $ARGV[0]'."; +- $bad = 1; last; +- } +- $data{fields} = shift @ARGV; +- # option f requires short raw listing format +- $data{format} = 's'; +- $rawprint = 1; +- } +- elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) { +- push @objects, $spc2; +- $histspec = is_object_spec("ticket/$_/history", $type); +- } +- elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) { +- push @objects, $spc3; +- $rawprint = 1 if $_ =~ /\/content$/; +- } +- elsif (my $spec = is_object_spec($_, $type)) { +- push @objects, $spec; +- $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/; +- } +- else { +- my $datum = /^-/ ? "option" : "argument"; +- whine "Unrecognised $datum '$_'."; +- $bad = 1; last; +- } +- } +- if ( ! $rawprint ) { +- push @objects, $histspec if $histspec; +- $data{format} = 'l' if ! exists $data{format}; +- } +- +- unless (@objects) { +- whine "No objects specified."; +- $bad = 1; +- } +- #return help("show", $type) if $bad; +- return suggest_help("show", $type, $bad) if $bad; +- +- my $r = submit("$REST/show", { id => \@objects, %data }); +- my $c = $r->content; +- # if this isn't a text reply, remove the trailing newline so we +- # don't corrupt things like tarballs when people do +- # show ticket/id/attachments/id/content > foo.tar.gz +- if ($r->content_type !~ /^text\//) { +- chomp($c); +- $rawprint = 1; +- } +- if ( $rawprint ) { +- print $c; +- } else { +- # I do not know how to get more than one form correctly returned +- $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg; +- my $forms = Form::parse($c); +- prettyshow ($forms); +- } +- return 0; +-} +- +-# To create a new object, we ask the server for a form with the defaults +-# filled in, allow the user to edit it, and send the form back. +-# +-# To edit an object, we must ask the server for a form representing that +-# object, make changes requested by the user (either on the command line +-# or interactively via $EDITOR), and send the form back. +- +-sub edit { +- my ($action) = @_; +- my (%data, $type, @objects); +- my ($cl, $text, $edit, $input, $output, $content_type); +- +- use vars qw(%set %add %del); +- %set = %add = %del = (); +- my $slurped = 0; +- my $bad = 0; +- +- while (@ARGV) { +- $_ = shift @ARGV; +- s/^#// if /^#\d+/; # get rid of leading hash +- +- if (/^-e$/) { $edit = 1 } +- elsif (/^-i$/) { $input = 1 } +- elsif (/^-o$/) { $output = 1 } +- elsif (/^-ct$/) { $content_type = shift @ARGV } +- elsif (/^-t$/) { +- $bad = 1, last unless defined($type = get_type_argument()); +- } +- elsif (/^-S$/) { +- $bad = 1, last unless get_var_argument(\%data); +- } +- elsif (/^-$/ && !($slurped || $input)) { +- chomp(my @lines = <STDIN>); +- foreach (@lines) { +- unless (is_object_spec($_, $type)) { +- whine "Invalid object on STDIN: '$_'."; +- $bad = 1; last; +- } +- push @objects, $_; +- } +- $slurped = 1; +- } +- elsif (/^set$/i) { +- my $vars = 0; +- +- while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) { +- my ($key, $op, $val) = ($1, $2, $3); +- my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del; +- +- vpush($hash, lc $key, $val); +- shift @ARGV; +- $vars++; +- } +- unless ($vars) { +- whine "No variables to set."; +- $bad = 1; last; +- } +- $cl = $vars; +- } +- elsif (/^(?:add|del)$/i) { +- my $vars = 0; +- my $hash = ($_ eq "add") ? \%add : \%del; +- +- while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) { +- my ($key, $val) = ($1, $2); +- +- vpush($hash, lc $key, $val); +- shift @ARGV; +- $vars++; +- } +- unless ($vars) { +- whine "No variables to set."; +- $bad = 1; last; +- } +- $cl = $vars; +- } +- elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) { +- push @objects, $spc2; +- } +- elsif (my $spec = is_object_spec($_, $type)) { +- push @objects, $spec; +- } +- else { +- my $datum = /^-/ ? "option" : "argument"; +- whine "Unrecognised $datum '$_'."; +- $bad = 1; last; +- } +- } +- +- if ($action =~ /^ed(?:it)?$/) { +- unless (@objects) { +- whine "No objects specified."; +- $bad = 1; +- } +- } +- else { +- if (@objects) { +- whine "You shouldn't specify objects as arguments to $action."; +- $bad = 1; +- } +- unless ($type) { +- whine "What type of object do you want to create?"; +- $bad = 1; +- } +- @objects = ("$type/new") if defined($type); +- } +- #return help($action, $type) if $bad; +- return suggest_help($action, $type, $bad) if $bad; +- +- # We need a form to make changes to. We usually ask the server for +- # one, but we can avoid that if we are fed one on STDIN, or if the +- # user doesn't want to edit the form by hand, and the command line +- # specifies only simple variable assignments. We *should* get a +- # form if we're creating a new ticket, so that the default values +- # get filled in properly. +- +- my @new_objects = grep /\/new$/, @objects; +- +- if ($input) { +- local $/ = undef; +- $text = <STDIN>; +- } +- elsif ($edit || %add || %del || !$cl || @new_objects) { +- my $r = submit("$REST/show", { id => \@objects, format => 'l' }); +- $text = $r->content; +- } +- +- # If any changes were specified on the command line, apply them. +- if ($cl) { +- if ($text) { +- # We're updating forms from the server. +- my $forms = Form::parse($text); +- +- foreach my $form (@$forms) { +- my ($c, $o, $k, $e) = @$form; +- my ($key, $val); +- +- next if ($e || !@$o); +- +- local %add = %add; +- local %del = %del; +- local %set = %set; +- +- # Make changes to existing fields. +- foreach $key (@$o) { +- if (exists $add{lc $key}) { +- $val = delete $add{lc $key}; +- vpush($k, $key, $val); +- $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/; +- } +- if (exists $del{lc $key}) { +- $val = delete $del{lc $key}; +- my %val = map {$_=>1} @{ vsplit($val) }; +- $k->{$key} = vsplit($k->{$key}); +- @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}}; +- } +- if (exists $set{lc $key}) { +- $k->{$key} = delete $set{lc $key}; +- } +- } +- +- # Then update the others. +- foreach $key (keys %set) { vpush($k, $key, $set{$key}) } +- foreach $key (keys %add) { +- vpush($k, $key, $add{$key}); +- $k->{$key} = vsplit($k->{$key}); +- } +- push @$o, (keys %add, keys %set); +- } +- +- $text = Form::compose($forms); +- } +- else { +- # We're rolling our own set of forms. +- my @forms; +- foreach (@objects) { +- my ($type, $ids, $args) = +- m{^($name)/($idlist|$labels)(?:(/.*))?$}o; +- +- $args ||= ""; +- foreach my $obj (expand_list($ids)) { +- my %set = (%set, id => "$type/$obj$args"); +- push @forms, ["", [keys %set], \%set]; +- } +- } +- $text = Form::compose(\@forms); +- } +- } +- +- if ($output) { +- print $text; +- return 0; +- } +- +- my @files; +- @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'}; +- +- my $synerr = 0; +- +-EDIT: +- # We'll let the user edit the form before sending it to the server, +- # unless we have enough information to submit it non-interactively. +- if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) { +- $text .= "Content-Type: $content_type\n" +- if $content_type and $content_type ne "text/plain"; +- } +- +- if ($edit || (!$input && !$cl)) { +- my ($newtext) = vi_form_while( +- $text, +- sub { +- my ($text, $form) = @_; +- return 1 unless exists $form->[2]{'Attachment'}; +- +- foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) { +- return (0, "File '$f' doesn't exist") unless -f $f; +- } +- @files = @{ vsplit($form->[2]{'Attachment'}) }; +- return 1; +- }, +- ); +- return $newtext unless $newtext; +- # We won't resubmit a bad form unless it was changed. +- $text = ($synerr && $newtext eq $text) ? undef : $newtext; +- } +- +- delete @data{ grep /^attachment_\d+$/, keys %data }; +- my $i = 1; +- foreach my $file (@files) { +- $data{"attachment_$i"} = bless([ $file ], "Attachment"); +- $i++; +- } +- +- if ($text) { +- my $r = submit("$REST/edit", {content => $text, %data}); +- if ($r->code == 409) { +- # If we submitted a bad form, we'll give the user a chance +- # to correct it and resubmit. +- if ($edit || (!$input && !$cl)) { +- my $content = $r->content . "\n"; +- $content =~ s/^(?!#)/# /mg; +- $text = $content . $text; +- $synerr = 1; +- goto EDIT; +- } +- else { +- print $r->content; +- return 0; +- } +- } +- print $r->content; +- } +- return 0; +-} +- +-# handler for special edit commands. A valid edit command is constructed and +-# further work is delegated to the edit handler +- +-sub setcommand { +- my ($action) = @_; +- my ($id, $bad, $what); +- if ( @ARGV ) { +- $_ = shift @ARGV; +- $id = $1 if (m|^(?:ticket/)?($idlist)$|); +- } +- if ( ! $id ) { +- $bad = 1; +- whine "No ticket number specified."; +- } +- if ( @ARGV ) { +- if ($action eq 'subject') { +- my $subject = '"'.join (" ", @ARGV).'"'; +- @ARGV = (); +- $what = "subject=$subject"; +- } elsif ($action eq 'give') { +- my $owner = shift @ARGV; +- $what = "owner=$owner"; +- } +- } else { +- if ( $action eq 'delete' or $action eq 'del' ) { +- $what = "status=deleted"; +- } elsif ($action eq 'resolve' or $action eq 'res' ) { +- $what = "status=resolved"; +- } elsif ($action eq 'take' ) { +- $what = "owner=$config{user}"; +- } elsif ($action eq 'untake') { +- $what = "owner=Nobody"; +- } +- } +- if (@ARGV) { +- $bad = 1; +- whine "Extraneous arguments for action $action: @ARGV."; +- } +- if ( ! $what ) { +- $bad = 1; +- whine "unrecognized action $action."; +- } +- return help("edit", undef, $bad) if $bad; +- @ARGV = ( $id, "set", $what ); +- print "Executing: rt edit @ARGV\n"; +- return edit("edit"); +-} +- +-# We roll "comment" and "correspond" into the same handler. +- +-sub comment { +- my ($action) = @_; +- my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit); +- my $bad = 0; +- my $status = ''; +- +- while (@ARGV) { +- $_ = shift @ARGV; +- +- if (/^-e$/) { +- $edit = 1; +- } +- elsif (/^-(?:[abcmws]|ct)$/) { +- unless (@ARGV) { +- whine "No argument specified with $_."; +- $bad = 1; last; +- } +- +- if (/-a/) { +- unless (-f $ARGV[0] && -r $ARGV[0]) { +- whine "Cannot read attachment: '$ARGV[0]'."; +- return 0; +- } +- push @files, shift @ARGV; +- } +- elsif (/-ct/) { +- $content_type = shift @ARGV; +- } +- elsif (/-s/) { +- $status = shift @ARGV; +- } +- elsif (/-([bc])/) { +- my $a = $_ eq "-b" ? \@bcc : \@cc; +- @$a = split /\s*,\s*/, shift @ARGV; +- } +- elsif (/-m/) { +- $msg = shift @ARGV; +- if ( $msg =~ /^-$/ ) { +- undef $msg; +- while (<STDIN>) { $msg .= $_ } +- } +- } +- elsif (/-w/) { $wtime = shift @ARGV } +- } +- elsif (!$id && m|^(?:ticket/)?($idlist)$|) { +- $id = $1; +- } +- else { +- my $datum = /^-/ ? "option" : "argument"; +- whine "Unrecognised $datum '$_'."; +- $bad = 1; last; +- } +- } +- +- unless ($id) { +- whine "No object specified."; +- $bad = 1; +- } +- #return help($action, "ticket") if $bad; +- return suggest_help($action, "ticket") if $bad; +- +- my $form = [ +- "", +- [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ], +- { +- Ticket => $id, +- Action => $action, +- Cc => [ @cc ], +- Bcc => [ @bcc ], +- Attachment => [ @files ], +- TimeWorked => $wtime || '', +- 'Content-Type' => $content_type || 'text/plain', +- Text => $msg || '', +- Status => $status +- } +- ]; +- if ($status ne '') { +- push(@{$form->[1]}, "Status"); +- } +- +- my $text = Form::compose([ $form ]); +- +- if ($edit || !$msg) { +- my ($tmp) = vi_form_while( +- $text, +- sub { +- my ($text, $form) = @_; +- foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) { +- return (0, "File '$f' doesn't exist") unless -f $f; +- } +- @files = @{ vsplit($form->[2]{'Attachment'}) }; +- return 1; +- }, +- ); +- return $tmp unless $tmp; +- $text = $tmp; +- } +- +- my $i = 1; +- foreach my $file (@files) { +- $data{"attachment_$i"} = bless([ $file ], "Attachment"); +- $i++; +- } +- $data{content} = $text; +- +- my $r = submit("$REST/ticket/$id/comment", \%data); +- print $r->content; +- return 0; +-} +- +-# Merge one ticket into another. +- +-sub merge { +- my @id; +- my $bad = 0; +- +- while (@ARGV) { +- $_ = shift @ARGV; +- s/^#// if /^#\d+/; # get rid of leading hash +- +- if (/^\d+$/) { +- push @id, $_; +- } +- else { +- whine "Unrecognised argument: '$_'."; +- $bad = 1; last; +- } +- } +- +- unless (@id == 2) { +- my $evil = @id > 2 ? "many" : "few"; +- whine "Too $evil arguments specified."; +- $bad = 1; +- } +- #return help("merge", "ticket") if $bad; +- return suggest_help("merge", "ticket", $bad) if $bad; +- +- my $r = submit("$REST/ticket/$id[0]/merge/$id[1]"); +- print $r->content; +- return 0; +-} +- +-# Link one ticket to another. +- +-sub link { +- my ($bad, $del, %data) = (0, 0, ()); +- my $type; +- +- my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo +- ReferredToBy HasMember MemberOf); +- +- while (@ARGV && $ARGV[0] =~ /^-/) { +- $_ = shift @ARGV; +- +- if (/^-d$/) { +- $del = 1; +- } +- elsif (/^-t$/) { +- $bad = 1, last unless defined($type = get_type_argument()); +- } +- else { +- whine "Unrecognised option: '$_'."; +- $bad = 1; last; +- } +- } +- +- $type = "ticket" unless $type; # default type to tickets +- +- if (@ARGV == 3) { +- my ($from, $rel, $to) = @ARGV; +- if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) { +- whine "Invalid link '$rel' for type $type specified."; +- $bad = 1; +- } +- %data = (id => $from, rel => $rel, to => $to, del => $del); +- } +- else { +- my $bad = @ARGV < 3 ? "few" : "many"; +- whine "Too $bad arguments specified."; +- $bad = 1; +- } +- return suggest_help("link", $type, $bad) if $bad; +- +- my $r = submit("$REST/$type/link", \%data); +- print $r->content; +- return 0; +-} +- +-# Take/steal a ticket +-sub take { +- my ($cmd) = @_; +- my ($bad, %data) = (0, ()); +- +- my $id; +- +- # get the ticket id +- if (@ARGV == 1) { +- ($id) = @ARGV; +- unless ($id =~ /^\d+$/) { +- whine "Invalid ticket ID $id specified."; +- $bad = 1; +- } +- my $form = [ +- "", +- [ "Ticket", "Action" ], +- { +- Ticket => $id, +- Action => $cmd, +- Status => '', +- } +- ]; +- +- my $text = Form::compose([ $form ]); +- $data{content} = $text; +- } +- else { +- $bad = @ARGV < 1 ? "few" : "many"; +- whine "Too $bad arguments specified."; +- $bad = 1; +- } +- return suggest_help("take", "ticket", $bad) if $bad; +- +- my $r = submit("$REST/ticket/$id/take", \%data); +- print $r->content; +- return 0; +-} +- +-# Grant/revoke a user's rights. +- +-sub grant { +- my ($cmd) = @_; +- +- whine "$cmd is unimplemented."; +- return 1; +-} +- +-# Client <-> Server communication. +-# -------------------------------- +-# +-# This function composes and sends an HTTP request to the RT server, and +-# interprets the response. It takes a request URI, and optional request +-# data (a string, or a reference to a set of key-value pairs). +- +-sub submit { +- my ($uri, $content) = @_; +- my ($req, $data); +- my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1); +- my $h = HTTP::Headers->new; +- +- # Did the caller specify any data to send with the request? +- $data = []; +- if (defined $content) { +- unless (ref $content) { +- # If it's just a string, make sure LWP handles it properly. +- # (By pretending that it's a file!) +- $content = [ content => [undef, "", Content => $content] ]; +- } +- elsif (ref $content eq 'HASH') { +- my @data; +- foreach my $k (keys %$content) { +- if (ref $content->{$k} eq 'ARRAY') { +- foreach my $v (@{ $content->{$k} }) { +- push @data, $k, $v; +- } +- } +- else { push @data, $k, $content->{$k} } +- } +- $content = \@data; +- } +- $data = $content; +- } +- +- # Should we send authentication information to start a new session? +- my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted'; +- my($server) = $config{server} =~ m{^.*//([^/]+)}; +- if ($config{externalauth}) { +- $h->authorization_basic($config{user}, $config{passwd} || read_passwd() ); +- print " Password will be sent to $server $how\n", +- " Press CTRL-C now if you do not want to continue\n" +- if ! $config{passwd}; +- } elsif ( $no_strong_auth ) { +- if (!defined $session->cookie) { +- print " Strong encryption not available, $no_strong_auth\n", +- " Password will be sent to $server $how\n", +- " Press CTRL-C now if you do not want to continue\n" +- if ! $config{passwd}; +- push @$data, ( user => $config{user} ); +- push @$data, ( pass => $config{passwd} || read_passwd() ); +- } +- } +- +- # Now, we construct the request. +- if (@$data) { +- $req = POST($uri, $data, Content_Type => 'form-data'); +- } +- else { +- $req = GET($uri); +- } +- $session->add_cookie_header($req); +- if ($config{externalauth}) { +- $req->header(%$h); +- } +- +- # Then we send the request and parse the response. +- DEBUG(3, $req->as_string); +- my $res = $ua->request($req); +- DEBUG(3, $res->as_string); +- +- if ($res->is_success) { +- # The content of the response we get from the RT server consists +- # of an HTTP-like status line followed by optional header lines, +- # a blank line, and arbitrary text. +- +- my ($head, $text) = split /\n\n/, $res->content, 2; +- my ($status, @headers) = split /\n/, $head; +- $text =~ s/\n*$/\n/ if ($text); +- +- # "RT/3.0.1 401 Credentials required" +- if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) { +- warn "rt: Malformed RT response from $server.\n"; +- warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3; +- exit -1; +- } +- +- # Our caller can pretend that the server returned a custom HTTP +- # response code and message. (Doing that directly is apparently +- # not sufficiently portable and uncomplicated.) +- $res->code($1); +- $res->message($2); +- $res->content($text); +- $session->update($res) if ($res->is_success || $res->code != 401); +- +- if (!$res->is_success) { +- # We can deal with authentication failures ourselves. Either +- # we sent invalid credentials, or our session has expired. +- if ($res->code == 401) { +- my %d = @$data; +- if (exists $d{user}) { +- warn "rt: Incorrect username or password.\n"; +- exit -1; +- } +- elsif ($req->header("Cookie")) { +- # We'll retry the request with credentials, unless +- # we only wanted to logout in the first place. +- $session->delete; +- return submit(@_) unless $uri eq "$REST/logout"; +- } +- } +- # Conflicts should be dealt with by the handler and user. +- # For anything else, we just die. +- elsif ($res->code != 409) { +- warn "rt: ", $res->content; +- #exit; +- } +- } +- } +- else { +- warn "rt: Server error: ", $res->message, " (", $res->code, ")\n"; +- exit -1; +- } +- +- return $res; +-} +- +-# Session management. +-# ------------------- +-# +-# Maintains a list of active sessions in the ~/.rt_sessions file. +-{ +- package Session; +- my ($s, $u); +- +- # Initialises the session cache. +- sub new { +- my ($class, $file) = @_; +- my $self = { +- file => $file || "$HOME/.rt_sessions", +- sids => { } +- }; +- +- # The current session is identified by the currently configured +- # server and user. +- ($s, $u) = @config{"server", "user"}; +- +- bless $self, $class; +- $self->load(); +- +- return $self; +- } +- +- # Returns the current session cookie. +- sub cookie { +- my ($self) = @_; +- my $cookie = $self->{sids}{$s}{$u}; +- return defined $cookie ? "RT_SID_$cookie" : undef; +- } +- +- # Deletes the current session cookie. +- sub delete { +- my ($self) = @_; +- delete $self->{sids}{$s}{$u}; +- } +- +- # Adds a Cookie header to an outgoing HTTP request. +- sub add_cookie_header { +- my ($self, $request) = @_; +- my $cookie = $self->cookie(); +- +- $request->header(Cookie => $cookie) if defined $cookie; +- } +- +- # Extracts the Set-Cookie header from an HTTP response, and updates +- # session information accordingly. +- sub update { +- my ($self, $response) = @_; +- my $cookie = $response->header("Set-Cookie"); +- +- if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) { +- $self->{sids}{$s}{$u} = $1; +- } +- } +- +- # Loads the session cache from the specified file. +- sub load { +- my ($self, $file) = @_; +- $file ||= $self->{file}; +- +- open( my $handle, '<', $file ) or return 0; +- +- $self->{file} = $file; +- my $sids = $self->{sids} = {}; +- while (<$handle>) { +- chomp; +- next if /^$/ || /^#/; +- next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#; +- my ($server, $user, $cookie) = split / /, $_; +- $sids->{$server}{$user} = $cookie; +- } +- return 1; +- } +- +- # Writes the current session cache to the specified file. +- sub save { +- my ($self, $file) = shift; +- $file ||= $self->{file}; +- +- open( my $handle, '>', "$file" ) or return 0; +- +- my $sids = $self->{sids}; +- foreach my $server (keys %$sids) { +- foreach my $user (keys %{ $sids->{$server} }) { +- my $sid = $sids->{$server}{$user}; +- if (defined $sid) { +- print $handle "$server $user $sid\n"; +- } +- } +- } +- close($handle); +- chmod 0600, $file; +- return 1; +- } +- +- sub DESTROY { +- my $self = shift; +- $self->save; +- } +-} +- +-# Form handling. +-# -------------- +-# +-# Forms are RFC822-style sets of (field, value) specifications with some +-# initial comments and interspersed blank lines allowed for convenience. +-# Sets of forms are separated by --\n (in a cheap parody of MIME). +-# +-# Each form is parsed into an array with four elements: commented text +-# at the start of the form, an array with the order of keys, a hash with +-# key/value pairs, and optional error text if the form syntax was wrong. +- +-# Returns a reference to an array of parsed forms. +-sub Form::parse { +- my $state = 0; +- my @forms = (); +- my @lines = split /\n/, $_[0] if $_[0]; +- my ($c, $o, $k, $e) = ("", [], {}, ""); +- +- LINE: +- while (@lines) { +- my $line = shift @lines; +- +- next LINE if $line eq ''; +- +- if ($line eq '--') { +- # We reached the end of one form. We'll ignore it if it was +- # empty, and store it otherwise, errors and all. +- if ($e || $c || @$o) { +- push @forms, [ $c, $o, $k, $e ]; +- $c = ""; $o = []; $k = {}; $e = ""; +- } +- $state = 0; +- } +- elsif ($state != -1) { +- if ($state == 0 && $line =~ /^#/) { +- # Read an optional block of comments (only) at the start +- # of the form. +- $state = 1; +- $c = $line; +- while (@lines && $lines[0] =~ /^#/) { +- $c .= "\n".shift @lines; +- } +- $c .= "\n"; +- } +- elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) { +- # Read a field: value specification. +- my $f = $1; +- my @v = ($2 || ()); +- +- # Read continuation lines, if any. +- while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) { +- push @v, shift @lines; +- } +- pop @v while (@v && $v[-1] eq ''); +- +- # Strip longest common leading indent from text. +- my $ws = ""; +- foreach my $ls (map {/^(\s+)/} @v[1..$#v]) { +- $ws = $ls if (!$ws || length($ls) < length($ws)); +- } +- s/^$ws// foreach @v; +- +- push(@$o, $f) unless exists $k->{$f}; +- vpush($k, $f, join("\n", @v)); +- +- $state = 1; +- } +- elsif ($line !~ /^#/) { +- # We've found a syntax error, so we'll reconstruct the +- # form parsed thus far, and add an error marker. (>>) +- $state = -1; +- $e = Form::compose([[ "", $o, $k, "" ]]); +- $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n"; +- } +- } +- else { +- # We saw a syntax error earlier, so we'll accumulate the +- # contents of this form until the end. +- $e .= "$line\n"; +- } +- } +- push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o); +- +- foreach my $l (keys %$k) { +- $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY'); +- } +- +- return \@forms; +-} +- +-# Returns text representing a set of forms. +-sub Form::compose { +- my ($forms) = @_; +- my @text; +- +- foreach my $form (@$forms) { +- my ($c, $o, $k, $e) = @$form; +- my $text = ""; +- +- if ($c) { +- $c =~ s/\n*$/\n/; +- $text = "$c\n"; +- } +- if ($e) { +- $text .= $e; +- } +- elsif ($o) { +- my @lines; +- +- foreach my $key (@$o) { +- my ($line, $sp); +- my $v = $k->{$key}; +- my @values = ref $v eq 'ARRAY' ? @$v : $v; +- +- $sp = " "x(length("$key: ")); +- $sp = " "x4 if length($sp) > 16; +- +- foreach $v (@values) { +- if ($v =~ /\n/) { +- $v =~ s/^/$sp/gm; +- $v =~ s/^$sp//; +- +- if ($line) { +- push @lines, "$line\n\n"; +- $line = ""; +- } +- elsif (@lines && $lines[-1] !~ /\n\n$/) { +- $lines[-1] .= "\n"; +- } +- push @lines, "$key: $v\n\n"; +- } +- elsif ($line && +- length($line)+length($v)-rindex($line, "\n") >= 70) +- { +- $line .= ",\n$sp$v"; +- } +- else { +- $line = $line ? "$line,$v" : "$key: $v"; +- } +- } +- +- $line = "$key:" unless @values; +- if ($line) { +- if ($line =~ /\n/) { +- if (@lines && $lines[-1] !~ /\n\n$/) { +- $lines[-1] .= "\n"; +- } +- $line .= "\n"; +- } +- push @lines, "$line\n"; +- } +- } +- +- $text .= join "", @lines; +- } +- else { +- chomp $text; +- } +- push @text, $text; +- } +- +- return join "\n--\n\n", @text; +-} +- +-# Configuration. +-# -------------- +- +-# Returns configuration information from the environment. +-sub config_from_env { +- my %env; +- +- foreach my $k (qw(EXTERNALAUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) { +- +- if (exists $ENV{"RT$k"}) { +- $env{lc $k} = $ENV{"RT$k"}; +- } +- } +- +- return %env; +-} +- +-# Finds a suitable configuration file and returns information from it. +-sub config_from_file { +- my ($rc) = @_; +- +- if ($rc =~ m#^/#) { +- # We'll use an absolute path if we were given one. +- return parse_config_file($rc); +- } +- else { +- # Otherwise we'll use the first file we can find in the current +- # directory, or in one of its (increasingly distant) ancestors. +- +- my @dirs = split /\//, cwd; +- while (@dirs) { +- my $file = join('/', @dirs, $rc); +- if (-r $file) { +- return parse_config_file($file); +- } +- +- # Remove the last directory component each time. +- pop @dirs; +- } +- +- # Still nothing? We'll fall back to some likely defaults. +- for ("$HOME/$rc", "local/etc/rt.conf", "/etc/rt.conf") { +- return parse_config_file($_) if (-r $_); +- } +- } +- +- return (); +-} +- +-# Makes a hash of the specified configuration file. +-sub parse_config_file { +- my %cfg; +- my ($file) = @_; +- local $_; # $_ may be aliased to a constant, from line 1163 +- +- open( my $handle, '<', $file ) or return; +- +- while (<$handle>) { +- chomp; +- next if (/^#/ || /^\s*$/); +- +- if (/^(externalauth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) { +- $cfg{$1} = $2; +- } +- else { +- die "rt: $file:$.: unknown configuration directive.\n"; +- } +- } +- +- return %cfg; +-} +- +-# Helper functions. +-# ----------------- +- +-sub whine { +- my $sub = (caller(1))[3]; +- $sub =~ s/^main:://; +- warn "rt: $sub: @_\n"; +- return 0; +-} +- +-sub read_passwd { +- eval 'require Term::ReadKey'; +- if ($@) { +- die "No password specified (and Term::ReadKey not installed).\n"; +- } +- +- print "Password: "; +- Term::ReadKey::ReadMode('noecho'); +- chomp(my $passwd = Term::ReadKey::ReadLine(0)); +- Term::ReadKey::ReadMode('restore'); +- print "\n"; +- +- return $passwd; +-} +- +-sub vi_form_while { +- my $text = shift; +- my $cb = shift; +- +- my $error = 0; +- my ($c, $o, $k, $e); +- do { +- my $ntext = vi($text); +- return undef if ($error && $ntext eq $text); +- +- $text = $ntext; +- +- my $form = Form::parse($text); +- $error = 0; +- ($c, $o, $k, $e) = @{ $form->[0] }; +- if ( $e ) { +- $error = 1; +- $c = "# Syntax error."; +- goto NEXT; +- } +- elsif (!@$o) { +- return 0; +- } +- +- my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] ); +- unless ( $status ) { +- $error = 1; +- $c = "# $msg"; +- } +- +- NEXT: +- $text = Form::compose([[$c, $o, $k, $e]]); +- } while ($error); +- +- return $text; +-} +- +-sub vi { +- my ($text) = @_; +- my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi"; +- +- local $/ = undef; +- +- my $handle = File::Temp->new; +- print $handle $text; +- close($handle); +- +- system($editor, $handle->filename) && die "Couldn't run $editor.\n"; +- +- open( $handle, '<', $handle->filename ) or die "$handle: $!\n"; +- $text = <$handle>; +- close($handle); +- +- return $text; +-} +- +-# Add a value to a (possibly multi-valued) hash key. +-sub vpush { +- my ($hash, $key, $val) = @_; +- my @val = ref $val eq 'ARRAY' ? @$val : $val; +- +- if (exists $hash->{$key}) { +- unless (ref $hash->{$key} eq 'ARRAY') { +- my @v = $hash->{$key} ne '' ? $hash->{$key} : (); +- $hash->{$key} = \@v; +- } +- push @{ $hash->{$key} }, @val; +- } +- else { +- $hash->{$key} = $val; +- } +-} +- +-# "Normalise" a hash key that's known to be multi-valued. +-sub vsplit { +- my ($val) = @_; +- my ($word, @words); +- my @values = ref $val eq 'ARRAY' ? @$val : $val; +- +- foreach my $line (map {split /\n/} @values) { +- # XXX: This should become a real parser, à la Text::ParseWords. +- $line =~ s/^\s+//; +- $line =~ s/\s+$//; +- my ( $a, $b ) = split /\s*,\s*/, $line, 2; +- +- while ($a) { +- no warnings 'uninitialized'; +- if ( $a =~ /^'/ ) { +- my $s = $a; +- while ( $a !~ /'$/ || ( $a !~ /(\\\\)+'$/ +- && $a =~ /(\\)+'$/ )) { +- ( $a, $b ) = split /\s*,\s*/, $b, 2; +- $s .= ',' . $a; +- } +- push @words, $s; +- } +- elsif ( $a =~ /^q\{/ ) { +- my $s = $a; +- while ( $a !~ /\}$/ ) { +- ( $a, $b ) = +- split /\s*,\s*/, $b, 2; +- $s .= ',' . $a; +- } +- $s =~ s/^q\{/'/; +- $s =~ s/\}/'/; +- push @words, $s; +- } +- else { +- push @words, $a; +- } +- ( $a, $b ) = split /\s*,\s*/, $b, 2; +- } +- +- +- } +- +- return \@words; +-} +- +-# WARN: this code is duplicated in lib/RT/Interface/REST.pm +-# change both functions at once +-sub expand_list { +- my ($list) = @_; +- +- my @elts; +- foreach (split /\s*,\s*/, $list) { +- push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_; +- } +- +- return map $_->[0], # schwartzian transform +- sort { +- defined $a->[1] && defined $b->[1]? +- # both numbers +- $a->[1] <=> $b->[1] +- :!defined $a->[1] && !defined $b->[1]? +- # both letters +- $a->[2] cmp $b->[2] +- # mix, number must be first +- :defined $a->[1]? -1: 1 +- } +- map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ], +- @elts; +-} +- +-sub get_type_argument { +- my $type; +- +- if (@ARGV) { +- $type = shift @ARGV; +- unless ($type =~ /^[A-Za-z0-9_.-]+$/) { +- # We want whine to mention our caller, not us. +- @_ = ("Invalid type '$type' specified."); +- goto &whine; +- } +- } +- else { +- @_ = ("No type argument specified with -t."); +- goto &whine; +- } +- +- $type =~ s/s$//; # "Plural". Ugh. +- return $type; +-} +- +-sub get_var_argument { +- my ($data) = @_; +- +- if (@ARGV) { +- my $kv = shift @ARGV; +- if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) { +- push @{ $data->{$k} }, $v; +- } +- else { +- @_ = ("Invalid variable specification: '$kv'."); +- goto &whine; +- } +- } +- else { +- @_ = ("No variable argument specified with -S."); +- goto &whine; +- } +-} +- +-sub is_object_spec { +- my ($spec, $type) = @_; +- +- $spec =~ s|^(?:$type/)?|$type/| if defined $type; +- return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o); +- return 0; +-} +- +-sub suggest_help { +- my ($action, $type, $rv) = @_; +- +- print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action; +- print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type; +- return $rv; +-} +- +-sub str2time { +- # simplified procedure for parsing date, avoid loading Date::Parse +- my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5, +- Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11); +- $_ = shift; +- my ($mon, $day, $hr, $min, $sec, $yr, $monstr); +- if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) { +- ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6); +- $mon = $month{$monstr} if exists $month{$monstr}; +- } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) { +- ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6); +- } +- if ( $yr and defined $mon and $day and defined $hr and defined $sec ) { +- return timelocal($sec,$min,$hr,$day,$mon,$yr); +- } else { +- print "Unknown date format in parsedate: $_\n"; +- return undef; +- } +-} +- +-sub date_diff { +- my ($old, $new) = @_; +- $new = time() if ! $new; +- $old = str2time($old) if $old !~ /^\d+$/; +- $new = str2time($new) if $new !~ /^\d+$/; +- return "???" if ! $old or ! $new; +- +- my %seconds = (min => 60, +- hr => 60*60, +- day => 60*60*24, +- wk => 60*60*24*7, +- mth => 60*60*24*30, +- yr => 60*60*24*365); +- +- my $diff = $new - $old; +- my $what = 'sec'; +- my $howmuch = $diff; +- for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) { +- last if $diff < $seconds{$_}; +- $what = $_; +- $howmuch = int($diff/$seconds{$_}); +- } +- return "$howmuch $what"; +-} +- +-sub prettyshow { +- my $forms = shift; +- my ($form) = grep { exists $_->[2]->{Queue} } @$forms; +- my $k = $form->[2]; +- # dates are in local time zone +- if ( $k ) { +- print "Date: $k->{Created}\n"; +- print "From: $k->{Requestors}\n"; +- print "Cc: $k->{Cc}\n" if $k->{Cc}; +- print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc}; +- print "X-Queue: $k->{Queue}\n"; +- print "Subject: [rt #$k->{id}] $k->{Subject}\n\n"; +- } +- # dates in these attributes are in GMT and will be converted +- foreach my $form (@$forms) { +- my ($c, $o, $k, $e) = @$form; +- next if ! $k->{id} or exists $k->{Queue}; +- if ( exists $k->{Created} ) { +- my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/); +- $m--; +- my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y)); +- if ( exists $k->{Description} ) { +- print "===> $k->{Description} on $created\n"; +- } +- } +- print "$k->{Content}\n" if exists $k->{Content} and +- $k->{Content} !~ /to have no content$/ and +- ($k->{Type}||'') ne 'EmailRecord'; +- print "$k->{Attachments}\n" if exists $k->{Attachments} and +- $k->{Attachments}; +- } +-} +- +-sub prettylist { +- my $forms = shift; +- my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n"; +- $heading .= '-' x 80 . "\n"; +- my (@open, @me); +- foreach my $form (@$forms) { +- my ($c, $o, $k, $e) = @$form; +- next if ! $k->{id}; +- print $heading if $heading; +- $heading = ''; +- my $id = $k->{id}; +- $id =~ s!^ticket/!!; +- my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner}; +- $owner = substr($owner, 0, 5); +- my $queue = substr($k->{Queue}, 0, 5); +- my $subject = substr($k->{Subject}, 0, 30); +- my $age = date_diff($k->{Created}); +- my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told}); +- my $status = substr($k->{Status}, 0, 6); +- my $requestor = substr($k->{Requestors}, 0, 9); +- my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n", +- $id, $owner, $queue, $age, $told, $status, $requestor, $subject; +- if ( $k->{Owner} eq 'Nobody' ) { +- push @open, $line; +- } elsif ($k->{Owner} eq $config{user} ) { +- push @me, $line; +- } else { +- print $line; +- } +- } +- print "No matches found\n" if $heading; +- printf "========== my %2d open tickets ==========\n", scalar @me if @me; +- print @me if @me; +- printf "========== %2d unowned tickets ==========\n", scalar @open if @open; +- print @open if @open; +-} +- +-__DATA__ +- +-Title: intro +-Title: introduction +-Text: +- +- This is a command-line interface to RT 3.0 or newer. +- +- It allows you to interact with an RT server over HTTP, and offers an +- interface to RT's functionality that is better-suited to automation +- and integration with other tools. +- +- In general, each invocation of this program should specify an action +- to perform on one or more objects, and any other arguments required +- to complete the desired action. +- +- For more information: +- +- - rt help usage (syntax information) +- - rt help objects (how to specify objects) +- - rt help actions (a list of possible actions) +- - rt help types (a list of object types) +- +- - rt help config (configuration details) +- - rt help examples (a few useful examples) +- - rt help topics (a list of help topics) +- +--- +- +-Title: usage +-Title: syntax +-Text: +- +- Syntax: +- +- rt <action> [options] [arguments] +- or +- rt shell +- +- Each invocation of this program must specify an action (e.g. "edit", +- "create"), options to modify behaviour, and other arguments required +- by the specified action. (For example, most actions expect a list of +- numeric object IDs to act upon.) +- +- The details of the syntax and arguments for each action are given by +- "rt help <action>". Some actions may be referred to by more than one +- name ("create" is the same as "new", for example). +- +- You may also call "rt shell", which will give you an 'rt>' prompt at +- which you can issue commands of the form "<action> [options] +- [arguments]". See "rt help shell" for details. +- +- Objects are identified by a type and an ID (which can be a name or a +- number, depending on the type). For some actions, the object type is +- implied (you can only comment on tickets); for others, the user must +- specify it explicitly. See "rt help objects" for details. +- +- In syntax descriptions, mandatory arguments that must be replaced by +- appropriate value are enclosed in <>, and optional arguments are +- indicated by [] (for example, <action> and [options] above). +- +- For more information: +- +- - rt help objects (how to specify objects) +- - rt help actions (a list of actions) +- - rt help types (a list of object types) +- - rt help shell (how to use the shell) +- +--- +- +-Title: conf +-Title: config +-Title: configuration +-Text: +- +- This program has two major sources of configuration information: its +- configuration files, and the environment. +- +- The program looks for configuration directives in a file named .rtrc +- (or $RTCONFIG; see below) in the current directory, and then in more +- distant ancestors, until it reaches /. If no suitable configuration +- files are found, it will also check for ~/.rtrc, local/etc/rt.conf +- and /etc/rt.conf. +- +- Configuration directives: +- +- The following directives may occur, one per line: +- +- - server <URL> URL to RT server. +- - user <username> RT username. +- - passwd <passwd> RT user's password. +- - query <RT Query> Default RT Query for list action +- - orderby <order> Default RT order for list action +- - queue <queuename> Default RT Queue for list action +- - externalauth <0|1> Use HTTP Basic authentication +- explicitely setting externalauth to 0 inhibits also GSSAPI based +- authentication, if LWP::Authen::Negotiate (and GSSAPI) is installed +- +- Blank and #-commented lines are ignored. +- +- Sample configuration file contents: +- +- server https://rt.somewhere.com/ +- # more than one queue can be given (by adding a query expression) +- queue helpdesk or queue=support +- query Status != resolved and Owner=myaccount +- +- +- Environment variables: +- +- The following environment variables override any corresponding +- values defined in configuration files: +- +- - RTUSER +- - RTPASSWD +- - RTEXTERNALAUTH +- - RTSERVER +- - RTDEBUG Numeric debug level. (Set to 3 for full logs.) +- - RTCONFIG Specifies a name other than ".rtrc" for the +- configuration file. +- - RTQUERY Default RT Query for rt list +- - RTORDERBY Default order for rt list +- +--- +- +-Title: objects +-Text: +- +- Syntax: +- +- <type>/<id>[/<attributes>] +- +- Every object in RT has a type (e.g. "ticket", "queue") and a numeric +- ID. Some types of objects can also be identified by name (like users +- and queues). Furthermore, objects may have named attributes (such as +- "ticket/1/history"). +- +- An object specification is like a path in a virtual filesystem, with +- object types as top-level directories, object IDs as subdirectories, +- and named attributes as further subdirectories. +- +- A comma-separated list of names, numeric IDs, or numeric ranges can +- be used to specify more than one object of the same type. Note that +- the list must be a single argument (i.e., no spaces). For example, +- "user/root,1-3,5,7-10,ams" is a list of ten users; the same list +- can also be written as "user/ams,root,1,2,3,5,7,8-10". +- +- If just a number is given as object specification it will be +- interpreted as ticket/<number> +- +- Examples: +- +- 1 # the same as ticket/1 +- ticket/1 +- ticket/1/attachments +- ticket/1/attachments/3 +- ticket/1/attachments/3/content +- ticket/1-3/links +- ticket/1-3,5-7/history +- +- user/ams +- +- For more information: +- +- - rt help <action> (action-specific details) +- - rt help <type> (type-specific details) +- +--- +- +-Title: actions +-Title: commands +-Text: +- +- You can currently perform the following actions on all objects: +- +- - list (list objects matching some condition) +- - show (display object details) +- - edit (edit object details) +- - create (create a new object) +- +- Each type may define actions specific to itself; these are listed in +- the help item about that type. +- +- For more information: +- +- - rt help <action> (action-specific details) +- - rt help types (a list of possible types) +- +- The following actions on tickets are also possible: +- +- - comment Add comments to a ticket +- - correspond Add comments to a ticket +- - merge Merge one ticket into another +- - link Link one ticket to another +- - take Take a ticket (steal and untake are possible as well) +- +- For several edit set subcommands that are frequently used abbreviations +- have been introduced. These abbreviations are: +- +- - delete or del delete a ticket (edit set status=deleted) +- - resolve or res resolve a ticket (edit set status=resolved) +- - subject change subject of ticket (edit set subject=string) +- - give give a ticket to somebody (edit set owner=user) +- +--- +- +-Title: types +-Text: +- +- You can currently operate on the following types of objects: +- +- - tickets +- - users +- - groups +- - queues +- +- For more information: +- +- - rt help <type> (type-specific details) +- - rt help objects (how to specify objects) +- - rt help actions (a list of possible actions) +- +--- +- +-Title: ticket +-Text: +- +- Tickets are identified by a numeric ID. +- +- The following generic operations may be performed upon tickets: +- +- - list +- - show +- - edit +- - create +- +- In addition, the following ticket-specific actions exist: +- +- - link +- - merge +- - comment +- - correspond +- - take +- - steal +- - untake +- - give +- - resolve +- - delete +- - subject +- +- Attributes: +- +- The following attributes can be used with "rt show" or "rt edit" +- to retrieve or edit other information associated with tickets: +- +- links A ticket's relationships with others. +- history All of a ticket's transactions. +- history/type/<type> Only a particular type of transaction. +- history/id/<id> Only the transaction of the specified id. +- attachments A list of attachments. +- attachments/<id> The metadata for an individual attachment. +- attachments/<id>/content The content of an individual attachment. +- +--- +- +-Title: user +-Title: group +-Text: +- +- Users and groups are identified by name or numeric ID. +- +- The following generic operations may be performed upon them: +- +- - list +- - show +- - edit +- - create +- +--- +- +-Title: queue +-Text: +- +- Queues are identified by name or numeric ID. +- +- Currently, they can be subjected to the following actions: +- +- - show +- - edit +- - create +- +--- +- +-Title: subject +-Text: +- +- Syntax: +- +- rt subject <id> <new subject text> +- +- Change the subject of a ticket whose ticket id is given. +- +--- +- +-Title: give +-Text: +- +- Syntax: +- +- rt give <id> <accountname> +- +- Give a ticket whose ticket id is given to another user. +- +--- +- +-Title: steal +-Text: +- +- rt steal <id> +- +- Steal a ticket whose ticket id is given, i.e. set the owner to myself. +- +--- +- +-Title: take +-Text: +- +- Syntax: +- +- rt take <id> +- +- Take a ticket whose ticket id is given, i.e. set the owner to myself. +- +--- +- +-Title: untake +-Text: +- +- Syntax: +- +- rt untake <id> +- +- Untake a ticket whose ticket id is given, i.e. set the owner to Nobody. +- +--- +- +-Title: resolve +-Title: res +-Text: +- +- Syntax: +- +- rt resolve <id> +- +- Resolves a ticket whose ticket id is given. +- +--- +- +-Title: delete +-Title: del +-Text: +- +- Syntax: +- +- rt delete <id> +- +- Deletes a ticket whose ticket id is given. +- +--- +- +-Title: logout +-Text: +- +- Syntax: +- +- rt logout +- +- Terminates the currently established login session. You will need to +- provide authentication credentials before you can continue using the +- server. (See "rt help config" for details about authentication.) +- +--- +- +-Title: ls +-Title: list +-Title: search +-Text: +- +- Syntax: +- +- rt <ls|list|search> [options] "query string" +- +- Displays a list of objects matching the specified conditions. +- ("ls", "list", and "search" are synonyms.) +- +- The query string must be supplied as one argument. +- +- if on tickets, query is in the SQL-like syntax used internally by +- RT. (For more information, see "rt help query".), otherwise, query +- is plain string with format "FIELD OP VALUE", e.g. "Name = General". +- +- if query string is absent, we limit to privileged ones on users and +- user defined ones on groups automatically. +- +- Options: +- +- The following options control how much information is displayed +- about each matching object: +- +- -i Numeric IDs only. (Useful for |rt edit -; see examples.) +- -s Short description. +- -l Longer description. +- -f <field[s] Display only the fields listed and the ticket id +- +- In addition, +- +- -o +/-<field> Orders the returned list by the specified field. +- -r reversed order (useful if a default was given) +- -q queue[s] restricts the query to the queue[s] given +- multiple queues are separated by comma +- -S var=val Submits the specified variable with the request. +- -t type Specifies the type of object to look for. (The +- default is "ticket".) +- +- Examples: +- +- rt ls "Priority > 5 and Status=new" +- rt ls -o +Subject "Priority > 5 and Status=new" +- rt ls -o -Created "Priority > 5 and Status=new" +- rt ls -i "Priority > 5"|rt edit - set status=resolved +- rt ls -t ticket "Subject like '[PATCH]%'" +- rt ls -q systems +- rt ls -f owner,subject +- rt ls -t queue 'Name = General' +- rt ls -t user 'EmailAddress like foo@xxxxxxx' +- rt ls -t group 'Name like foo' +- +--- +- +-Title: show +-Text: +- +- Syntax: +- +- rt show [options] <object-ids> +- +- Displays details of the specified objects. +- +- For some types, object information is further classified into named +- attributes (for example, "1-3/links" is a valid ticket specification +- that refers to the links for tickets 1-3). Consult "rt help <type>" +- and "rt help objects" for further details. +- +- If only a number is given it will be interpreted as the objects +- ticket/number and ticket/number/history +- +- This command writes a set of forms representing the requested object +- data to STDOUT. +- +- Options: +- +- The following options control how much information is displayed +- about each matching object: +- +- Without any formatting options prettyprinted output is generated. +- Giving any of the two options below reverts to raw output. +- -s Short description (history and attachments only). +- -l Longer description (history and attachments only). +- +- In addition, +- - Read IDs from STDIN instead of the command-line. +- -t type Specifies object type. +- -f a,b,c Restrict the display to the specified fields. +- -S var=val Submits the specified variable with the request. +- +- Examples: +- +- rt show -t ticket -f id,subject,status 1-3 +- rt show ticket/3/attachments/29 +- rt show ticket/3/attachments/29/content +- rt show ticket/1-3/links +- rt show ticket/3/history +- rt show -l ticket/3/history +- rt show -t user 2 +- rt show 2 +- +--- +- +-Title: new +-Title: edit +-Title: create +-Text: +- +- Syntax: +- +- rt edit [options] <object-ids> set field=value [field=value] ... +- add field=value [field=value] ... +- del field=value [field=value] ... +- +- Edits information corresponding to the specified objects. +- +- A purely numeric object id nnn is translated into ticket/nnn +- +- If, instead of "edit", an action of "new" or "create" is specified, +- then a new object is created. In this case, no numeric object IDs +- may be specified, but the syntax and behaviour remain otherwise +- unchanged. +- +- This command typically starts an editor to allow you to edit object +- data in a form for submission. If you specified enough information +- on the command-line, however, it will make the submission directly. +- +- The command line may specify field-values in three different ways. +- "set" sets the named field to the given value, "add" adds a value +- to a multi-valued field, and "del" deletes the corresponding value. +- Each "field=value" specification must be given as a single argument. +- +- For some types, object information is further classified into named +- attributes (for example, "1-3/links" is a valid ticket specification +- that refers to the links for tickets 1-3). These attributes may also +- be edited. Consult "rt help <type>" and "rt help object" for further +- details. +- +- Options: +- +- - Read numeric IDs from STDIN instead of the command-line. +- (Useful with rt ls ... | rt edit -; see examples below.) +- -i Read a completed form from STDIN before submitting. +- -o Dump the completed form to STDOUT instead of submitting. +- -e Allows you to edit the form even if the command-line has +- enough information to make a submission directly. +- -S var=val +- Submits the specified variable with the request. +- -t type Specifies object type. +- -ct content-type Specifies content type of message(tickets only). +- +- Examples: +- +- # Interactive (starts $EDITOR with a form). +- rt edit ticket/3 +- rt create -t ticket +- rt create -t ticket -ct text/html +- +- # Non-interactive. +- rt edit ticket/1-3 add cc=foo@xxxxxxxxxxx set priority=3 due=tomorrow +- rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved +- rt edit ticket/4 set priority=3 owner=bar@xxxxxxxxxxx \ +- add cc=foo@xxxxxxxxxxx bcc=quux@xxxxxxxxxxx +- rt create -t ticket set subject='new ticket' priority=10 \ +- add cc=foo@xxxxxxxxxxx +- +--- +- +-Title: comment +-Title: correspond +-Text: +- +- Syntax: +- +- rt <comment|correspond> [options] <ticket-id> +- +- Adds a comment (or correspondence) to the specified ticket (the only +- difference being that comments aren't sent to the requestors.) +- +- This command will typically start an editor and allow you to type a +- comment into a form. If, however, you specified all the necessary +- information on the command line, it submits the comment directly. +- +- (See "rt help forms" for more information about forms.) +- +- Options: +- +- -m <text> Specify comment text. +- -ct <content-type> Specify content-type of comment text. +- -a <file> Attach a file to the comment. (May be used more +- than once to attach multiple files.) +- -c <addrs> A comma-separated list of Cc addresses. +- -b <addrs> A comma-separated list of Bcc addresses. +- -s <status> Set a new status for the ticket (default will +- leave the status unchanged) +- -w <time> Specify the time spent working on this ticket. +- -e Starts an editor before the submission, even if +- arguments from the command line were sufficient. +- +- Examples: +- +- rt comment -m 'Not worth fixing.' -a stddisclaimer.h 23 +- +--- +- +-Title: merge +-Text: +- +- Syntax: +- +- rt merge <from-id> <to-id> +- +- Merges the first ticket specified into the second ticket specified. +- +--- +- +-Title: link +-Text: +- +- Syntax: +- +- rt link [-d] <id-A> <link> <id-B> +- +- Creates (or, with -d, deletes) a link between the specified tickets. +- The link can (irrespective of case) be any of: +- +- DependsOn/DependedOnBy: A depends upon B (or vice versa). +- RefersTo/ReferredToBy: A refers to B (or vice versa). +- MemberOf/HasMember: A is a member of B (or vice versa). +- +- To view a ticket's links, use "rt show ticket/3/links". (See +- "rt help ticket" and "rt help show".) +- +- Options: +- +- -d Deletes the specified link. +- +- Examples: +- +- rt link 2 dependson 3 +- rt link -d 4 referredtoby 6 # 6 no longer refers to 4 +- +--- +- +-Title: query +-Text: +- +- RT uses an SQL-like syntax to specify object selection constraints. +- See the <RT:...> documentation for details. +- +- (XXX: I'm going to have to write it, aren't I?) +- +- Until it exists here a short description of important constructs: +- +- The two simple forms of query expressions are the constructs +- Attribute like Value and +- Attribute = Value or Attribute != Value +- +- Whether attributes can be matched using like or using = is built into RT. +- The attributes id, Queue, Owner Priority and Status require the = or != +- tests. +- +- If Value is a string it must be quoted and may contain the wildcard +- character %. If the string does not contain white space, the quoting +- may however be omitted, it will be added automatically when parsing +- the input. +- +- Simple query expressions can be combined using and, or and parentheses +- can be used to group expressions. +- +- As a special case a standalone string (which would not form a correct +- query) is transformed into (Owner='string' or Requestor like 'string%') +- and added to the default query, i.e. the query is narrowed down. +- +- If no Queue=name clause is contained in the query, a default clause +- Queue=$config{queue} is added. +- +- Examples: +- Status!='resolved' and Status!='rejected' +- (Owner='myaccount' or Requestor like 'myaccount%') and Status!='resolved' +- +--- +- +-Title: form +-Title: forms +-Text: +- +- This program uses RFC822 header-style forms to represent object data +- in a form that's suitable for processing both by humans and scripts. +- +- A form is a set of (field, value) specifications, with some initial +- commented text and interspersed blank lines allowed for convenience. +- Field names may appear more than once in a form; a comma-separated +- list of multiple field values may also be specified directly. +- +- Field values can be wrapped as in RFC822, with leading whitespace. +- The longest sequence of leading whitespace common to all the lines +- is removed (preserving further indentation). There is no limit on +- the length of a value. +- +- Multiple forms are separated by a line containing only "--\n". +- +- (XXX: A more detailed specification will be provided soon. For now, +- the server-side syntax checking will suffice.) +- +--- +- +-Title: topics +-Text: +- +- Syntax: +- +- rt help <topic> +- +- Get help on any of the following subjects: +- +- - tickets, users, groups, queues. +- - show, edit, ls/list/search, new/create. +- +- - query (search query syntax) +- - forms (form specification) +- +- - objects (how to specify objects) +- - types (a list of object types) +- - actions/commands (a list of actions) +- - usage/syntax (syntax details) +- - conf/config/configuration (configuration details) +- - examples (a few useful examples) +- +--- +- +-Title: example +-Title: examples +-Text: +- +- some useful examples +- +- All the following list requests will be restricted to the default queue. +- That can be changed by adding the option -q queuename +- +- List all tickets that are not rejected/resolved +- rt ls +- List all tickets that are new and do not have an owner +- rt ls "status=new and owner=nobody" +- List all tickets which I have sent or of which I am the owner +- rt ls myaccount +- List all attributes for the ticket 6977 (ls -l instead of ls) +- rt ls -l 6977 +- Show the content of ticket 6977 +- rt show 6977 +- Show all attributes in the ticket and in the history of the ticket +- rt show -l 6977 +- Comment a ticket (mail is sent to all queue watchers, i.e. AdminCc's) +- rt comment 6977 +- This will open an editor and lets you add text (attribute Text:) +- Other attributes may be changed as well, but usually don't do that. +- Correspond a ticket (like comment, but mail is also sent to requestors) +- rt correspond 6977 +- Edit a ticket (generic change, interactive using the editor) +- rt edit 6977 +- Change the owner of a ticket non interactively +- rt edit 6977 set owner=myaccount +- or +- rt give 6977 account +- or +- rt take 6977 +- Change the status of a ticket +- rt edit 6977 set status=resolved +- or +- rt resolve 6977 +- Change the status of all tickets I own to resolved !!! +- rt ls -i owner=myaccount | rt edit - set status=resolved +- +--- +- +-Title: shell +-Text: +- +- Syntax: +- +- rt shell +- +- Opens an interactive shell, at which you can issue commands of +- the form "<action> [options] [arguments]". +- +- To exit the shell, type "quit" or "exit". +- +- Commands can be given at the shell in the same form as they would +- be given at the command line without the leading 'rt' invocation. +- +- Example: +- $ rt shell +- rt> create -t ticket set subject='new' add cc=foo@xxxxxxxxxxx +- # Ticket 8 created. +- rt> quit +- $ +- +--- +- +-Title: take +-Title: untake +-Title: steal +-Text: +- +- Syntax: +- +- rt <take|untake|steal> <ticket-id> +- +- Sets the owner of the specified ticket to the current user, +- assuming said user has the bits to do so, or releases the +- ticket. +- +- 'Take' is used on tickets which are not currently owned +- (Owner: Nobody), 'steal' is used on tickets which *are* +- currently owned, and 'untake' is used to "release" a ticket +- (reset its Owner to Nobody). 'Take' cannot be used on +- tickets which are currently owned. +- +- Example: +- alice$ rt create -t ticket set subject="New ticket" +- # Ticket 7 created. +- alice$ rt take 7 +- # Owner changed from Nobody to alice +- alice$ su bob +- bob$ rt steal 7 +- # Owner changed from alice to bob +- bob$ rt untake 7 +- # Owner changed from bob to Nobody +- +--- +- +-Title: quit +-Title: exit +-Text: +- +- Use "quit" or "exit" to leave the shell. Only valid within shell +- mode. +- +- Example: +- $ rt shell +- rt> quit +- $ +- +-__END__ +- +-=head1 NAME +- +-rt - command-line interface to RT 3.0 or newer +- +-=head1 SYNOPSIS +- +- rt help +- +-=head1 DESCRIPTION +- +-This script allows you to interact with an RT server over HTTP, and offers an +-interface to RT's functionality that is better-suited to automation and +-integration with other tools. +- +-In general, each invocation of this program should specify an action to +-perform on one or more objects, and any other arguments required to complete +-the desired action. +- +diff --git a/bin/rt-crontool b/bin/rt-crontool +deleted file mode 100755 +index 7575bf0..0000000 +--- a/bin/rt-crontool ++++ /dev/null +@@ -1,456 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +-use Carp; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use RT; +- +-use Getopt::Long; +- +-use RT::Interface::CLI qw(CleanEnv GetCurrentUser GetMessageContent loc); +- +-my ( $search, $condition, $action, $search_arg, $condition_arg, $action_arg, +- $template, $template_id, $transaction, $transaction_type, $help, $log, $verbose ); +-GetOptions( +- "search=s" => \$search, +- "search-arg=s" => \$search_arg, +- "condition=s" => \$condition, +- "condition-arg=s" => \$condition_arg, +- "action-arg=s" => \$action_arg, +- "action=s" => \$action, +- "template=s" => \$template, +- "template-id=s" => \$template_id, +- "transaction=s" => \$transaction, +- "transaction-type=s" => \$transaction_type, +- "log=s" => \$log, +- "verbose|v" => \$verbose, +- "help" => \$help, +-); +- +-# Load the config file +-RT::LoadConfig(); +- +-# adjust logging to the screen according to options +-RT->Config->Set( LogToSTDERR => $log ) if $log; +- +-#Connect to the database and get RT::SystemUser and RT::Nobody loaded +-RT::Init(); +- +-# Clean out all the nasties from the environment +-CleanEnv(); +- +-require RT::Tickets; +-require RT::Template; +- +-#Get the current user all loaded +-my $CurrentUser = GetCurrentUser(); +- +-# show help even if there is no current user +-help() if $help; +- +-unless ( $CurrentUser->Id ) { +- print loc("No RT user found. Please consult your RT administrator.") . "\n"; +- exit(1); +-} +- +-help() unless $search && $action; +- +-$transaction = lc( $transaction||'' ); +-if ( $transaction && $transaction !~ /^(first|all|last)$/i ) { +- print STDERR loc("--transaction argument could be only 'first', 'last' or 'all'"); +- exit 1; +-} +- +-if ( $template && $template_id ) { +- print STDERR loc("--template-id is deprecated argument and can not be used with --template"); +- exit 1; +-} +-elsif ( $template_id ) { +-# don't warn +- $template = $template_id; +-} +- +-# We _must_ have a search object +-load_module($search); +-load_module($action) if ($action); +-load_module($condition) if ($condition); +- +-my $void_scrip = RT::Scrip->new( $CurrentUser ); +-my $void_scrip_action = RT::ScripAction->new( $CurrentUser ); +- +-#At the appointed time: +- +-#find a bunch of tickets +-my $tickets = RT::Tickets->new($CurrentUser); +-$search = $search->new( +- TicketsObj => $tickets, +- Argument => $search_arg, +- CurrentUser => $CurrentUser +-); +-$search->Prepare(); +- +-#for each ticket we've found +-while ( my $ticket = $tickets->Next() ) { +- print $ticket->Id() . ":\n" if ($verbose); +- +- my $template_obj = get_template( $ticket ); +- +- if ( $transaction ) { +- my $txns = get_transactions($ticket); +- my $found = 0; +- while ( my $txn = $txns->Next ) { +- print "\t".loc("Using transaction #[_1]...", $txn->id)."\n" +- if $verbose; +- process($ticket, $txn, $template_obj); +- $found = 1; +- } +- print "\t".loc("Couldn't find suitable transaction, skipping")."\n" +- if $verbose && !$found; +- } else { +- print "\t".loc("Processing without transaction, some conditions and actions may fail. Consider using --transaction argument")."\n" +- if $verbose; +- +- process($ticket, undef, $template_obj); +- } +-} +- +-sub process { +- my $ticket = shift; +- my $transaction = shift; +- my $template_obj = shift; +- +- # perform some more advanced check +- if ($condition) { +- my $condition_obj = $condition->new( +- TransactionObj => $transaction, +- TicketObj => $ticket, +- ScripObj => $void_scrip, +- TemplateObj => $template_obj, +- Argument => $condition_arg, +- CurrentUser => $CurrentUser, +- ); +- +- # if the condition doesn't apply, get out of here +- +- return unless $condition_obj->IsApplicable; +- print "\t".loc("Condition matches...")."\n" if $verbose; +- } +- +- #prepare our action +- my $action_obj = $action->new( +- TicketObj => $ticket, +- TransactionObj => $transaction, +- TemplateObj => $template_obj, +- Argument => $action_arg, +- ScripObj => $void_scrip, +- ScripActionObj => $void_scrip_action, +- CurrentUser => $CurrentUser, +- ); +- +- #if our preparation, move onto the next ticket +- return unless $action_obj->Prepare; +- print "\t".loc("Action prepared...")."\n" if $verbose; +- +- #commit our action. +- return unless $action_obj->Commit; +- print "\t".loc("Action committed.")."\n" if $verbose; +-} +- +-# =head2 get_transactions +-# +-# Takes ticket and returns L<RT::Transactions> object with transactions +-# of the ticket according to command line arguments C<--transaction> +-# and <--transaction-type>. +-# +-# =cut +- +-sub get_transactions { +- my $ticket = shift; +- my $txns = $ticket->Transactions; +- my $order = $transaction eq 'last'? 'DESC': 'ASC'; +- $txns->OrderByCols( +- { FIELD => 'Created', ORDER => $order }, +- { FIELD => 'id', ORDER => $order }, +- ); +- if ( $transaction_type ) { +- $transaction_type =~ s/^\s+//; +- $transaction_type =~ s/\s+$//; +- foreach my $type ( split /\s*,\s*/, $transaction_type ) { +- $txns->Limit( FIELD => 'Type', VALUE => $type, ENTRYAGGREGATOR => 'OR' ); +- } +- } +- $txns->RowsPerPage(1) unless $transaction eq 'all'; +- return $txns; +-} +- +-# =head2 get_template +-# +-# Takes a ticket and returns a template according to command line options. +-# +-# =cut +- +-sub get_template { +- my $ticket = shift; +- return undef unless $template; +- +- unless ( $template =~ /\D/ ) { +- # by id +- my $template_obj = RT::Template->new( RT->SystemUser ); +- $template_obj->Load( $template ); +- die "Failed to load template '$template'" +- unless $template_obj->id; +- return $template_obj; +- } +- +- my $queue = $ticket->Queue; +- +- my $res = RT::Template->new( RT->SystemUser ); +- $res->LoadQueueTemplate( Queue => $queue, Name => $template ); +- unless ( $res->id ) { +- $res->LoadGlobalTemplate( $template ); +- die "Failed to load template '$template', either for queue #$queue or global" +- unless $res->id; +- } +- return $res; +-} +- +- +-# =head2 load_module +-# +-# Loads a perl module, dying nicely if it can't find it. +-# +-# =cut +- +-sub load_module { +- my $modname = shift; +- unless ($modname->require) { +- my $error = $@; +- die loc( "Failed to load module [_1]. ([_2])", $modname, $error ); +- } +- +-} +- +- +-sub help { +- +- print loc( "[_1] is a tool to act on tickets from an external scheduling tool, such as cron.", $0 ) +- . "\n"; +- print loc("It takes several arguments:") . "\n\n"; +- +- print " " +- . loc( "[_1] - Specify the search module you want to use", "--search" ) +- . "\n"; +- print " " +- . loc( "[_1] - An argument to pass to [_2]", "--search-arg", "--search" ) +- . "\n"; +- +- print " " +- . loc( "[_1] - Specify the condition module you want to use", "--condition" ) +- . "\n"; +- print " " +- . loc( "[_1] - An argument to pass to [_2]", "--condition-arg", "--condition" ) +- . "\n"; +- print " " +- . loc( "[_1] - Specify the action module you want to use", "--action" ) +- . "\n"; +- print " " +- . loc( "[_1] - An argument to pass to [_2]", "--action-arg", "--action" ) +- . "\n"; +- print " " +- . loc( "[_1] - Specify name or id of template(s) you want to use", "--template" ) +- . "\n"; +- print " " +- . loc( "[_1] - Specify if you want to use either 'first', 'last' or 'all' transactions", "--transaction" ) +- . "\n"; +- print " " +- . loc( "[_1] - Specify the comma separated list of transactions' types you want to use", "--transaction-type" ) +- . "\n"; +- print " " +- . loc( "[_1] - Adjust LogToSTDERR config option", "--log" ) . "\n"; +- print " " +- . loc( "[_1] - Output status updates to STDOUT", "--verbose" ) . "\n"; +- print "\n"; +- print "\n"; +- print loc("Security:")."\n"; +- print loc("This tool allows the user to run arbitrary perl modules from within RT.")." ". +- loc("If this tool were setgid, a hostile local user could use this tool to gain administrative access to RT.")." ". +- loc("It is incredibly important that nonprivileged users not be allowed to run this tool."). " " . +- loc("It is suggested that you create a non-privileged unix user with the correct group membership and RT access to run this tool.")."\n"; +- print "\n"; +- print loc("Example:"); +- print "\n"; +- print " " +- . loc( "The following command will find all active tickets in the queue 'general' and set their priority to 99 if they are overdue:" +- ) +- . "\n\n"; +- +- print " bin/rt-crontool \\\n"; +- print " --search RT::Search::ActiveTicketsInQueue --search-arg general \\\n"; +- print " --condition RT::Condition::Overdue \\\n"; +- print " --action RT::Action::SetPriority --action-arg 99 \\\n"; +- print " --verbose\n"; +- +- print "\n"; +- print loc("Escalate tickets"). "\n"; +- print " bin/rt-crontool \\\n"; +- print " --search RT::Search::ActiveTicketsInQueue --search-arg general \\\n"; +- print" --action RT::Action::EscalatePriority\n"; +- +- +- +- +- +- +- exit(0); +-} +- +-__END__ +- +-=head1 NAME +- +-rt-crontool - a tool to act on tickets from an external scheduling tool +- +-=head1 SYNOPSIS +- +- # find all active tickets in the queue 'general' and set their priority to 99 if they are overdue: +- rt-crontool \ +- --search RT::Search::ActiveTicketsInQueue --search-arg general \ +- --condition RT::Condition::Overdue \ +- --action RT::Action::SetPriority --action-arg 99 \ +- --verbose +- +- # Escalate tickets +- rt-crontool \ +- --search RT::Search::ActiveTicketsInQueue --search-arg general \ +- --action RT::Action::EscalatePriority +- +-=head1 DESCRIPTION +- +-This script is a tool to act on tickets from an external scheduling tool, such +-as cron. +- +-Security: +- +-This tool allows the user to run arbitrary perl modules from within RT. If +-this tool were setgid, a hostile local user could use this tool to gain +-administrative access to RT. It is incredibly important that nonprivileged +-users not be allowed to run this tool. It is suggested that you create a +-non-privileged unix user with the correct group membership and RT access to +-run this tool. +- +- +-=head1 OPTIONS +- +-=over +- +-=item search +- +-Specify the search module you want to use +- +-=item search-arg +- +-An argument to pass to --search +- +-=item condition +- +-Specify the condition module you want to use +- +-=item condition-arg +- +-An argument to pass to --condition +- +-=item action +- +-Specify the action module you want to use +- +-=item action-arg +- +-An argument to pass to --action +- +-=item template +- +-Specify name or id of template(s) you want to use +- +-=item transaction +- +-Specify if you want to use either 'first', 'last' or 'all' transactions +- +- +-=item transaction-type +- +-Specify the comma separated list of transactions' types you want to use +- +-=item log +- +-Adjust LogToSTDERR config option +- +-=item verbose +- +-Output status updates to STDOUT +- +-=back +- +diff --git a/bin/rt-mailgate b/bin/rt-mailgate +deleted file mode 100755 +index 701ee8f..0000000 +--- a/bin/rt-mailgate ++++ /dev/null +@@ -1,512 +0,0 @@ +-#!/usr/bin/perl -w +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-=head1 NAME +- +-rt-mailgate - Mail interface to RT. +- +-=cut +- +-use strict; +-use warnings; +- +-use Getopt::Long; +- +-my $opts = { }; +-GetOptions( $opts, "queue=s", "action=s", "url=s", +- "jar=s", "help", "debug", "extension=s", +- "timeout=i", "verify-ssl!", "ca-file=s", +- ); +- +-my $gateway = RT::Client::MailGateway->new(); +- +-$gateway->run($opts); +- +-package RT::Client::MailGateway; +- +-use LWP::UserAgent; +-use HTTP::Request::Common qw($DYNAMIC_FILE_UPLOAD); +-use File::Temp qw(tempfile tempdir); +-$DYNAMIC_FILE_UPLOAD = 1; +- +-use constant EX_TEMPFAIL => 75; +-use constant BUFFER_SIZE => 8192; +- +-sub new { +- my $class = shift; +- my $self = bless {}, $class; +- return $self; +-} +- +-sub run { +- my $self = shift; +- my $opts = shift; +- +- if ( $opts->{running_in_test_harness} ) { +- $self->{running_in_test_harness} = 1; +- } +- +- $self->validate_cli_flags($opts); +- +- my $ua = $self->get_useragent($opts); +- my $post_params = $self->setup_session($opts); +- $self->upload_message( $ua => $post_params ); +- $self->exit_with_success(); +-} +- +-sub exit_with_success { +- my $self = shift; +- if ( $self->{running_in_test_harness} ) { +- return 1; +- } else { +- exit 0; +- } +-} +- +-sub tempfail { +- my $self = shift; +- if ( $self->{running_in_test_harness} ) { +- die "tempfail"; +- } else { +- +- exit EX_TEMPFAIL; +- } +-} +- +-sub permfail { +- my $self = shift; +- if ( $self->{running_in_test_harness} ) { +- die "permfail"; +- } else { +- +- exit 1; +- } +-} +- +-sub validate_cli_flags { +- my $self = shift; +- my $opts = shift; +- if ( $opts->{'help'} ) { +- require Pod::Usage; +- Pod::Usage::pod2usage( { verbose => 2 } ); +- return $self->permfail() +- ; # Don't want to succeed if this is really an email! +- } +- +- unless ( $opts->{'url'} ) { +- print STDERR +- "$0 invoked improperly\n\nNo 'url' provided to mail gateway!\n"; +- return $self->permfail(); +- } +- +- $opts->{"verify-ssl"} = 1 unless defined $opts->{"verify-ssl"}; +-} +- +-sub get_useragent { +- my $self = shift; +- my $opts = shift; +- my $ua = LWP::UserAgent->new(); +- $ua->agent("rt-mailgate/4.2.10 "); +- $ua->cookie_jar( { file => $opts->{'jar'} } ) if $opts->{'jar'}; +- +- $ua->ssl_opts( verify_hostname => $opts->{'verify-ssl'} ); +- $ua->ssl_opts( SSL_ca_file => $opts->{'ca-file'} ) +- if $opts->{'ca-file'}; +- +- return $ua; +-} +- +-sub setup_session { +- my $self = shift; +- my $opts = shift; +- my %post_params; +- foreach (qw(queue action)) { +- $post_params{$_} = $opts->{$_} if defined $opts->{$_}; +- } +- +- if ( ( $opts->{'extension'} || '' ) =~ /^(?:action|queue|ticket)$/i ) { +- $post_params{ lc $opts->{'extension'} } = $ENV{'EXTENSION'} +- || $opts->{ $opts->{'extension'} }; +- } elsif ( $opts->{'extension'} && $ENV{'EXTENSION'} ) { +- print STDERR +- "Value of the --extension argument is not action, queue or ticket" +- . ", but environment variable EXTENSION is also defined. The former is ignored.\n"; +- } +- +- # add ENV{'EXTENSION'} as X-RT-MailExtension to the message header +- if ( my $value = ( $ENV{'EXTENSION'} || $opts->{'extension'} ) ) { +- +- # prepare value to avoid MIME format breakage +- # strip trailing newline symbols +- $value =~ s/(\r*\n)+$//; +- +- # make a correct multiline header field, +- # with tabs in the beginning of each line +- $value =~ s/(\r*\n)/$1\t/g; +- $opts->{'headers'} .= "X-RT-Mail-Extension: $value\n"; +- } +- +- # Read the message in from STDIN +- # _raw_message is used for testing +- my $message = $opts->{'_raw_message'} || $self->slurp_message(); +- unless ( $message->{'filename'} ) { +- $post_params{'message'} = [ +- undef, '', +- 'Content-Type' => 'application/octet-stream', +- Content => ${ $message->{'content'} }, +- ]; +- } else { +- $post_params{'message'} = [ +- $message->{'filename'}, '', +- 'Content-Type' => 'application/octet-stream', +- ]; +- } +- +- return \%post_params; +-} +- +-sub upload_message { +- my $self = shift; +- my $ua = shift; +- my $post_params = shift; +- my $full_url = $opts->{'url'} . "/REST/1.0/NoAuth/mail-gateway"; +- print STDERR "$0: connecting to $full_url\n" if $opts->{'debug'}; +- +- $ua->timeout( exists( $opts->{'timeout'} ) ? $opts->{'timeout'} : 180 ); +- my $r = $ua->post( $full_url, $post_params, Content_Type => 'form-data' ); +- +- # Follow 3 redirects +- my $n = 0; +- while ($n++ < 3 and $r->is_redirect) { +- $full_url = $r->header( "Location" ); +- $r = $ua->post( $full_url, $post_params, Content_Type => 'form-data' ); +- } +- +- $self->check_failure($r); +- +- my $content = $r->content; +- print STDERR $content . "\n" if $opts->{'debug'}; +- +- return if ( $content =~ /^(ok|not ok)/ ); +- +- # It's not the server's fault if the mail is bogus. We just want to know that +- # *something* came out of the server. +- print STDERR <<EOF; +-RT server error. +- +-The RT server which handled your email did not behave as expected. It +-said: +- +-$content +-EOF +- +- return $self->tempfail(); +-} +- +-sub check_failure { +- my $self = shift; +- my $r = shift; +- return if $r->is_success; +- +- print STDERR "HTTP request failed: @{[ $r->status_line ]}. " +- ."Your webserver logs may have more information or there may be a network problem.\n"; +- print STDERR "\n$0: undefined server error\n" if $opts->{'debug'}; +- return $self->tempfail(); +-} +- +-sub slurp_message { +- my $self = shift; +- +- local $@; +- +- my %message; +- my ( $fh, $filename ) +- = eval { tempfile( DIR => tempdir( CLEANUP => 1 ) ) }; +- if ( !$fh || $@ ) { +- print STDERR "$0: Couldn't create temp file, using memory\n"; +- print STDERR "error: $@\n" if $@; +- +- my $message = \do { local ( @ARGV, $/ ); <STDIN> }; +- unless ( $$message =~ /\S/ ) { +- print STDERR "$0: no message passed on STDIN\n"; +- $self->exit_with_success; +- } +- $$message = $opts->{'headers'} . $$message if $opts->{'headers'}; +- return ( { content => $message } ); +- } +- +- binmode $fh; +- binmode \*STDIN; +- +- print $fh $opts->{'headers'} if $opts->{'headers'}; +- +- my $buf; +- my $empty = 1; +- while (1) { +- my $status = read \*STDIN, $buf, BUFFER_SIZE; +- unless ( defined $status ) { +- print STDERR "$0: couldn't read message: $!\n"; +- return $self->tempfail(); +- } elsif ( !$status ) { +- last; +- } +- $empty = 0 if $buf =~ /\S/; +- print $fh $buf; +- } +- close $fh; +- +- if ($empty) { +- print STDERR "$0: no message passed on STDIN\n"; +- $self->exit_with_success; +- } +- print STDERR "$0: temp file is '$filename'\n" if $opts->{'debug'}; +- return ( { filename => $filename } ); +-} +- +-=head1 SYNOPSIS +- +- rt-mailgate --help : this text +- +-Usual invocation (from MTA): +- +- rt-mailgate --action (correspond|comment|...) --queue queuename +- --url http://your.rt.server/ +- [ --debug ] +- [ --extension (queue|action|ticket) ] +- [ --timeout seconds ] +- +- +- +-=head1 OPTIONS +- +-=over 3 +- +-=item C<--action> +- +-Specifies what happens to email sent to this alias. The avaliable +-basic actions are: C<correspond>, C<comment>. +- +- +-If you've set the RT configuration variable B<< C<UnsafeEmailCommands> >>, +-C<take> and C<resolve> are also available. You can execute two or more +-actions on a single message using a C<-> separated list. RT will execute +-the actions in the listed order. For example you can use C<take-comment>, +-C<correspond-resolve> or C<take-comment-resolve> as actions. +- +-Note that C<take> and C<resolve> actions ignore message text if used +-alone. Include a C<comment> or C<correspond> action if you want RT +-to record the incoming message. +- +-The default action is C<correspond>. +- +-=item C<--queue> +- +-This flag determines which queue this alias should create a ticket in if no ticket identifier +-is found. +- +-=item C<--url> +- +-This flag tells the mail gateway where it can find your RT server. You should +-probably use the same URL that users use to log into RT. +- +-If you have a self-signed SSL certificate, you may also need to pass +-C<--ca-file> or C<--no-verify-ssl>, below. +- +-=item C<--ca-file> I<path> +- +-Specifies the path to the public SSL certificate for the certificate +-authority that should be used to verify the website's SSL certificate. +-If your webserver uses a self-signed certificate, you should +-preferentially use this option over C<--no-verify-ssl>, as it will +-ensure that the self-signed certificate that the mailgate is seeing the +-I<right> self-signed certificate. +- +-=item C<--no-verify-ssl> +- +-This flag tells the mail gateway to trust all SSL certificates, +-regardless of if their hostname matches the certificate, and regardless +-of CA. This is required if you have a self-signed certificate, or some +-other certificate which is not traceable back to an certificate your +-system ultimitely trusts. +- +-=item C<--extension> OPTIONAL +- +-Some MTAs will route mail sent to user-foo@host or user+foo@host to user@host +-and present "foo" in the environment variable $EXTENSION. By specifying +-the value "queue" for this parameter, the queue this message should be +-submitted to will be set to the value of $EXTENSION. By specifying +-"ticket", $EXTENSION will be interpreted as the id of the ticket this message +-is related to. "action" will allow the user to specify either "comment" or +-"correspond" in the address extension. +- +-=item C<--debug> OPTIONAL +- +-Print debugging output to standard error +- +- +-=item C<--timeout> OPTIONAL +- +-Configure the timeout for posting the message to the web server. The +-default timeout is 3 minutes (180 seconds). +- +-=back +- +- +-=head1 DESCRIPTION +- +-The RT mail gateway is the primary mechanism for communicating with RT +-via email. This program simply directs the email to the RT web server, +-which handles filing correspondence and sending out any required mail. +-It is designed to be run as part of the mail delivery process, either +-called directly by the MTA or C<procmail>, or in a F<.forward> or +-equivalent. +- +-=head1 SETUP +- +-Much of the set up of the mail gateway depends on your MTA and mail +-routing configuration. +- +-You need to route mail to C<rt-mailgate> for the queues you're +-monitoring. For instance, if you're using F</etc/aliases> and you have a +-"bugs" queue, you will want something like this: +- +- bugs: "|/opt/rt4/bin/rt-mailgate --queue bugs --action correspond +- --url http://rt.mycorp.com/" +- +- bugs-comment: "|/opt/rt4/bin/rt-mailgate --queue bugs --action comment +- --url http://rt.mycorp.com/" +- +-Note that you don't have to run your RT server on your mail server, as +-the mail gateway will happily relay to a different machine. +- +-=head1 CUSTOMIZATION +- +-By default, the mail gateway will accept mail from anyone. However, +-there are situations in which you will want to authenticate users +-before allowing them to communicate with the system. You can do this +-via a plug-in mechanism in the RT configuration. +- +-You can set the array C<@MailPlugins> to be a list of plugins. The +-default plugin, if this is not given, is C<Auth::MailFrom> - that is, +-authentication of the person is done based on the C<From> header of the +-email. If you have additional filters or authentication mechanisms, you +-can list them here and they will be called in order: +- +- Set( @MailPlugins => +- "Filter::SpamAssassin", +- "Auth::LDAP", +- # ... +- ); +- +-See the documentation for any additional plugins you have. +- +-You may also put Perl subroutines into the C<@MailPlugins> array, if +-they behave as described below. +- +-=head1 WRITING PLUGINS +- +-What's actually going on in the above is that C<@MailPlugins> is a +-list of Perl modules; RT prepends C<RT::Interface::Email::> to the name, +-to form a package name, and then C<use>'s this module. The module is +-expected to provide a C<GetCurrentUser> subroutine, which takes a hash of +-several parameters: +- +-=over 4 +- +-=item Message +- +-A C<MIME::Entity> object representing the email +- +-=item CurrentUser +- +-An C<RT::CurrentUser> object +- +-=item AuthStat +- +-The authentication level returned from the previous plugin. +- +-=item Ticket [OPTIONAL] +- +-The ticket under discussion +- +-=item Queue [OPTIONAL] +- +-If we don't already have a ticket id, we need to know which queue we're talking about +- +-=item Action +- +-The action being performed. At the moment, it's one of "comment" or "correspond" +- +-=back +- +-It returns two values, the new C<RT::CurrentUser> object, and the new +-authentication level. The authentication level can be zero, not allowed +-to communicate with RT at all, (a "permission denied" error is mailed to +-the correspondent) or one, which is the normal mode of operation. +-Additionally, if C<-1> is returned, then the processing of the plug-ins +-stops immediately and the message is ignored. +- +-=head1 ENVIRONMENT +- +-=over 4 +- +-=item EXTENSION +- +-Some MTAs will route mail sent to user-foo@host or user+foo@host to user@host +-and present "foo" in the environment variable C<EXTENSION>. Mailgate adds value +-of this variable to message in the C<X-RT-Mail-Extension> field of the message +-header. +- +-See also C<--extension> option. Note that value of the environment variable is +-always added to the message header when it's not empty even if C<--extension> +-option is not provided. +- +-=back +- +-=cut +- +diff --git a/etc/RT_Config.pm b/etc/RT_Config.pm +deleted file mode 100644 +index 013352c..0000000 +--- a/etc/RT_Config.pm ++++ /dev/null +@@ -1,3037 +0,0 @@ +-# +-# RT was configured with: +-# +-# $ ./configure --with-db-type=SQLite --enable-layout=relative --with-web-handler=standalone +-# +- +-package RT; +- +-############################# WARNING ############################# +-# # +-# NEVER EDIT RT_Config.pm ! # +-# # +-# Instead, copy any sections you want to change to # +-# RT_SiteConfig.pm and edit them there. Otherwise, # +-# your changes will be lost when you upgrade RT. # +-# # +-############################# WARNING ############################# +- +-=head1 NAME +- +-RT::Config +- +-=head1 Base configuration +- +-=over 4 +- +-=item C<$rtname> +- +-C<$rtname> is the string that RT will look for in mail messages to +-figure out what ticket a new piece of mail belongs to. +- +-Your domain name is recommended, so as not to pollute the namespace. +-Once you start using a given tag, you should probably never change it; +-otherwise, mail for existing tickets won't get put in the right place. +- +-=cut +- +-Set($rtname, "example.com"); +- +-=item C<$Organization> +- +-You should set this to your organization's DNS domain. For example, +-I<fsck.com> or I<asylum.arkham.ma.us>. It is used by the linking +-interface to guarantee that ticket URIs are unique and easy to +-construct. Changing it after you have created tickets in the system +-will B<break> all existing ticket links! +- +-=cut +- +-Set($Organization, "example.com"); +- +-=item C<$CorrespondAddress>, C<$CommentAddress> +- +-RT is designed such that any mail which already has a ticket-id +-associated with it will get to the right place automatically. +- +-C<$CorrespondAddress> and C<$CommentAddress> are the default addresses +-that will be listed in From: and Reply-To: headers of correspondence +-and comment mail tracked by RT, unless overridden by a queue-specific +-address. They should be set to email addresses which have been +-configured as aliases for F<rt-mailgate>. +- +-=cut +- +-Set($CorrespondAddress, ''); +- +-Set($CommentAddress, ''); +- +-=item C<$WebDomain> +- +-Domain name of the RT server, e.g. 'www.example.com'. It should not +-contain anything except the server name. +- +-=cut +- +-Set($WebDomain, "localhost"); +- +-=item C<$WebPort> +- +-If we're running as a superuser, run on port 80. Otherwise, pick a +-high port for this user. +- +-443 is default port for https protocol. +- +-=cut +- +-Set($WebPort, 80); +- +-=item C<$WebPath> +- +-If you're putting the web UI somewhere other than at the root of your +-server, you should set C<$WebPath> to the path you'll be serving RT +-at. +- +-C<$WebPath> requires a leading / but no trailing /, or it can be +-blank. +- +-In most cases, you should leave C<$WebPath> set to "" (an empty +-value). +- +-=cut +- +-Set($WebPath, ""); +- +-=item C<$Timezone> +- +-C<$Timezone> is the default timezone, used to convert times entered by +-users into GMT, as they are stored in the database, and back again; +-users can override this. It should be set to a timezone recognized by +-your server. +- +-=cut +- +-Set($Timezone, "US/Eastern"); +- +-=item C<@Plugins> +- +-Once a plugin has been downloaded and installed, use C<Plugin()> to add +-to the enabled C<@Plugins> list: +- +- Plugin( "RT::Extension::SLA" ); +- Plugin( "RT::Authen::ExternalAuth" ); +- +-=cut +- +-Set(@Plugins, ()); +- +-=item C<@StaticRoots> +- +-Set C<@StaticRoots> to serve extra paths with a static handler. The +-contents of each hashref should be the the same arguments as +-L<Plack::Middleware::Static> takes. These paths will be checked before +-any plugin or core static paths. +- +-Example: +- +- Set( @StaticRoots, +- { +- path => qr{^/static/}, +- root => '/local/path/to/static/parent', +- }, +- ); +- +-=cut +- +-Set( @StaticRoots, () ); +- +-=back +- +- +- +- +-=head1 Database connection +- +-=over 4 +- +-=item C<$DatabaseType> +- +-Database driver being used; case matters. Valid types are "mysql", +-"Oracle", and "Pg". "SQLite" is also available for non-production use. +- +-=cut +- +-Set($DatabaseType, "SQLite"); +- +-=item C<$DatabaseHost>, C<$DatabaseRTHost> +- +-The domain name of your database server. If you're running MySQL and +-on localhost, leave it blank for enhanced performance. +- +-C<DatabaseRTHost> is the fully-qualified hostname of your RT server, +-for use in granting ACL rights on MySQL. +- +-=cut +- +-Set($DatabaseHost, "localhost"); +-Set($DatabaseRTHost, "localhost"); +- +-=item C<$DatabasePort> +- +-The port that your database server is running on. Ignored unless it's +-a positive integer. It's usually safe to leave this blank; RT will +-choose the correct default. +- +-=cut +- +-Set($DatabasePort, ""); +- +-=item C<$DatabaseUser> +- +-The name of the user to connect to the database as. +- +-=cut +- +-Set($DatabaseUser, "rt_user"); +- +-=item C<$DatabasePassword> +- +-The password the C<$DatabaseUser> should use to access the database. +- +-=cut +- +-Set($DatabasePassword, q{rt_pass}); +- +-=item C<$DatabaseName> +- +-The name of the RT database on your database server. For Oracle, the +-SID and database objects are created in C<$DatabaseUser>'s schema. +- +-=cut +- +-Set($DatabaseName, q{rt4}); +- +-=item C<$DatabaseRequireSSL> +- +-If you're using PostgreSQL and have compiled in SSL support, set +-C<$DatabaseRequireSSL> to 1 to turn on SSL communication with the +-database. +- +-=cut +- +-Set($DatabaseRequireSSL, undef); +- +-=item C<$DatabaseAdmin> +- +-The name of the database administrator to connect to the database as +-during upgrades. +- +-=cut +- +-Set($DatabaseAdmin, "root"); +- +-=back +- +- +- +- +-=head1 Logging +- +-The default is to log anything except debugging information to syslog. +-Check the L<Log::Dispatch> POD for information about how to get things +-by syslog, mail or anything else, get debugging info in the log, etc. +- +-It might generally make sense to send error and higher by email to +-some administrator. If you do this, be careful that this email isn't +-sent to this RT instance. Mail loops will generate a critical log +-message. +- +-=over 4 +- +-=item C<$LogToSyslog>, C<$LogToSTDERR> +- +-The minimum level error that will be logged to the specific device. +-From lowest to highest priority, the levels are: +- +- debug info notice warning error critical alert emergency +- +-Many syslogds are configured to discard or file debug messages away, so +-if you're attempting to debug RT you may need to reconfigure your +-syslogd or use one of the other logging options. +- +-Logging to your screen affects scripts run from the command line as well +-as the STDERR sent to your webserver (so these logs will usually show up +-in your web server's error logs). +- +-=cut +- +-Set($LogToSyslog, "info"); +-Set($LogToSTDERR, "info"); +- +-=item C<$LogToFile>, C<$LogDir>, C<$LogToFileNamed> +- +-Logging to a standalone file is also possible. The file needs to both +-exist and be writable by all direct users of the RT API. This generally +-includes the web server and whoever rt-crontool runs as. Note that +-rt-mailgate and the RT CLI go through the webserver, so their users do +-not need to have write permissions to this file. If you expect to have +-multiple users of the direct API, Best Practical recommends using syslog +-instead of direct file logging. +- +-You should set C<$LogToFile> to one of the levels documented above. +- +-=cut +- +-Set($LogToFile, undef); +-Set($LogDir, q{var/log}); +-Set($LogToFileNamed, "rt.log"); #log to rt.log +- +-=item C<$LogStackTraces> +- +-If set to a log level then logging will include stack traces for +-messages with level equal to or greater than specified. +- +-NOTICE: Stack traces include parameters supplied to functions or +-methods. It is possible for stack trace logging to reveal sensitive +-information such as passwords or ticket content in your logs. +- +-=cut +- +-Set($LogStackTraces, ""); +- +-=item C<@LogToSyslogConf> +- +-Additional options to pass to L<Log::Dispatch::Syslog>; the most +-interesting flags include C<facility>, C<logopt>, and possibly C<ident>. +-See the L<Log::Dispatch::Syslog> documentation for more information. +- +-=cut +- +-Set(@LogToSyslogConf, ()); +- +-=back +- +- +- +-=head1 Incoming mail gateway +- +-=over 4 +- +-=item C<$EmailSubjectTagRegex> +- +-This regexp controls what subject tags RT recognizes as its own. If +-you're not dealing with historical C<$rtname> values, or historical +-queue-specific subject tags, you'll likely never have to change this +-configuration. +- +-Be B<very careful> with it. Note that it overrides C<$rtname> for +-subject token matching. +- +-The setting below would make RT behave exactly as it does without the +-setting enabled. +- +-=cut +- +-# Set($EmailSubjectTagRegex, qr/\Q$rtname\E/i ); +- +-=item C<$OwnerEmail> +- +-C<$OwnerEmail> is the address of a human who manages RT. RT will send +-errors generated by the mail gateway to this address; it will also be +-displayed as the contact person on the RT's login page. Because RT +-sends errors to this address, it should I<not> be an address that's +-managed by your RT instance, to avoid mail loops. +- +-=cut +- +-Set($OwnerEmail, 'root'); +- +-=item C<$LoopsToRTOwner> +- +-If C<$LoopsToRTOwner> is defined, RT will send mail that it believes +-might be a loop to C<$OwnerEmail>. +- +-=cut +- +-Set($LoopsToRTOwner, 1); +- +-=item C<$StoreLoops> +- +-If C<$StoreLoops> is defined, RT will record messages that it believes +-to be part of mail loops. As it does this, it will try to be careful +-not to send mail to the sender of these messages. +- +-=cut +- +-Set($StoreLoops, undef); +- +-=item C<$MaxAttachmentSize> +- +-C<$MaxAttachmentSize> sets the maximum size (in bytes) of attachments +-stored in the database. This setting is irrelevant unless one of +-$TruncateLongAttachments or $DropLongAttachments (below) are set, B<OR> +-the database is stored in Oracle. On Oracle, attachments larger than +-this can be fully stored, but will be truncated to this length when +-read. +- +-=cut +- +-Set($MaxAttachmentSize, 10_000_000); # 10M +- +-=item C<$TruncateLongAttachments> +- +-If this is set to a non-undef value, RT will truncate attachments +-longer than C<$MaxAttachmentSize>. +- +-=cut +- +-Set($TruncateLongAttachments, undef); +- +-=item C<$DropLongAttachments> +- +-If this is set to a non-undef value, RT will silently drop attachments +-longer than C<MaxAttachmentSize>. C<$TruncateLongAttachments>, above, +-takes priority over this. +- +-=cut +- +-Set($DropLongAttachments, undef); +- +-=item C<$RTAddressRegexp> +- +-C<$RTAddressRegexp> is used to make sure RT doesn't add itself as a +-ticket CC if C<$ParseNewMessageForTicketCcs>, above, is enabled. It +-is important that you set this to a regular expression that matches +-all addresses used by your RT. This lets RT avoid sending mail to +-itself. It will also hide RT addresses from the list of "One-time Cc" +-and Bcc lists on ticket reply. +- +-If you have a number of addresses configured in your RT database +-already, you can generate a naive first pass regexp by using: +- +- perl etc/upgrade/generate-rtaddressregexp +- +-If left blank, RT will compare each address to your configured +-C<$CorrespondAddress> and C<$CommentAddress> before searching for a +-Queue configured with a matching "Reply Address" or "Comment Address" +-on the Queue Admin page. +- +-=cut +- +-Set($RTAddressRegexp, undef); +- +-=item C<$CanonicalizeEmailAddressMatch>, C<$CanonicalizeEmailAddressReplace> +- +-RT provides functionality which allows the system to rewrite incoming +-email addresses, using L<RT::User/CanonicalizeEmailAddress>. The +-default implementation replaces all occurrences of the regular +-expression in C<CanonicalizeEmailAddressMatch> with +-C<CanonicalizeEmailAddressReplace>, via C<s/$Match/$Replace/gi>. The +-most common use of this is to replace C<@something.example.com> with +-C<@example.com>. If more complex noramlization is required, +-L<RT::User/CanonicalizeEmailAddress> can be overridden to provide it. +- +-=cut +- +-# Set($CanonicalizeEmailAddressMatch, '@subdomain\.example\.com$'); +-# Set($CanonicalizeEmailAddressReplace, '@example.com'); +- +-=item C<$ValidateUserEmailAddresses> +- +-By default C<$ValidateUserEmailAddresses> is 1, and RT will refuse to create +-users with an invalid email address (as specified in RFC 2822) or with +-an email address made of multiple email addresses. +- +-Set this to 0 to skip any email address validation. Doing so may open up +-vulnerabilities. +- +-=cut +- +-Set($ValidateUserEmailAddresses, 1); +- +-=item C<@MailPlugins> +- +-C<@MailPlugins> is a list of authentication plugins for +-L<RT::Interface::Email> to use; see L<rt-mailgate> +- +-=cut +- +-=item C<$UnsafeEmailCommands> +- +-C<$UnsafeEmailCommands>, if set to 1, enables 'take' and 'resolve' +-as possible actions via the mail gateway. As its name implies, this +-is very unsafe, as it allows email with a forged sender to possibly +-resolve arbitrary tickets! +- +-=cut +- +-=item C<$ExtractSubjectTagMatch>, C<$ExtractSubjectTagNoMatch> +- +-The default "extract remote tracking tags" scrip settings; these +-detect when your RT is talking to another RT, and adjust the subject +-accordingly. +- +-=cut +- +-Set($ExtractSubjectTagMatch, qr/\[[^\]]+? #\d+\]/); +-Set($ExtractSubjectTagNoMatch, ( ${RT::EmailSubjectTagRegex} +- ? qr/\[(?:${RT::EmailSubjectTagRegex}) #\d+\]/ +- : qr/\[\Q$RT::rtname\E #\d+\]/)); +- +-=item C<$CheckMoreMSMailHeaders> +- +-Some email clients create a plain text version of HTML-formatted +-email to help other clients that read only plain text. +-Unfortunately, the plain text parts sometimes end up with +-doubled newlines and these can then end up in RT. This +-is most often seen in MS Outlook. +- +-Enable this option to have RT check for additional mail headers +-and attempt to identify email from MS Outlook. When detected, +-RT will then clean up double newlines. Note that it may +-clean up intentional double newlines as well. +- +-=cut +- +-Set( $CheckMoreMSMailHeaders, 0); +- +-=back +- +- +- +-=head1 Outgoing mail +- +-=over 4 +- +-=item C<$MailCommand> +- +-C<$MailCommand> defines which method RT will use to try to send mail. +-We know that 'sendmailpipe' works fairly well. If 'sendmailpipe' +-doesn't work well for you, try 'sendmail'. 'qmail' is also a supported +-value. +- +-For testing purposes, or to simply disable sending mail out into the +-world, you can set C<$MailCommand> to 'testfile' which writes all mail +-to a temporary file. RT will log the location of the temporary file +-so you can extract mail from it afterward. +- +-On shutdown, RT will clean up the temporary file created when using +-the 'testfile' option. If testing while the RT server is still running, +-you can find the files in the location noted in the log file. If you run +-a tool like C<rt-crontool> however, or if you look after stopping the server, +-the files will have been deleted when the process completed. If you need to +-keep the files for development or debugging, you can manually set +-C<< UNLINK => 0 >> where the testfile config is processed in +-F<lib/RT/Interface/Email.pm>. +- +-=cut +- +-Set($MailCommand, "sendmailpipe"); +- +-=item C<$SetOutgoingMailFrom> +- +-C<$SetOutgoingMailFrom> tells RT to set the sender envelope to the +-Correspond mail address of the ticket's queue. +- +-Warning: If you use this setting, bounced mails will appear to be +-incoming mail to the system, thus creating new tickets. +- +-If the value contains an C<@>, it is assumed to be an email address and used as +-a global envelope sender. Expected usage in this case is to simply set the +-same envelope sender on all mail from RT, without defining +-C<$OverrideOutgoingMailFrom>. If you do define C<$OverrideOutgoingMailFrom>, +-anything specified there overrides the global value (including Default). +- +-This option only works if C<$MailCommand> is set to 'sendmailpipe'. +- +-=cut +- +-Set($SetOutgoingMailFrom, 0); +- +-=item C<$OverrideOutgoingMailFrom> +- +-C<$OverrideOutgoingMailFrom> is used for overwriting the Correspond +-address of the queue as it is handed to sendmail -f. This helps force +-the From_ header away from www-data or other email addresses that show +-up in the "Sent by" line in Outlook. +- +-The option is a hash reference of queue id/name to email address. If +-there is no ticket involved, then the value of the C<Default> key will +-be used. +- +-This option only works if C<$SetOutgoingMailFrom> is enabled and +-C<$MailCommand> is set to 'sendmailpipe'. +- +-=cut +- +-Set($OverrideOutgoingMailFrom, { +-# 'Default' => 'admin@xxxxxxxxxxxxxx', +-# 'General' => 'general@xxxxxxxxxxxxxx', +-}); +- +-=item C<$DefaultMailPrecedence> +- +-C<$DefaultMailPrecedence> is used to control the default Precedence +-level of outgoing mail where none is specified. By default it is +-C<bulk>, but if you only send mail to your staff, you may wish to +-change it. +- +-Note that you can set the precedence of individual templates by +-including an explicit Precedence header. +- +-If you set this value to C<undef> then we do not set a default +-Precedence header to outgoing mail. However, if there already is a +-Precedence header, it will be preserved. +- +-=cut +- +-Set($DefaultMailPrecedence, "bulk"); +- +-=item C<$DefaultErrorMailPrecedence> +- +-C<$DefaultErrorMailPrecedence> is used to control the default +-Precedence level of outgoing mail that indicates some kind of error +-condition. By default it is C<bulk>, but if you only send mail to your +-staff, you may wish to change it. +- +-If you set this value to C<undef> then we do not add a Precedence +-header to error mail. +- +-=cut +- +-Set($DefaultErrorMailPrecedence, "bulk"); +- +-=item C<$UseOriginatorHeader> +- +-C<$UseOriginatorHeader> is used to control the insertion of an +-RT-Originator Header in every outgoing mail, containing the mail +-address of the transaction creator. +- +-=cut +- +-Set($UseOriginatorHeader, 1); +- +-=item C<$UseFriendlyFromLine> +- +-By default, RT sets the outgoing mail's "From:" header to "SenderName +-via RT". Setting C<$UseFriendlyFromLine> to 0 disables it. +- +-=cut +- +-Set($UseFriendlyFromLine, 1); +- +-=item C<$FriendlyFromLineFormat> +- +-C<sprintf()> format of the friendly 'From:' header; its arguments are +-SenderName and SenderEmailAddress. +- +-=cut +- +-Set($FriendlyFromLineFormat, "\"%s via RT\" <%s>"); +- +-=item C<$UseFriendlyToLine> +- +-RT can optionally set a "Friendly" 'To:' header when sending messages +-to Ccs or AdminCcs (rather than having a blank 'To:' header. +- +-This feature DOES NOT WORK WITH SENDMAIL[tm] BRAND SENDMAIL. If you +-are using sendmail, rather than postfix, qmail, exim or some other +-MTA, you _must_ disable this option. +- +-=cut +- +-Set($UseFriendlyToLine, 0); +- +-=item C<$FriendlyToLineFormat> +- +-C<sprintf()> format of the friendly 'To:' header; its arguments are +-WatcherType and TicketId. +- +-=cut +- +-Set($FriendlyToLineFormat, "\"%s of ". RT->Config->Get('rtname') ." Ticket #%s\":;"); +- +-=item C<$NotifyActor> +- +-By default, RT doesn't notify the person who performs an update, as +-they already know what they've done. If you'd like to change this +-behavior, Set C<$NotifyActor> to 1 +- +-=cut +- +-Set($NotifyActor, 0); +- +-=item C<$RecordOutgoingEmail> +- +-By default, RT records each message it sends out to its own internal +-database. To change this behavior, set C<$RecordOutgoingEmail> to 0 +- +-If this is disabled, users' digest mail delivery preferences +-(i.e. EmailFrequency) will also be ignored. +- +-=cut +- +-Set($RecordOutgoingEmail, 1); +- +-=item C<$VERPPrefix>, C<$VERPDomain> +- +-Setting these options enables VERP support +-L<http://cr.yp.to/proto/verp.txt>. +- +-Uncomment the following two directives to generate envelope senders +-of the form C<${VERPPrefix}${originaladdress}@${VERPDomain}> +-(i.e. rt-jesse=fsck.com@xxxxxxxxxxxxxx ). +- +-This currently only works with sendmail and sendmailpipe. +- +-=cut +- +-# Set($VERPPrefix, "rt-"); +-# Set($VERPDomain, $RT::Organization); +- +- +-=item C<$ForwardFromUser> +- +-By default, RT forwards a message using queue's address and adds RT's +-tag into subject of the outgoing message, so recipients' replies go +-into RT as correspondents. +- +-To change this behavior, set C<$ForwardFromUser> to 1 and RT +-will use the address of the current user and remove RT's subject tag. +- +-=cut +- +-Set($ForwardFromUser, 0); +- +-=item C<$HTMLFormatter> +- +-RT's default pure-perl formatter may fail to successfully convert even +-on some relatively simple HTML; this will result in blank C<text/plain> +-parts, which is particuarly unfortunate if HTML templates are not in +-use. +- +-If the optional dependency L<HTML::FormatExternal> is installed, RT will +-use external programs to render HTML to plain text. The default is to +-try, in order, C<w3m>, C<elinks>, C<html2text>, C<links>, C<lynx>, and +-then fall back to the C<core> pure-perl formatter if none are installed. +- +-Set C<$HTMLFormatter> to one of the above programs (or the full path to +-such) to use a different program than the above would choose by default. +-Setting this requires that L<HTML::FormatExternal> be installed. +- +-If the chosen formatter is not in the webserver's $PATH, you may set +-this option the full path to one of the aforementioned executables. +- +-=cut +- +-Set($HTMLFormatter, undef); +- +-=back +- +-=head2 Email dashboards +- +-=over 4 +- +-=item C<$DashboardAddress> +- +-The email address from which RT will send dashboards. If none is set, +-then C<$OwnerEmail> will be used. +- +-=cut +- +-Set($DashboardAddress, ''); +- +-=item C<$DashboardSubject> +- +-Lets you set the subject of dashboards. Arguments are the frequency (Daily, +-Weekly, Monthly) of the dashboard and the dashboard's name. +- +-=cut +- +-Set($DashboardSubject, "%s Dashboard: %s"); +- +-=item C<@EmailDashboardRemove> +- +-A list of regular expressions that will be used to remove content from +-mailed dashboards. +- +-=cut +- +-Set(@EmailDashboardRemove, ()); +- +-=back +- +- +- +-=head2 Sendmail configuration +- +-These options only take effect if C<$MailCommand> is 'sendmail' or +-'sendmailpipe' +- +-=over 4 +- +-=item C<$SendmailArguments> +- +-C<$SendmailArguments> defines what flags to pass to C<$SendmailPath> +-These options are good for most sendmail wrappers and work-a-likes. +- +-These arguments are good for sendmail brand sendmail 8 and newer: +-C<Set($SendmailArguments,"-oi -ODeliveryMode=b -OErrorMode=m");> +- +-=cut +- +-Set($SendmailArguments, "-oi"); +- +- +-=item C<$SendmailBounceArguments> +- +-C<$SendmailBounceArguments> defines what flags to pass to C<$Sendmail> +-assuming RT needs to send an error (i.e. bounce). +- +-=cut +- +-Set($SendmailBounceArguments, '-f "<>"'); +- +-=item C<$SendmailPath> +- +-If you selected 'sendmailpipe' above, you MUST specify the path to +-your sendmail binary in C<$SendmailPath>. +- +-=cut +- +-Set($SendmailPath, "/usr/sbin/sendmail"); +- +- +-=back +- +-=head2 Other mailers +- +-=over 4 +- +-=item C<@MailParams> +- +-C<@MailParams> defines a list of options passed to $MailCommand if it +-is not 'sendmailpipe' or 'sendmail'; +- +-=cut +- +-Set(@MailParams, ()); +- +-=back +- +- +-=head1 Web interface +- +-=over 4 +- +-=item C<$WebDefaultStylesheet> +- +-This determines the default stylesheet the RT web interface will use. +-RT ships with several themes by default: +- +- rudder The default theme for RT 4.2 +- aileron The default layout for RT 4.0 +- web2 The default layout for RT 3.8 +- ballard Theme which doesn't rely on JavaScript for menuing +- +-This value actually specifies a directory in F<share/static/css/> +-from which RT will try to load the file main.css (which should @import +-any other files the stylesheet needs). This allows you to easily and +-cleanly create your own stylesheets to apply to RT. This option can +-be overridden by users in their preferences. +- +-=cut +- +-Set($WebDefaultStylesheet, "rudder"); +- +-=item C<$DefaultQueue> +- +-Use this to select the default queue name that will be used for +-creating new tickets. You may use either the queue's name or its +-ID. This only affects the queue selection boxes on the web interface. +- +-=cut +- +-# Set($DefaultQueue, "General"); +- +-=item C<$RememberDefaultQueue> +- +-When a queue is selected in the new ticket dropdown, make it the new +-default for the new ticket dropdown. +- +-=cut +- +-# Set($RememberDefaultQueue, 1); +- +-=item C<$EnableReminders> +- +-Hide all links and portlets related to Reminders by setting this to 0 +- +-=cut +- +-Set($EnableReminders, 1); +- +-=item C<@CustomFieldValuesSources> +- +-Set C<@CustomFieldValuesSources> to a list of class names which extend +-L<RT::CustomFieldValues::External>. This can be used to pull lists of +-custom field values from external sources at runtime. +- +-=cut +- +-Set(@CustomFieldValuesSources, ()); +- +-=item C<%CustomFieldGroupings> +- +-This option affects the display of ticket and user custom fields in the +-web interface. It does not address the sorting of custom fields within +-the groupings; which is controlled by the Ticket Custom Fields tab in +-Queue Configuration in the Admin UI. +- +-A nested datastructure defines how to group together custom fields +-under a mix of built-in and arbitrary headings ("groupings"). +- +-Set C<%CustomFieldGroupings> to a nested structure similar to the following: +- +- Set(%CustomFieldGroupings, +- 'RT::Ticket' => [ +- 'Grouping Name' => ['CF Name', 'Another CF'], +- 'Another Grouping' => ['Some CF'], +- 'Dates' => ['Shipped date'], +- ], +- 'RT::User' => [ +- 'Phones' => ['Fax number'], +- ], +- ); +- +-The first level keys are record types for which CFs may be used, and the +-values are either hashrefs or arrayrefs -- if arrayrefs, then the +-ordering is preserved during display, otherwise groupings are displayed +-alphabetically. The second level keys are the grouping names and the +-values are array refs containing a list of CF names. +- +-There are several special built-in groupings which RT displays in +-specific places (usually the collapsible box of the same title). The +-ordering of these standard groupings cannot be modified. You may also +-only append Custom Fields to the list in these boxes, not reorder or +-remove core fields. +- +-For C<RT::Ticket>, these groupings are: C<Basics>, C<Dates>, C<Links>, C<People> +- +-For C<RT::User>: C<Identity>, C<Access control>, C<Location>, C<Phones> +- +-Extensions may also add their own built-in groupings, refer to the individual +-extension documentation for those. +- +-=item C<$CanonicalizeRedirectURLs> +- +-Set C<$CanonicalizeRedirectURLs> to 1 to use C<$WebURL> when +-redirecting rather than the one we get from C<%ENV>. +- +-Apache's UseCanonicalName directive changes the hostname that RT +-finds in C<%ENV>. You can read more about what turning it On or Off +-means in the documentation for your version of Apache. +- +-If you use RT behind a reverse proxy, you almost certainly want to +-enable this option. +- +-=cut +- +-Set($CanonicalizeRedirectURLs, 0); +- +-=item C<$CanonicalizeURLsInFeeds> +- +-Set C<$CanonicalizeURLsInFeeds> to 1 to use C<$WebURL> in feeds +-rather than the one we get from request. +- +-If you use RT behind a reverse proxy, you almost certainly want to +-enable this option. +- +-=cut +- +-Set($CanonicalizeURLsInFeeds, 0); +- +-=item C<@JSFiles> +- +-A list of additional JavaScript files to be included in head. +- +-=cut +- +-Set(@JSFiles, qw//); +- +-=item C<$JSMinPath> +- +-Path to the jsmin binary; if specified, it will be used to minify +-C<JSFiles>. The default, and the fallback if the binary cannot be +-found, is to simply concatenate the files. +- +-jsmin can be installed by running 'make jsmin' from the RT install +-directory, or from http://www.crockford.com/javascript/jsmin.html +- +-=cut +- +-# Set($JSMinPath, "/path/to/jsmin"); +- +-=item C<@CSSFiles> +- +-A list of additional CSS files to be included in head. +- +-If you're a plugin author, refer to RT->AddStyleSheets. +- +-=cut +- +-Set(@CSSFiles, qw//); +- +-=item C<$UsernameFormat> +- +-This determines how user info is displayed. 'concise' will show the +-first of RealName, Name or EmailAddress that has a value. 'verbose' will +-show EmailAddress, and the first of RealName or Name which is defined. +-The default, 'role', uses 'verbose' for unprivileged users, and the Name +-followed by the RealName for privileged users. +- +-=cut +- +-Set($UsernameFormat, "role"); +- +-=item C<$UserSearchResultFormat> +- +-This controls the display of lists of users returned from the User +-Summary Search. The display of users in the Admin interface is +-controlled by C<%AdminSearchResultFormat>. +- +-=cut +- +-Set($UserSearchResultFormat, +- q{ '<a href="__WebPath__/User/Summary.html?id=__id__">__id__</a>/TITLE:#'} +- .q{,'<a href="__WebPath__/User/Summary.html?id=__id__">__Name__</a>/TITLE:Name'} +- .q{,__RealName__, __EmailAddress__} +-); +- +-=item C<@UserSummaryPortlets> +- +-A list of portlets to be displayed on the User Summary page. +-By default, we show all of the available portlets. +-Extensions may provide their own portlets for this page. +- +-=cut +- +-Set(@UserSummaryPortlets, (qw/ExtraInfo CreateTicket ActiveTickets InactiveTickets/)); +- +-=item C<$UserSummaryExtraInfo> +- +-This controls what information is displayed on the User Summary +-portal. By default the user's Real Name, Email Address and Username +-are displayed. You can remove these or add more as needed. This +-expects a Format string of user attributes. Please note that not all +-the attributes are supported in this display because we're not +-building a table. +- +-=cut +- +-Set($UserSummaryExtraInfo, "RealName, EmailAddress, Name"); +- +-=item C<$UserSummaryTicketListFormat> +- +-Control the appearance of the Active and Inactive ticket lists in the +-User Summary. +- +-=cut +- +-Set($UserSummaryTicketListFormat, q{ +- '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__id__</a></B>/TITLE:#', +- '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject', +- Status, +- QueueName, +- Owner, +- Priority, +- '__NEWLINE__', +- '', +- '<small>__Requestors__</small>', +- '<small>__CreatedRelative__</small>', +- '<small>__ToldRelative__</small>', +- '<small>__LastUpdatedRelative__</small>', +- '<small>__TimeLeft__</small>' +-}); +- +-=item C<$WebBaseURL>, C<$WebURL> +- +-Usually you don't want to set these options. The only obvious reason +-is if RT is accessible via https protocol on a non standard port, e.g. +-'https://rt.example.com:9999'. In all other cases these options are +-computed using C<$WebDomain>, C<$WebPort> and C<$WebPath>. +- +-C<$WebBaseURL> is the scheme, server and port +-(e.g. 'http://rt.example.com') for constructing URLs to the web +-UI. C<$WebBaseURL> doesn't need a trailing /. +- +-C<$WebURL> is the C<$WebBaseURL>, C<$WebPath> and trailing /, for +-example: 'http://www.example.com/rt/'. +- +-=cut +- +-my $port = RT->Config->Get('WebPort'); +-Set($WebBaseURL, +- ($port == 443? 'https': 'http') .'://' +- . RT->Config->Get('WebDomain') +- . ($port != 80 && $port != 443? ":$port" : '') +-); +- +-Set($WebURL, RT->Config->Get('WebBaseURL') . RT->Config->Get('WebPath') . "/"); +- +-=item C<$WebImagesURL> +- +-C<$WebImagesURL> points to the base URL where RT can find its images. +-Define the directory name to be used for images in RT web documents. +- +-=cut +- +-Set($WebImagesURL, RT->Config->Get('WebPath') . "/static/images/"); +- +-=item C<$LogoURL> +- +-C<$LogoURL> points to the URL of the RT logo displayed in the web UI. +-This can also be configured via the web UI. +- +-=cut +- +-Set($LogoURL, RT->Config->Get('WebImagesURL') . "bpslogo.png"); +- +-=item C<$LogoLinkURL> +- +-C<$LogoLinkURL> is the URL that the RT logo hyperlinks to. +- +-=cut +- +-Set($LogoLinkURL, "http://bestpractical.com"); +- +-=item C<$LogoAltText> +- +-C<$LogoAltText> is a string of text for the alt-text of the logo. It +-will be passed through C<loc> for localization. +- +-=cut +- +-Set($LogoAltText, "Best Practical Solutions, LLC corporate logo"); +- +-=item C<$WebNoAuthRegex> +- +-What portion of RT's URL space should not require authentication. The +-default is almost certainly correct, and should only be changed if you +-are extending RT. +- +-=cut +- +-Set($WebNoAuthRegex, qr{^ (?:/+NoAuth/ | /+REST/\d+\.\d+/NoAuth/) }x ); +- +-=item C<$SelfServiceRegex> +- +-What portion of RT's URLspace should be accessible to Unprivileged +-users This does not override the redirect from F</Ticket/Display.html> +-to F</SelfService/Display.html> when Unprivileged users attempt to +-access ticked displays. +- +-=cut +- +-Set($SelfServiceRegex, qr!^(?:/+SelfService/)!x ); +- +-=item C<$WebFlushDbCacheEveryRequest> +- +-By default, RT clears its database cache after every page view. This +-ensures that you've always got the most current information when +-working in a multi-process (mod_perl or FastCGI) Environment. Setting +-C<$WebFlushDbCacheEveryRequest> to 0 will turn this off, which will +-speed RT up a bit, at the expense of a tiny bit of data accuracy. +- +-=cut +- +-Set($WebFlushDbCacheEveryRequest, 1); +- +-=item C<%ChartFont> +- +-The L<GD> module (which RT uses for graphs) ships with a built-in font +-that doesn't have full Unicode support. You can use a given TrueType +-font for a specific language by setting %ChartFont to (language =E<gt> +-the absolute path of a font) pairs. Your GD library must have support +-for TrueType fonts to use this option. If there is no entry for a +-language in the hash then font with 'others' key is used. +- +-RT comes with two TrueType fonts covering most available languages. +- +-=cut +- +-Set( +- %ChartFont, +- 'zh-cn' => "$RT::BasePath/share/fonts/DroidSansFallback.ttf", +- 'zh-tw' => "$RT::BasePath/share/fonts/DroidSansFallback.ttf", +- 'ja' => "$RT::BasePath/share/fonts/DroidSansFallback.ttf", +- 'others' => "$RT::BasePath/share/fonts/DroidSans.ttf", +-); +- +-=item C<$ChartsTimezonesInDB> +- +-RT stores dates using the UTC timezone in the DB, so charts grouped by +-dates and time are not representative. Set C<$ChartsTimezonesInDB> to 1 +-to enable timezone conversions using your DB's capabilities. You may +-need to do some work on the DB side to use this feature, read more in +-F<docs/customizing/timezones_in_charts.pod>. +- +-At this time, this feature only applies to MySQL and PostgreSQL. +- +-=cut +- +-Set($ChartsTimezonesInDB, 0); +- +-=item C<@ChartColors> +- +-An array of 6-digit hexadecimal RGB color values used for chart series. By +-default there are 12 distinct colors. +- +-=cut +- +-Set(@ChartColors, qw( +- 66cc66 ff6666 ffcc66 663399 +- 3333cc 339933 993333 996633 +- 33cc33 cc3333 cc9933 6633cc +-)); +- +-=back +- +- +- +-=head2 Home page +- +-=over 4 +- +-=item C<$DefaultSummaryRows> +- +-C<$DefaultSummaryRows> is default number of rows displayed in for +-search results on the front page. +- +-=cut +- +-Set($DefaultSummaryRows, 10); +- +-=item C<$HomePageRefreshInterval> +- +-C<$HomePageRefreshInterval> is default number of seconds to refresh +-the RT home page. Choose from [0, 120, 300, 600, 1200, 3600, 7200]. +- +-=cut +- +-Set($HomePageRefreshInterval, 0); +- +-=item C<$HomepageComponents> +- +-C<$HomepageComponents> is an arrayref of allowed components on a +-user's customized homepage ("RT at a glance"). +- +-=cut +- +-Set( +- $HomepageComponents, +- [ +- qw(QuickCreate Quicksearch MyAdminQueues MySupportQueues MyReminders RefreshHomepage Dashboards SavedSearches FindUser) # loc_qw +- ] +-); +- +-=back +- +- +- +- +-=head2 Ticket search +- +-=over 4 +- +-=item C<$UseSQLForACLChecks> +- +-Historically, ACLs were checked on display, which could lead to empty +-search pages and wrong ticket counts. Set C<$UseSQLForACLChecks> to 0 +-to go back to this method; this will reduce the complexity of the +-generated SQL statements, at the cost of the aforementioned bugs. +- +-=cut +- +-Set($UseSQLForACLChecks, 1); +- +-=item C<$TicketsItemMapSize> +- +-On the display page of a ticket from search results, RT provides links +-to the first, next, previous and last ticket from the results. In +-order to build these links, RT needs to fetch the full result set from +-the database, which can be resource-intensive. +- +-Set C<$TicketsItemMapSize> to number of tickets you want RT to examine +-to build these links. If the full result set is larger than this +-number, RT will omit the "last" link in the menu. Set this to zero to +-always examine all results. +- +-=cut +- +-Set($TicketsItemMapSize, 1000); +- +-=item C<$SearchResultsRefreshInterval> +- +-C<$SearchResultsRefreshInterval> is default number of seconds to +-refresh search results in RT. Choose from [0, 120, 300, 600, 1200, +-3600, 7200]. +- +-=cut +- +-Set($SearchResultsRefreshInterval, 0); +- +-=item C<$DefaultSearchResultFormat> +- +-C<$DefaultSearchResultFormat> is the default format for RT search +-results +- +-=cut +- +-Set ($DefaultSearchResultFormat, qq{ +- '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__id__</a></B>/TITLE:#', +- '<B><A HREF="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject', +- Status, +- QueueName, +- Owner, +- Priority, +- '__NEWLINE__', +- '__NBSP__', +- '<small>__Requestors__</small>', +- '<small>__CreatedRelative__</small>', +- '<small>__ToldRelative__</small>', +- '<small>__LastUpdatedRelative__</small>', +- '<small>__TimeLeft__</small>'}); +- +-=item C<$DefaultSearchResultOrderBy> +- +-What Tickets column should we order by for RT Ticket search results. +- +-=cut +- +-Set($DefaultSearchResultOrderBy, 'id'); +- +-=item C<$DefaultSearchResultOrder> +- +-When ordering RT Ticket search results by C<$DefaultSearchResultOrderBy>, +-should the sort be ascending (ASC) or descending (DESC). +- +-=cut +- +-Set($DefaultSearchResultOrder, 'ASC'); +- +-=item C<$DefaultSelfServiceSearchResultFormat> +- +-C<$DefaultSelfServiceSearchResultFormat> is the default format of +-searches displayed in the SelfService interface. +- +-=cut +- +-Set($DefaultSelfServiceSearchResultFormat, qq{ +- '<B><A HREF="__WebPath__/SelfService/Display.html?id=__id__">__id__</a></B>/TITLE:#', +- '<B><A HREF="__WebPath__/SelfService/Display.html?id=__id__">__Subject__</a></B>/TITLE:Subject', +- Status, +- Requestors, +- Owner}); +- +-=item C<%FullTextSearch> +- +-Full text search (FTS) without database indexing is a very slow +-operation, and is thus disabled by default. +- +-Before setting C<Indexed> to 1, read F<docs/full_text_indexing.pod> for +-the full details of FTS on your particular database. +- +-It is possible to enable FTS without database indexing support, simply +-by setting the C<Enable> key to 1, while leaving C<Indexed> set to 0. +-This is not generally suggested, as unindexed full-text searching can +-cause severe performance problems. +- +-=cut +- +-Set(%FullTextSearch, +- Enable => 0, +- Indexed => 0, +-); +- +-=item C<$DontSearchFileAttachments> +- +-If C<$DontSearchFileAttachments> is set to 1, then uploaded files +-(attachments with file names) are not searched during content +-search. +- +-Note that if you use indexed FTS then named attachments are still +-indexed by default regardless of this option. +- +-=cut +- +-Set($DontSearchFileAttachments, undef); +- +-=item C<$OnlySearchActiveTicketsInSimpleSearch> +- +-When query in simple search doesn't have status info, use this to only +-search active ones. +- +-=cut +- +-Set($OnlySearchActiveTicketsInSimpleSearch, 1); +- +-=item C<$SearchResultsAutoRedirect> +- +-When only one ticket is found in search, use this to redirect to the +-ticket display page automatically. +- +-=cut +- +-Set($SearchResultsAutoRedirect, 0); +- +-=back +- +- +- +-=head2 Ticket display +- +-=over 4 +- +-=item C<$ShowMoreAboutPrivilegedUsers> +- +-This determines if the 'More about requestor' box on +-Ticket/Display.html is shown for Privileged Users. +- +-=cut +- +-Set($ShowMoreAboutPrivilegedUsers, 0); +- +-=item C<$MoreAboutRequestorTicketList> +- +-This can be set to Active, Inactive, All or None. It controls what +-ticket list will be displayed in the 'More about requestor' box on +-Ticket/Display.html. This option can be controlled by users also. +- +-=cut +- +-Set($MoreAboutRequestorTicketList, "Active"); +- +-=item C<$MoreAboutRequestorTicketListFormat> +- +-Control the appearance of the ticket lists in the 'More About Requestors' box. +- +-=cut +- +-Set($MoreAboutRequestorTicketListFormat, q{ +- '<a href="__WebPath__/Ticket/Display.html?id=__id__">__id__</a>', +- '__Owner__', +- '<a href="__WebPath__/Ticket/Display.html?id=__id__">__Subject__</a>', +- '__Status__', +-}); +- +- +-=item C<$MoreAboutRequestorExtraInfo> +- +-By default, the 'More about requestor' box on Ticket/Display.html +-shows the Requestor's name and ticket list. If you would like to see +-extra information about the user, this expects a Format string of user +-attributes. Please note that not all the attributes are supported in +-this display because we're not building a table. +- +-Example: +-C<Set($MoreAboutRequestorExtraInfo,"Organization, Address1")> +- +-=cut +- +-Set($MoreAboutRequestorExtraInfo, ""); +- +-=item C<$MoreAboutRequestorGroupsLimit> +- +-By default, the 'More about requestor' box on Ticket/Display.html +-shows all the groups of the Requestor. Use this to limit the number +-of groups; a value of undef removes the group display entirely. +- +-=cut +- +-Set($MoreAboutRequestorGroupsLimit, 0); +- +-=item C<$UseSideBySideLayout> +- +-Should the ticket create and update forms use a more space efficient +-two column layout. This layout may not work in narrow browsers if you +-set a MessageBoxWidth (below). +- +-=cut +- +-Set($UseSideBySideLayout, 1); +- +-=item C<$EditCustomFieldsSingleColumn> +- +-When displaying a list of Ticket Custom Fields for editing, RT +-defaults to a 2 column list. If you set this to 1, it will instead +-display the Custom Fields in a single column. +- +-=cut +- +-Set($EditCustomFieldsSingleColumn, 0); +- +-=item C<$ShowUnreadMessageNotifications> +- +-If set to 1, RT will prompt users when there are new, +-unread messages on tickets they are viewing. +- +-=cut +- +-Set($ShowUnreadMessageNotifications, 0); +- +-=item C<$AutocompleteOwners> +- +-If set to 1, the owner drop-downs for ticket update/modify and the query +-builder are replaced by text fields that autocomplete. This can +-alleviate the sometimes huge owner list for installations where many +-users have the OwnTicket right. +- +-Autocompleter is automatically turned on if list contains more than +-50 users, but penalty of executing potentially slow query is still paid. +- +-Drop down doesn't show unprivileged users. If your setup allows unprivileged +-to own ticket then you have to enable autocompleting. +- +-=cut +- +-Set($AutocompleteOwners, 0); +- +-=item C<$AutocompleteOwnersForSearch> +- +-If set to 1, the owner drop-downs for the query builder are always +-replaced by text field that autocomplete and C<$AutocompleteOwners> +-is ignored. Helpful when owners list is huge in the query builder. +- +-=cut +- +-Set($AutocompleteOwnersForSearch, 0); +- +-=item C<$UserSearchFields> +- +-Used by the User Autocompleter as well as the User Search. +- +-Specifies which fields of L<RT::User> to match against and how to match +-each field when autocompleting users. Valid match methods are LIKE, +-STARTSWITH, ENDSWITH, =, and !=. Valid search fields are the core User +-fields, as well as custom fields, which are specified as "CF.1234" or +-"CF.Name" +- +-=cut +- +-Set($UserSearchFields, { +- EmailAddress => 'STARTSWITH', +- Name => 'STARTSWITH', +- RealName => 'LIKE', +-}); +- +-=item C<$AllowUserAutocompleteForUnprivileged> +- +-Should unprivileged users (users of SelfService) be allowed to +-autocomplete users. Setting this option to 1 means unprivileged users +-will be able to search all your users. +- +-=cut +- +-Set($AllowUserAutocompleteForUnprivileged, 0); +- +-=item C<$TicketAutocompleteFields> +- +-Specifies which fields of L<RT::Ticket> to match against and how to match each +-field when autocompleting users. Valid match methods are LIKE, STARTSWITH, +-ENDSWITH, C<=>, and C<!=>. +- +-Not all Ticket fields are publically accessible and hence won't work for +-autocomplete unless you override their accessibility using a local overlay or a +-plugin. Out of the box the following fields are public: id, Subject. +- +-=cut +- +-Set( $TicketAutocompleteFields, { +- id => 'STARTSWITH', +- Subject => 'LIKE', +-}); +- +-=item C<$DisplayTicketAfterQuickCreate> +- +-Enable this to redirect to the created ticket display page +-automatically when using QuickCreate. +- +-=cut +- +-Set($DisplayTicketAfterQuickCreate, 0); +- +-=item C<$WikiImplicitLinks> +- +-Support implicit links in WikiText custom fields? Setting this to 1 +-causes InterCapped or ALLCAPS words in WikiText fields to automatically +-become links to searches for those words. If used on Articles, it links +-to the Article with that name. +- +-=cut +- +-Set($WikiImplicitLinks, 0); +- +-=item C<$PreviewScripMessages> +- +-Set C<$PreviewScripMessages> to 1 if the scrips preview on the ticket +-reply page should include the content of the messages to be sent. +- +-=cut +- +-Set($PreviewScripMessages, 0); +- +-=item C<$SimplifiedRecipients> +- +-If C<$SimplifiedRecipients> is set, a simple list of who will receive +-B<any> kind of mail will be shown on the ticket reply page, instead of a +-detailed breakdown by scrip. +- +-=cut +- +-Set($SimplifiedRecipients, 0); +- +-=item C<$HideResolveActionsWithDependencies> +- +-If set to 1, this option will skip ticket menu actions which can't be +-completed successfully because of outstanding active Depends On tickets. +- +-By default, all ticket actions are displayed in the menu even if some of +-them can't be successful until all Depends On links are resolved or +-transitioned to another inactive status. +- +-=cut +- +-Set($HideResolveActionsWithDependencies, 0); +- +-=back +- +- +- +-=head2 Articles +- +-=over 4 +- +-=item C<$ArticleOnTicketCreate> +- +-Set this to 1 to display the Articles interface on the Ticket Create +-page in addition to the Reply/Comment page. +- +-=cut +- +-Set($ArticleOnTicketCreate, 0); +- +-=item C<$HideArticleSearchOnReplyCreate> +- +-Set this to 1 to hide the search and include boxes from the Article +-UI. This assumes you have enabled Article Hotlist feature, otherwise +-you will have no access to Articles. +- +-=cut +- +-Set($HideArticleSearchOnReplyCreate, 0); +- +-=back +- +- +- +-=head2 Message box properties +- +-=over 4 +- +-=item C<$MessageBoxWidth>, C<$MessageBoxHeight> +- +-For message boxes, set the entry box width, height and what type of +-wrapping to use. These options can be overridden by users in their +-preferences. +- +-When the width is set to undef, no column count is specified and the +-message box will take up 100% of the available width. Combining this +-with HARD messagebox wrapping (below) is not recommended, as it will +-lead to inconsistent width in transactions between browsers. +- +-These settings only apply to the non-RichText message box. See below +-for Rich Text settings. +- +-=cut +- +-Set($MessageBoxWidth, undef); +-Set($MessageBoxHeight, 15); +- +-=item C<$MessageBoxRichText> +- +-Should "rich text" editing be enabled? This option lets your users +-send HTML email messages from the web interface. +- +-=cut +- +-Set($MessageBoxRichText, 1); +- +-=item C<$MessageBoxRichTextHeight> +- +-Height of rich text JavaScript enabled editing boxes (in pixels) +- +-=cut +- +-Set($MessageBoxRichTextHeight, 200); +- +-=item C<$MessageBoxIncludeSignature> +- +-Should your users' signatures (from their Preferences page) be +-included in Comments and Replies. +- +-=cut +- +-Set($MessageBoxIncludeSignature, 1); +- +-=item C<$MessageBoxIncludeSignatureOnComment> +- +-Should your users' signatures (from their Preferences page) be +-included in Comments. Setting this to 0 overrides +-C<$MessageBoxIncludeSignature>. +- +-=cut +- +-Set($MessageBoxIncludeSignatureOnComment, 1); +- +-=back +- +- +-=head2 Transaction display +- +-=over 4 +- +-=item C<$OldestTransactionsFirst> +- +-By default, RT shows newest transactions at the bottom of the ticket +-history page, if you want see them at the top set this to 0. This +-option can be overridden by users in their preferences. +- +-=cut +- +-Set($OldestTransactionsFirst, 1); +- +-=item C<$ShowHistory> +- +-This option controls how history is shown on the ticket display page. It +-accepts one of three possible modes and is overrideable on a per-user +-preference level. If you regularly deal with long tickets and don't care much +-about the history, you may wish to change this option to C<click>. +- +-=over +- +-=item C<delay> (the default) +- +-When set to C<delay>, history is loaded via javascript after the rest of the +-page has been loaded. This speeds up apparent page load times and generally +-provides a smoother experience. You may notice slight delays before the ticket +-history appears on very long tickets. +- +-=item C<click> +- +-When set to C<click>, history is loaded on demand when a placeholder link is +-clicked. This speeds up ticket display page loads and history is never loaded +-if not requested. +- +-=item C<always> +- +-When set to C<always>, history is loaded before showing the page. This ensures +-history is always available immediately, but at the expense of longer page load +-times. This behaviour was the default in RT 4.0. +- +-=back +- +-=cut +- +-Set($ShowHistory, 'delay'); +- +-=item C<$ShowBccHeader> +- +-By default, RT hides from the web UI information about blind copies +-user sent on reply or comment. +- +-=cut +- +-Set($ShowBccHeader, 0); +- +-=item C<$TrustHTMLAttachments> +- +-If C<TrustHTMLAttachments> is not defined, we will display them as +-text. This prevents malicious HTML and JavaScript from being sent in a +-request (although there is probably more to it than that) +- +-=cut +- +-Set($TrustHTMLAttachments, undef); +- +-=item C<$AlwaysDownloadAttachments> +- +-Always download attachments, regardless of content type. If set, this +-overrides C<TrustHTMLAttachments>. +- +-=cut +- +-Set($AlwaysDownloadAttachments, undef); +- +-=item C<$PreferRichText> +- +-By default, RT shows rich text (HTML) messages if possible. If +-C<$PreferRichText> is set to 0, RT will show plain text messages in +-preference to any rich text alternatives. +- +-As a security precaution, RT limits the HTML that is displayed to a +-known-good subset -- as allowing arbitrary HTML to be displayed exposes +-multiple vectors for XSS and phishing attacks. If +-L</$TrustHTMLAttachments> is enabled, the original HTML is available for +-viewing via the "Download" link. +- +-If the optional L<HTML::Gumbo> dependency is installed, RT will leverage +-this to allow a broader set of HTML through, including tables. +- +-=cut +- +-Set($PreferRichText, 1); +- +-=item C<$MaxInlineBody> +- +-C<$MaxInlineBody> is the maximum attachment size that we want to see +-inline when viewing a transaction. RT will inline any text if the +-value is undefined or 0. This option can be overridden by users in +-their preferences. +- +-=cut +- +-Set($MaxInlineBody, 12000); +- +-=item C<$ShowTransactionImages> +- +-By default, RT shows images attached to incoming (and outgoing) ticket +-updates inline. Set this variable to 0 if you'd like to disable that +-behavior. +- +-=cut +- +-Set($ShowTransactionImages, 1); +- +-=item C<$ShowRemoteImages> +- +-By default, RT doesn't show remote images attached to incoming (and outgoing) +-ticket updates inline. Set this variable to 1 if you'd like to enable remote +-image display. Showing remote images may allow spammers and other senders to +-track when messages are viewed and see referer information. +- +-Note that this setting is independent of L</$ShowTransactionImages> above. +- +-=cut +- +-Set($ShowRemoteImages, 0); +- +-=item C<$PlainTextMono> +- +-Normally plaintext attachments are displayed as HTML with line breaks +-preserved. This causes space- and tab-based formatting not to be +-displayed correctly. Set C<$PlainTextMono> to 1 to use a monospaced +-font and preserve formatting. +- +-=cut +- +-Set($PlainTextMono, 0); +- +-=item C<$SuppressInlineTextFiles> +- +-If C<$SuppressInlineTextFiles> is set to 1, then uploaded text files +-(text-type attachments with file names) are prevented from being +-displayed in-line when viewing a ticket's history. +- +-=cut +- +-Set($SuppressInlineTextFiles, undef); +- +- +-=item C<@Active_MakeClicky> +- +-MakeClicky detects various formats of data in headers and email +-messages, and extends them with supporting links. By default, RT +-provides two formats: +- +-* 'httpurl': detects http:// and https:// URLs and adds '[Open URL]' +- link after the URL. +- +-* 'httpurl_overwrite': also detects URLs as 'httpurl' format, but +- replaces the URL with a link. Enabled by default. +- +-See F<share/html/Elements/MakeClicky> for documentation on how to add +-your own styles of link detection. +- +-=cut +- +-Set(@Active_MakeClicky, qw(httpurl_overwrite)); +- +-=item C<$QuoteFolding> +- +-Quote folding is the hiding of old replies in transaction history. +-It defaults to on. Set this to 0 to disable it. +- +-=cut +- +-Set($QuoteFolding, 1); +- +-=item C<$AllowLoginPasswordAutoComplete> +- +-Allow browsers to remember the user's password on login (in case the +-browser can do so, and has the appropriate setting enabled). Default +-is 0. +- +-=cut +- +-Set($AllowLoginPasswordAutoComplete, 0); +- +-=back +- +- +-=head1 Application logic +- +-=over 4 +- +-=item C<$ParseNewMessageForTicketCcs> +- +-If C<$ParseNewMessageForTicketCcs> is set to 1, RT will attempt to +-divine Ticket 'Cc' watchers from the To and Cc lines of incoming +-messages that create new Tickets. This option does not apply to replies +-or comments on existing Tickets. Be forewarned that if you have I<any> +-addresses which forward mail to RT automatically and you enable this +-option without modifying C<$RTAddressRegexp> below, you will get +-yourself into a heap of trouble. +- +-=cut +- +-Set($ParseNewMessageForTicketCcs, undef); +- +-=item C<$UseTransactionBatch> +- +-Set C<$UseTransactionBatch> to 1 to execute transactions in batches, +-such that a resolve and comment (for example) would happen +-simultaneously, instead of as two transactions, unaware of each +-others' existence. +- +-=cut +- +-Set($UseTransactionBatch, 1); +- +-=item C<$StrictLinkACL> +- +-When this feature is enabled a user needs I<ModifyTicket> rights on +-both tickets to link them together; otherwise, I<ModifyTicket> rights +-on either of them is sufficient. +- +-=cut +- +-Set($StrictLinkACL, 1); +- +-=item C<$RedistributeAutoGeneratedMessages> +- +-Should RT redistribute correspondence that it identifies as machine +-generated? A 1 will do so; setting this to 0 will cause no +-such messages to be redistributed. You can also use 'privileged' (the +-default), which will redistribute only to privileged users. This helps +-to protect against malformed bounces and loops caused by auto-created +-requestors with bogus addresses. +- +-=cut +- +-Set($RedistributeAutoGeneratedMessages, "privileged"); +- +-=item C<$ApprovalRejectionNotes> +- +-Should rejection notes from approvals be sent to the requestors? +- +-=cut +- +-Set($ApprovalRejectionNotes, 1); +- +-=item C<$ForceApprovalsView> +- +-Should approval tickets only be viewed and modified through the standard +-approval interface? With this setting enabled (by default), any attempt to use +-the normal ticket display and modify page for approval tickets will be +-redirected. +- +-For example, with this option set to 1 and an approval ticket #123: +- +- /Ticket/Display.html?id=123 +- +-is redirected to +- +- /Approval/Display.html?id=123 +- +-With this option set to 0, the redirect won't happen. +- +-=back +- +-=cut +- +-Set($ForceApprovalsView, 1); +- +-=head1 Extra security +- +-This is a list of extra security measures to enable that help keep your RT +-safe. If you don't know what these mean, you should almost certainly leave the +-defaults alone. +- +-=over 4 +- +-=item C<$DisallowExecuteCode> +- +-If set to 1, the C<ExecuteCode> right will be removed from +-all users, B<including> the superuser. This is intended for when RT is +-installed into a shared environment where even the superuser should not +-be allowed to run arbitrary Perl code on the server via scrips. +- +-=cut +- +-Set($DisallowExecuteCode, 0); +- +-=item C<$Framebusting> +- +-If set to 0, framekiller javascript will be disabled and the +-X-Frame-Options: DENY header will be suppressed from all responses. +-This disables RT's clickjacking protection. +- +-=cut +- +-Set($Framebusting, 1); +- +-=item C<$RestrictReferrer> +- +-If set to 0, the HTTP C<Referer> (sic) header will not be +-checked to ensure that requests come from RT's own domain. As RT allows +-for GET requests to alter state, disabling this opens RT up to +-cross-site request forgery (CSRF) attacks. +- +-=cut +- +-Set($RestrictReferrer, 1); +- +-=item C<$RestrictLoginReferrer> +- +-If set to 0, RT will allow the user to log in from any link +-or request, merely by passing in C<user> and C<pass> parameters; setting +-it to 1 forces all logins to come from the login box, so the +-user is aware that they are being logged in. The default is off, for +-backwards compatability. +- +-=cut +- +-Set($RestrictLoginReferrer, 0); +- +-=item C<@ReferrerWhitelist> +- +-This is a list of hostname:port combinations that RT will treat as being +-part of RT's domain. This is particularly useful if you access RT as +-multiple hostnames or have an external auth system that needs to +-redirect back to RT once authentication is complete. +- +- Set(@ReferrerWhitelist, qw(www.example.com:443 www3.example.com:80)); +- +-If the "RT has detected a possible cross-site request forgery" error is triggered +-by a host:port sent by your browser that you believe should be valid, you can copy +-the host:port from the error message into this list. +- +-Simple wildcards, similar to SSL certificates, are allowed. For example: +- +- *.example.com:80 # matches foo.example.com +- # but not example.com +- # or foo.bar.example.com +- +- www*.example.com:80 # matches www3.example.com +- # and www-test.example.com +- # and www.example.com +- +-=cut +- +-Set(@ReferrerWhitelist, qw()); +- +- +-=item C<$BcryptCost> +- +-This sets the default cost parameter used for the C<bcrypt> key +-derivation function. Valid values range from 4 to 31, inclusive, with +-higher numbers denoting greater effort. +- +-=cut +- +-Set($BcryptCost, 10); +- +-=back +- +- +- +-=head1 Authorization and user configuration +- +-=over 4 +- +-=item C<$WebRemoteUserAuth> +- +-If C<$WebRemoteUserAuth> is defined, RT will defer to the environment's +-REMOTE_USER variable, which should be set by the webserver's +-authentication layer. +- +-=cut +- +-Set($WebRemoteUserAuth, undef); +- +-=item C<$WebRemoteUserContinuous> +- +-If C<$WebRemoteUserContinuous> is defined, RT will check for the +-REMOTE_USER on each access. If you would prefer this to only happen +-once (at initial login) set this to 0. The default +-setting will help ensure that if your webserver's authentication layer +-deauthenticates a user, RT notices as soon as possible. +- +-=cut +- +-Set($WebRemoteUserContinuous, 1); +- +-=item C<$WebFallbackToRTLogin> +- +-If C<$WebFallbackToRTLogin> is defined, the user is allowed a +-chance of fallback to the login screen, even if REMOTE_USER failed. +- +-=cut +- +-Set($WebFallbackToRTLogin, undef); +- +-=item C<$WebRemoteUserGecos> +- +-C<$WebRemoteUserGecos> means to match 'gecos' field as the user +-identity; useful with C<mod_auth_external>. +- +-=cut +- +-Set($WebRemoteUserGecos, undef); +- +-=item C<$WebRemoteUserAutocreate> +- +-C<$WebRemoteUserAutocreate> will create users under the same name as +-REMOTE_USER upon login, if they are missing from the Users table. +- +-=cut +- +-Set($WebRemoteUserAutocreate, undef); +- +-=item C<$UserAutocreateDefaultsOnLogin> +- +-If C<$WebRemoteUserAutocreate> is set to 1, C<$UserAutocreateDefaultsOnLogin> +-will be passed to L<RT::User/Create>. Use it to set defaults, such as +-creating unprivileged users with C<<{ Privileged => 0 }>>. This must be +-a hashref. +- +-=cut +- +-Set($UserAutocreateDefaultsOnLogin, undef); +- +-=item C<$WebSessionClass> +- +-C<$WebSessionClass> is the class you wish to use for storing sessions. On +-MySQL, Pg, and Oracle it defaults to using your database, in other cases +-sessions are stored in files using L<Apache::Session::File>. Other installed +-Apache::Session::* modules can be used to store sessions. +- +- Set($WebSessionClass, "Apache::Session::File"); +- +-=cut +- +-Set($WebSessionClass, undef); +- +-=item C<%WebSessionProperties> +- +-C<%WebSessionProperties> is the hash to configure class L</$WebSessionClass> +-in case custom class is used. By default it's empty and values are picked +-depending on the class. Make sure that it's empty if you're using DB as session +-backend. +- +-=cut +- +-Set( %WebSessionProperties ); +- +-=item C<$AutoLogoff> +- +-By default, RT's user sessions persist until a user closes his or her +-browser. With the C<$AutoLogoff> option you can setup session lifetime +-in minutes. A user will be logged out if he or she doesn't send any +-requests to RT for the defined time. +- +-=cut +- +-Set($AutoLogoff, 0); +- +-=item C<$LogoutRefresh> +- +-The number of seconds to wait after logout before sending the user to +-the login page. By default, 1 second, though you may want to increase +-this if you display additional information on the logout page. +- +-=cut +- +-Set($LogoutRefresh, 1); +- +-=item C<$WebSecureCookies> +- +-By default, RT's session cookie isn't marked as "secure". Some web +-browsers will treat secure cookies more carefully than non-secure +-ones, being careful not to write them to disk, only sending them over +-an SSL secured connection, and so on. To enable this behavior, set +-C<$WebSecureCookies> to 1. NOTE: You probably don't want to turn this +-on I<unless> users are only connecting via SSL encrypted HTTPS +-connections. +- +-=cut +- +-Set($WebSecureCookies, 0); +- +-=item C<$WebHttpOnlyCookies> +- +-Default RT's session cookie to not being directly accessible to +-javascript. The content is still sent during regular and AJAX requests, +-and other cookies are unaffected, but the session-id is less +-programmatically accessible to javascript. Turning this off should only +-be necessary in situations with odd client-side authentication +-requirements. +- +-=cut +- +-Set($WebHttpOnlyCookies, 1); +- +-=item C<$MinimumPasswordLength> +- +-C<$MinimumPasswordLength> defines the minimum length for user +-passwords. Setting it to 0 disables this check. +- +-=cut +- +-Set($MinimumPasswordLength, 5); +- +-=back +- +- +-=head1 Internationalization +- +-=over 4 +- +-=item C<@LexiconLanguages> +- +-An array that contains languages supported by RT's +-internationalization interface. Defaults to all *.po lexicons; +-setting it to C<qw(en ja)> will make RT bilingual instead of +-multilingual, but will save some memory. +- +-=cut +- +-Set(@LexiconLanguages, qw(*)); +- +-=item C<@EmailInputEncodings> +- +-An array that contains default encodings used to guess which charset +-an attachment uses, if it does not specify one explicitly. All +-options must be recognized by L<Encode::Guess>. The first element may +-also be '*', which enables encoding detection using +-L<Encode::Detect::Detector>, if installed. +- +-=cut +- +-Set(@EmailInputEncodings, qw(utf-8 iso-8859-1 us-ascii)); +- +-=item C<$EmailOutputEncoding> +- +-The charset for localized email. Must be recognized by Encode. +- +-=cut +- +-Set($EmailOutputEncoding, "utf-8"); +- +-=back +- +- +- +- +- +- +- +-=head1 Date and time handling +- +-=over 4 +- +-=item C<$DateTimeFormat> +- +-You can choose date and time format. See the "Output formatters" +-section in perldoc F<lib/RT/Date.pm> for more options. This option +-can be overridden by users in their preferences. +- +-Some examples: +- +-C<Set($DateTimeFormat, "LocalizedDateTime");> +-C<Set($DateTimeFormat, { Format => "ISO", Seconds => 0 });> +-C<Set($DateTimeFormat, "RFC2822");> +-C<Set($DateTimeFormat, { Format => "RFC2822", Seconds => 0, DayOfWeek => 0 });> +- +-=cut +- +-Set($DateTimeFormat, "DefaultFormat"); +- +-# Next two options are for Time::ParseDate +- +-=item C<$DateDayBeforeMonth> +- +-Set this to 1 if your local date convention looks like "dd/mm/yy" +-instead of "mm/dd/yy". Used only for parsing, not for displaying +-dates. +- +-=cut +- +-Set($DateDayBeforeMonth, 1); +- +-=item C<$AmbiguousDayInPast>, C<$AmbiguousDayInFuture> +- +-Should an unspecified day or year in a date refer to a future or a +-past value? For example, should a date of "Tuesday" default to mean +-the date for next Tuesday or last Tuesday? Should the date "March 1" +-default to the date for next March or last March? +- +-Set C<$AmbiguousDayInPast> for the last date, or +-C<$AmbiguousDayInFuture> for the next date; the default is usually +-correct. If both are set, C<$AmbiguousDayInPast> takes precedence. +- +-=cut +- +-Set($AmbiguousDayInPast, 0); +-Set($AmbiguousDayInFuture, 0); +- +-=item C<$DefaultTimeUnitsToHours> +- +-Use this to set the default units for time entry to hours instead of +-minutes. Note that this only effects entry, not display. +- +-=cut +- +-Set($DefaultTimeUnitsToHours, 0); +- +-=item C<$TimeInICal> +- +-By default, events in the iCal feed on the ticket search page +-contain only dates, making them all day calendar events. Set +-C<$TimeInICal> if you have start or due dates on tickets that +-have significant time values and you want those times to be +-included in the events in the iCal feed. +- +-This option can also be set as an individual user preference. +- +-=cut +- +-Set($TimeInICal, 0); +- +-=back +- +- +- +-=head1 Cryptography +- +-A complete description of RT's cryptography capabilities can be found in +-L<RT::Crypt>. At this moment, GnuPG (PGP) and SMIME security protocols are +-supported. +- +-=over 4 +- +-=item C<%Crypt> +- +-The following options apply to all cryptography protocols. +- +-By default, all enabled security protocols will analyze each incoming +-email. You may set C<Incoming> to a subset of this list, if some enabled +-protocols do not apply to incoming mail; however, this is usually +-unnecessary. Note that for any verification or decryption to occur for +-incoming mail, the C<Auth::Crypt> mail plugin must be added to +-L</@MailPlugins> as specified in L<RT::Crypt/Handling incoming messages>. +- +-For outgoing emails, the first security protocol from the above list is +-used. Use the C<Outgoing> option to set a security protocol that should +-be used in outgoing emails. At this moment, only one protocol can be +-used to protect outgoing emails. +- +-Set C<RejectOnUnencrypted> to 1 if all incoming email must be +-properly encrypted. All unencrypted emails will be rejected by RT. +- +-Set C<RejectOnMissingPrivateKey> to 0 if you don't want to reject +-emails encrypted for key RT doesn't have and can not decrypt. +- +-Set C<RejectOnBadData> to 0 if you don't want to reject letters +-with incorrect data. +- +-If you want to allow people to encrypt attachments inside the DB then +-set C<AllowEncryptDataInDB> to 1. +- +-Set C<Dashboards> to a hash with Encrypt and Sign keys to control +-whether dashboards should be encrypted and/or signed correspondingly. +-By default they are not encrypted or signed. +- +-=back +- +-=cut +- +-Set( %Crypt, +- Incoming => undef, # ['GnuPG', 'SMIME'] +- Outgoing => undef, # 'SMIME' +- +- RejectOnUnencrypted => 0, +- RejectOnMissingPrivateKey => 1, +- RejectOnBadData => 1, +- +- AllowEncryptDataInDB => 0, +- +- Dashboards => { +- Encrypt => 0, +- Sign => 0, +- }, +-); +- +-=head2 SMIME configuration +- +-A full description of the SMIME integration can be found in +-L<RT::Crypt::SMIME>. +- +-=over 4 +- +-=item C<%SMIME> +- +-Set C<Enable> to 0 or 1 to disable or enable SMIME for +-encrypting and signing messages. +- +-Set C<OpenSSL> to path to F<openssl> executable. +- +-Set C<Keyring> to directory with key files. Key and certificates should +-be stored in a PEM file in this directory named named, e.g., +-F<email.address@xxxxxxxxxxxxxxx>. +- +-Set C<CAPath> to either a PEM-formatted certificate of a single signing +-certificate authority, or a directory of such (including hash symlinks +-as created by the openssl tool C<c_rehash>). Only SMIME certificates +-signed by these certificate authorities will be treated as valid +-signatures. If left unset (and C<AcceptUntrustedCAs> is unset, as it is +-by default), no signatures will be marked as valid! +- +-Set C<AcceptUntrustedCAs> to allow arbitrary SMIME certificates, no +-matter their signing entities. Such mails will be marked as untrusted, +-but signed; C<CAPath> will be used to mark which mails are signed by +-trusted certificate authorities. This configuration is generally +-insecure, as it allows the possibility of accepting forged mail signed +-by an untrusted certificate authority. +- +-Setting C<AcceptUntrustedCAs> also allows encryption to users with +-certificates created by untrusted CAs. +- +-Set C<Passphrase> to a scalar (to use for all keys), an anonymous +-function, or a hash (to look up by address). If the hash is used, the +-'' key is used as a default. +- +-See L<RT::Crypt::SMIME> for details. +- +-=back +- +-=cut +- +-Set( %SMIME, +- Enable => 0, +- OpenSSL => 'openssl', +- Keyring => q{var/data/smime}, +- CAPath => undef, +- AcceptUntrustedCAs => undef, +- Passphrase => undef, +-); +- +-=head2 GnuPG configuration +- +-A full description of the (somewhat extensive) GnuPG integration can +-be found by running the command `perldoc L<RT::Crypt::GnuPG>` (or +-`perldoc lib/RT/Crypt/GnuPG.pm` from your RT install directory). +- +-=over 4 +- +-=item C<%GnuPG> +- +-Set C<Enable> to 0 or 1 to disable or enable GnuPG interfaces +-for encrypting and signing outgoing messages. +- +-Set C<GnuPG> to the name or path of the gpg binary to use. +- +-Set C<Passphrase> to a scalar (to use for all keys), an anonymous +-function, or a hash (to look up by address). If the hash is used, the +-'' key is used as a default. +- +-Set C<OutgoingMessagesFormat> to 'inline' to use inline encryption and +-signatures instead of 'RFC' (GPG/MIME: RFC3156 and RFC1847) format. +- +-=cut +- +-Set(%GnuPG, +- Enable => 0, +- GnuPG => 'gpg', +- Passphrase => undef, +- OutgoingMessagesFormat => "RFC", # Inline +-); +- +-=item C<%GnuPGOptions> +- +-Options to pass to the GnuPG program. +- +-If you override this in your RT_SiteConfig, you should be sure to +-include a homedir setting. +- +-Note that options with '-' character MUST be quoted. +- +-=cut +- +-Set(%GnuPGOptions, +- homedir => q{var/data/gpg}, +- +-# URL of a keyserver +-# keyserver => 'hkp://subkeys.pgp.net', +- +-# enables the automatic retrieving of keys when encrypting +-# 'auto-key-locate' => 'keyserver', +- +-# enables the automatic retrieving of keys when verifying signatures +-# 'keyserver-options' => 'auto-key-retrieve', +-); +- +-=back +- +- +- +-=head1 Lifecycles +- +-=head2 Lifecycle definitions +- +-Each lifecycle is a list of possible statuses split into three logic +-sets: B<initial>, B<active> and B<inactive>. Each status in a +-lifecycle must be unique. (Statuses may not be repeated across sets.) +-Each set may have any number of statuses. +- +-For example: +- +- default => { +- initial => ['new'], +- active => ['open', 'stalled'], +- inactive => ['resolved', 'rejected', 'deleted'], +- ... +- }, +- +-Status names can be from 1 to 64 ASCII characters. Statuses are +-localized using RT's standard internationalization and localization +-system. +- +-=over 4 +- +-=item initial +- +-You can define multiple B<initial> statuses for tickets in a given +-lifecycle. +- +-RT will automatically set its B<Started> date when you change a +-ticket's status from an B<initial> state to an B<active> or +-B<inactive> status. +- +-=item active +- +-B<Active> tickets are "currently in play" - they're things that are +-being worked on and not yet complete. +- +-=item inactive +- +-B<Inactive> tickets are typically in their "final resting state". +- +-While you're free to implement a workflow that ignores that +-description, typically once a ticket enters an inactive state, it will +-never again enter an active state. +- +-RT will automatically set the B<Resolved> date when a ticket's status +-is changed from an B<Initial> or B<Active> status to an B<Inactive> +-status. +- +-B<deleted> is still a special status and protected by the +-B<DeleteTicket> right, unless you re-defined rights (read below). If +-you don't want to allow ticket deletion at any time simply don't +-include it in your lifecycle. +- +-=back +- +-Statuses in each set are ordered and listed in the UI in the defined +-order. +- +-Changes between statuses are constrained by transition rules, as +-described below. +- +-=head2 Default values +- +-In some cases a default value is used to display in UI or in API when +-value is not provided. You can configure defaults using the following +-syntax: +- +- default => { +- ... +- defaults => { +- on_create => 'new', +- on_resolve => 'resolved', +- ... +- }, +- }, +- +-The following defaults are used. +- +-=over 4 +- +-=item on_create +- +-If you (or your code) doesn't specify a status when creating a ticket, +-RT will use the this status. See also L</Statuses available during +-ticket creation>. +- +-=item on_merge +- +-When tickets are merged, the status of the ticket that was merged +-away is forced to this value. It should be one of inactive statuses; +-'resolved' or its equivalent is most probably the best candidate. +- +-=item approved +- +-When an approval is accepted, the status of depending tickets will +-be changed to this value. +- +-=item denied +- +-When an approval is denied, the status of depending tickets will +-be changed to this value. +- +-=item reminder_on_open +- +-When a reminder is opened, the status will be changed to this value. +- +-=item reminder_on_resolve +- +-When a reminder is resolved, the status will be changed to this value. +- +-=back +- +-=head2 Transitions between statuses and UI actions +- +-A B<Transition> is a change of status from A to B. You should define +-all possible transitions in each lifecycle using the following format: +- +- default => { +- ... +- transitions => { +- '' => [qw(new open resolved)], +- new => [qw(open resolved rejected deleted)], +- open => [qw(stalled resolved rejected deleted)], +- stalled => [qw(open)], +- resolved => [qw(open)], +- rejected => [qw(open)], +- deleted => [qw(open)], +- }, +- ... +- }, +- +-The order of items in the listing for each transition line affects +-the order they appear in the drop-down. If you change the config +-for 'open' state listing to: +- +- open => [qw(stalled rejected deleted resolved)], +- +-then the 'resolved' status will appear as the last item in the drop-down. +- +-=head3 Statuses available during ticket creation +- +-By default users can create tickets with a status of new, +-open, or resolved, but cannot create tickets with a status of +-rejected, stalled, or deleted. If you want to change the statuses +-available during creation, update the transition from '' (empty +-string), like in the example above. +- +-=head3 Protecting status changes with rights +- +-A transition or group of transitions can be protected by a specific +-right. Additionally, you can name new right names, which will be added +-to the system to control that transition. For example, if you wished to +-create a lesser right than ModifyTicket for rejecting tickets, you could +-write: +- +- default => { +- ... +- rights => { +- '* -> deleted' => 'DeleteTicket', +- '* -> rejected' => 'RejectTicket', +- '* -> *' => 'ModifyTicket', +- }, +- ... +- }, +- +-This would create a new C<RejectTicket> right in the system which you +-could assign to whatever groups you choose. +- +-On the left hand side you can have the following variants: +- +- '<from> -> <to>' +- '* -> <to>' +- '<from> -> *' +- '* -> *' +- +-Valid transitions are listed in order of priority. If a user attempts +-to change a ticket's status from B<new> to B<open> then the lifecycle +-is checked for presence of an exact match, then for 'any to B<open>', +-'B<new> to any' and finally 'any to any'. +- +-If you don't define any rights, or there is no match for a transition, +-RT will use the B<DeleteTicket> or B<ModifyTicket> as appropriate. +- +-=head3 Labeling and defining actions +- +-For each transition you can define an action that will be shown in the +-UI; each action annotated with a label and an update type. +- +-Each action may provide a default update type, which can be +-B<Comment>, B<Respond>, or absent. For example, you may want your +-staff to write a reply to the end user when they change status from +-B<new> to B<open>, and thus set the update to B<Respond>. Neither +-B<Comment> nor B<Respond> are mandatory, and user may leave the +-message empty, regardless of the update type. +- +-This configuration can be used to accomplish what +-$ResolveDefaultUpdateType was used for in RT 3.8. +- +-Use the following format to define labels and actions of transitions: +- +- default => { +- ... +- actions => [ +- 'new -> open' => { label => 'Open it', update => 'Respond' }, +- 'new -> resolved' => { label => 'Resolve', update => 'Comment' }, +- 'new -> rejected' => { label => 'Reject', update => 'Respond' }, +- 'new -> deleted' => { label => 'Delete' }, +- +- 'open -> stalled' => { label => 'Stall', update => 'Comment' }, +- 'open -> resolved' => { label => 'Resolve', update => 'Comment' }, +- 'open -> rejected' => { label => 'Reject', update => 'Respond' }, +- +- 'stalled -> open' => { label => 'Open it' }, +- 'resolved -> open' => { label => 'Re-open', update => 'Comment' }, +- 'rejected -> open' => { label => 'Re-open', update => 'Comment' }, +- 'deleted -> open' => { label => 'Undelete' }, +- ], +- ... +- }, +- +-In addition, you may define multiple actions for the same transition. +-Alternately, you may use '* -> x' to match more than one transition. +-For example: +- +- default => { +- ... +- actions => [ +- ... +- 'new -> rejected' => { label => 'Reject', update => 'Respond' }, +- 'new -> rejected' => { label => 'Quick Reject' }, +- ... +- '* -> deleted' => { label => 'Delete' }, +- ... +- ], +- ... +- }, +- +-=head2 Moving tickets between queues with different lifecycles +- +-Unless there is an explicit mapping between statuses in two different +-lifecycles, you can not move tickets between queues with these +-lifecycles -- even if both use the exact same set of statuses. +-Such a mapping is defined as follows: +- +- __maps__ => { +- 'from lifecycle -> to lifecycle' => { +- 'status in left lifecycle' => 'status in right lifecycle', +- ... +- }, +- ... +- }, +- +-=cut +- +-Set(%Lifecycles, +- default => { +- initial => [qw(new)], # loc_qw +- active => [qw(open stalled)], # loc_qw +- inactive => [qw(resolved rejected deleted)], # loc_qw +- +- defaults => { +- on_create => 'new', +- on_merge => 'resolved', +- approved => 'open', +- denied => 'rejected', +- reminder_on_open => 'open', +- reminder_on_resolve => 'resolved', +- }, +- +- transitions => { +- "" => [qw(new open resolved)], +- +- # from => [ to list ], +- new => [qw( open stalled resolved rejected deleted)], +- open => [qw(new stalled resolved rejected deleted)], +- stalled => [qw(new open rejected resolved deleted)], +- resolved => [qw(new open stalled rejected deleted)], +- rejected => [qw(new open stalled resolved deleted)], +- deleted => [qw(new open stalled rejected resolved )], +- }, +- rights => { +- '* -> deleted' => 'DeleteTicket', +- '* -> *' => 'ModifyTicket', +- }, +- actions => [ +- 'new -> open' => { label => 'Open It', update => 'Respond' }, # loc{label} +- 'new -> resolved' => { label => 'Resolve', update => 'Comment' }, # loc{label} +- 'new -> rejected' => { label => 'Reject', update => 'Respond' }, # loc{label} +- 'new -> deleted' => { label => 'Delete', }, # loc{label} +- 'open -> stalled' => { label => 'Stall', update => 'Comment' }, # loc{label} +- 'open -> resolved' => { label => 'Resolve', update => 'Comment' }, # loc{label} +- 'open -> rejected' => { label => 'Reject', update => 'Respond' }, # loc{label} +- 'stalled -> open' => { label => 'Open It', }, # loc{label} +- 'resolved -> open' => { label => 'Re-open', update => 'Comment' }, # loc{label} +- 'rejected -> open' => { label => 'Re-open', update => 'Comment' }, # loc{label} +- 'deleted -> open' => { label => 'Undelete', }, # loc{label} +- ], +- }, +-# don't change lifecyle of the approvals, they are not capable to deal with +-# custom statuses +- approvals => { +- initial => [ 'new' ], +- active => [ 'open', 'stalled' ], +- inactive => [ 'resolved', 'rejected', 'deleted' ], +- +- defaults => { +- on_create => 'new', +- on_merge => 'resolved', +- reminder_on_open => 'open', +- reminder_on_resolve => 'resolved', +- }, +- +- transitions => { +- '' => [qw(new open resolved)], +- +- # from => [ to list ], +- new => [qw(open stalled resolved rejected deleted)], +- open => [qw(new stalled resolved rejected deleted)], +- stalled => [qw(new open rejected resolved deleted)], +- resolved => [qw(new open stalled rejected deleted)], +- rejected => [qw(new open stalled resolved deleted)], +- deleted => [qw(new open stalled rejected resolved)], +- }, +- rights => { +- '* -> deleted' => 'DeleteTicket', +- '* -> rejected' => 'ModifyTicket', +- '* -> *' => 'ModifyTicket', +- }, +- actions => [ +- 'new -> open' => { label => 'Open It', update => 'Respond' }, # loc{label} +- 'new -> resolved' => { label => 'Resolve', update => 'Comment' }, # loc{label} +- 'new -> rejected' => { label => 'Reject', update => 'Respond' }, # loc{label} +- 'new -> deleted' => { label => 'Delete', }, # loc{label} +- 'open -> stalled' => { label => 'Stall', update => 'Comment' }, # loc{label} +- 'open -> resolved' => { label => 'Resolve', update => 'Comment' }, # loc{label} +- 'open -> rejected' => { label => 'Reject', update => 'Respond' }, # loc{label} +- 'stalled -> open' => { label => 'Open It', }, # loc{label} +- 'resolved -> open' => { label => 'Re-open', update => 'Comment' }, # loc{label} +- 'rejected -> open' => { label => 'Re-open', update => 'Comment' }, # loc{label} +- 'deleted -> open' => { label => 'Undelete', }, # loc{label} +- ], +- }, +-); +- +- +- +- +- +-=head1 Administrative interface +- +-=over 4 +- +-=item C<$ShowRTPortal> +- +-RT can show administrators a feed of recent RT releases and other +-related announcements and information from Best Practical on the top +-level Admin page. This feature helps you stay up to date on +-RT security announcements and version updates. +- +-RT provides this feature using an "iframe" on C</Admin/index.html> +-which asks the administrator's browser to show an inline page from +-Best Practical's website. +- +-If you'd rather not make this feature available to your +-administrators, set C<$ShowRTPortal> to 0. +- +-=cut +- +-Set($ShowRTPortal, 1); +- +-=item C<%AdminSearchResultFormat> +- +-In the admin interface, format strings similar to tickets result +-formats are used. Use C<%AdminSearchResultFormat> to define the format +-strings used in the admin interface on a per-RT-class basis. +- +-=cut +- +-Set(%AdminSearchResultFormat, +- Queues => +- q{'<a href="__WebPath__/Admin/Queues/Modify.html?id=__id__">__id__</a>/TITLE:#'} +- .q{,'<a href="__WebPath__/Admin/Queues/Modify.html?id=__id__">__Name__</a>/TITLE:Name'} +- .q{,__Description__,__Address__,__Priority__,__DefaultDueIn__,__Lifecycle__,__SubjectTag__,__Disabled__}, +- +- Groups => +- q{'<a href="__WebPath__/Admin/Groups/Modify.html?id=__id__">__id__</a>/TITLE:#'} +- .q{,'<a href="__WebPath__/Admin/Groups/Modify.html?id=__id__">__Name__</a>/TITLE:Name'} +- .q{,'__Description__',__Disabled__}, +- +- Users => +- q{'<a href="__WebPath__/Admin/Users/Modify.html?id=__id__">__id__</a>/TITLE:#'} +- .q{,'<a href="__WebPath__/Admin/Users/Modify.html?id=__id__">__Name__</a>/TITLE:Name'} +- .q{,__RealName__, __EmailAddress__,__Disabled__}, +- +- CustomFields => +- q{'<a href="__WebPath__/Admin/CustomFields/Modify.html?id=__id__">__id__</a>/TITLE:#'} +- .q{,'<a href="__WebPath__/Admin/CustomFields/Modify.html?id=__id__">__Name__</a>/TITLE:Name'} +- .q{,__AddedTo__, __FriendlyType__, __FriendlyPattern__,__Disabled__}, +- +- Scrips => +- q{'<a href="__WebPath__/Admin/Scrips/Modify.html?id=__id____From__">__id__</a>/TITLE:#'} +- .q{,'<a href="__WebPath__/Admin/Scrips/Modify.html?id=__id____From__">__Description__</a>/TITLE:Description'} +- .q{,__Condition__, __Action__, __Template__, __Disabled__}, +- +- Templates => +- q{'<a href="__WebPath__/__WebRequestPathDir__/Template.html?Queue=__QueueId__&Template=__id__">__id__</a>/TITLE:#'} +- .q{,'<a href="__WebPath__/__WebRequestPathDir__/Template.html?Queue=__QueueId__&Template=__id__">__Name__</a>/TITLE:Name'} +- .q{,'__Description__','__UsedBy__','__IsEmpty__'}, +- Classes => +- q{ '<a href="__WebPath__/Admin/Articles/Classes/Modify.html?id=__id__">__id__</a>/TITLE:#'} +- .q{,'<a href="__WebPath__/Admin/Articles/Classes/Modify.html?id=__id__">__Name__</a>/TITLE:Name'} +- .q{,__Description__,__Disabled__}, +-); +- +-=item C<%AdminSearchResultRows> +- +-Use C<%AdminSearchResultRows> to define the search result rows in the admin +-interface on a per-RT-class basis. +- +-=cut +- +-Set(%AdminSearchResultRows, +- Queues => 50, +- Groups => 50, +- Users => 50, +- CustomFields => 50, +- Scrips => 50, +- Templates => 50, +- Classes => 50, +-); +- +-=back +- +- +- +- +-=head1 Development options +- +-=over 4 +- +-=item C<$DevelMode> +- +-RT comes with a "Development mode" setting. This setting, as a +-convenience for developers, turns on several of development options +-that you most likely don't want in production: +- +-=over 4 +- +-=item * +- +-Disables CSS and JS minification and concatenation. Both CSS and JS +-will be instead be served as a number of individual smaller files, +-unchanged from how they are stored on disk. +- +-=item * +- +-Uses L<Module::Refresh> to reload changed Perl modules on each +-request. +- +-=item * +- +-Turns off Mason's C<static_source> directive; this causes Mason to +-reload template files which have been modified on disk. +- +-=item * +- +-Turns on Mason's HTML C<error_format>; this renders compilation errors +-to the browser, along with a full stack trace. It is possible for +-stack traces to reveal sensitive information such as passwords or +-ticket content. +- +-=item * +- +-Turns off caching of callbacks; this enables additional callbacks to +-be added while the server is running. +- +-=back +- +-=cut +- +-Set($DevelMode, 0); +- +- +-=item C<$RecordBaseClass> +- +-What abstract base class should RT use for its records. You should +-probably never change this. +- +-Valid values are C<DBIx::SearchBuilder::Record> or +-C<DBIx::SearchBuilder::Record::Cachable> +- +-=cut +- +-Set($RecordBaseClass, "DBIx::SearchBuilder::Record::Cachable"); +- +- +-=item C<@MasonParameters> +- +-C<@MasonParameters> is the list of parameters for the constructor of +-HTML::Mason's Apache or CGI Handler. This is normally only useful for +-debugging, e.g. profiling individual components with: +- +- use MasonX::Profiler; # available on CPAN +- Set(@MasonParameters, (preamble => 'my $p = MasonX::Profiler->new($m, $r);')); +- +-=cut +- +-Set(@MasonParameters, ()); +- +-=item C<$StatementLog> +- +-RT has rudimentary SQL statement logging support; simply set +-C<$StatementLog> to be the level that you wish SQL statements to be +-logged at. +- +-Enabling this option will also expose the SQL Queries page in the +-Admin -> Tools menu for SuperUsers. +- +-=cut +- +-Set($StatementLog, undef); +- +-=back +- +-=cut +- +-1; +diff --git a/etc/upgrade/3.8-ical-extension b/etc/upgrade/3.8-ical-extension +deleted file mode 100755 +index 6dfd808..0000000 +--- a/etc/upgrade/3.8-ical-extension ++++ /dev/null +@@ -1,96 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use 5.10.1; +-use strict; +-use warnings; +- +-use lib "local/lib"; +-use lib "lib"; +- +-use RT -init; +- +-$| = 1; +- +-use RT::Attributes; +-my $attrs = RT::Attributes->new( RT->SystemUser ); +-$attrs->Limit(FIELD => 'ObjectType', OPERATOR=> '=', VALUE => 'RT::User'); +-$attrs->Limit(FIELD => 'Name', OPERATOR=> '=', VALUE => 'ical-auth-token'); +-while ( my $attr = $attrs->Next ) { +- my $uid = $attr->ObjectId; +- print "Processing auth token of user #". $uid ."...\n"; +- +- my $user = RT::User->new( RT->SystemUser ); +- $user->Load( $uid ); +- unless ( $user->id ) { +- print STDERR "\tERROR. Couldn't load user record\n"; +- next; +- } +- +- my ($status, $msg); +- +- ($status, $msg) = $user->DeleteAttribute('AuthToken') +- if $user->FirstAttribute('AuthToken'); +- unless ( $status ) { +- print STDERR "\tERROR. Couldn't delete duplicated attribute: $msg\n"; +- next; +- } else { +- print "\tdeleted duplicate attribute\n"; +- } +- +- ($status, $msg) = $attr->SetName('AuthToken'); +- unless ( $status ) { +- print STDERR "\tERROR. Couldn't rename attribute: $msg\n"; +- next; +- } else { +- print "\trenamed attribute\n"; +- } +- print "\tDONE\n"; +-} +- +-exit 0; +diff --git a/etc/upgrade/4.0-customfield-checkbox-extension b/etc/upgrade/4.0-customfield-checkbox-extension +deleted file mode 100755 +index 6a39e55..0000000 +--- a/etc/upgrade/4.0-customfield-checkbox-extension ++++ /dev/null +@@ -1,87 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use 5.10.1; +-use strict; +-use warnings; +- +-use lib "local/lib"; +-use lib "lib"; +- +-use RT -init; +- +-$| = 1; +- +-use RT::CustomFields; +-my $cfs = RT::CustomFields->new( RT->SystemUser ); +-$cfs->{find_disabled_rows} = 1; +-$cfs->Limit( +- FIELD => 'Type', +- VALUE => 'SelectCheckbox', +-); +- +-while ( my $cf = $cfs->Next ) { +- print 'Processing custom field #' . $cf->id . "\n"; +- my ( $ret, $msg ) = $cf->SetType('Select'); +- unless ($ret) { +- warn "Failed to set custom field #" +- . $cf->id +- . " Type to 'Select': $msg\n"; +- } +- +- ( $ret, $msg ) = $cf->SetRenderType('List'); +- unless ($ret) { +- warn "Failed to set custom field #" +- . $cf->id +- . " RenderType to 'List': $msg\n"; +- } +-} +- +-print "DONE\n"; +- +-exit 0; +diff --git a/etc/upgrade/generate-rtaddressregexp b/etc/upgrade/generate-rtaddressregexp +deleted file mode 100755 +index a516b7c..0000000 +--- a/etc/upgrade/generate-rtaddressregexp ++++ /dev/null +@@ -1,108 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use 5.10.1; +-use strict; +-use warnings; +- +-use lib "local/lib"; +-use lib "lib"; +- +-use RT -init; +-RT->Config->Set('LogToSTDERR' => 'debug'); +- +-$| = 1; +- +-if (my $re = RT->Config->Get('RTAddressRegexp')) { +- print "No need to use this script, you already have RTAddressRegexp set to $re\n"; +- exit; +-} +- +-use RT::Queues; +-my $queues = RT::Queues->new( RT->SystemUser ); +-$queues->UnLimit; +- +-my %merged; +-merge(\%merged, RT->Config->Get('CorrespondAddress'), RT->Config->Get('CommentAddress')); +-while ( my $queue = $queues->Next ) { +- merge(\%merged, $queue->CorrespondAddress, $queue->CommentAddress); +-} +- +-my @domains; +-for my $domain (sort keys %merged) { +- my @addresses; +- for my $base (sort keys %{$merged{$domain}}) { +- my @subbits = keys(%{$merged{$domain}{$base}}); +- if (@subbits > 1) { +- push @addresses, "\Q$base\E(?:".join("|",@subbits).")"; +- } else { +- push @addresses, "\Q$base\E$subbits[0]"; +- } +- } +- if (@addresses > 1) { +- push @domains, "(?:".join("|", @addresses).")\Q\@$domain\E"; +- } else { +- push @domains, "$addresses[0]\Q\@$domain\E"; +- } +-} +-my $re = join "|", @domains; +- +-print <<ENDDESCRIPTION; +-You can add the following to RT_SiteConfig.pm, but may want to collapse it into a more efficient regexp. +-Keep in mind that this only contains the email addresses that RT knows about, you should also examine +-your mail system for aliases that reach RT but which RT doesn't know about. +-ENDDESCRIPTION +-print "Set(\$RTAddressRegexp,qr{^(?:${re})\$}i);\n"; +- +-sub merge { +- my $merged = shift; +- for my $address (grep {defined and length} @_) { +- $address =~ /^\s*(.*?)(-comments?)?\@(.*?)\s*$/; +- $merged->{lc $3}{$1}{$2||''}++; +- } +-} +diff --git a/etc/upgrade/split-out-cf-categories b/etc/upgrade/split-out-cf-categories +deleted file mode 100755 +index 63f310d..0000000 +--- a/etc/upgrade/split-out-cf-categories ++++ /dev/null +@@ -1,170 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use 5.10.1; +-use strict; +-use warnings; +- +-use lib "local/lib"; +-use lib "lib"; +- +-use RT -init; +-RT->Config->Set('LogToSTDERR' => 'debug'); +- +-$| = 1; +- +-$RT::Handle->BeginTransaction(); +- +-use RT::CustomFields; +-my $CFs = RT::CustomFields->new( RT->SystemUser ); +-$CFs->UnLimit; +-$CFs->Limit( FIELD => 'Type', VALUE => 'Select' ); +- +-my $seen; +-while (my $cf = $CFs->Next ) { +- next if $cf->BasedOnObj->Id; +- my @categories; +- my %mapping; +- my $values = $cf->Values; +- while (my $value = $values->Next) { +- next unless defined $value->Category and length $value->Category; +- push @categories, $value->Category unless grep {$_ eq $value->Category} @categories; +- $mapping{$value->Name} = $value->Category; +- } +- next unless @categories; +- +- $seen++; +- print "Found CF '@{[$cf->Name]}' with categories:\n"; +- print " $_\n" for @categories; +- +- print "Split this CF's categories into a hierarchical custom field (Y/n)? "; +- my $dothis = <>; +- next if $dothis =~ /n/i; +- +- print "Enter name of CF to create as category ('@{[$cf->Name]} category'): "; +- my $newname = <>; +- chomp $newname; +- $newname = $cf->Name . " category" unless length $newname; +- +- # bump the CF's sort oder up by one +- $cf->SetSortOrder( ($cf->SortOrder || 0) + 1 ); +- +- # ..and add a new CF before it +- my $new = RT::CustomField->new( RT->SystemUser ); +- my ($id, $msg) = $new->Create( +- Name => $newname, +- Type => 'Select', +- MaxValues => 1, +- LookupType => $cf->LookupType, +- SortOrder => $cf->SortOrder - 1, +- ); +- die "Can't create custom field '$newname': $msg" unless $id; +- +- # Set the CF to be based on what we just made +- $cf->SetBasedOn( $new->Id ); +- +- # Apply it to all of the same things +- { +- my $ocfs = RT::ObjectCustomFields->new( RT->SystemUser ); +- $ocfs->LimitToCustomField( $cf->Id ); +- while (my $ocf = $ocfs->Next) { +- my $newocf = RT::ObjectCustomField->new( RT->SystemUser ); +- ($id, $msg) = $newocf->Create( +- SortOrder => $ocf->SortOrder, +- CustomField => $new->Id, +- ObjectId => $ocf->ObjectId, +- ); +- die "Can't create ObjectCustomField: $msg" unless $id; +- } +- } +- +- # Copy over all of the rights +- { +- my $acl = RT::ACL->new( RT->SystemUser ); +- $acl->LimitToObject( $cf ); +- while (my $ace = $acl->Next) { +- my $newace = RT::ACE->new( RT->SystemUser ); +- ($id, $msg) = $newace->Create( +- PrincipalId => $ace->PrincipalId, +- PrincipalType => $ace->PrincipalType, +- RightName => $ace->RightName, +- Object => $new, +- ); +- die "Can't assign rights: $msg" unless $id; +- } +- } +- +- # Add values for all of the categories +- for my $i (0..$#categories) { +- ($id, $msg) = $new->AddValue( +- Name => $categories[$i], +- SortOrder => $i + 1, +- ); +- die "Can't create custom field value: $msg" unless $id; +- } +- +- # Grovel through all ObjectCustomFieldValues, and add the +- # appropriate category +- { +- my $ocfvs = RT::ObjectCustomFieldValues->new( RT->SystemUser ); +- $ocfvs->LimitToCustomField( $cf->Id ); +- while (my $ocfv = $ocfvs->Next) { +- next unless exists $mapping{$ocfv->Content}; +- my $newocfv = RT::ObjectCustomFieldValue->new( RT->SystemUser ); +- ($id, $msg) = $newocfv->Create( +- CustomField => $new->Id, +- ObjectType => $ocfv->ObjectType, +- ObjectId => $ocfv->ObjectId, +- Content => $mapping{$ocfv->Content}, +- ); +- } +- } +-} +- +-$RT::Handle->Commit; +-print "No custom fields with categories found\n" unless $seen; +diff --git a/etc/upgrade/switch-templates-to b/etc/upgrade/switch-templates-to +deleted file mode 100755 +index c1b1633..0000000 +--- a/etc/upgrade/switch-templates-to ++++ /dev/null +@@ -1,147 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use 5.10.1; +-use strict; +-use warnings; +- +-use lib "local/lib"; +-use lib "lib"; +- +-use RT -init; +-RT->Config->Set('LogToSTDERR' => 'info'); +- +-$| = 1; +- +-my $to = shift || ''; +-my $from; +- +-if ($to =~ /html|text/i) { +- $to = $to =~ /html/i ? 'html' : 'text'; +- $from = $to eq 'html' ? 'text' : 'html'; +-} else { +- print "Usage: $0 [html|text]\n"; +- warn "Please specify if you'd like to switch to HTML or text templates.\n"; +- exit 1; +-} +- +- +-my @templates = ( +- "Autoreply", +- "Transaction", +- "Admin Correspondence", +- "Correspondence", +- "Admin Comment", +- "Status Change", +- "Resolved", +- "New Pending Approval", +- "Approval Passed", +- "All Approvals Passed", +- "Approval Rejected", +- "Approval Ready for Owner", +-); +- +-$RT::Handle->BeginTransaction(); +- +-use RT::Scrips; +-my $scrips = RT::Scrips->new( RT->SystemUser ); +-$scrips->UnLimit; +- +-for (@templates) { +- $scrips->Limit( +- FIELD => 'Template', +- VALUE => ($to eq 'html' ? $_ : "$_ in HTML"), +- ENTRYAGGREGATOR => 'OR' +- ); +-} +- +-my $switched = 0; +-while ( my $s = $scrips->Next ) { +- my $new = $s->TemplateObj->Name; +- +- if ($to eq 'html') { +- $new .= ' in HTML'; +- } else { +- $new =~ s/ in HTML$//; +- } +- +- print $s->id, ": ", $s->Description, "\n"; +- print " ", $s->TemplateObj->Name, " -> $new\n\n"; +- +- my ($ok, $msg) = $s->SetTemplate($new); +- +- if ($ok) { +- $switched++; +- } else { +- warn " Couldn't switch templates: $msg\n"; +- } +-} +- +-$RT::Handle->Commit; +- +-if ($switched) { +- print <<" EOT"; +-Switched $switched scrips to $to templates. You should now manually port any +-customizations from the old templates to the new templates. +- EOT +- exit 1 if $switched != $scrips->Count; +-} +-elsif ($scrips->Count) { +- print <<" EOT"; +-@{[$scrips->Count]} scrips using $from templates were found, but none were +-successfully switched to $to. See the errors above. +- EOT +- exit 1; +-} +-else { +- print <<" EOT"; +-No scrips were found using the $from templates, so none were switched to +-$to templates. +- EOT +-} +- +diff --git a/etc/upgrade/upgrade-articles b/etc/upgrade/upgrade-articles +deleted file mode 100755 +index 482d7b3..0000000 +--- a/etc/upgrade/upgrade-articles ++++ /dev/null +@@ -1,264 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use 5.10.1; +-use strict; +-use warnings; +- +-use lib "local/lib"; +-use lib "lib"; +- +-use RT -init; +- +-RT->Config->Set('LogToSTDERR' => 'debug'); +- +-$| = 1; +- +-my $db_name = RT->Config->Get('DatabaseName'); +-my $db_type = RT->Config->Get('DatabaseType'); +- +-my $dbh = $RT::Handle->dbh; +- +-my $found_fm_tables; +-foreach my $name ( $RT::Handle->_TableNames ) { +- next unless $name =~ /^fm_/i; +- $found_fm_tables->{lc $name}++; +-} +- +-unless ( $found_fm_tables->{fm_topics} && $found_fm_tables->{fm_objecttopics} ) { +- warn "Couldn't find topics tables, it appears you have RTFM 2.0 or earlier."; +- warn "This script cannot yet upgrade RTFM versions which are that old"; +- exit; +-} +- +-{ # port over Articles +- my @columns = qw(id Name Summary SortOrder Class Parent URI Creator Created LastUpdatedBy LastUpdated); +- copy_tables('FM_Articles','Articles',\@columns); +- +-} +- +- +-{ # port over Classes +- my @columns = qw(id Name Description SortOrder Disabled Creator Created LastUpdatedBy LastUpdated); +- if ( grep lc($_) eq 'hotlist', $RT::Handle->Fields('FM_Classes') ) { +- push @columns, 'HotList'; +- } +- copy_tables('FM_Classes','Classes',\@columns); +-} +- +-{ # port over Topics +- my @columns = qw(id Parent Name Description ObjectType ObjectId); +- copy_tables('FM_Topics','Topics',\@columns); +-} +- +-{ # port over ObjectTopics +- my @columns = qw(id Topic ObjectType ObjectId); +- copy_tables('FM_ObjectTopics','ObjectTopics',\@columns); +-} +- +-sub copy_tables { +- my ($source, $dest, $columns) = @_; +- my $column_list = join(', ',@$columns); +- my $sql; +- # SQLite: http://www.sqlite.org/lang_insert.html +- if ( $db_type eq 'mysql' || $db_type eq 'SQLite' ) { +- $sql = "insert into $dest ($column_list) select $column_list from $source"; +- } +- # Oracle: http://www.adp-gmbh.ch/ora/sql/insert/select_and_subquery.html +- elsif ( $db_type eq 'Pg' || $db_type eq 'Oracle' ) { +- $sql = "insert into $dest ($column_list) (select $column_list from $source)"; +- } +- $RT::Logger->debug($sql); +- $dbh->do($sql); +-} +- +-{ # create ObjectClasses +- # this logic will need updating when folks have an FM_ObjectClasses table +- use RT::Classes; +- use RT::ObjectClass; +- +- my $classes = RT::Classes->new(RT->SystemUser); +- $classes->UnLimit; +- while ( my $class = $classes->Next ) { +- my $objectclass = RT::ObjectClass->new(RT->SystemUser); +- my ($ret, $msg ) = $objectclass->Create( Class => $class->Id, ObjectType => 'RT::System', ObjectId => 0 ); +- if ($ret) { +- warn("Applied Class '".$class->Name."' globally"); +- } else { +- warn("Couldn't create linkage for Class ".$class->Name.": $msg"); +- } +- } +-} +- +-{ # update ACLs +- use RT::ACL; +- my $acl = RT::ACL->new(RT->SystemUser); +- $acl->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Class' ); +- $acl->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::System' ); +- while ( my $ace = $acl->Next ) { +- if ( $ace->__Value('ObjectType') eq 'RT::FM::Class' ) { +- my ($ret, $msg ) = $ace->__Set( Field => 'ObjectType', Value => 'RT::Class'); +- warn "Fixing ACL ".$ace->Id." to refer to RT::Class: $msg"; +- } elsif ( $ace->__Value('ObjectType') eq 'RT::FM::System' ) { +- my ($ret, $msg) = $ace->__Set(Field => 'ObjectType', Value => 'RT::System'); +- warn "Fixing ACL ".$ace->Id." to refer to RT::System: $msg"; +- } +- } +- +- +-} +- +-{ # update CustomFields +- use RT::CustomFields; +- my $cfs = RT::CustomFields->new(RT->SystemUser); +- $cfs->Limit( FIELD => 'LookupType', VALUE => 'RT::FM::Class-RT::FM::Article' ); +- $cfs->{'find_disabled_rows'} = 1; +- while ( my $cf = $cfs->Next ) { +- my ($ret, $msg) = $cf->__Set( Field => 'LookupType', Value => 'RT::Class-RT::Article' ); +- warn "Update Custom Field LookupType for CF.".$cf->Id." $msg"; +- } +-} +- +-{ # update ObjectCustomFieldValues +- use RT::ObjectCustomFieldValues; +- my $ocfvs = RT::ObjectCustomFieldValues->new(RT->System); +- $ocfvs->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Article' ); +- $ocfvs->{'find_expired_rows'} = 1; +- while ( my $ocfv = $ocfvs->Next ) { +- my ($ret, $msg) = $ocfv->__Set( Field => 'ObjectType', Value => 'RT::Article' ); +- warn "Updated CF ".$ocfv->__Value('CustomField')." Value for Article ".$ocfv->__Value('ObjectId'); +- } +- +-} +- +-{ # update Topics +- use RT::Topics; +- my $topics = RT::Topics->new(RT->SystemUser); +- $topics->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Class' ); +- $topics->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::System' ); +- while ( my $topic = $topics->Next ) { +- if ( $topic->__Value('ObjectType') eq 'RT::FM::Class' ) { +- my ($ret, $msg ) = $topic->__Set( Field => 'ObjectType', Value => 'RT::Class'); +- warn "Fixing Topic ".$topic->Id." to refer to RT::Class: $msg"; +- } elsif ( $topic->__Value('ObjectType') eq 'RT::FM::System' ) { +- my ($ret, $msg) = $topic->__Set(Field => 'ObjectType', Value => 'RT::System'); +- warn "Fixing Topic ".$topic->Id." to refer to RT::System: $msg"; +- } +- } +-} +- +-{ # update ObjectTopics +- use RT::ObjectTopics; +- my $otopics = RT::ObjectTopics->new(RT->SystemUser); +- $otopics->UnLimit; +- while ( my $otopic = $otopics->Next ) { +- if ( $otopic->ObjectType eq 'RT::FM::Article' ) { +- my ($ret, $msg) = $otopic->SetObjectType('RT::Article'); +- warn "Fixing Topic ".$otopic->Topic." to apply to article: $msg"; +- } +- } +-} +- +-{ # update Links +- use RT::Links; +- my $links = RT::Links->new(RT->SystemUser); +- $links->Limit(FIELD => 'Base', VALUE => 'rtfm', OPERATOR => 'LIKE', SUBCLAUSE => 'stopanding', ENTRYAGGREGATOR => 'OR'); +- $links->Limit(FIELD => 'Target', VALUE => 'rtfm', OPERATOR => 'LIKE', SUBCLAUSE => 'stopanding', ENTRYAGGREGATOR => 'OR' ); +- while ( my $link = $links->Next ) { +- my $base = $link->__Value('Base'); +- my $target = $link->__Value('Target'); +- if ( $base =~ s/rtfm/article/i ) { +- my ($ret, $msg) = $link->__Set( Field => 'Base', Value => $base ); +- warn "Updating base to $base: $msg for link ".$link->id; +- } +- if ( $target =~ s/rtfm/article/i ) { +- my ($ret, $msg) = $link->__Set( Field => 'Target', Value => $target ); +- warn "Updating target to $target: $msg for link ".$link->id; +- } +- +- } +-} +- +-{ # update Transactions +- # we only keep article transactions at this point +- no warnings 'once'; +- use RT::Transactions; +- # Next calls Type to check readability and Type calls _Accessible +- # which called CurrentUserCanSee which calls Object which tries to instantiate +- # an RT::FM::Article. Rather than a shim RT::FM::Article class, I'm just avoiding +- # the ACL check since we're running around as the superuser. +- local *RT::Transaction::Type = sub { shift->__Value('Type') }; +- my $transactions = RT::Transactions->new(RT->SystemUser); +- $transactions->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Article' ); +- while ( my $t = $transactions->Next ) { +- my ($ret, $msg) = $t->__Set( Field => 'ObjectType', Value => 'RT::Article' ); +- warn "Updated Transaction ".$t->Id." to point to RT::Article"; +- } +- +- # we also need to change links that point to articles +- $transactions = RT::Transactions->new(RT->SystemUser); +- $transactions->Limit( FIELD => 'Type', VALUE => 'AddLink' ); +- $transactions->Limit( FIELD => 'NewValue', VALUE => 'rtfm', OPERATOR => 'LIKE' ); +- while ( my $t = $transactions->Next ) { +- my $value = $t->__Value('NewValue'); +- $value =~ s/rtfm/article/; +- my ($ret, $msg) = $t->__Set( Field => 'NewValue', Value => $value ); +- warn "Updated Transaction ".$t->Id." to link to $value"; +- } +-} +- +-{ # update Attributes +- # these are all things we should make real columns someday +- use RT::Attributes; +- my $attributes = RT::Attributes->new(RT->SystemUser); +- $attributes->Limit( FIELD => 'ObjectType', VALUE => 'RT::FM::Class' ); +- while ( my $a = $attributes->Next ) { +- my ($ret,$msg) = $a->__Set( Field => 'ObjectType', Value => 'RT::Class' ); +- warn "Updating Attribute ".$a->Name." to point to RT::Class"; +- } +-} +diff --git a/etc/upgrade/vulnerable-passwords b/etc/upgrade/vulnerable-passwords +deleted file mode 100755 +index c7616e9..0000000 +--- a/etc/upgrade/vulnerable-passwords ++++ /dev/null +@@ -1,141 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use 5.10.1; +-use strict; +-use warnings; +- +-use lib "local/lib"; +-use lib "lib"; +- +-use RT -init; +- +-$| = 1; +- +-use Getopt::Long; +-use Digest::SHA; +-my $fix; +-GetOptions("fix!" => \$fix); +- +-use RT::Users; +-my $users = RT::Users->new( $RT::SystemUser ); +-$users->Limit( +- FIELD => 'Password', +- OPERATOR => 'IS NOT', +- VALUE => 'NULL', +- ENTRYAGGREGATOR => 'AND', +-); +-$users->Limit( +- FIELD => 'Password', +- OPERATOR => '!=', +- VALUE => '*NO-PASSWORD*', +- ENTRYAGGREGATOR => 'AND', +-); +-$users->Limit( +- FIELD => 'Password', +- OPERATOR => 'NOT STARTSWITH', +- VALUE => '!', +- ENTRYAGGREGATOR => 'AND', +-); +-push @{$users->{'restrictions'}{ "main.Password" }}, "AND", { +- field => 'LENGTH(main.Password)', +- op => '<', +- value => '40', +-}; +- +-# we want to update passwords on disabled users +-$users->{'find_disabled_rows'} = 1; +- +-my $count = $users->Count; +-if ($count == 0) { +- print "No users with unsalted or weak cryptography found.\n"; +- exit 0; +-} +- +-if ($fix) { +- print "Upgrading $count users...\n"; +- while (my $u = $users->Next) { +- my $stored = $u->__Value("Password"); +- my $raw; +- if (length $stored == 32) { +- $raw = pack("H*",$stored); +- } elsif (length $stored == 22) { +- $raw = MIME::Base64::decode_base64($stored); +- } elsif (length $stored == 13) { +- printf "%20s => Old crypt() format, cannot upgrade\n", $u->Name; +- } else { +- printf "%20s => Unknown password format!\n", $u->Name; +- } +- next unless $raw; +- +- my $salt = pack("C4",map{int rand(256)} 1..4); +- my $sha = Digest::SHA::sha256( +- $salt . $raw +- ); +- $u->_Set( +- Field => "Password", +- Value => MIME::Base64::encode_base64( +- $salt . substr($sha,0,26), ""), +- ); +- } +- print "Done.\n"; +- exit 0; +-} else { +- if ($count < 20) { +- print "$count users found with unsalted or weak-cryptography passwords:\n"; +- print " Id | Name\n", "-"x9, "+", "-"x9, "\n"; +- while (my $u = $users->Next) { +- printf "%8d | %s\n", $u->Id, $u->Name; +- } +- } else { +- print "$count users found with unsalted or weak-cryptography passwords\n"; +- } +- +- print "\n", "Run again with --fix to upgrade.\n"; +- exit 1; +-} +diff --git a/lib/RT/Generated.pm b/lib/RT/Generated.pm +deleted file mode 100644 +index eceef66..0000000 +--- a/lib/RT/Generated.pm ++++ /dev/null +@@ -1,84 +0,0 @@ +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +- +-package RT; +-use warnings; +-use strict; +- +-our $VERSION = '4.2.10'; +-our ($MAJOR_VERSION, $MINOR_VERSION, $REVISION) = $VERSION =~ /^(\d)\.(\d)\.(\d+)/; +- +- +- +-$BasePath = '/opt/rt4'; +-$EtcPath = 'etc'; +-$BinPath = 'bin'; +-$SbinPath = 'sbin'; +-$VarPath = 'var'; +-$LexiconPath = 'share/po'; +-$StaticPath = 'share/static'; +-$PluginPath = 'plugins'; +-$LocalPath = 'local'; +-$LocalEtcPath = 'local/etc'; +-$LocalLibPath = 'local/lib'; +-$LocalLexiconPath = 'local/po'; +-$LocalStaticPath = 'local/static'; +-$LocalPluginPath = 'local/plugins'; +-# $MasonComponentRoot is where your rt instance keeps its mason html files +-$MasonComponentRoot = 'share/html'; +-# $MasonLocalComponentRoot is where your rt instance keeps its site-local +-# mason html files. +-$MasonLocalComponentRoot = 'local/html'; +-# $MasonDataDir Where mason keeps its datafiles +-$MasonDataDir = 'var/mason_data'; +-# RT needs to put session data (for preserving state between connections +-# via the web interface) +-$MasonSessionDir = 'var/session_data'; +- +- +-1; +diff --git a/sbin/rt-attributes-viewer b/sbin/rt-attributes-viewer +deleted file mode 100755 +index e8c0cdc..0000000 +--- a/sbin/rt-attributes-viewer ++++ /dev/null +@@ -1,115 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long; +-my %opt; +-GetOptions( \%opt, 'help|h', ); +- +-my $id = shift; +- +-if ( $opt{help} || !$id ) { +- require Pod::Usage; +- Pod::Usage::pod2usage({ verbose => 2 }); +- exit; +-} +- +-require RT; +-RT::LoadConfig(); +-RT::Init(); +- +-require RT::Attribute; +-my $attr = RT::Attribute->new( RT->SystemUser ); +-$attr->Load( $id ); +-unless ( $attr->id ) { +- print STDERR "Couldn't load attribute #$id\n"; +- exit 1; +-} +- +-my %res = (); +-$res{$_} = $attr->$_() foreach qw(ObjectType ObjectId Name Description Content ContentType); +- +-use Data::Dumper; +-print "Content of attribute #$id: ". Dumper( \%res ); +- +-__END__ +- +-=head1 NAME +- +-rt-attributes-viewer - show the content of an attribute +- +-=head1 SYNOPSIS +- +- # show the content of attribute 2 +- rt-attributes-viewer 2 +- +-=head1 DESCRIPTION +- +-This script deserializes and print content of an attribute defined +-by <attribute id>. May be useful for developers and for troubleshooting +-problems. +- +diff --git a/sbin/rt-clean-sessions b/sbin/rt-clean-sessions +deleted file mode 100755 +index 7a7b613..0000000 +--- a/sbin/rt-clean-sessions ++++ /dev/null +@@ -1,183 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long; +-my %opt; +-GetOptions( \%opt, "older=s", "debug", "help|h", "skip-user" ); +- +- +-if ( $opt{help} ) { +- require Pod::Usage; +- Pod::Usage::pod2usage({ verbose => 2 }); +- exit; +-} +- +- +-if( $opt{'older'} ) { +- unless( $opt{'older'} =~ /^\s*([0-9]+)\s*(H|D|M|Y)?$/i ) { +- print STDERR "wrong format of the 'older' argumnet\n"; +- exit(1); +- } +- my ($num,$unit) = ($1, uc($2 ||'D')); +- my %factor = ( H => 60*60 ); +- $factor{'D'} = $factor{'H'}*24; +- $factor{'M'} = $factor{'D'}*31; +- $factor{'Y'} = $factor{'D'}*365; +- $opt{'older'} = $num * $factor{ $unit }; +-} +- +-require RT; +-RT::LoadConfig(); +- +-if( $opt{'debug'} ) { +- RT->Config->Set( LogToSTDERR => 'debug' ); +-} else { +- RT->Config->Set( LogToSTDERR => undef ); +-} +- +-RT::ConnectToDatabase(); +-RT::InitLogging(); +- +-require RT::Interface::Web::Session; +- +-my $alogoff = int RT->Config->Get('AutoLogoff'); +-if ( $opt{'older'} or $alogoff ) { +- my $min; +- foreach ($alogoff*60, $opt{'older'}) { +- next unless $_; +- $min = $_ unless $min; +- $min = $_ if $_ < $min; +- } +- +- RT::Interface::Web::Session->ClearOld( $min ); +-} +- +-RT::Interface::Web::Session->ClearByUser +- unless $opt{'skip-user'}; +- +-exit(0); +- +-__END__ +- +-=head1 NAME +- +-rt-clean-sessions - clean old and duplicate RT sessions +- +-=head1 SYNOPSIS +- +- rt-clean-sessions [--debug] [--older <NUM>[H|D|M|Y]] +- +- rt-clean-sessions +- rt-clean-sessions --debug +- rt-clean-sessions --older 10D +- rt-clean-sessions --debug --older 1M +- rt-clean-sessions --older 10D --skip-user +- +-=head1 DESCRIPTION +- +-Script cleans RT sessions from DB or dir with sessions data. +-Leaves in DB only one session per RT user and sessions that aren't older +-than specified(see options). +- +-Script is safe because data in the sessions is temporary and can be deleted. +- +-=head1 OPTIONS +- +-=over 4 +- +-=item older +- +-Date interval in the C<< <NUM>[<unit>] >> format. Default unit is D(ays), +-H(our), M(onth) and Y(ear) are also supported. +- +-For example: C<rt-clean-sessions --older 1M> would delete all sessions that are +-older than 1 month. +- +-=item skip-user +- +-By default only one session per user left in the DB, so users that have +-sessions on multiple computers or in different browsers will be logged out. +-Use this option to avoid this. +- +-=item debug +- +-Turn on debug output. +- +-=back +- +-=head1 NOTES +- +-Functionality similar to this is implemented in +-html/Elements/SetupSessionCookie ; however, that does not guarantee +-that a session will be removed from disk and database soon after the +-timeout expires. This script, if run from a cron job, will ensure +-that the timed out sessions are actually removed from disk; the Mason +-component just ensures that the old sessions are not reusable before +-the cron job gets to them. +- +-=cut +diff --git a/sbin/rt-dump-metadata b/sbin/rt-dump-metadata +deleted file mode 100755 +index 46c240f..0000000 +--- a/sbin/rt-dump-metadata ++++ /dev/null +@@ -1,336 +0,0 @@ +-#!/usr/bin/perl -w +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# As we specify that XML is UTF-8 and we output it to STDOUT, we must be sure +-# it is UTF-8 so further XMLin will not break +-binmode( STDOUT, ":utf8" ); +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long; +-my %opt; +-GetOptions( \%opt, "help|h", +- "limit-to-privileged|l", +- "skip-disabled|s", +- "all|a", +-); +- +-if ( $opt{help} ) { +- require Pod::Usage; +- Pod::Usage::pod2usage( { verbose => 2 } ); +- exit; +-} +- +-require RT; +-require XML::Simple; +- +-RT::LoadConfig(); +-RT::Init(); +- +-my %RV; +-my %Ignore = ( +- All => [ +- qw( +- id Created Creator LastUpdated LastUpdatedBy +- ) +- ], +-); +- +-my $SystemUserId = RT->SystemUser->Id; +-my @classes = qw( +- Users Groups Queues ScripActions ScripConditions +- Templates Scrips ACL CustomFields +- ); +-foreach my $class (@classes) { +- require "RT/$class.pm"; +- my $objects = "RT::$class"->new( RT->SystemUser ); +- $objects->{find_disabled_rows} = 1 unless $opt{'skip-disabled'}; +- $objects->UnLimit; +- $objects->LimitToPrivileged if $class eq 'Users' +- && $opt{'limit-to-privileged'}; +- $objects->Limit( +- FIELD => 'Domain', +- OPERATOR => '=', +- VALUE => 'UserDefined', +- CASESENSITIVE => 0, +- ) if $class eq 'Groups'; +- +- if ( $class eq 'CustomFields' ) { +- $objects->OrderByCols( +- { FIELD => 'LookupType' }, +- { FIELD => 'SortOrder' }, +- { FIELD => 'Id' }, +- ); +- } else { +- $objects->OrderBy( FIELD => 'Id' ); +- } +- +- unless ($opt{all}) { +- next if $class eq 'ACL'; # XXX - would go into infinite loop - XXX +- $objects->Limit( +- FIELD => 'LastUpdatedBy', +- OPERATOR => '!=', +- VALUE => $SystemUserId +- ) unless $class eq 'Groups'; +- $objects->Limit( +- FIELD => 'Id', +- OPERATOR => '!=', +- VALUE => $SystemUserId +- ) if $class eq 'Users'; +- } +- +- my %fields; +-OBJECT: +- while ( my $obj = $objects->Next ) { +- next +- if $obj->can('LastUpdatedBy') +- and $obj->LastUpdatedBy == $SystemUserId; +- +- if ( !%fields ) { +- %fields = map { $_ => 1 } keys %{ $obj->_ClassAccessible }; +- delete @fields{ @{ $Ignore{$class} ||= [] }, +- @{ $Ignore{All} ||= [] }, }; +- } +- +- my $rv; +- +- if ( $class ne 'ACL' ) { +- # next if $obj-> # skip default names +- foreach my $field ( sort keys %fields ) { +- my $value = $obj->__Value($field); +- $rv->{$field} = $value if ( defined($value) && length($value) ); +- } +- delete $rv->{Disabled} unless $rv->{Disabled}; +- +- foreach my $record ( map { /ACL/ ? 'ACE' : substr( $_, 0, -1 ) } +- @classes ) +- { +- foreach my $key ( map "$record$_", ( '', 'Id' ) ) { +- next unless exists $rv->{$key}; +- my $id = $rv->{$key} or next; +- my $obj = "RT::$record"->new( RT->SystemUser ); +- $obj->LoadByCols( Id => $id ) or next; +- $rv->{$key} = $obj->__Value('Name') || 0; +- } +- } +- +- if ( $class eq 'Users' and defined $obj->Privileged ) { +- $rv->{Privileged} = int( $obj->Privileged ); +- } elsif ( $class eq 'CustomFields' ) { +- my $values = $obj->Values; +- while ( my $value = $values->Next ) { +- push @{ $rv->{Values} }, { +- map { ( $_ => $value->__Value($_) ) } +- qw( +- Name Description SortOrder +- ), +- }; +- } +- if ( $obj->LookupType eq 'RT::Queue-RT::Ticket' ) { +- # XXX-TODO: unused CF's turn into global CF when importing +- # as the sub InsertData in RT::Handle creates a global CF +- # when no queue is specified. +- $rv->{Queue} = []; +- my $applies = $obj->AppliedTo; +- while ( my $queue = $applies->Next ) { +- push @{ $rv->{Queue} }, $queue->Name; +- } +- } +- } +- } +- else { +- # 1) pick the right +- $rv->{Right} = $obj->RightName; +- +- # 2) Pick a level: Granted on Queue, CF, CF+Queue, or Globally? +- for ( $obj->ObjectType ) { +- if ( /^RT::Queue$/ ) { +- next OBJECT if $opt{'skip-disabled'} && $obj->Object->Disabled; +- $rv->{Queue} = $obj->Object->Name; +- } +- elsif ( /^RT::CustomField$/ ) { +- next OBJECT if $opt{'skip-disabled'} && $obj->Object->Disabled; +- $rv->{CF} = $obj->Object->Name; +- } +- elsif ( /^RT::Group$/ ) { +- # No support for RT::Group ACLs in RT::Handle yet. +- next OBJECT; +- } +- elsif ( /^RT::System$/ ) { +- # skip setting anything on $rv; +- # "Specifying none of the above will get you a global right." +- } +- } +- +- # 3) Pick a Principal; User or Group or Role +- if ( $obj->PrincipalType eq 'Group' ) { +- next OBJECT if $opt{'skip-disabled'} && $obj->PrincipalObj->Disabled; +- my $group = $obj->PrincipalObj->Object; +- for ( $group->Domain ) { +- # An internal user group +- if ( /^SystemInternal$/ ) { +- $rv->{GroupDomain} = $group->Domain; +- $rv->{GroupType} = $group->Type; +- } +- # An individual user +- elsif ( /^ACLEquivalence$/ ) { +- my $member = $group->MembersObj->Next->MemberObj; +- next OBJECT if $opt{'skip-disabled'} && $member->Disabled; +- $rv->{UserId} = $member->Object->Name; +- } +- # A group you created +- elsif ( /^UserDefined$/ ) { +- $rv->{GroupDomain} = 'UserDefined'; +- $rv->{GroupId} = $group->Name; +- } +- } +- } else { +- $rv->{GroupType} = $obj->PrincipalType; +- # A system-level role +- if ( $obj->ObjectType eq 'RT::System' ) { +- $rv->{GroupDomain} = 'RT::System-Role'; +- } +- # A queue-level role +- elsif ( $obj->ObjectType eq 'RT::Queue' ) { +- $rv->{GroupDomain} = 'RT::Queue-Role'; +- } +- } +- } +- +- if ( RT::Attributes->require ) { +- my $attributes = $obj->Attributes; +- while ( my $attribute = $attributes->Next ) { +- my $content = $attribute->Content; +- if ( $class eq 'Users' and $attribute->Name eq 'Bookmarks' ) { +- next; +- } +- $rv->{Attributes}{ $attribute->Name } = $content +- if length($content); +- } +- } +- +- push @{ $RV{$class} }, $rv; +- } +-} +- +-print(<< "."); +-no strict; use XML::Simple; *_ = XMLin(do { local \$/; readline(DATA) }, ForceArray => [qw( +- @classes Values +-)], NoAttr => 1, SuppressEmpty => ''); *\$_ = (\$_{\$_} || []) for keys \%_; 1; # vim: ft=xml +-__DATA__ +-. +- +-print XML::Simple::XMLout( +- { map { ( $_ => ( $RV{$_} || [] ) ) } @classes }, +- RootName => 'InitialData', +- NoAttr => 1, +- SuppressEmpty => '', +- XMLDecl => '<?xml version="1.0" encoding="UTF-8"?>', +-); +- +-__END__ +- +-=head1 NAME +- +-rt-dump-metadata - dump configuration metadata from an RT database +- +-=head1 SYNOPSIS +- +- rt-dump-metdata [--all] +- +-=head1 DESCRIPTION +- +-C<rt-dump-metadata> is a tool that dumps configuration metadata from the +-Request Tracker database into XML format, suitable for feeding into +-C<rt-setup-database>. To dump and load a full RT database, you should generally +-use the native database tools instead, as well as performing any necessary +-steps from UPGRADING. +- +-This is NOT a tool for backing up an RT database. See also +-L<docs/initialdata> for more straightforward means of importing data. +- +-=head1 OPTIONS +- +-=over +- +-=item C<--all> or C<-a> +- +-When run with C<--all>, the dump will include all configuration +-metadata; otherwise, the metadata dump will only include 'local' +-configuration changes, i.e. those done manually in the web interface. +- +-=item C<--limit-to-privileged> or C<-l> +- +-Causes the dumper to only dump privileged users. +- +-=item C<--skip-disabled> or C<-s> +- +-Ignores disabled rows in the database. +- +-=back +- +-=cut +- +diff --git a/sbin/rt-email-dashboards b/sbin/rt-email-dashboards +deleted file mode 100755 +index cf27f82..0000000 +--- a/sbin/rt-email-dashboards ++++ /dev/null +@@ -1,165 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-# Read in the options +-my %opts; +-use Getopt::Long; +-GetOptions( \%opts, +- "help|h", "dryrun", "time=i", "epoch=i", "all" +-); +- +-if ($opts{'help'}) { +- require Pod::Usage; +- print Pod::Usage::pod2usage(-verbose => 2); +- exit; +-} +- +-require RT; +-require RT::Interface::CLI; +-RT::Interface::CLI->import(qw{ CleanEnv loc }); +- +-# Load the config file +-RT::LoadConfig(); +- +-# Connect to the database and get RT::SystemUser and RT::Nobody loaded +-RT::Init(); +- +-# Clean out all the nasties from the environment +-CleanEnv(); +- +-require RT::Dashboard::Mailer; +-RT::Dashboard::Mailer->MailDashboards( +- All => $opts{all}, +- DryRun => $opts{dryrun}, +- Time => ($opts{time} || $opts{epoch} || time), # epoch is the old-style +- Opts => \%opts, +-); +- +-=head1 NAME +- +-rt-email-dashboards - Send email dashboards +- +-=head1 SYNOPSIS +- +- rt-email-dashboards [options] +- +-=head1 DESCRIPTION +- +-This tool will send users email based on how they have subscribed to +-dashboards. A dashboard is a set of saved searches, the subscription controls +-how often that dashboard is sent and how it's displayed. +- +-Each subscription has an hour, and possibly day of week or day of month. These +-are taken to be in the user's timezone if available, UTC otherwise. +- +-=head1 SETUP +- +-You'll need to have cron run this script every hour. Here's an example crontab +-entry to do this. +- +- 0 * * * * /opt/rt4/sbin/rt-email-dashboards +- +-This will run the script every hour on the hour. This may need some further +-tweaking to be run as the correct user. +- +-=head1 OPTIONS +- +-This tool supports a few options. Most are for debugging. +- +-=over 8 +- +-=item -h +- +-=item --help +- +-Display this documentation +- +-=item --dryrun +- +-Figure out which dashboards would be sent, but don't actually generate or email +-any of them +- +-=item --time SECONDS +- +-Instead of using the current time to figure out which dashboards should be +-sent, use SECONDS (usually since midnight Jan 1st, 1970, so C<1192216018> would +-be Oct 12 19:06:58 GMT 2007). +- +-=item --epoch SECONDS +- +-Back-compat for --time SECONDS. +- +-=item --all +- +-Ignore subscription frequency when considering each dashboard (should only be +-used with --dryrun for testing and debugging) +- +-=back +- +-=cut +- +diff --git a/sbin/rt-email-digest b/sbin/rt-email-digest +deleted file mode 100755 +index dfc9b2c..0000000 +--- a/sbin/rt-email-digest ++++ /dev/null +@@ -1,374 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use warnings; +-use strict; +- +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Date::Format qw( strftime ); +-use Getopt::Long; +-use RT; +-use RT::Interface::CLI qw( CleanEnv loc ); +-use RT::Interface::Email; +- +-RT::LoadConfig(); +-RT::Init(); +-CleanEnv(); +- +-sub usage { +- my ($error) = @_; +- print loc("Usage:") . " $0 -m (daily|weekly) [--print] [--help]\n"; +- print loc( +- "[_1] is a utility, meant to be run from cron, that dispatches all deferred RT notifications as a per-user digest.", +- $0 +- ) . "\n"; +- print "\n\t-m, --mode\t" +- . loc("Specify whether this is a daily or weekly run.") . "\n"; +- print "\t-p, --print\t" +- . loc("Print the resulting digest messages to STDOUT; don't mail them. Do not mark them as sent") +- . "\n"; +- print "\t-v, --verbose\t" . loc("Give output even on messages successfully sent") . "\n"; +- print "\t-h, --help\t" . loc("Print this message") . "\n"; +- +- if ( $error eq 'help' ) { +- exit 0; +- } else { +- print loc("Error") . ": " . loc($error) . "\n"; +- exit 1; +- } +-} +- +-my ( $frequency, $print, $verbose, $help ) = ( '', '', '', '' ); +-GetOptions( +- 'mode=s' => \$frequency, +- 'print' => \$print, +- 'verbose' => \$verbose, +- 'help' => \$help, +-); +- +-usage('help') if $help; +-usage("Mode argument must be 'daily' or 'weekly'") +- unless $frequency =~ /^(daily|weekly)$/; +- +-run( $frequency, $print ); +- +-sub run { +- my $frequency = shift; +- my $print = shift; +- +-## Find all the tickets that have been modified within the time frame +-## described by $frequency. +- +- my ( $all_digest, $sent_transactions ) = find_transactions($frequency); +- +-## Iterate through our huge hash constructing the digest message +-## for each user and sending it. +- +- foreach my $user ( keys %$all_digest ) { +- my ( $contents_list, $contents_body ) = build_digest_for_user( $user, $all_digest->{$user} ); +- # Now we have a content head and a content body. We can send a message. +- if ( send_digest( $user, $contents_list, $contents_body ) ) { +- print "Sent message to $user\n" if $verbose; +- mark_transactions_sent( $frequency, $user, values %{$sent_transactions->{$user}} ) unless ($print); +- } else { +- print "Failed to send message to $user\n"; +- } +- } +-} +-exit 0; +- +-# Subroutines. +- +-sub send_digest { +- my ( $to, $index, $messages ) = @_; +- +- # Combine the index and the messages. +- +- my $body = "============== Tickets with activity in the last " +- . ( $frequency eq 'daily' ? "day" : "seven days" ) . "\n\n"; +- +- $body .= $index; +- $body .= "\n\n============== Messages recorded in the last " +- . ( $frequency eq 'daily' ? "day" : "seven days" ) . "\n\n"; +- $body .= $messages; +- +- # Load our template. If we cannot load the template, abort +- # immediately rather than failing through many loops. +- my $digest_template = RT::Template->new( RT->SystemUser ); +- my ( $ret, $msg ) = $digest_template->Load('Email Digest'); +- unless ($ret) { +- print loc("Failed to load template") +- . " 'Email Digest': " +- . $msg +- . ". Cannot continue.\n"; +- exit 1; +- } +- ( $ret, $msg ) = $digest_template->Parse( Argument => $body ); +- unless ($ret) { +- print loc("Failed to parse template") +- . " 'Email Digest'. Cannot continue.\n"; +- exit 1; +- } +- +- # Set our sender and recipient. +- $digest_template->MIMEObj->head->replace( +- 'From', Encode::encode( "UTF-8", RT::Config->Get('CorrespondAddress') ) ); +- $digest_template->MIMEObj->head->replace( +- 'To', Encode::encode( "UTF-8", $to ) ); +- +- if ($print) { +- $digest_template->MIMEObj->print; +- return 1; +- } else { +- return RT::Interface::Email::SendEmail( Entity => $digest_template->MIMEObj) +- } +-} +- +-# =item mark_transactions_sent( $frequency, $user, @txn_list ); +-# +-# Takes a frequency string (either 'daily' or 'weekly'), a user and one or more +-# transaction objects as its arguments. Marks the given deferred +-# notifications as sent. +-# +-# =cut +- +-sub mark_transactions_sent { +- my ( $freq, $user, @txns ) = @_; +- return unless $freq =~ /(daily|weekly)/; +- return unless @txns; +- foreach my $txn (@txns) { +- +- # Grab the attribute, mark the "sent" as true, and store the new +- # value. +- if ( my $attr = $txn->FirstAttribute('DeferredRecipients') ) { +- my $deferred = $attr->Content; +- $deferred->{$freq}->{$user}->{'_sent'} = 1; +- $txn->SetAttribute( +- Name => 'DeferredRecipients', +- Description => 'Deferred recipients for this message', +- Content => $deferred, +- ); +- } +- } +-} +- +-sub since_date { +- my $frequency = shift; +- +- # Specify a short time for digest overlap, in case we aren't starting +- # this process exactly on time. +- my $OVERLAP_HEDGE = -30; +- +- my $since_date = RT::Date->new( RT->SystemUser ); +- $since_date->Set( Format => 'unix', Value => time() ); +- if ( $frequency eq 'daily' ) { +- $since_date->AddDays(-1); +- } else { +- $since_date->AddDays(-7); +- } +- +- $since_date->AddSeconds($OVERLAP_HEDGE); +- +- return $since_date; +-} +- +-sub find_transactions { +- my $frequency = shift; +- my $since_date = since_date($frequency); +- +- my $txns = RT::Transactions->new( RT->SystemUser ); +- +- # First limit to recent transactions. +- $txns->Limit( +- FIELD => 'Created', +- OPERATOR => '>', +- VALUE => $since_date->ISO +- ); +- +- # Next limit to ticket transactions. +- $txns->Limit( +- FIELD => 'ObjectType', +- OPERATOR => '=', +- VALUE => 'RT::Ticket', +- ENTRYAGGREGATOR => 'AND' +- ); +- my $all_digest = {}; +- my $sent_transactions = {}; +- +- while ( my $txn = $txns->Next ) { +- my $ticket = $txn->Ticket; +- my $queue = $txn->TicketObj->QueueObj->Name; +- # Xxx todo - may clobber if two queues have the same name +- foreach my $user ( $txn->DeferredRecipients($frequency) ) { +- $all_digest->{$user}->{$queue}->{$ticket}->{ $txn->id } = $txn; +- $sent_transactions->{$user}->{ $txn->id } = $txn; +- } +- } +- +- return ( $all_digest, $sent_transactions ); +-} +- +-sub build_digest_for_user { +- my $user = shift; +- my $user_digest = shift; +- +- my $contents_list = ''; # Holds the digest index. +- my $contents_body = ''; # Holds the digest body. +- +- # Has the user been disabled since a message was deferred on his/her +- # behalf? +- my $user_obj = RT::User->new( RT->SystemUser ); +- $user_obj->LoadByEmail($user); +- if ( $user_obj->PrincipalObj->Disabled ) { +- print STDERR loc("Skipping disabled user") . " $user\n"; +- next; +- } +- +- print loc("Message for user") . " $user:\n\n" if $print; +- foreach my $queue ( keys %$user_digest ) { +- $contents_list .= "Queue $queue:\n"; +- $contents_body .= "Queue $queue:\n"; +- foreach my $ticket ( sort keys %{ $user_digest->{$queue} } ) { +- my $tkt_txns = $user_digest->{$queue}->{$ticket}; +- my $ticket_obj = RT::Ticket->new( RT->SystemUser ); +- $ticket_obj->Load($ticket); +- +- # Spit out the index entry for this ticket. +- my $ticket_title = sprintf( +- "#%d %s [%s]\t%s\n", +- $ticket, $ticket_obj->Status, $ticket_obj->OwnerObj->Name, +- $ticket_obj->Subject +- ); +- $contents_list .= $ticket_title; +- +- # Spit out the messages for the transactions on this ticket. +- $contents_body .= "\n== $ticket_title\n"; +- foreach my $txn ( sort keys %$tkt_txns ) { +- my $top = $tkt_txns->{$txn}->Attachments->First; +- +- # $top contains the top-most RT::Attachment with our +- # outgoing message. It may not be the MIME part with +- # the content. Print a few headers from it for +- # clarity's sake. +- $contents_body .= "From: " . $top->GetHeader('From') . "\n"; +- my $date = $top->GetHeader('Date '); +- unless ($date) { +- my $txn_obj = RT::Transaction->new( RT->SystemUser ); +- $txn_obj->Load($txn); +- my $date_obj = RT::Date->new( RT->SystemUser ); +- $date_obj->Set( +- Format => 'sql', +- Value => $txn_obj->Created +- ); +- $date = strftime( '%a, %d %b %Y %H:%M:%S %z', +- @{ [ localtime( $date_obj->Unix ) ] } ); +- } +- $contents_body .= "Date: $date\n\n"; +- $contents_body .= $tkt_txns->{$txn}->ContentObj->Content . "\n"; +- $contents_body .= "-------\n"; +- } # foreach transaction +- } # foreach ticket +- } # foreach queue +- +- return ( $contents_list, $contents_body ); +- +-} +- +-__END__ +- +-=head1 NAME +- +-rt-email-digest - dispatch deferred notifications as a per-user digest +- +-=head1 SYNOPSIS +- +- rt-email-digest -m (daily|weekly) [--print] [--help] +- +-=head1 DESCRIPTION +- +-This script is a tool to dispatch all deferred RT notifications as a per-user +-object. +- +-=head1 OPTIONS +- +-=over +- +-=item mode +- +-Specify whether this is a daily or weekly run. +- +---mode is equal to -m +- +-=item print +- +-Print the resulting digest messages to STDOUT; don't mail them. Do not mark them as sent +- +---print is equal to -p +- +-=item help +- +-Print this message +- +---help is equal to -h +- +-=back +diff --git a/sbin/rt-email-group-admin b/sbin/rt-email-group-admin +deleted file mode 100755 +index a526a96..0000000 +--- a/sbin/rt-email-group-admin ++++ /dev/null +@@ -1,520 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-=head1 NAME +- +-rt-email-group-admin - Command line tool for administrating NotifyGroup actions +- +-=head1 SYNOPSIS +- +- rt-email-group-admin --list +- rt-email-group-admin --create 'Notify foo team' --group Foo +- rt-email-group-admin --create 'Notify foo team as comment' --comment --group Foo +- rt-email-group-admin --create 'Notify group Foo and Bar' --group Foo --group Bar +- rt-email-group-admin --create 'Notify user foo@xxxxxxx' --user foo@xxxxxxx +- rt-email-group-admin --create 'Notify VIPs' --user vip1@xxxxxxx +- rt-email-group-admin --add 'Notify VIPs' --user vip2@xxxxxxx --group vip1 --user vip3@xxxxxxx +- rt-email-group-admin --rename 'Notify VIPs' --newname 'Inform VIPs' +- rt-email-group-admin --switch 'Notify VIPs' +- rt-email-group-admin --delete 'Notify user foo@xxxxxxx' +- +-=head1 DESCRIPTION +- +-This script list, create, modify or delete scrip actions in the RT DB. Once +-you've created an action you can use it in a scrip. +- +-For example you can create the following action using this script: +- +- rt-email-group-admin --create 'Notify developers' --group 'Development Team' +- +-Then you can add the followoing scrip to your Bugs queue: +- +- Condition: On Create +- Action: Notify developers +- Template: Transaction +- Stage: TransactionCreate +- +-Your development team will be notified on every new ticket in the queue. +- +-=cut +- +-use warnings; +-use strict; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long qw(GetOptions); +-Getopt::Long::Configure( "pass_through" ); +- +-our $cmd = 'usage'; +-our $opts = {}; +- +-sub parse_args { +- my $tmp; +- if ( GetOptions( 'list' => \$tmp ) && $tmp ) { +- $cmd = 'list'; +- } +- elsif ( GetOptions( 'create=s' => \$tmp ) && $tmp ) { +- $cmd = 'create'; +- $opts->{'name'} = $tmp; +- $opts->{'groups'} = []; +- $opts->{'users'} = []; +- GetOptions( 'comment' => \$opts->{'comment'} ); +- GetOptions( 'group:s@' => $opts->{'groups'} ); +- GetOptions( 'user:s@' => $opts->{'users'} ); +- unless ( @{ $opts->{'users'} } + @{ $opts->{'groups'} } ) { +- usage(); +- exit(-1); +- } +- } +- elsif ( GetOptions( 'add=s' => \$tmp ) && $tmp ) { +- $cmd = 'add'; +- $opts->{'name'} = $tmp; +- $opts->{'groups'} = []; +- $opts->{'users'} = []; +- GetOptions( 'group:s@' => $opts->{'groups'} ); +- GetOptions( 'user:s@' => $opts->{'users'} ); +- unless ( @{ $opts->{'users'} } + @{ $opts->{'groups'} } ) { +- usage(); +- exit(-1); +- } +- } +- elsif ( GetOptions( 'switch=s' => \$tmp ) && $tmp ) { +- $cmd = 'switch'; +- $opts->{'name'} = $tmp; +- } +- elsif ( GetOptions( 'rename=s' => \$tmp ) && $tmp ) { +- $cmd = 'rename'; +- $opts->{'name'} = $tmp; +- GetOptions( 'newname=s' => \$opts->{'newname'} ); +- unless ( $opts->{'newname'} ) { +- usage(); +- exit(-1); +- } +- } +- elsif ( GetOptions( 'delete=s' => \$tmp ) && $tmp) { +- $cmd = 'delete'; +- $opts->{'name'} = $tmp; +- } else { +- $cmd = 'usage'; +- } +- +- return; +-} +- +-sub usage { +- require Pod::Usage; +- Pod::Usage::pod2usage({ verbose => 2 }); +-} +- +-my $help; +-if ( GetOptions( 'help|h' => \$help ) && $help ) { +- usage(); +- exit; +-} +- +-parse_args(); +- +-require RT; +-RT->LoadConfig; +-RT->Init; +- +-require RT::Principal; +-require RT::User; +-require RT::Group; +-require RT::ScripActions; +- +- +-{ +- eval "main::$cmd()"; +- if ( $@ ) { +- print STDERR $@ ."\n"; +- } +-} +- +-exit(0); +- +-=head1 USAGE +- +-rt-email-group-admin --COMMAND ARGS +- +-=head1 COMMANDS +- +-=head2 list +- +-Lists actions and its descriptions. +- +-=cut +- +-sub list { +- my $actions = _get_our_actions(); +- while( my $a = $actions->Next ) { +- _list( $a ); +- } +- return; +-} +- +-sub _list { +- my $action = shift; +- +- print "Name: ". $action->Name() ."\n"; +- print "Module: ". $action->ExecModule() ."\n"; +- +- my @princ = argument_to_list( $action ); +- +- print "Members: \n"; +- foreach( @princ ) { +- my $obj = RT::Principal->new( RT->SystemUser ); +- $obj->Load( $_ ); +- next unless $obj->id; +- +- print "\t". $obj->PrincipalType; +- print "\t=> ". $obj->Object->Name; +- print "(Disabled!!!)" if $obj->Disabled; +- print "\n"; +- } +- print "\n"; +- return; +-} +- +-=head2 create NAME [--comment] [--group GNAME] [--user NAME-OR-EMAIL] +- +-Creates new action with NAME and adds users and/or groups to its +-recipient list. Would be notify as comment if --comment specified. The +-user, if specified, will be autocreated if necessary. +- +-=cut +- +-sub create { +- my $actions = RT::ScripActions->new( RT->SystemUser ); +- $actions->Limit( +- FIELD => 'Name', +- VALUE => $opts->{'name'}, +- ); +- if ( $actions->Count ) { +- print STDERR "ScripAction '". $opts->{'name'} ."' allready exists\n"; +- exit(-1); +- } +- +- my @groups = _check_groups( @{ $opts->{'groups'} } ); +- my @users = _check_users( @{ $opts->{'users'} } ); +- unless ( @users + @groups ) { +- print STDERR "List of groups and users is empty\n"; +- exit(-1); +- } +- +- my $action = __create_empty( $opts->{'name'}, $opts->{'comment'} ); +- +- __add( $action, $_ ) foreach( @users ); +- __add( $action, $_ ) foreach( @groups ); +- +- return; +-} +- +-sub __create_empty { +- my $name = shift; +- my $as_comment = shift || 0; +- require RT::ScripAction; +- my $action = RT::ScripAction->new( RT->SystemUser ); +- $action->Create( +- Name => $name, +- Description => "Created with rt-email-group-admin script", +- ExecModule => $as_comment? 'NotifyGroupAsComment': 'NotifyGroup', +- Argument => '', +- ); +- +- return $action; +-} +- +-sub _check_groups +-{ +- return map {$_->[1]} +- grep { $_->[1] ? 1: do { print STDERR "Group '$_->[0]' skipped, doesn't exist\n"; 0; } } +- map { [$_, __check_group($_)] } @_; +-} +- +-sub __check_group +-{ +- my $instance = shift; +- require RT::Group; +- my $obj = RT::Group->new( RT->SystemUser ); +- $obj->LoadUserDefinedGroup( $instance ); +- return $obj->id ? $obj : undef; +-} +- +-sub _check_users +-{ +- return map {$_->[1]} +- grep { $_->[1] ? 1: do { print STDERR "User '$_->[0]' skipped, doesn't exist and couldn't autocreate\n"; 0; } } +- map { [$_, __check_user($_)] } @_; +-} +- +-sub __check_user +-{ +- my $instance = shift; +- require RT::User; +- my $obj = RT::User->new( RT->SystemUser ); +- $obj->Load( $instance ); +- $obj->LoadByEmail( $instance ) +- if not $obj->id and $instance =~ /@/; +- +- unless ($obj->id) { +- my ($ok, $msg) = $obj->Create( +- Name => $instance, +- EmailAddress => $instance, +- Privileged => 0, +- Comments => 'Autocreated when added to notify action via rt-email-group-admin', +- ); +- print STDERR "Autocreate of user '$instance' failed: $msg\n" +- unless $ok; +- } +- +- return $obj->id ? $obj : undef; +-} +- +-=head2 add NAME [--group GNAME] [--user NAME-OR-EMAIL] +- +-Adds groups and/or users to recipients of the action NAME. The user, if +-specified, will be autocreated if necessary. +- +-=cut +- +-sub add { +- my $action = _get_action_by_name( $opts->{'name'} ); +- unless ( $action ) { +- print STDERR "ScripAction '". $opts->{'name'} ."' doesn't exist\n"; +- exit(-1); +- } +- +- my @groups = _check_groups( @{ $opts->{'groups'} } ); +- my @users = _check_users( @{ $opts->{'users'} } ); +- +- unless ( @users + @groups ) { +- print STDERR "List of groups and users is empty\n"; +- exit(-1); +- } +- +- __add( $action, $_ ) foreach @users; +- __add( $action, $_ ) foreach @groups; +- +- return; +-} +- +-sub __add +-{ +- my $action = shift; +- my $obj = shift; +- +- my @cur = argument_to_list( $action ); +- +- my $id = $obj->id; +- return if grep $_ == $id, @cur; +- +- push @cur, $id; +- +- return $action->__Set( Field => 'Argument', Value => join(',', @cur) ); +-} +- +-=head2 delete NAME +- +-Deletes action NAME if scrips doesn't use it. +- +-=cut +- +-sub delete { +- my $action = _get_action_by_name( $opts->{'name'} ); +- unless ( $action ) { +- print STDERR "ScripAction '". $opts->{'name'} ."' doesn't exist\n"; +- exit(-1); +- } +- +- require RT::Scrips; +- my $scrips = RT::Scrips->new( RT->SystemUser ); +- $scrips->Limit( FIELD => 'ScripAction', VALUE => $action->id ); +- $scrips->FindAllRows; +- if ( $scrips->Count ) { +- my @sid; +- while( my $s = $scrips->Next ) { +- push @sid, $s->id; +- } +- print STDERR "ScripAction '". $opts->{'name'} ."'" +- . " is in use by Scrip(s) ". join( ", ", map "#$_", @sid ) +- . "\n"; +- exit(-1); +- } +- +- return __delete( $action ); +-} +- +-sub __delete { +- require DBIx::SearchBuilder::Record; +- return DBIx::SearchBuilder::Record::Delete( shift ); +-} +- +-sub _get_action_by_name { +- my $name = shift; +- my $actions = _get_our_actions(); +- $actions->Limit( +- FIELD => 'Name', +- VALUE => $name +- ); +- +- if ( $actions->Count > 1 ) { +- print STDERR "More then one ScripAction with name '$name'\n"; +- } +- +- return $actions->First; +-} +- +-=head2 switch NAME +- +-Switch action NAME from notify as correspondence to comment and back. +- +-=cut +- +-sub switch { +- my $action = _get_action_by_name( $opts->{'name'} ); +- unless ( $action ) { +- print STDERR "ScripAction '". $opts->{'name'} ."' doesn't exist\n"; +- exit(-1); +- } +- +- my %h = ( +- NotifyGroup => 'NotifyGroupAsComment', +- NotifyGroupAsComment => 'NotifyGroup' +- ); +- +- return $action->__Set( +- Field => 'ExecModule', +- Value => $h{ $action->ExecModule } +- ); +-} +- +-=head2 rename NAME --newname NEWNAME +- +-Renames action NAME to NEWNAME. +- +-=cut +- +-sub rename { +- my $action = _get_action_by_name( $opts->{'name'} ); +- unless ( $action ) { +- print STDERR "ScripAction '". $opts->{'name'} ."' doesn't exist\n"; +- exit(-1); +- } +- +- my $actions = RT::ScripActions->new( RT->SystemUser ); +- $actions->Limit( FIELD => 'Name', VALUE => $opts->{'newname'} ); +- if ( $actions->Count ) { +- print STDERR "ScripAction '". $opts->{'newname'} ."' allready exists\n"; +- exit(-1); +- } +- +- return $action->__Set( +- Field => 'Name', +- Value => $opts->{'newname'}, +- ); +-} +- +-=head2 NOTES +- +-If command has option --group or --user then you can use it more then once, +-if other is not specified. +- +-=cut +- +-############### +-#### Utils #### +-############### +- +-sub argument_to_list { +- my $action = shift; +- require RT::Action::NotifyGroup; +- return RT::Action::NotifyGroup->__SplitArg( $action->Argument ); +-} +- +-sub _get_our_actions { +- my $actions = RT::ScripActions->new( RT->SystemUser ); +- $actions->Limit( +- FIELD => 'ExecModule', +- VALUE => 'NotifyGroup', +- ENTRYAGGREGATOR => 'OR', +- ); +- $actions->Limit( +- FIELD => 'ExecModule', +- VALUE => 'NotifyGroupAsComment', +- ENTRYAGGREGATOR => 'OR', +- ); +- +- return $actions; +-} +- +-=head1 AUTHOR +- +-Ruslan U. Zakirov E<lt>ruz@xxxxxxxxxxxxxxxxxx<gt> +- +-=head1 SEE ALSO +- +-L<RT::Action::NotifyGroup>, L<RT::Action::NotifyGroupAsComment> +- +-=cut +diff --git a/sbin/rt-fulltext-indexer b/sbin/rt-fulltext-indexer +deleted file mode 100755 +index c7a99c0..0000000 +--- a/sbin/rt-fulltext-indexer ++++ /dev/null +@@ -1,416 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +-no warnings 'once'; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-BEGIN { +- use RT; +- RT::LoadConfig(); +- RT::Init(); +-}; +-use RT::Interface::CLI (); +- +-my %OPT = ( +- help => 0, +- debug => 0, +- quiet => 0, +-); +-my @OPT_LIST = qw(help|h! debug! quiet); +- +-my $db_type = RT->Config->Get('DatabaseType'); +-if ( $db_type eq 'Pg' ) { +- %OPT = ( +- %OPT, +- limit => 0, +- all => 0, +- ); +- push @OPT_LIST, 'limit=i', 'all!'; +-} +-elsif ( $db_type eq 'mysql' ) { +- %OPT = ( +- %OPT, +- limit => 0, +- all => 0, +- ); +- push @OPT_LIST, 'limit=i', 'all!'; +-} +-elsif ( $db_type eq 'Oracle' ) { +- %OPT = ( +- %OPT, +- memory => '2M', +- ); +- push @OPT_LIST, qw(memory=s); +-} +- +-use Getopt::Long qw(GetOptions); +-GetOptions( \%OPT, @OPT_LIST ); +- +-if ( $OPT{'help'} ) { +- RT::Interface::CLI->ShowHelp( +- Sections => 'NAME|DESCRIPTION|'. uc($db_type), +- ); +-} +- +-use Fcntl ':flock'; +-if ( !flock main::DATA, LOCK_EX | LOCK_NB ) { +- if ( $OPT{quiet} ) { +- RT::Logger->info("$0 is already running; aborting silently, as requested"); +- exit; +- } +- else { +- print STDERR "$0 is already running\n"; +- exit 1; +- } +-} +- +-my $fts_config = RT->Config->Get('FullTextSearch') || {}; +-unless ( $fts_config->{'Enable'} ) { +- print STDERR <<EOT; +- +-Full text search is disabled in your RT configuration. Run +-/opt/rt4/sbin/rt-setup-fulltext-index to configure and enable it. +- +-EOT +- exit 1; +-} +-unless ( $fts_config->{'Indexed'} ) { +- print STDERR <<EOT; +- +-Full text search is enabled in your RT configuration, but not with any +-full-text database indexing -- hence this tool is not required. Read +-the documentation for %FullTextSearch in your RT_Config for more details. +- +-EOT +- exit 1; +-} +- +-if ( $db_type eq 'Oracle' ) { +- my $index = $fts_config->{'IndexName'} || 'rt_fts_index'; +- $RT::Handle->dbh->do( +- "begin ctx_ddl.sync_index(?, ?); end;", undef, +- $index, $OPT{'memory'} +- ); +- exit; +-} elsif ( $fts_config->{Sphinx} ) { +- print STDERR <<EOT; +- +-Updates to the external Sphinx index are done via running the sphinx +-`indexer` tool: +- +- indexer rt +- +-EOT +- exit 1; +-} +- +-my @types = qw(text html); +-foreach my $type ( @types ) { +- REDO: +- my $attachments = attachments($type); +- $attachments->Limit( +- FIELD => 'id', +- OPERATOR => '>', +- VALUE => last_indexed($type) +- ); +- $attachments->OrderBy( FIELD => 'id', ORDER => 'asc' ); +- $attachments->RowsPerPage( $OPT{'limit'} || 100 ); +- +- my $found = 0; +- while ( my $a = $attachments->Next ) { +- next if filter( $type, $a ); +- debug("Found attachment #". $a->id ); +- my $txt = extract($type, $a) or next; +- $found++; +- process( $type, $a, $txt ); +- debug("Processed attachment #". $a->id ); +- } +- goto REDO if $OPT{'all'} and $attachments->Count == ($OPT{'limit'} || 100) +-} +- +-sub attachments { +- my $type = shift; +- my $res = RT::Attachments->new( RT->SystemUser ); +- my $txn_alias = $res->Join( +- ALIAS1 => 'main', +- FIELD1 => 'TransactionId', +- TABLE2 => 'Transactions', +- FIELD2 => 'id', +- ); +- $res->Limit( +- ALIAS => $txn_alias, +- FIELD => 'ObjectType', +- VALUE => 'RT::Ticket', +- ); +- my $ticket_alias = $res->Join( +- ALIAS1 => $txn_alias, +- FIELD1 => 'ObjectId', +- TABLE2 => 'Tickets', +- FIELD2 => 'id', +- ); +- $res->Limit( +- ALIAS => $ticket_alias, +- FIELD => 'Status', +- OPERATOR => '!=', +- VALUE => 'deleted' +- ); +- +- return goto_specific( +- suffix => $type, +- error => "Don't know how to find $type attachments", +- arguments => [$res], +- ); +-} +- +-sub last_indexed { +- my ($type) = (@_); +- return goto_specific( +- suffix => $db_type, +- error => "Don't know how to find last indexed $type attachment for $db_type DB", +- arguments => \@_, +- ); +-} +- +-sub filter { +- my $type = shift; +- return goto_specific( +- suffix => $type, +- arguments => \@_, +- ); +-} +- +-sub extract { +- my $type = shift; +- return goto_specific( +- suffix => $type, +- error => "No way to convert $type attachment into text", +- arguments => \@_, +- ); +-} +- +-sub process { +- return goto_specific( +- suffix => $db_type, +- error => "No processer for $db_type DB", +- arguments => \@_, +- ); +-} +- +-sub last_indexed_mysql { last_indexed_pg(@_); } +-sub process_mysql { +- my ($type, $attachment, $text) = (@_); +- +- my $dbh = $RT::Handle->dbh; +- my $table = $fts_config->{'Table'}; +- +- my $query; +- if ( my ($id) = $dbh->selectrow_array("SELECT id FROM $table WHERE id = ?", undef, $attachment->id) ) { +- $query = "UPDATE $table SET Content = ? WHERE id = ?"; +- } else { +- $query = "INSERT INTO $table(Content, id) VALUES(?, ?)"; +- } +- +- $dbh->do( $query, undef, $$text, $attachment->id ); +-} +- +-sub last_indexed_pg { +- my $type = shift; +- my $attachments = attachments( $type ); +- my $alias = 'main'; +- if ( $fts_config->{'Table'} && $fts_config->{'Table'} ne 'Attachments' ) { +- $alias = $attachments->Join( +- TYPE => 'left', +- FIELD1 => 'id', +- TABLE2 => $fts_config->{'Table'}, +- FIELD2 => 'id', +- ); +- } +- $attachments->Limit( +- ALIAS => $alias, +- FIELD => $fts_config->{'Column'}, +- OPERATOR => 'IS NOT', +- VALUE => 'NULL', +- ); +- $attachments->OrderBy( FIELD => 'id', ORDER => 'desc' ); +- $attachments->RowsPerPage( 1 ); +- my $res = $attachments->First; +- return 0 unless $res; +- return $res->id; +-} +- +-sub process_pg { +- my ($type, $attachment, $text) = (@_); +- +- my $dbh = $RT::Handle->dbh; +- my $table = $fts_config->{'Table'}; +- my $column = $fts_config->{'Column'}; +- +- my $query; +- if ( $table ) { +- if ( my ($id) = $dbh->selectrow_array("SELECT id FROM $table WHERE id = ?", undef, $attachment->id) ) { +- $query = "UPDATE $table SET $column = to_tsvector(?) WHERE id = ?"; +- } else { +- $query = "INSERT INTO $table($column, id) VALUES(to_tsvector(?), ?)"; +- } +- } else { +- $query = "UPDATE Attachments SET $column = to_tsvector(?) WHERE id = ?"; +- } +- +- my $status = eval { $dbh->do( $query, undef, $$text, $attachment->id ) }; +- unless ( $status ) { +- if ( $dbh->err == 7 && $dbh->state eq '54000' ) { +- warn "Attachment @{[$attachment->id]} cannot be indexed. Most probably it contains too many unique words. Error: ". $dbh->errstr; +- } elsif ( $dbh->err == 7 && $dbh->state eq '22021' ) { +- warn "Attachment @{[$attachment->id]} cannot be indexed. Most probably it contains invalid UTF8 bytes. Error: ". $dbh->errstr; +- } else { +- die "error: ". $dbh->errstr; +- } +- +- # Insert an empty tsvector, so we count this row as "indexed" +- # for purposes of knowing where to pick up +- eval { $dbh->do( $query, undef, "", $attachment->id ) } +- or die "Failed to insert empty tsvector: " . $dbh->errstr; +- } +-} +- +-sub attachments_text { +- my $res = shift; +- $res->Limit( FIELD => 'ContentType', VALUE => 'text/plain' ); +- return $res; +-} +- +-sub extract_text { +- my $attachment = shift; +- my $text = $attachment->Content; +- return undef unless defined $text && length($text); +- return \$text; +-} +- +-sub attachments_html { +- my $res = shift; +- $res->Limit( FIELD => 'ContentType', VALUE => 'text/html' ); +- return $res; +-} +- +-sub filter_html { +- my $attachment = shift; +- if ( my $parent = $attachment->ParentObj ) { +-# skip html parts that are alternatives +- return 1 if $parent->id +- && $parent->ContentType eq 'mulitpart/alternative'; +- } +- return 0; +-} +- +-sub extract_html { +- my $attachment = shift; +- my $text = $attachment->Content; +- return undef unless defined $text && length($text); +-# the rich text editor generates html entities for characters +-# but Pg doesn't index them, so decode to something it can index. +- require HTML::Entities; +- HTML::Entities::decode_entities($text); +- return \$text; +-} +- +-sub goto_specific { +- my %args = (@_); +- +- my $func = (caller(1))[3]; +- $func =~ s/.*:://; +- my $call = $func ."_". lc $args{'suffix'}; +- unless ( defined &$call ) { +- return undef unless $args{'error'}; +- require Carp; Carp::croak( $args{'error'} ); +- } +- @_ = @{ $args{'arguments'} }; +- goto &$call; +-} +- +- +-# helper functions +-sub debug { print @_, "\n" if $OPT{debug}; 1 } +-sub error { $RT::Logger->error(_(@_)); 1 } +-sub warning { $RT::Logger->warn(_(@_)); 1 } +- +-=head1 NAME +- +-rt-fulltext-indexer - Indexer for full text search +- +-=head1 DESCRIPTION +- +-This is a helper script to keep full text indexes in sync with data. +-Read F<docs/full_text_indexing.pod> for complete details on how and when +-to run it. +- +-=head1 AUTHOR +- +-Ruslan Zakirov E<lt>ruz@xxxxxxxxxxxxxxxxxx<gt>, +-Alex Vandiver E<lt>alexmv@xxxxxxxxxxxxxxxxxx<gt> +- +-=cut +- +-__DATA__ +diff --git a/sbin/rt-importer b/sbin/rt-importer +deleted file mode 100755 +index 785fcc2..0000000 +--- a/sbin/rt-importer ++++ /dev/null +@@ -1,283 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# fix lib paths, some may be relative +-BEGIN { +- require File::Spec; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- unless ($bin_path) { +- if ( File::Spec->file_name_is_absolute(__FILE__) ) { +- $bin_path = ( File::Spec->splitpath(__FILE__) )[1]; +- } +- else { +- require FindBin; +- no warnings "once"; +- $bin_path = $FindBin::Bin; +- } +- } +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use RT; +-RT::LoadConfig(); +-RT::Init(); +- +-@RT::Record::ISA = qw( DBIx::SearchBuilder::Record RT::Base ); +- +-use RT::Migrate; +-use RT::Migrate::Importer::File; +-use Getopt::Long; +-use Pod::Usage qw//; +-use Time::HiRes qw//; +- +-my %OPT = (resume => 1); +-GetOptions( +- \%OPT, +- "help|?", +- "quiet|q!", +- "list|l!", +- +- "resume!", +- "originalid|i=s", +- +- "ask", +- "ignore-errors", +- +- "dump=s@", +-) or Pod::Usage::pod2usage(); +- +-Pod::Usage::pod2usage(-verbose => 1) if $OPT{help}; +- +-Pod::Usage::pod2usage() unless @ARGV == 1; +-my ($dir) = @ARGV; +-$dir =~ s|/$||; +-die "No such directory $dir\n" unless -d $dir; +-die "$dir doesn't appear to contain serialized data\n" +- unless -f "$dir/001.dat"; +- +-if ($OPT{dump}) { +- die "Dumping objects only works in conjunction with --list\n" +- unless $OPT{list}; +- +- $OPT{dump} = [ split /,/, join(',', @{$OPT{dump}}) ]; +-} +- +-my $error_handler; +-if ($OPT{ask}) { +- die "Interactive mode (--ask) doesn't work when STDERR and STDIN aren't terminals.\n" +- unless -t STDERR and -t STDIN; +- +- $error_handler = sub { +- my $importer = shift; +- local $| = 1; +- print STDERR "\n", @_, "\n"; +- print STDERR "Hit any key to abort import, or type 'ignore' to continue anyway.\n"; +- print STDERR "Continuing may leave you with a corrupt database. > "; +- chomp( my $resp = <STDIN> ); +- return lc($resp) eq 'ignore'; +- }; +-} +-elsif ($OPT{'ignore-errors'}) { +- $error_handler = sub { +- my $importer = shift; +- warn "Ignoring error: ", @_; +- return 1; +- }; +-} +- +-my $import = RT::Migrate::Importer::File->new( +- Directory => $dir, +- OriginalId => $OPT{originalid}, +- DumpObjects => $OPT{dump}, +- Resume => $OPT{resume}, +- HandleError => $error_handler, +-); +- +-if ($import->Metadata and -t STDOUT and not $OPT{quiet}) { +- $import->Progress( +- RT::Migrate::progress( +- counts => sub { $import->ObjectCount }, +- max => $import->Metadata->{ObjectCount}, +- ) +- ); +-} +- +-my $log = RT::Migrate::setup_logging( $dir => 'importer.log' ); +-print "Logging warnings and errors to $log\n" if $log; +- +-my %counts; +-if ($OPT{list}) { +- %counts = $import->List; +- +- my $org = $import->Organization; +- print "=========== Dump of $org ===========\n\n"; +-} else { +- %counts = $import->Import; +- +- my $org = $import->Organization; +- print "========== Import of $org ==========\n\n"; +-} +- +-print "Total object counts:\n"; +-for (sort {$counts{$b} <=> $counts{$a}} keys %counts) { +- printf "%8d %s\n", $counts{$_}, $_; +-} +- +-my @missing = $import->Missing; +-if (@missing) { +- warn "The following UIDs were expected but never observed:\n"; +- warn " $_\n" for @missing; +-} +- +-my @invalid = $import->Invalid; +-if (@invalid) { +- warn "The following UIDs (serialized => imported) referred to objects missing from the original database:\n"; +- for my $info (@invalid) { +- my $uid = delete $info->{uid}; +- my $obj = $import->LookupObj($uid); +- warn sprintf " %s => %s (%s)\n", +- $uid, +- ($obj && $obj->Id ? $obj->UID : '(not imported)'), +- join(", ", map { "$_ => $info->{$_}" } +- grep { defined $info->{$_} } +- sort keys %$info); +- } +-} +- +-if ($log and -s $log) { +- print STDERR "\n! Some warnings or errors occurred during import." +- ."\n! Please see $log for details.\n\n"; +-} +- +-exit @missing; +- +-=head1 NAME +- +-rt-importer - Import a serialized RT database on top of the current one +- +-=head1 SYNOPSIS +- +- rt-importer path/to/export/directory +- +-This script is used to import the contents of a dump created by +-C<rt-serializer>. It will create all of the objects in the dump in the +-current database; this may include users, queues, and tickets. +- +-It is possible to stop the import process with ^C; it can be later +-resumed by re-running the importer. +- +-=head2 OPTIONS +- +-=over +- +-=item B<--list> +- +-Print a summary of the data contained in the dump. +- +-=item B<--originalid> I<cfname> +- +-Places the original ticket organization and ID into a global custom +-field with the given name. If no global ticket custom field with that +-name is found in the current database, it will create one. +- +-=item B<--ask> +- +-Prompt for action when an error occurs inserting a record into the +-database. This can often happen when importing data from very old RTs +-where some attachments (usually spam) contain invalid UTF-8. +- +-The importer will pause and ask if you want to ignore the error and +-continue on or abort (potentially to restart later). Ignoring errors +-will result in missing records in the database, which may cause database +-integrity problems later. If you ignored any errors, you should run +-C<rt-validator> after import. +- +-=item B<--ignore-errors> +- +-Ignore all record creation errors and continue on when importing. This +-is equivalent to running with C<--ask> and manually typing "ignore" at +-every prompt. You should always run C<rt-validator> after importing +-with errors ignored. +- +-B<This option can be dangerous and leave you with a broken RT!> +- +-=item B<--dump> I<class>[,I<class>] +- +-Prints L<Data::Dumper> representations of the objects of type I<class> in the +-serialized data. This is mostly useful for debugging. +- +-Works only in conjunction with C<--list>. +- +-=back +- +- +-=head1 CLONED DATA +- +-Some dumps may have been taken as complete clones of the RT system, +-which are only suitable for inserting into a schema with no data in it. +-You can setup the required database state for the receiving RT instance +-by running: +- +- /opt/rt4/sbin/rt-setup-database --action create,schema,acl --prompt-for-dba-password +- +-The normal C<make initdb> step will B<not> work because it also inserts +-core system data. +- +- +-=cut +diff --git a/sbin/rt-preferences-viewer b/sbin/rt-preferences-viewer +deleted file mode 100755 +index 7bb6e70..0000000 +--- a/sbin/rt-preferences-viewer ++++ /dev/null +@@ -1,144 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-BEGIN { +- use RT; +- RT::LoadConfig(); +- RT::Init(); +-}; +- +-use Getopt::Long; +-my %opt; +-GetOptions( \%opt, 'help|h', 'user|u=s', 'option|o=s' ); +- +-if ( $opt{help} ) { +- require Pod::Usage; +- Pod::Usage::pod2usage({ verbose => 2 }); +- exit; +-} +- +-require RT::Attributes; +-my $attrs = RT::Attributes->new( RT->SystemUser ); +-$attrs->Limit( FIELD => 'Name', VALUE => 'Pref-RT::System-1' ); +-$attrs->Limit( FIELD => 'ObjectType', VALUE => 'RT::User' ); +- +-if ($opt{user}) { +- my $user = RT::User->new( RT->SystemUser ); +- my ($val, $msg) = $user->Load($opt{user}); +- unless ($val) { +- RT->Logger->error("Unable to load $opt{user}: $msg"); +- exit(1); +- } +- $attrs->Limit( FIELD => 'ObjectId', VALUE => $user->Id ); +-} +- +-use Data::Dumper; +-$Data::Dumper::Terse = 1; +- +-while (my $attr = $attrs->Next ) { +- my $user = RT::User->new( RT->SystemUser ); +- my ($val, $msg) = $user->Load($attr->ObjectId); +- unless ($val) { +- RT->Logger->warn("Unable to load User ".$attr->ObjectId." $msg"); +- next; +- } +- next if $user->Disabled; +- +- my $content = $attr->Content; +- if ( my $config_name = $opt{option} ) { +- if ( exists $content->{$config_name} ) { +- my $setting = $content->{$config_name}; +- print $user->Name, "\t$config_name: $setting\n"; +- } +- } else { +- print $user->Name, " => ", Dumper($content); +- } +- +-} +- +-__END__ +- +-=head1 NAME +- +-rt-preferences-viewer - show user defined preferences +- +-=head1 SYNOPSIS +- +- rt-preferences-viewer +- +- rt-preferences-viewer --user=falcone +- show only the falcone user's preferences +- +- rt-preferences-viewer --option=EmailFrequency +- show users who have set the EmailFrequence config option +- +-=head1 DESCRIPTION +- +-This script shows user settings of preferences. If a user is using the system +-default, it will not be listed. You can limit to a user name or id or to users +-with a particular option set. +diff --git a/sbin/rt-serializer b/sbin/rt-serializer +deleted file mode 100755 +index b90a709..0000000 +--- a/sbin/rt-serializer ++++ /dev/null +@@ -1,399 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# fix lib paths, some may be relative +-BEGIN { +- require File::Spec; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- unless ($bin_path) { +- if ( File::Spec->file_name_is_absolute(__FILE__) ) { +- $bin_path = ( File::Spec->splitpath(__FILE__) )[1]; +- } +- else { +- require FindBin; +- no warnings "once"; +- $bin_path = $FindBin::Bin; +- } +- } +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use RT; +-RT::LoadConfig(); +-RT::Init(); +- +-@RT::Record::ISA = qw( DBIx::SearchBuilder::Record RT::Base ); +- +-use RT::Migrate; +-use RT::Migrate::Serializer::File; +-use Getopt::Long; +-use Pod::Usage qw//; +-use Time::HiRes qw//; +- +-my %OPT; +-GetOptions( +- \%OPT, +- "help|?", +- "verbose|v!", +- "quiet|q!", +- +- "directory|d=s", +- "force|f!", +- "size|s=i", +- +- "users!", +- "groups!", +- "deleted!", +- +- "scrips!", +- "tickets!", +- "acls!", +- +- "clone", +- "incremental", +- +- "gc=i", +- "page=i", +-) or Pod::Usage::pod2usage(); +- +-Pod::Usage::pod2usage(-verbose => 1) if $OPT{help}; +- +-my %args; +-$args{Directory} = $OPT{directory}; +-$args{Force} = $OPT{force}; +-$args{MaxFileSize} = $OPT{size} if $OPT{size}; +- +-$args{AllUsers} = $OPT{users} if defined $OPT{users}; +-$args{AllGroups} = $OPT{groups} if defined $OPT{groups}; +-$args{FollowDeleted} = $OPT{deleted} if defined $OPT{deleted}; +- +-$args{FollowScrips} = $OPT{scrips} if defined $OPT{scrips}; +-$args{FollowTickets} = $OPT{tickets} if defined $OPT{tickets}; +-$args{FollowACL} = $OPT{acls} if defined $OPT{acls}; +- +-$args{Clone} = $OPT{clone} if $OPT{clone}; +-$args{Incremental} = $OPT{incremental} if $OPT{incremental}; +- +-$args{GC} = defined $OPT{gc} ? $OPT{gc} : 5000; +-$args{Page} = defined $OPT{page} ? $OPT{page} : 100; +- +-if (($OPT{clone} or $OPT{incremental}) +- and grep { /^(users|groups|deleted|scrips|tickets|acls)$/ } keys %OPT) { +- die "You cannot specify object types when cloning.\n\nPlease see $0 --help.\n"; +-} +- +-my $walker; +- +-my $gnuplot = `which gnuplot`; +-my $msg = ""; +-if (-t STDOUT and not $OPT{verbose} and not $OPT{quiet}) { +- $args{Progress} = RT::Migrate::progress( +- top => \&gnuplot, +- bottom => sub { print "\n$msg"; $msg = ""; }, +- counts => sub { $walker->ObjectCount }, +- max => { estimate() }, +- ); +- $args{MessageHandler} = sub { +- print "\r", " "x60, "\r", $_[-1]; $msg = $_[-1]; +- }; +- $args{Verbose} = 0; +-} +-$args{Verbose} = 0 if $OPT{quiet}; +- +- +-$walker = RT::Migrate::Serializer::File->new( %args ); +- +-my $log = RT::Migrate::setup_logging( $walker->{Directory} => 'serializer.log' ); +-print "Logging warnings and errors to $log\n" if $log; +- +-print "Beginning database serialization..."; +-my %counts = $walker->Export; +- +-my @files = $walker->Files; +-print "Wrote @{[scalar @files]} files:\n"; +-print " $_\n" for @files; +-print "\n"; +- +-print "Total object counts:\n"; +-for (sort {$counts{$b} <=> $counts{$a}} keys %counts) { +- printf "%8d %s\n", $counts{$_}, $_; +-} +- +-if ($log and -s $log) { +- print STDERR "\n! Some warnings or errors occurred during serialization." +- ."\n! Please see $log for details.\n\n"; +-} else { +- unlink $log; +-} +- +-sub estimate { +- $| = 1; +- my %e; +- +- # Expected types we'll serialize +- my @types = map {"RT::$_"} qw/ +- Queue Ticket Transaction Attachment Link +- User Group GroupMember Attribute +- CustomField CustomFieldValue +- ObjectCustomField ObjectCustomFieldValue +- /; +- +- for my $class (@types) { +- print "Estimating $class count..."; +- my $collection = $class . "s"; +- if ($collection->require) { +- my $objs = $collection->new( RT->SystemUser ); +- $objs->FindAllRows; +- $objs->UnLimit; +- $objs->{allow_deleted_search} = 1 if $class eq "RT::Ticket"; +- $e{$class} = $objs->DBIx::SearchBuilder::Count; +- } +- print "\r", " "x60, "\r"; +- } +- +- return %e; +-} +- +- +-sub gnuplot { +- my ($elapsed, $rows, $cols) = @_; +- my $length = $walker->StackSize; +- my $file = $walker->Directory . "/progress.plot"; +- open(my $dat, ">>", $file); +- printf $dat "%10.3f\t%8d\n", $elapsed, $length; +- close $dat; +- +- if ($rows <= 24 or not $gnuplot) { +- print "\n\n"; +- } elsif ($elapsed) { +- my $gnuplot = qx| +- gnuplot -e ' +- set term dumb $cols @{[$rows - 12]}; +- set xlabel "Seconds"; +- unset key; +- set xrange [0:*]; +- set yrange [0:*]; +- set title "Queue length"; +- plot "$file" using 1:2 with lines +- ' +- |; +- if ($? == 0 and $gnuplot) { +- $gnuplot =~ s/^(\s*\n)//; +- print $gnuplot; +- unlink $file; +- } else { +- warn "Couldn't run gnuplot (\$? == $?): $!\n"; +- } +- } else { +- print "\n" for 1..($rows - 13); +- } +-} +- +-=head1 NAME +- +-rt-serializer - Serialize an RT database to disk +- +-=head1 SYNOPSIS +- +- rt-validator --check && rt-serializer +- +-This script is used to write out the entire RT database to disk, for +-later import into a different RT instance. It requires that the data in +-the database be self-consistent, in order to do so; please make sure +-that the database being exported passes validation by L<rt-validator> +-before attempting to use C<rt-serializer>. +- +-While running, it will attempt to estimate the number of remaining +-objects to be serialized; these estimates are pessimistic, and will be +-incorrect if C<--no-users>, C<--no-groups>, or C<--no-tickets> are used. +- +-If the controlling terminal is large enough (more than 25 columns high) +-and the C<gnuplot> program is installed, it will also show a textual +-graph of the queue size over time. +- +-=head2 OPTIONS +- +-=over +- +-=item B<--directory> I<name> +- +-The name of the output directory to write data files to, which should +-not exist yet; it is a fatal error if it does. Defaults to +-C<< ./I<$Organization>:I<Date>/ >>, where I<$Organization> is as set in +-F<RT_SiteConfig.pm>, and I<Date> is today's date. +- +-=item B<--force> +- +-Remove the output directory before starting. +- +-=item B<--size> I<megabytes> +- +-By default, C<rt-serializer> chunks its output into data files which are +-around 32Mb in size; this option is used to set a different threshold +-size, in megabytes. Note that this is the threshold after which it +-rotates to writing a new file, and is as such the I<lower bound> on the +-size of each output file. +- +-=item B<--no-users> +- +-By default, all privileged users are serialized; passing C<--no-users> +-limits it to only those users which are referenced by serialized tickets +-and history, and are thus necessary for internal consistency. +- +-=item B<--no-groups> +- +-By default, all groups are serialized; passing C<--no-groups> limits it +-to only system-internal groups, which are needed for internal +-consistency. +- +-=item B<--no-deleted> +- +-By default, all tickets, including deleted tickets, are serialized; +-passing C<--no-deleted> skips deleted tickets during serialization. +- +-=item B<--scrips> +- +-No scrips or templates are serialized by default; this option forces all +-scrips and templates to be serialized. +- +-=item B<--acls> +- +-No ACLs are serialized by default; this option forces all ACLs to be +-serialized. +- +-=item B<--no-tickets> +- +-Skip serialization of all ticket data. +- +-=item B<--clone> +- +-Serializes your entire database, creating a clone. This option should +-be used if you want to migrate your RT database from one database type +-to another (e.g. MySQL to Postgres). It is an error to combine +-C<--clone> with any option that limits object types serialized. No +-dependency walking is performed when cloning. C<rt-importer> will detect +-that your serialized data set was generated by a clone. +- +-=item B<--incremental> +- +-Will generate an incremenal serialized dataset using the data stored in +-your IncrementalRecords database table. This assumes that you have created +-that table and run RT using the Record_Local.pm shim as documented in +-C<docs/incremental-export/>. +- +-=item B<--gc> I<n> +- +-Adjust how often the garbage collection sweep is done; lower numbers are +-more frequent. See L</GARBAGE COLLECTION>. +- +-=item B<--page> I<n> +- +-Adjust how many rows are pulled from the database in a single query. Disable +-paging by setting this to 0. Defaults to 100. +- +-Keep in mind that rows from RT's Attachments table are the limiting factor when +-determining page size. You should likely be aiming for 60-75% of your total +-memory on an otherwise unloaded box. +- +-=item B<--quiet> +- +-Do not show graphical progress UI. +- +-=item B<--verbose> +- +-Do not show graphical progress UI, but rather log was each row is +-written out. +- +-=back +- +-=head1 GARBAGE COLLECTION +- +-C<rt-serializer> maintains a priority queue of objects to serialize, or +-searches which may result in objects to serialize. When inserting into +-this queue, it does no checking if the object in question is already in +-the queue, or if the search will contain any results. These checks are +-done when the object reaches the front of the queue, or during periodic +-garbage collection. +- +-During periodic garbage collection, the entire queue is swept for +-objects which have already been serialized, occur more than once in the +-queue, and searches which contain no results in the database. This is +-done to reduce the memory footprint of the serialization process, and is +-triggered when enough new objects have been placed in the queue. This +-parameter is tunable via the C<--gc> parameter, which defaults to +-running garbage collection every 5,000 objects inserted into the queue; +-smaller numbers will result in more frequent garbage collection. +- +-The default of 5,000 is roughly tuned based on a database with several +-thousand tickets, but optimal values will vary wildly depending on +-database configuration and size. Values as low as 25 have provided +-speedups with smaller databases; if speed is a factor, experimenting +-with different C<--gc> values may be helpful. Note that there are +-significant boundary condition changes in serialization rate, as the +-queue empties and fills, causing the time estimates to be rather +-imprecise near the start and end of the process. +- +-Setting C<--gc> to 0 turns off all garbage collection. Be aware that +-this will bloat the memory usage of the serializer. Any negative value +-for C<--gc> turns off periodic garbage collection and instead objects +-already serialized or in the queue are checked for at the time they +-would be inserted. +- +-=cut +- +diff --git a/sbin/rt-server b/sbin/rt-server +deleted file mode 100755 +index d3711f1..0000000 +--- a/sbin/rt-server ++++ /dev/null +@@ -1,181 +0,0 @@ +-#!/usr/bin/perl -w +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use warnings; +-use strict; +- +-BEGIN { +- die <<EOT if ${^TAINT}; +-RT does not run under Perl's "taint mode". Remove -T from the command +-line, or remove the PerlTaintCheck parameter from your mod_perl +-configuration. +-EOT +-} +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long; +-no warnings 'once'; +- +-if (grep { m/help/ } @ARGV) { +- require Pod::Usage; +- print Pod::Usage::pod2usage( { verbose => 2 } ); +- exit; +-} +- +-require RT; +-die "Wrong version of RT $RT::VERSION found; need 4.2.*" +- unless $RT::VERSION =~ /^4\.2\./; +- +-RT->LoadConfig(); +-RT->InitPluginPaths(); +-RT->InitLogging(); +- +-require RT::Handle; +-my ($integrity, $state, $msg) = RT::Handle->CheckIntegrity; +- +-unless ( $integrity ) { +- print STDERR <<EOF; +- +-RT couldn't connect to the database where tickets are stored. +-If this is a new installation of RT, you should visit the URL below +-to configure RT and initialize your database. +- +-If this is an existing RT installation, this may indicate a database +-connectivity problem. +- +-The error RT got back when trying to connect to your database was: +- +-$msg +- +-EOF +- +- require RT::Installer; +- # don't enter install mode if the file exists but is unwritable +- if (-e RT::Installer->ConfigFile && !-w _) { +- die 'Since your configuration exists (' +- . RT::Installer->ConfigFile +- . ") but is not writable, I'm refusing to do anything.\n"; +- } +- +- RT->Config->Set( 'LexiconLanguages' => '*' ); +- RT::I18N->Init; +- +- RT->InstallMode(1); +-} else { +- RT->Init( Heavy => 1 ); +- +- my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'post'); +- unless ( $status ) { +- print STDERR $msg, "\n\n"; +- exit -1; +- } +-} +- +-# we must disconnect DB before fork +-if ($RT::Handle) { +- $RT::Handle->dbh->disconnect if $RT::Handle->dbh; +- $RT::Handle->dbh(undef); +- undef $RT::Handle; +-} +- +-require RT::PlackRunner; +-# when used as a psgi file +-if (caller) { +- return RT::PlackRunner->app; +-} +- +- +-my $r = RT::PlackRunner->new( RT->InstallMode ? ( server => 'Standalone' ) : +- $0 =~ /standalone/ ? ( server => 'Standalone' ) : +- $0 =~ /fcgi$/ ? ( server => 'FCGI', env => "deployment" ) +- : ( server => 'Starlet', env => "deployment" ) ); +-$r->parse_options(@ARGV); +- +-# Try to clean up wrong-permissions var/ +-$SIG{INT} = sub { +- local $@; +- system("chown", "-R", "www-data:www-data", "/opt/rt4/var"); +- exit 0; +-} if $> == 0; +- +-$r->run; +- +-__END__ +- +-=head1 NAME +- +-rt-server - RT standalone server +- +-=head1 SYNOPSIS +- +- # runs prefork server listening on port 8080, requires Starlet +- rt-server --port 8080 +- +- # runs server listening on port 8080 +- rt-server --server Standalone --port 8080 +- # or +- standalone_httpd --port 8080 +- +- # runs other PSGI server on port 8080 +- rt-server --server Starman --port 8080 +diff --git a/sbin/rt-server.fcgi b/sbin/rt-server.fcgi +deleted file mode 100755 +index d3711f1..0000000 +--- a/sbin/rt-server.fcgi ++++ /dev/null +@@ -1,181 +0,0 @@ +-#!/usr/bin/perl -w +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use warnings; +-use strict; +- +-BEGIN { +- die <<EOT if ${^TAINT}; +-RT does not run under Perl's "taint mode". Remove -T from the command +-line, or remove the PerlTaintCheck parameter from your mod_perl +-configuration. +-EOT +-} +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long; +-no warnings 'once'; +- +-if (grep { m/help/ } @ARGV) { +- require Pod::Usage; +- print Pod::Usage::pod2usage( { verbose => 2 } ); +- exit; +-} +- +-require RT; +-die "Wrong version of RT $RT::VERSION found; need 4.2.*" +- unless $RT::VERSION =~ /^4\.2\./; +- +-RT->LoadConfig(); +-RT->InitPluginPaths(); +-RT->InitLogging(); +- +-require RT::Handle; +-my ($integrity, $state, $msg) = RT::Handle->CheckIntegrity; +- +-unless ( $integrity ) { +- print STDERR <<EOF; +- +-RT couldn't connect to the database where tickets are stored. +-If this is a new installation of RT, you should visit the URL below +-to configure RT and initialize your database. +- +-If this is an existing RT installation, this may indicate a database +-connectivity problem. +- +-The error RT got back when trying to connect to your database was: +- +-$msg +- +-EOF +- +- require RT::Installer; +- # don't enter install mode if the file exists but is unwritable +- if (-e RT::Installer->ConfigFile && !-w _) { +- die 'Since your configuration exists (' +- . RT::Installer->ConfigFile +- . ") but is not writable, I'm refusing to do anything.\n"; +- } +- +- RT->Config->Set( 'LexiconLanguages' => '*' ); +- RT::I18N->Init; +- +- RT->InstallMode(1); +-} else { +- RT->Init( Heavy => 1 ); +- +- my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'post'); +- unless ( $status ) { +- print STDERR $msg, "\n\n"; +- exit -1; +- } +-} +- +-# we must disconnect DB before fork +-if ($RT::Handle) { +- $RT::Handle->dbh->disconnect if $RT::Handle->dbh; +- $RT::Handle->dbh(undef); +- undef $RT::Handle; +-} +- +-require RT::PlackRunner; +-# when used as a psgi file +-if (caller) { +- return RT::PlackRunner->app; +-} +- +- +-my $r = RT::PlackRunner->new( RT->InstallMode ? ( server => 'Standalone' ) : +- $0 =~ /standalone/ ? ( server => 'Standalone' ) : +- $0 =~ /fcgi$/ ? ( server => 'FCGI', env => "deployment" ) +- : ( server => 'Starlet', env => "deployment" ) ); +-$r->parse_options(@ARGV); +- +-# Try to clean up wrong-permissions var/ +-$SIG{INT} = sub { +- local $@; +- system("chown", "-R", "www-data:www-data", "/opt/rt4/var"); +- exit 0; +-} if $> == 0; +- +-$r->run; +- +-__END__ +- +-=head1 NAME +- +-rt-server - RT standalone server +- +-=head1 SYNOPSIS +- +- # runs prefork server listening on port 8080, requires Starlet +- rt-server --port 8080 +- +- # runs server listening on port 8080 +- rt-server --server Standalone --port 8080 +- # or +- standalone_httpd --port 8080 +- +- # runs other PSGI server on port 8080 +- rt-server --server Starman --port 8080 +diff --git a/sbin/rt-session-viewer b/sbin/rt-session-viewer +deleted file mode 100755 +index 3b9e98d..0000000 +--- a/sbin/rt-session-viewer ++++ /dev/null +@@ -1,114 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long; +-my %opt; +-GetOptions( \%opt, 'help|h', ); +- +-my $session_id = shift; +- +-if ( $opt{help} || !$session_id ) { +- require Pod::Usage; +- Pod::Usage::pod2usage({ verbose => 2 }); +- exit; +-} +- +-require RT; +-RT::LoadConfig(); +-RT::Init(); +- +-require RT::Interface::Web::Session; +-my %session; +- +-tie %session, 'RT::Interface::Web::Session', $session_id; +-unless ( $session{'_session_id'} eq $session_id ) { +- print STDERR "Couldn't load session $session_id\n"; +- exit 1; +-} +- +-use Data::Dumper; +-print "Content of session $session_id: ". Dumper( \%session); +- +-__END__ +- +-=head1 NAME +- +-rt-session-viewer - show the content of a user's session +- +-=head1 SYNOPSIS +- +- # show the content of a session +- rt-session-viewer 2c21c8a2909c14eff12975dd2cc7b9a3 +- +-=head1 DESCRIPTION +- +-This script deserializes and print content of a session identified +-by <session id>. May be useful for developers and for troubleshooting +-problems. +- +-=cut +diff --git a/sbin/rt-setup-database b/sbin/rt-setup-database +deleted file mode 100755 +index c694e3d..0000000 +--- a/sbin/rt-setup-database ++++ /dev/null +@@ -1,795 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-use vars qw($Nobody $SystemUser $item); +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Term::ReadKey; +-use Getopt::Long; +-use Data::GUID; +- +-$| = 1; # unbuffer all output. +- +-my %args = ( +- package => 'RT', +-); +-GetOptions( +- \%args, +- 'action=s', +- 'force', 'debug', +- 'dba=s', 'dba-password=s', 'prompt-for-dba-password', 'package=s', +- 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=s', +- 'package=s', 'ext-version=s', +- 'upgrade-from=s', 'upgrade-to=s', +- 'help|h', +-); +- +-no warnings 'once'; +-if ( $args{help} || ! $args{'action'} ) { +- require Pod::Usage; +- Pod::Usage::pod2usage({ verbose => 2 }); +- exit; +-} +- +-require RT; +-RT->LoadConfig(); +-RT->InitClasses(); +- +-# Force warnings to be output to STDERR if we're not already logging +-# them at a higher level +-RT->Config->Set( LogToSTDERR => 'warning') +- unless ( RT->Config->Get( 'LogToSTDERR' ) +- && RT->Config->Get( 'LogToSTDERR' ) =~ /^(debug|info|notice)$/ ); +-RT::InitLogging(); +- +-# get customized root password +-my $root_password; +-if ( $args{'root-password-file'} ) { +- open( my $fh, '<', $args{'root-password-file'} ) +- or die "Couldn't open 'args{'root-password-file'}' for reading: $!"; +- $root_password = <$fh>; +- chomp $root_password; +- my $min_length = RT->Config->Get('MinimumPasswordLength'); +- if ($min_length) { +- die +-"password needs to be at least $min_length long, please check file '$args{'root-password-file'}'" +- if length $root_password < $min_length; +- } +- close $fh; +-} +- +- +-# check and setup @actions +-my @actions = grep $_, split /,/, $args{'action'}; +-if ( @actions > 1 && $args{'datafile'} ) { +- print STDERR "You can not use --datafile option with multiple actions.\n"; +- exit(-1); +-} +-foreach ( @actions ) { +- unless ( /^(?:init|create|drop|schema|acl|indexes|coredata|insert|upgrade)$/ ) { +- print STDERR "$0 called with an invalid --action parameter.\n"; +- exit(-1); +- } +- if ( /^(?:init|drop|upgrade)$/ && @actions > 1 ) { +- print STDERR "You can not mix init, drop or upgrade action with any action.\n"; +- exit(-1); +- } +-} +- +-# convert init to multiple actions +-my $init = 0; +-if ( $actions[0] eq 'init' ) { +- if ($args{'skip-create'}) { +- @actions = qw(schema coredata insert); +- } else { +- @actions = qw(create schema acl coredata insert); +- } +- $init = 1; +-} +- +-# set options from environment +-foreach my $key(qw(Type Host Name User Password)) { +- next unless exists $ENV{ 'RT_DB_'. uc $key }; +- print "Using Database$key from RT_DB_". uc($key) ." environment variable.\n"; +- RT->Config->Set( "Database$key", $ENV{ 'RT_DB_'. uc $key }); +-} +- +-my $db_type = RT->Config->Get('DatabaseType') || ''; +-my $db_host = RT->Config->Get('DatabaseHost') || ''; +-my $db_port = RT->Config->Get('DatabasePort') || ''; +-my $db_name = RT->Config->Get('DatabaseName') || ''; +-my $db_user = RT->Config->Get('DatabaseUser') || ''; +-my $db_pass = RT->Config->Get('DatabasePassword') || ''; +- +-# load it here to get error immidiatly if DB type is not supported +-require RT::Handle; +- +-if ( $db_type eq 'SQLite' && !File::Spec->file_name_is_absolute($db_name) ) { +- $db_name = File::Spec->catfile($RT::VarPath, $db_name); +- RT->Config->Set( DatabaseName => $db_name ); +-} +- +-my $dba_user = $args{'dba'} || $ENV{'RT_DBA_USER'} || RT->Config->Get('DatabaseAdmin') || ''; +-my $dba_pass = $args{'dba-password'} || $ENV{'RT_DBA_PASSWORD'}; +- +-if ($args{'skip-create'}) { +- $dba_user = $db_user; +- $dba_pass = $db_pass; +-} else { +- if ( !$args{force} && ( !defined $dba_pass || $args{'prompt-for-dba-password'} ) ) { +- $dba_pass = get_dba_password(); +- chomp $dba_pass if defined($dba_pass); +- } +-} +- +-my $version_word_regex = join '|', RT::Handle->version_words; +-my $version_dir = qr/^\d+\.\d+\.\d+(?:$version_word_regex)?\d*$/; +- +-print "Working with:\n" +- ."Type:\t$db_type\nHost:\t$db_host\nPort:\t$db_port\nName:\t$db_name\n" +- ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n"; +- +-my $package = $args{'package'} || 'RT'; +-my $ext_version = $args{'ext-version'}; +-my $full_id = Data::GUID->new->as_string; +- +-my $log_actions = 0; +-if ($args{'package'} ne 'RT') { +- RT->ConnectToDatabase(); +- RT->InitSystemObjects(); +- $log_actions = 1; +-} +- +-foreach my $action ( @actions ) { +- no strict 'refs'; +- my ($status, $msg) = *{ 'action_'. $action }{'CODE'}->( %args ); +- error($action, $msg) unless $status; +- print $msg .".\n" if $msg; +- print "Done.\n"; +-} +- +-sub action_create { +- my %args = @_; +- my $dbh = get_system_dbh(); +- my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'create' ); +- return ($status, $msg) unless $status; +- +- print "Now creating a $db_type database $db_name for RT.\n"; +- return RT::Handle->CreateDatabase( $dbh ); +-} +- +-sub action_drop { +- my %args = @_; +- +- print "Dropping $db_type database $db_name.\n"; +- unless ( $args{'force'} ) { +- print <<END; +- +-About to drop $db_type database $db_name on $db_host (port '$db_port'). +-WARNING: This will erase all data in $db_name. +- +-END +- exit(-2) unless _yesno(); +- } +- +- my $dbh = get_system_dbh(); +- return RT::Handle->DropDatabase( $dbh ); +-} +- +-sub action_schema { +- my %args = @_; +- my $dbh = get_admin_dbh(); +- my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'schema' ); +- return ($status, $msg) unless $status; +- +- my $individual_id = Data::GUID->new->as_string(); +- my %upgrade_data = ( +- action => 'schema', +- filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''), +- stage => 'before', +- full_id => $full_id, +- individual_id => $individual_id, +- ); +- $upgrade_data{'ext_version'} = $ext_version if $ext_version; +- RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; +- +- print "Now populating database schema.\n"; +- my @ret = RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} ); +- +- %upgrade_data = ( +- stage => 'after', +- individual_id => $individual_id, +- return_value => [ @ret ], +- ); +- RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; +- +- return @ret; +-} +- +-sub action_acl { +- my %args = @_; +- my $dbh = get_admin_dbh(); +- my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'acl' ); +- return ($status, $msg) unless $status; +- +- my $individual_id = Data::GUID->new->as_string(); +- my %upgrade_data = ( +- action => 'acl', +- filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''), +- stage => 'before', +- full_id => $full_id, +- individual_id => $individual_id, +- ); +- $upgrade_data{'ext_version'} = $ext_version if $ext_version; +- RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; +- +- print "Now inserting database ACLs.\n"; +- my @ret = RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} ); +- +- %upgrade_data = ( +- stage => 'after', +- individual_id => $individual_id, +- return_value => [ @ret ], +- ); +- RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; +- +- return @ret; +-} +- +-sub action_indexes { +- my %args = @_; +- RT->ConnectToDatabase; +- my $individual_id = Data::GUID->new->as_string(); +- my %upgrade_data = ( +- action => 'indexes', +- filename => Cwd::abs_path($args{'datafile'} || $args{'datadir'} || ''), +- stage => 'before', +- full_id => $full_id, +- individual_id => $individual_id, +- ); +- $upgrade_data{'ext_version'} = $ext_version if $ext_version; +- RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; +- +- my $dbh = get_admin_dbh(); +- $RT::Handle = RT::Handle->new; +- $RT::Handle->dbh( $dbh ); +- RT::InitLogging(); +- +- print "Now inserting database indexes.\n"; +- my @ret = RT::Handle->InsertIndexes( $dbh, $args{'datafile'} || $args{'datadir'} ); +- +- $RT::Handle = RT::Handle->new; +- $RT::Handle->dbh( undef ); +- RT->ConnectToDatabase; +- %upgrade_data = ( +- stage => 'after', +- individual_id => $individual_id, +- return_value => [ @ret ], +- ); +- RT->System->AddUpgradeHistory($package => \%upgrade_data) if $log_actions; +- +- return @ret; +-} +- +-sub action_coredata { +- my %args = @_; +- $RT::Handle = RT::Handle->new; +- $RT::Handle->dbh( undef ); +- RT::ConnectToDatabase(); +- my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'coredata' ); +- return ($status, $msg) unless $status; +- +- print "Now inserting RT core system objects.\n"; +- return $RT::Handle->InsertInitialData; +-} +- +-sub action_insert { +- my %args = @_; +- $RT::Handle = RT::Handle->new; +- RT::Init(); +- $log_actions = 1; +- +- my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'insert' ); +- return ($status, $msg) unless $status; +- +- print "Now inserting data.\n"; +- my $file = $args{'datafile'}; +- $file = $RT::EtcPath . "/initialdata" if $init && !$file; +- $file ||= $args{'datadir'}."/content"; +- +- my $individual_id = Data::GUID->new->as_string(); +- my %upgrade_data = ( +- action => 'insert', +- filename => Cwd::abs_path($file), +- stage => 'before', +- full_id => $full_id, +- individual_id => $individual_id +- ); +- $upgrade_data{'ext_version'} = $ext_version if $ext_version; +- +- open my $handle, '<', $file or warn "Unable to open $file: $!"; +- $upgrade_data{content} = do {local $/; <$handle>} if $handle; +- +- RT->System->AddUpgradeHistory($package => \%upgrade_data); +- +- my @ret; +- +- my $upgrade = sub { @ret = $RT::Handle->InsertData( $file, $root_password ) }; +- +- for my $file (@{$args{backcompat} || []}) { +- my $lines = do {local $/; local @ARGV = ($file); <>}; +- my $sub = eval "sub {\n# line 1 $file\n$lines\n}"; +- unless ($sub) { +- warn "Failed to load backcompat $file: $@"; +- next; +- } +- my $current = $upgrade; +- $upgrade = sub { $sub->($current) }; +- } +- +- $upgrade->(); +- +- # XXX Reconnecting to insert the history entry +- # until we can sort out removing +- # the disconnect at the end of InsertData. +- RT->ConnectToDatabase(); +- +- %upgrade_data = ( +- stage => 'after', +- individual_id => $individual_id, +- return_value => [ @ret ], +- ); +- +- RT->System->AddUpgradeHistory($package => \%upgrade_data); +- +- my $db_type = RT->Config->Get('DatabaseType'); +- $RT::Handle->Disconnect() unless $db_type eq 'SQLite'; +- +- return @ret; +-} +- +-sub action_upgrade { +- my %args = @_; +- my $base_dir = $args{'datadir'} || "./etc/upgrade"; +- return (0, "Couldn't read dir '$base_dir' with upgrade data") +- unless -d $base_dir || -r _; +- +- my $upgrading_from = undef; +- do { +- if ( defined $upgrading_from ) { +- print "Doesn't match #.#.#: "; +- } else { +- print "Enter $args{package} version you're upgrading from: "; +- } +- $upgrading_from = $args{'upgrade-from'} || scalar <STDIN>; +- chomp $upgrading_from; +- $upgrading_from =~ s/\s+//g; +- } while $upgrading_from !~ /$version_dir/; +- +- my $upgrading_to = $RT::VERSION; +- return (0, "The current version $upgrading_to is lower than $upgrading_from") +- if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) > 0; +- +- return (1, "The version $upgrading_to you're upgrading to is up to date") +- if RT::Handle::cmp_version( $upgrading_from, $upgrading_to ) == 0; +- +- my @versions = get_versions_from_to($base_dir, $upgrading_from, undef); +- return (1, "No DB changes since $upgrading_from") +- unless @versions; +- +- if (RT::Handle::cmp_version($versions[-1], $upgrading_to) > 0) { +- print "\n***** There are upgrades for $versions[-1], which is later than $upgrading_to,\n"; +- print "***** which you are nominally upgrading to. Upgrading to $versions[-1] instead.\n"; +- $upgrading_to = $versions[-1]; +- } +- +- print "\nGoing to apply following upgrades:\n"; +- print map "* $_\n", @versions; +- +- { +- my $custom_upgrading_to = undef; +- do { +- if ( defined $custom_upgrading_to ) { +- print "Doesn't match #.#.#: "; +- } else { +- print "\nEnter $args{package} version if you want to stop upgrade at some point,\n"; +- print " or leave it blank if you want apply above upgrades: "; +- } +- $custom_upgrading_to = $args{'upgrade-to'} || scalar <STDIN>; +- chomp $custom_upgrading_to; +- $custom_upgrading_to =~ s/\s+//g; +- last unless $custom_upgrading_to; +- } while $custom_upgrading_to !~ /$version_dir/; +- +- if ( $custom_upgrading_to ) { +- return ( +- 0, "The version you entered ($custom_upgrading_to) is lower than\n" +- ."version you're upgrading from ($upgrading_from)" +- ) if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) > 0; +- +- return (1, "The version you're upgrading to is up to date") +- if RT::Handle::cmp_version( $upgrading_from, $custom_upgrading_to ) == 0; +- +- if ( RT::Handle::cmp_version( $RT::VERSION, $custom_upgrading_to ) < 0 ) { +- print "Version you entered is greater than installed ($RT::VERSION).\n"; +- _yesno() or exit(-2); +- } +- # ok, checked everything no let's refresh list +- $upgrading_to = $custom_upgrading_to; +- @versions = get_versions_from_to($base_dir, $upgrading_from, $upgrading_to); +- +- return (1, "No DB changes between $upgrading_from and $upgrading_to") +- unless @versions; +- +- print "\nGoing to apply following upgrades:\n"; +- print map "* $_\n", @versions; +- } +- } +- +- unless ( $args{'force'} ) { +- print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n"; +- _yesno() or exit(-2); +- } +- +- RT->ConnectToDatabase(); +- RT->InitSystemObjects(); +- $log_actions = 1; +- +- RT->System->AddUpgradeHistory($package => { +- type => 'full upgrade', +- action => 'upgrade', +- stage => 'before', +- from => $upgrading_from, +- to => $upgrading_to, +- versions => [@versions], +- full_id => $full_id, +- individual_id => $full_id +- }); +- +- # Ensure that the Attributes column is big enough to hold the +- # upgrade steps we're going to add; this step exists in 4.0.6 for +- # mysql, but that may be too late. Run it as soon as possible. +- if (RT->Config->Get('DatabaseType') eq 'mysql' +- and RT::Handle::cmp_version( $upgrading_from, '4.0.6') < 0) { +- my $dbh = get_admin_dbh(); +- # Before the binary switch in 3.7.87, we want to alter text -> +- # longtext, not blob -> longblob +- if (RT::Handle::cmp_version( $upgrading_from, '3.7.87') < 0) { +- $dbh->do("ALTER TABLE Attributes MODIFY Content LONGTEXT") +- } else { +- $dbh->do("ALTER TABLE Attributes MODIFY Content LONGBLOB") +- } +- } +- +- my $previous = $upgrading_from; +- my ( $ret, $msg ); +- foreach my $n ( 0..$#versions ) { +- my $v = $versions[$n]; +- my $individual_id = Data::GUID->new->as_string(); +- +- my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions; +- print "Processing $v\n"; +- +- RT->System->AddUpgradeHistory($package => { +- action => 'upgrade', +- type => 'individual upgrade', +- stage => 'before', +- from => $previous, +- to => $v, +- full_id => $full_id, +- individual_id => $individual_id, +- }); +- +- my %tmp = (%args, datadir => "$base_dir/$v", datafile => undef, backcompat => \@back); +- +- if ( -e "$base_dir/$v/schema.$db_type" ) { +- ( $ret, $msg ) = action_schema( %tmp ); +- return ( $ret, $msg ) unless $ret; +- } +- if ( -e "$base_dir/$v/acl.$db_type" ) { +- ( $ret, $msg ) = action_acl( %tmp ); +- return ( $ret, $msg ) unless $ret; +- } +- if ( -e "$base_dir/$v/indexes" ) { +- ( $ret, $msg ) = action_indexes( %tmp ); +- return ( $ret, $msg ) unless $ret; +- } +- if ( -e "$base_dir/$v/content" ) { +- ( $ret, $msg ) = action_insert( %tmp ); +- return ( $ret, $msg ) unless $ret; +- } +- +- # XXX: Another connect since the insert called +- # previous to this step will disconnect. +- +- RT->ConnectToDatabase(); +- +- RT->System->AddUpgradeHistory($package => { +- stage => 'after', +- individual_id => $individual_id, +- }); +- +- $previous = $v; +- } +- +- RT->System->AddUpgradeHistory($package => { +- stage => 'after', +- individual_id => $full_id, +- }); +- +- return 1; +-} +- +-sub get_versions_from_to { +- my ($base_dir, $from, $to) = @_; +- +- opendir( my $dh, $base_dir ) or die "couldn't open dir: $!"; +- my @versions = grep -d "$base_dir/$_" && /$version_dir/, readdir $dh; +- closedir $dh; +- +- die "\nERROR: No upgrade data found in '$base_dir'! Perhaps you specified the wrong --datadir?\n" +- unless @versions; +- +- return +- grep defined $to ? RT::Handle::cmp_version($_, $to) <= 0 : 1, +- grep RT::Handle::cmp_version($_, $from) > 0, +- sort RT::Handle::cmp_version @versions; +-} +- +-sub error { +- my ($action, $msg) = @_; +- print STDERR "Couldn't finish '$action' step.\n\n"; +- print STDERR "ERROR: $msg\n\n"; +- exit(-1); +-} +- +-sub get_dba_password { +- print "In order to create or update your RT database," +- . " this script needs to connect to your " +- . " $db_type instance on $db_host (port '$db_port') as $dba_user\n"; +- print "Please specify that user's database password below. If the user has no database\n"; +- print "password, just press return.\n\n"; +- print "Password: "; +- ReadMode('noecho'); +- my $password = ReadLine(0); +- ReadMode('normal'); +- print "\n"; +- return ($password); +-} +- +-# get_system_dbh +-# Returns L<DBI> database handle connected to B<system> with DBA credentials. +-# See also L<RT::Handle/SystemDSN>. +- +- +-sub get_system_dbh { +- return _get_dbh( RT::Handle->SystemDSN, $dba_user, $dba_pass ); +-} +- +-sub get_admin_dbh { +- return _get_dbh( RT::Handle->DSN, $dba_user, $dba_pass ); +-} +- +-# get_rt_dbh [USER, PASSWORD] +- +-# Returns L<DBI> database handle connected to RT database, +-# you may specify credentials(USER and PASSWORD) to connect +-# with. By default connects with credentials from RT config. +- +-sub get_rt_dbh { +- return _get_dbh( RT::Handle->DSN, $db_user, $db_pass ); +-} +- +-sub _get_dbh { +- my ($dsn, $user, $pass) = @_; +- my $dbh = DBI->connect( +- $dsn, $user, $pass, +- { RaiseError => 0, PrintError => 0 }, +- ); +- unless ( $dbh ) { +- my $msg = "Failed to connect to $dsn as user '$user': ". $DBI::errstr; +- if ( $args{'debug'} ) { +- require Carp; Carp::confess( $msg ); +- } else { +- print STDERR $msg; exit -1; +- } +- } +- return $dbh; +-} +- +-sub _yesno { +- print "Proceed [y/N]:"; +- my $x = scalar(<STDIN>); +- $x =~ /^y/i; +-} +- +-1; +- +-__END__ +- +-=head1 NAME +- +-rt-setup-database - Set up RT's database +- +-=head1 SYNOPSIS +- +- rt-setup-database --action ... +- +-=head1 OPTIONS +- +-=over +- +-=item action +- +-Several actions can be combined using comma separated list. +- +-=over +- +-=item init +- +-Initialize the database. This is combination of multiple actions listed below. +-Create DB, schema, setup acl, insert core data and initial data. +- +-=item upgrade +- +-Apply all needed schema/acl/content updates (will ask for version to upgrade +-from) +- +-=item create +- +-Create the database. +- +-=item drop +- +-Drop the database. This will B<ERASE ALL YOUR DATA>. +- +-=item schema +- +-Initialize only the database schema +- +-To use a local or supplementary datafile, specify it using the '--datadir' +-option below. +- +-=item acl +- +-Initialize only the database ACLs +- +-To use a local or supplementary datafile, specify it using the '--datadir' +-option below. +- +-=item coredata +- +-Insert data into RT's database. This data is required for normal functioning of +-any RT instance. +- +-=item insert +- +-Insert data into RT's database. By default, will use RT's installation data. +-To use a local or supplementary datafile, specify it using the '--datafile' +-option below. +- +-=back +- +-=item datafile +- +-file path of the data you want to action on +- +-e.g. C<--datafile /path/to/datafile> +- +-=item datadir +- +-Used to specify a path to find the local database schema and acls to be +-installed. +- +-e.g. C<--datadir /path/to/> +- +-=item dba +- +-dba's username +- +-=item dba-password +- +-dba's password +- +-=item prompt-for-dba-password +- +-Ask for the database administrator's password interactively +- +-=item skip-create +- +-for 'init': skip creating the database and the user account, so we don't need +-administrator privileges +- +-=item root-password-file +- +-for 'init' and 'insert': rather than using the default administrative password +-for RT's "root" user, use the password in this file. +- +-=item package +- +-the name of the entity performing a create or upgrade. Used for logging changes +-in the DB. Defaults to RT, otherwise it should be the fully qualified package name +-of the extension or plugin making changes to the DB. +- +-=item ext-version +- +-current version of extension making a change. Not needed for RT since RT has a +-more elaborate system to track upgrades across multiple versions. +- +-=item upgrade-from +- +-for 'upgrade': specifies the version to upgrade from, and do not prompt +-for it if it appears to be a valid version. +- +-=item upgrade-to +- +-for 'upgrade': specifies the version to upgrade to, and do not prompt +-for it if it appears to be a valid version. +- +-=back +- +-=cut +diff --git a/sbin/rt-setup-fulltext-index b/sbin/rt-setup-fulltext-index +deleted file mode 100755 +index 1a8d312..0000000 +--- a/sbin/rt-setup-fulltext-index ++++ /dev/null +@@ -1,763 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +-no warnings 'once'; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-BEGIN { +- use RT; +- RT::LoadConfig(); +- RT::Init(); +-}; +-use RT::Interface::CLI (); +- +-my %DB = ( +- type => scalar RT->Config->Get('DatabaseType'), +- user => scalar RT->Config->Get('DatabaseUser'), +- admin => scalar RT->Config->Get('DatabaseAdmin'), +- admin_password => undef, +-); +- +-my %OPT = ( +- help => 0, +- ask => 1, +- dryrun => 0, +- attachments => 1, +-); +- +-my %DEFAULT; +-if ( $DB{'type'} eq 'Pg' ) { +- %DEFAULT = ( +- table => 'Attachments', +- column => 'ContentIndex', +- ); +-} +-elsif ( $DB{'type'} eq 'mysql' ) { +- %DEFAULT = ( +- table => 'AttachmentsIndex', +- ); +-} +-elsif ( $DB{'type'} eq 'Oracle' ) { +- %DEFAULT = ( +- prefix => 'rt_fts_', +- ); +-} +- +-use Getopt::Long qw(GetOptions); +-GetOptions( +- 'h|help!' => \$OPT{'help'}, +- 'ask!' => \$OPT{'ask'}, +- 'dry-run!' => \$OPT{'dryrun'}, +- 'attachments!' => \$OPT{'attachments'}, +- +- 'table=s' => \$OPT{'table'}, +- 'column=s' => \$OPT{'column'}, +- 'url=s' => \$OPT{'url'}, +- 'maxmatches=i' => \$OPT{'maxmatches'}, +- 'index-type=s' => \$OPT{'index-type'}, +- +- 'dba=s' => \$DB{'admin'}, +- 'dba-password=s' => \$DB{'admin_password'}, +-) or show_help(); +- +-if ( $OPT{'help'} || (!$DB{'admin'} && $DB{'type'} eq 'Oracle' ) ) { +- show_help( !$OPT{'help'} ); +-} +- +-my $dbh = $RT::Handle->dbh; +-$dbh->{'RaiseError'} = 1; +-$dbh->{'PrintError'} = 1; +- +-# MySQL could either be native of sphinx; find out which +-if ($DB{'type'} eq "mysql") { +- my $index_type = lc($OPT{'index-type'} || ''); +- +- # Default to sphinx on < 5.6, and error if they provided mysql +- my $msg; +- if ($RT::Handle->dbh->{mysql_serverversion} < 50600) { +- $msg = "Complete support for full-text search requires MySQL 5.6 or higher. For prior\n" +- ."versions such as yours, full-text indexing can either be provided using MyISAM\n" +- ."tables, or the external Sphinx indexer. Using MyISAM tables requires that your\n" +- ."database be tuned to support them, as RT uses InnoDB tables for all other content.\n" +- ."Using Sphinx will require recompiling MySQL. Which indexing solution would you\n" +- ."prefer?" +- } else { +- $msg = "MySQL 5.6 and above support native full-text indexing; for compatibility\n" +- ."with earlier versions of RT, the external Sphinx indexer is still supported.\n" +- ."Which indexing solution would you prefer?" +- } +- +- while ( $index_type ne 'sphinx' and $index_type ne 'mysql' ) { +- $index_type = lc prompt( +- message => $msg, +- default => 'mysql', +- silent => !$OPT{'ask'}, +- ); +- }; +- $DB{'type'} = $index_type; +-} +- +-if ( $DB{'type'} eq 'mysql' ) { +- # MySQL 5.6 has FTS on InnoDB "text" columns -- which the +- # Attachments table doesn't have, but we can make it have. +- my $table = $OPT{'table'} || prompt( +- message => "Enter the name of a new MySQL table that will be used to store the\n" +- . "full-text content and indexes:", +- default => $DEFAULT{'table'}, +- silent => !$OPT{'ask'}, +- ); +- do_error_is_ok( dba_handle() => "DROP TABLE $table" ) +- unless $OPT{'dryrun'}; +- +- my $engine = $RT::Handle->dbh->{mysql_serverversion} < 50600 ? "MyISAM" : "InnoDB"; +- my $schema = "CREATE TABLE $table ( " +- ."id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY," +- ."Content LONGTEXT, FULLTEXT(Content) ) ENGINE=$engine CHARACTER SET utf8"; +- insert_schema( $schema ); +- +- print_rt_config( Table => $table ); +-} elsif ($DB{'type'} eq 'sphinx') { +- check_sphinx(); +- my $table = $OPT{'table'} || prompt( +- message => "Enter name of a new MySQL table that will be used to connect to the\n" +- . "Sphinx server:", +- default => $DEFAULT{'table'}, +- silent => !$OPT{'ask'}, +- ); +- +- my $url = 'sphinx://localhost:3312/rt'; +- my $version = ($dbh->selectrow_array("show variables like 'version'"))[1]; +- $url = 'sphinx://127.0.0.1:3312/rt' +- if $version and $version =~ /^(\d+\.\d+)/ and $1 >= 5.5; +- +- $url = $OPT{'url'} || prompt( +- message => "Enter URL of the sphinx search server; this should be of the form\n" +- . "sphinx://<server>:<port>/<index name>", +- default => $url, +- silent => !$OPT{'ask'}, +- ); +- my $maxmatches = $OPT{'maxmatches'} || prompt( +- message => "Maximum number of matches to return; this is the maximum number of\n" +- . "attachment records returned by the search, not the maximum number\n" +- . "of tickets. Both your RT_SiteConfig.pm and your sphinx.conf must\n" +- . "agree on this value. Larger values cause your Sphinx server to\n" +- . "consume more memory and CPU time per query.", +- default => 10000, +- silent => !$OPT{'ask'}, +- ); +- +- my $schema = <<END; +-CREATE TABLE $table ( +- id BIGINT NOT NULL, +- weight INTEGER NOT NULL, +- query VARCHAR(3072) NOT NULL, +- INDEX(query) +-) ENGINE=SPHINX CONNECTION="$url" CHARACTER SET utf8 +-END +- +- do_error_is_ok( dba_handle() => "DROP TABLE $table" ) +- unless $OPT{'dryrun'}; +- insert_schema( $schema ); +- +- print_rt_config( Table => $table, MaxMatches => $maxmatches ); +- +- require URI; +- my $urlo = URI->new( $url ); +- my ($host, $port) = split /:/, $urlo->authority; +- my $index = $urlo->path; +- $index =~ s{^/+}{}; +- +- my $var_path = $RT::VarPath; +- +- my %sphinx_conf = (); +- $sphinx_conf{'host'} = RT->Config->Get('DatabaseHost'); +- $sphinx_conf{'db'} = RT->Config->Get('DatabaseName'); +- $sphinx_conf{'user'} = RT->Config->Get('DatabaseUser'); +- $sphinx_conf{'pass'} = RT->Config->Get('DatabasePassword'); +- +- print <<END +- +-Below is a simple Sphinx configuration which can be used to index all +-text/plain attachments in your database. This configuration is not +-ideal; you should read the Sphinx documentation to understand how to +-configure it to better suit your needs. It assumes that you create the +-$var_path/sphinx/ directory, and that is is writable by the sphinx +-user. +- +-source rt { +- type = mysql +- +- sql_host = $sphinx_conf{'host'} +- sql_db = $sphinx_conf{'db'} +- sql_user = $sphinx_conf{'user'} +- sql_pass = $sphinx_conf{'pass'} +- +- sql_query_pre = SET NAMES utf8 +- sql_query = \\ +- SELECT a.id, a.content FROM Attachments a \\ +- JOIN Transactions txn ON a.TransactionId = txn.id AND txn.ObjectType = 'RT::Ticket' \\ +- JOIN Tickets t ON txn.ObjectId = t.id \\ +- WHERE a.ContentType = 'text/plain' AND t.Status != 'deleted' +- +- sql_query_info = SELECT * FROM Attachments WHERE id=\$id +-} +- +-index $index { +- source = rt +- path = $var_path/sphinx/index +- docinfo = extern +- charset_type = utf-8 +-} +- +-indexer { +- mem_limit = 32M +-} +- +-searchd { +- port = $port +- log = $var_path/sphinx/searchd.log +- query_log = $var_path/sphinx/query.log +- read_timeout = 5 +- max_children = 30 +- pid_file = $var_path/sphinx/searchd.pid +- max_matches = $maxmatches +- seamless_rotate = 1 +- preopen_indexes = 0 +- unlink_old = 1 +-} +- +-END +- +-} +-elsif ( $DB{'type'} eq 'Pg' ) { +- check_tsvalue(); +- my $table = $OPT{'table'} || prompt( +- message => "Enter the name of a DB table that will be used to store the Pg tsvector.\n" +- . "You may either use the existing Attachments table, or create a new\n" +- . "table.", +- default => $DEFAULT{'table'}, +- silent => !$OPT{'ask'}, +- ); +- my $column = $OPT{'column'} || prompt( +- message => 'Enter the name of a column that will be used to store the Pg tsvector:', +- default => $DEFAULT{'column'}, +- silent => !$OPT{'ask'}, +- ); +- +- my $schema; +- my $drop; +- if ( lc($table) eq 'attachments' ) { +- $drop = "ALTER TABLE $table DROP COLUMN $column"; +- $schema = "ALTER TABLE $table ADD COLUMN $column tsvector"; +- } else { +- $drop = "DROP TABLE $table"; +- $schema = "CREATE TABLE $table ( " +- ."id INTEGER NOT NULL," +- ."$column tsvector )"; +- } +- +- my $index_type = lc($OPT{'index-type'} || ''); +- while ( $index_type ne 'gist' and $index_type ne 'gin' ) { +- $index_type = lc prompt( +- message => "You may choose between GiST or GIN indexes; the former is several times\n" +- . "slower to search, but takes less space on disk and is faster to update.", +- default => 'GiST', +- silent => !$OPT{'ask'}, +- ); +- } +- +- do_error_is_ok( dba_handle() => $drop ) +- unless $OPT{'dryrun'}; +- insert_schema( $schema ); +- insert_schema("CREATE INDEX ${column}_idx ON $table USING $index_type($column)"); +- +- print_rt_config( Table => $table, Column => $column ); +-} +-elsif ( $DB{'type'} eq 'Oracle' ) { +- { +- my $dbah = dba_handle(); +- do_print_error( $dbah => 'GRANT CTXAPP TO '. $DB{'user'} ); +- do_print_error( $dbah => 'GRANT EXECUTE ON CTXSYS.CTX_DDL TO '. $DB{'user'} ); +- } +- +- my %PREFERENCES = ( +- datastore => { +- type => 'DIRECT_DATASTORE', +- }, +- filter => { +- type => 'AUTO_FILTER', +-# attributes => { +-# timeout => 120, # seconds +-# timeout_type => 'HEURISTIC', # or 'FIXED' +-# }, +- }, +- lexer => { +- type => 'WORLD_LEXER', +- }, +- word_list => { +- type => 'BASIC_WORDLIST', +- attributes => { +- stemmer => 'AUTO', +- fuzzy_match => 'AUTO', +-# fuzzy_score => undef, +-# fuzzy_numresults => undef, +-# substring_index => undef, +-# prefix_index => undef, +-# prefix_length_min => undef, +-# prefix_length_max => undef, +-# wlidcard_maxterms => undef, +- }, +- }, +- 'section_group' => { +- type => 'NULL_SECTION_GROUP', +- }, +- +- storage => { +- type => 'BASIC_STORAGE', +- attributes => { +- R_TABLE_CLAUSE => 'lob (data) store as (cache)', +- I_INDEX_CLAUSE => 'compress 2', +- }, +- }, +- ); +- +- my @params = (); +- push @params, ora_create_datastore( %{ $PREFERENCES{'datastore'} } ); +- push @params, ora_create_filter( %{ $PREFERENCES{'filter'} } ); +- push @params, ora_create_lexer( %{ $PREFERENCES{'lexer'} } ); +- push @params, ora_create_word_list( %{ $PREFERENCES{'word_list'} } ); +- push @params, ora_create_stop_list(); +- push @params, ora_create_section_group( %{ $PREFERENCES{'section_group'} } ); +- push @params, ora_create_storage( %{ $PREFERENCES{'storage'} } ); +- +- my $index_params = join "\n", @params; +- my $index_name = $DEFAULT{prefix} .'index'; +- do_error_is_ok( $dbh => "DROP INDEX $index_name" ) +- unless $OPT{'dryrun'}; +- $dbh->do( +- "CREATE INDEX $index_name ON Attachments(Content) +- indextype is ctxsys.context parameters(' +- $index_params +- ')", +- ) unless $OPT{'dryrun'}; +- +- print_rt_config( IndexName => $index_name ); +-} +-else { +- die "Full-text indexes on $DB{type} are not yet supported"; +-} +- +-sub check_tsvalue { +- my $dbh = $RT::Handle->dbh; +- my $fts = ($dbh->selectrow_array(<<EOQ))[0]; +-SELECT 1 FROM information_schema.routines WHERE routine_name = 'plainto_tsquery' +-EOQ +- unless ($fts) { +- print STDERR <<EOT; +- +-Your PostgreSQL server does not include full-text support. You will +-need to upgrade to PostgreSQL version 8.3 or higher to use full-text +-indexing. +- +-EOT +- exit 1; +- } +-} +- +-sub check_sphinx { +- return if $RT::Handle->CheckSphinxSE; +- +- print STDERR <<EOT; +- +-Your MySQL server has not been compiled with the Sphinx storage engine +-(sphinxse). You will need to recompile MySQL according to the +-instructions in Sphinx's documentation at +-http://sphinxsearch.com/docs/current.html#sphinxse-installing +- +-EOT +- exit 1; +-} +- +-sub ora_create_datastore { +- return sprintf 'datastore %s', ora_create_preference( +- @_, +- name => 'datastore', +- ); +-} +- +-sub ora_create_filter { +- my $res = ''; +- $res .= sprintf "format column %s\n", ora_create_format_column(); +- $res .= sprintf 'filter %s', ora_create_preference( +- @_, +- name => 'filter', +- ); +- return $res; +-} +- +-sub ora_create_lexer { +- return sprintf 'lexer %s', ora_create_preference( +- @_, +- name => 'lexer', +- ); +-} +- +-sub ora_create_word_list { +- return sprintf 'wordlist %s', ora_create_preference( +- @_, +- name => 'word_list', +- ); +-} +- +-sub ora_create_stop_list { +- my $file = shift || 'etc/stopwords/en.txt'; +- return '' unless -e $file; +- +- my $name = $DEFAULT{'prefix'} .'stop_list'; +- unless ($OPT{'dryrun'}) { +- do_error_is_ok( $dbh => 'begin ctx_ddl.drop_stoplist(?); end;', $name ); +- +- $dbh->do( +- 'begin ctx_ddl.create_stoplist(?, ?); end;', +- undef, $name, 'BASIC_STOPLIST' +- ); +- +- open( my $fh, '<:utf8', $file ) +- or die "couldn't open file '$file': $!"; +- while ( my $word = <$fh> ) { +- chomp $word; +- $dbh->do( +- 'begin ctx_ddl.add_stopword(?, ?); end;', +- undef, $name, $word +- ); +- } +- close $fh; +- } +- return sprintf 'stoplist %s', $name; +-} +- +-sub ora_create_section_group { +- my %args = @_; +- my $name = $DEFAULT{'prefix'} .'section_group'; +- unless ($OPT{'dryrun'}) { +- do_error_is_ok( $dbh => 'begin ctx_ddl.drop_section_group(?); end;', $name ); +- $dbh->do( +- 'begin ctx_ddl.create_section_group(?, ?); end;', +- undef, $name, $args{'type'} +- ); +- } +- return sprintf 'section group %s', $name; +-} +- +-sub ora_create_storage { +- return sprintf 'storage %s', ora_create_preference( +- @_, +- name => 'storage', +- ); +-} +- +-sub ora_create_format_column { +- my $column_name = 'ContentOracleFormat'; +- return $column_name if $OPT{'dryrun'}; +- unless ( +- $dbh->column_info( +- undef, undef, uc('Attachments'), uc( $column_name ) +- )->fetchrow_array +- ) { +- $dbh->do(qq{ +- ALTER TABLE Attachments ADD $column_name VARCHAR2(10) +- }); +- } +- +- my $detect_format = qq{ +- CREATE OR REPLACE FUNCTION $DEFAULT{prefix}detect_format_simple( +- parent IN NUMBER, +- type IN VARCHAR2, +- encoding IN VARCHAR2, +- fname IN VARCHAR2 +- ) +- RETURN VARCHAR2 +- AS +- format VARCHAR2(10); +- BEGIN +- format := CASE +- }; +- unless ( $OPT{'attachments'} ) { +- $detect_format .= qq{ +- WHEN fname IS NOT NULL THEN 'ignore' +- }; +- } +- $detect_format .= qq{ +- WHEN type = 'text' THEN 'text' +- WHEN type = 'text/rtf' THEN 'ignore' +- WHEN type LIKE 'text/%' THEN 'text' +- WHEN type LIKE 'message/%' THEN 'text' +- ELSE 'ignore' +- END; +- RETURN format; +- END; +- }; +- ora_create_procedure( $detect_format ); +- +- $dbh->do(qq{ +- UPDATE Attachments +- SET $column_name = $DEFAULT{prefix}detect_format_simple( +- Parent, +- ContentType, ContentEncoding, +- Filename +- ) +- WHERE $column_name IS NULL +- }); +- $dbh->do(qq{ +- CREATE OR REPLACE TRIGGER $DEFAULT{prefix}set_format +- BEFORE INSERT +- ON Attachments +- FOR EACH ROW +- BEGIN +- :new.$column_name := $DEFAULT{prefix}detect_format_simple( +- :new.Parent, +- :new.ContentType, :new.ContentEncoding, +- :new.Filename +- ); +- END; +- }); +- return $column_name; +-} +- +-sub ora_create_preference { +- my %info = @_; +- my $name = $DEFAULT{'prefix'} . $info{'name'}; +- return $name if $OPT{'dryrun'}; +- do_error_is_ok( $dbh => 'begin ctx_ddl.drop_preference(?); end;', $name ); +- $dbh->do( +- 'begin ctx_ddl.create_preference(?, ?); end;', +- undef, $name, $info{'type'} +- ); +- return $name unless $info{'attributes'}; +- +- while ( my ($attr, $value) = each %{ $info{'attributes'} } ) { +- $dbh->do( +- 'begin ctx_ddl.set_attribute(?, ?, ?); end;', +- undef, $name, $attr, $value +- ); +- } +- +- return $name; +-} +- +-sub ora_create_procedure { +- my $text = shift; +- +- return if $OPT{'dryrun'}; +- my $status = $dbh->do($text, { RaiseError => 0 }); +- +- # Statement succeeded +- return if $status; +- +- if ( 6550 != $dbh->err ) { +- # Utter failure +- die $dbh->errstr; +- } +- else { +- my $msg = $dbh->func( 'plsql_errstr' ); +- die $dbh->errstr if !defined $msg; +- die $msg if $msg; +- } +-} +- +-sub dba_handle { +- if ( $DB{'type'} eq 'Oracle' ) { +- $ENV{'NLS_LANG'} = "AMERICAN_AMERICA.AL32UTF8"; +- $ENV{'NLS_NCHAR'} = "AL32UTF8"; +- } +- my $dsn = do { my $h = new RT::Handle; $h->BuildDSN; $h->DSN }; +- my $dbh = DBI->connect( +- $dsn, $DB{admin}, $DB{admin_password}, +- { RaiseError => 1, PrintError => 1 }, +- ); +- unless ( $dbh ) { +- die "Failed to connect to $dsn as user '$DB{admin}': ". $DBI::errstr; +- } +- return $dbh; +-} +- +-sub do_error_is_ok { +- my $dbh = shift; +- local $dbh->{'RaiseError'} = 0; +- local $dbh->{'PrintError'} = 0; +- return $dbh->do(shift, undef, @_); +-} +- +-sub do_print_error { +- my $dbh = shift; +- local $dbh->{'RaiseError'} = 0; +- local $dbh->{'PrintError'} = 1; +- return $dbh->do(shift, undef, @_); +-} +- +-sub prompt { +- my %args = ( @_ ); +- return $args{'default'} if $args{'silent'}; +- +- local $| = 1; +- print $args{'message'}; +- if ( $args{'default'} ) { +- print "\n[". $args{'default'} .']: '; +- } else { +- print ":\n"; +- } +- +- my $res = <STDIN>; +- chomp $res; +- print "\n"; +- return $args{'default'} if !$res && $args{'default'}; +- return $res; +-} +- +-sub verbose { print @_, "\n" if $OPT{verbose} || $OPT{verbose}; 1 } +-sub debug { print @_, "\n" if $OPT{debug}; 1 } +-sub error { $RT::Logger->error( @_ ); verbose(@_); 1 } +-sub warning { $RT::Logger->warning( @_ ); verbose(@_); 1 } +- +-sub show_help { +- my $error = shift; +- RT::Interface::CLI->ShowHelp( +- ExitValue => $error, +- Sections => 'NAME|DESCRIPTION', +- ); +-} +- +-sub print_rt_config { +- my %args = @_; +- my $config = <<END; +- +-You can now configure RT to use the newly-created full-text index by +-adding the following to your RT_SiteConfig.pm: +- +-Set( %FullTextSearch, +- Enable => 1, +- Indexed => 1, +-END +- +- $config .= sprintf(" %-10s => '$args{$_}',\n",$_) +- foreach grep defined $args{$_}, keys %args; +- $config .= ");\n"; +- +- print $config; +-} +- +-sub insert_schema { +- my $dbh = dba_handle(); +- my $message = "Going to run the following in the DB:"; +- my $schema = shift; +- print "$message\n"; +- my $disp = $schema; +- $disp =~ s/^/ /mg; +- print "$disp\n\n"; +- return if $OPT{'dryrun'}; +- +- my $res = $dbh->do( $schema ); +- unless ( $res ) { +- die "Couldn't run DDL query: ". $dbh->errstr; +- } +-} +- +-=head1 NAME +- +-rt-setup-fulltext-index - Create indexes for full text search +- +-=head1 DESCRIPTION +- +-This script creates the appropriate tables, columns, functions, and / or +-views necessary for full-text searching for your database type. It will +-drop any existing indexes in the process. +- +-Please read F<docs/full_text_indexing.pod> for complete documentation on +-full-text indexing for your database type. +- +-If you have a non-standard database administrator user or password, you +-may use the C<--dba> and C<--dba-password> parameters to set them +-explicitly: +- +- rt-setup-fulltext-index --dba sysdba --dba-password 'secret' +- +-To test what will happen without running any DDL, pass the C<--dryrun> +-flag. +- +-The Oracle index determines which content-types it will index at +-creation time. By default, textual message bodies and textual uploaded +-attachments (attachments with filenames) are indexed; to ignore textual +-attachments, pass the C<--no-attachments> flag when the index is +-created. +- +- +-=head1 AUTHOR +- +-Ruslan Zakirov E<lt>ruz@xxxxxxxxxxxxxxxxxx<gt>, +-Alex Vandiver E<lt>alexmv@xxxxxxxxxxxxxxxxxx<gt> +- +-=cut +- +diff --git a/sbin/rt-shredder b/sbin/rt-shredder +deleted file mode 100755 +index 208df2f..0000000 +--- a/sbin/rt-shredder ++++ /dev/null +@@ -1,317 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-=head1 NAME +- +-rt-shredder - Script which wipe out tickets from RT DB +- +-=head1 SYNOPSIS +- +- rt-shredder --plugin list +- rt-shredder --plugin help-Tickets +- rt-shredder --plugin 'Tickets=query,Queue="general" and Status="deleted"' +- +- rt-shredder --sqldump unshred.sql --plugin ... +- rt-shredder --force --plugin ... +- +-=head1 DESCRIPTION +- +-rt-shredder - is script that allow you to wipe out objects +-from RT DB. This script uses API that L<RT::Shredder> module adds to RT. +-Script can be used as example of usage of the shredder API. +- +-=head1 USAGE +- +-You can use several options to control which objects script +-should wipeout. +- +-=head1 OPTIONS +- +-=head2 --sqldump <filename> +- +-Outputs INSERT queries into file. This dump can be used to restore data +-after wiping out. +- +-By default creates files named F<< <ISO_date>-XXXX.sql >> in the current +-directory. +- +-=head2 --object (DEPRECATED) +- +-Option has been deprecated, use plugin C<Objects> instead. +- +-=head2 --plugin '<plugin name>[=<arg>,<val>[;<arg>,<val>]...]' +- +-You can use plugins to select RT objects with various conditions. +-See also --plugin list and --plugin help options. +- +-=head2 --plugin list +- +-Output list of the available plugins. +- +-=head2 --plugin help-<plugin name> +- +-Outputs help for specified plugin. +- +-=head2 --force +- +-Script doesn't ask any questions. +- +-=head1 SEE ALSO +- +-L<RT::Shredder> +- +-=cut +- +-use strict; +-use warnings FATAL => 'all'; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use RT -init; +- +-require RT::Shredder; +- +-use Getopt::Long qw(GetOptions); +-use File::Spec (); +- +-use RT::Shredder::Plugin (); +-# prefetch list of plugins +-our %plugins = RT::Shredder::Plugin->List; +- +-our %opt; +-parse_args(); +- +-my $shredder = RT::Shredder->new; +- +-{ +- my $plugin = eval { $shredder->AddDumpPlugin( Arguments => { +- file_name => $opt{'sqldump'}, +- from_storage => 0, +- } ) }; +- if( $@ ) { +- print STDERR "ERROR: Couldn't open SQL dump file: $@\n"; +- exit 1 if $opt{'sqldump'}; +- +- print STDERR "WARNING: It's strongly recommended to use '--sqldump <filename>' option\n"; +- unless( $opt{'force'} ) { +- exit 0 unless prompt_yN( "Do you want to proceed?" ); +- } +- } else { +- print "SQL dump file is '". $plugin->FileName ."'\n"; +- } +-} +- +-my @objs = process_plugins( $shredder ); +-prompt_delete_objs( \@objs ) unless $opt{'force'}; +- +-$shredder->PutObjects( Objects => $_ ) foreach @objs; +-eval { $shredder->WipeoutAll }; +-if( $@ ) { +- require RT::Shredder::Exceptions; +- if( my $e = RT::Shredder::Exception::Info->caught ) { +- print "\nERROR: $e\n\n"; +- exit 1; +- } +- die $@; +-} +- +-sub prompt_delete_objs +-{ +- my( $objs ) = @_; +- unless( @$objs ) { +- print "Objects list is empty, try refine search options\n"; +- exit 0; +- } +- my $list = "Next ". scalar( @$objs ) ." objects would be deleted:\n"; +- foreach my $o( @$objs ) { +- $list .= "\t". $o->_AsString ." object\n"; +- } +- print $list; +- exit(0) unless prompt_yN( "Do you want to proceed?" ); +-} +- +-sub prompt_yN +-{ +- my $text = shift; +- print "$text [y/N] "; +- unless( <STDIN> =~ /^(?:y|yes)$/i ) { +- return 0; +- } +- return 1; +-} +- +-sub usage +-{ +- require RT::Shredder::POD; +- RT::Shredder::POD::shredder_cli( $0, \*STDOUT ); +- exit 1; +-} +- +-sub parse_args +-{ +- my $tmp; +- Getopt::Long::Configure( "pass_through" ); +- my @objs = (); +- if( GetOptions( 'object=s' => \@objs ) && @objs ) { +- print STDERR "Option --object had been deprecated, use plugin 'Objects' instead\n"; +- exit(1); +- } +- +- my @plugins = (); +- if( GetOptions( 'plugin=s' => \@plugins ) && @plugins ) { +- $opt{'plugin'} = \@plugins; +- foreach my $str( @plugins ) { +- if( $str =~ /^\s*list\s*$/ ) { +- show_plugin_list(); +- } elsif( $str =~ /^\s*help-(\w+)\s*$/ ) { +- show_plugin_help( $1 ); +- } elsif( $str =~ /^(\w+)(=.*)?$/ && !$plugins{$1} ) { +- print "Couldn't find plugin '$1'\n"; +- show_plugin_list(); +- } +- } +- } +- +- # other options make no sense without previouse +- usage() unless keys %opt; +- +- if( GetOptions( 'force' => \$tmp ) && $tmp ) { +- $opt{'force'}++; +- } +- $tmp = undef; +- if( GetOptions( 'sqldump=s' => \$tmp ) && $tmp ) { +- $opt{'sqldump'} = $tmp; +- } +- return; +-} +- +-sub process_plugins +-{ +- my $shredder = shift; +- +- my @res; +- foreach my $str( @{ $opt{'plugin'} } ) { +- my $plugin = RT::Shredder::Plugin->new; +- my( $status, $msg ) = $plugin->LoadByString( $str ); +- unless( $status ) { +- print STDERR "Couldn't load plugin\n"; +- print STDERR "Error: $msg\n"; +- exit(1); +- } +- if ( lc $plugin->Type eq 'search' ) { +- push @res, _process_search_plugin( $shredder, $plugin ); +- } +- elsif ( lc $plugin->Type eq 'dump' ) { +- _process_dump_plugin( $shredder, $plugin ); +- } +- } +- return RT::Shredder->CastObjectsToRecords( Objects => \@res ); +-} +- +-sub _process_search_plugin { +- my ($shredder, $plugin) = @_; +- my ($status, @objs) = $plugin->Run; +- unless( $status ) { +- print STDERR "Couldn't run plugin\n"; +- print STDERR "Error: $objs[1]\n"; +- exit(1); +- } +- +- my $msg; +- ($status, $msg) = $plugin->SetResolvers( Shredder => $shredder ); +- unless( $status ) { +- print STDERR "Couldn't set conflicts resolver\n"; +- print STDERR "Error: $msg\n"; +- exit(1); +- } +- return @objs; +-} +- +-sub _process_dump_plugin { +- my ($shredder, $plugin) = @_; +- $shredder->AddDumpPlugin( +- Object => $plugin, +- ); +-} +- +-sub show_plugin_list +-{ +- print "Plugins list:\n"; +- print "\t$_\n" foreach( grep !/^Base$/, keys %plugins ); +- exit(1); +-} +- +-sub show_plugin_help +-{ +- my( $name ) = @_; +- require RT::Shredder::POD; +- unless( $plugins{ $name } ) { +- print "Couldn't find plugin '$name'\n"; +- show_plugin_list(); +- } +- RT::Shredder::POD::plugin_cli( $plugins{'Base'}, \*STDOUT, 1 ); +- RT::Shredder::POD::plugin_cli( $plugins{ $name }, \*STDOUT ); +- exit(1); +-} +- +-exit(0); +diff --git a/sbin/rt-test-dependencies b/sbin/rt-test-dependencies +deleted file mode 100755 +index 62f0f9c..0000000 +--- a/sbin/rt-test-dependencies ++++ /dev/null +@@ -1,652 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-# +-# This is just a basic script that checks to make sure that all +-# the modules needed by RT before you can install it. +-# +- +-use strict; +-use warnings; +-no warnings qw(numeric redefine); +-use Getopt::Long; +-use Cwd qw(abs_path); +-my %args; +-my %deps; +-my @orig_argv = @ARGV; +-# Save our path because installers or tests can change cwd +-my $script_path = abs_path($0); +- +-GetOptions( +- \%args, 'v|verbose', +- 'install!', +- 'with-MYSQL', 'with-PG', 'with-SQLITE', 'with-ORACLE', +- 'with-FASTCGI', 'with-MODPERL1', 'with-MODPERL2', 'with-STANDALONE', +- +- 'with-DEVELOPER', +- +- 'with-GPG', +- 'with-ICAL', +- 'with-GRAPHVIZ', +- 'with-GD', +- 'with-DASHBOARDS', +- 'with-USERLOGO', +- 'with-HTML-DOC', +- +- 'list-deps', +- 'help|h', +-); +- +-if ( $args{help} ) { +- require Pod::Usage; +- Pod::Usage::pod2usage( { verbose => 2 } ); +- exit; +-} +- +-# Set up defaults +-my %default = ( +- 'with-CORE' => 1, +- 'with-CLI' => 1, +- 'with-MAILGATE' => 1, +- 'with-DEVELOPER' => 0, +- 'with-GPG' => 1, +- 'with-SMIME' => 1, +- 'with-ICAL' => 1, +- 'with-GRAPHVIZ' => 1, +- 'with-GD' => 1, +- 'with-DASHBOARDS' => 1, +- 'with-USERLOGO' => 1, +- 'with-HTML-DOC' => 0, +-); +-$args{$_} = $default{$_} foreach grep !exists $args{$_}, keys %default; +- +-{ +- my $section; +- my %always_show_sections = ( +- perl => 1, +- users => 1, +- ); +- +- sub section { +- my $s = shift; +- $section = $s; +- print "$s:\n" unless $args{'list-deps'}; +- } +- +- sub print_found { +- my $msg = shift; +- my $test = shift; +- my $extra = shift; +- +- unless ( $args{'list-deps'} ) { +- if ( $args{'v'} or not $test or $always_show_sections{$section} ) { +- print "\t$msg ..."; +- print $test ? "found" : "MISSING"; +- print "\n"; +- } +- +- print "\t\t$extra\n" if defined $extra; +- } +- } +-} +- +-sub conclude { +- my %missing_by_type = @_; +- +- unless ( $args{'list-deps'} ) { +- unless ( keys %missing_by_type ) { +- print "\nAll dependencies have been found.\n"; +- return; +- } +- +- print "\nSOME DEPENDENCIES WERE MISSING.\n"; +- +- for my $type ( keys %missing_by_type ) { +- my $missing = $missing_by_type{$type}; +- +- print "$type missing dependencies:\n"; +- for my $name ( keys %$missing ) { +- my $module = $missing->{$name}; +- my $version = $module->{version}; +- my $error = $module->{error}; +- print_found( $name . ( $version && !$error ? " >= $version" : "" ), +- 0, $module->{error} ); +- } +- } +- +- print "\nPerl library path for /usr/bin/perl:\n"; +- print " $_\n" for @INC; +- +- exit 1; +- } +-} +- +-sub text_to_hash { +- my %hash; +- for my $line ( split /\n/, $_[0] ) { +- my($key, $value) = $line =~ /(\S+)\s*(\S*)/; +- $value ||= ''; +- $hash{$key} = $value; +- } +- +- return %hash; +-} +-sub set_dep { +- my ($name, $module, $version) = @_; +- my %list = @{$deps{$name}}; +- $list{$module} = ($version || ''); +- $deps{$name} = [ %list ]; +-} +- +-$deps{'CORE'} = [ text_to_hash( << '.') ]; +-Apache::Session 1.53 +-CGI 3.38 +-CGI::Cookie 1.20 +-CGI::Emulate::PSGI +-CGI::PSGI 0.12 +-Class::Accessor 0.34 +-Crypt::Eksblowfish +-CSS::Squish 0.06 +-Data::GUID +-Date::Extract 0.02 +-Date::Manip +-DateTime 0.44 +-DateTime::Format::Natural 0.67 +-DateTime::Locale 0.40 +-DBI 1.37 +-DBIx::SearchBuilder 1.65 +-Devel::GlobalDestruction +-Devel::StackTrace 1.19 +-Digest::base +-Digest::MD5 2.27 +-Digest::SHA +-Email::Address 1.897 +-Email::Address::List 0.02 +-Encode 2.64 +-Errno +-File::Glob +-File::ShareDir +-File::Spec 0.8 +-File::Temp 0.19 +-HTML::Entities +-HTML::FormatText::WithLinks 0.14 +-HTML::FormatText::WithLinks::AndTables +-HTML::Mason 1.43 +-HTML::Mason::PSGIHandler 0.52 +-HTML::Quoted +-HTML::RewriteAttributes 0.05 +-HTML::Scrubber 0.08 +-HTTP::Message 6.0 +-IPC::Run3 +-JSON +-LWP::Simple +-List::MoreUtils +-Locale::Maketext 1.06 +-Locale::Maketext::Fuzzy 0.11 +-Locale::Maketext::Lexicon 0.32 +-Log::Dispatch 2.30 +-Mail::Header 2.12 +-Mail::Mailer 1.57 +-MIME::Entity 5.504 +-Module::Refresh 0.03 +-Module::Versions::Report 1.05 +-Net::CIDR +-Plack 1.0002 +-Plack::Handler::Starlet +-Regexp::Common +-Regexp::Common::net::CIDR +-Regexp::IPv6 +-Role::Basic 0.12 +-Scalar::Util +-Storable 2.08 +-Symbol::Global::Name 0.04 +-Sys::Syslog 0.16 +-Text::Password::Pronounceable +-Text::Quoted 2.07 +-Text::Template 1.44 +-Text::WikiFormat 0.76 +-Text::Wrapper +-Time::HiRes +-Time::ParseDate +-Tree::Simple 1.04 +-UNIVERSAL::require +-XML::RSS 1.05 +-. +-set_dep( CORE => 'Symbol::Global::Name' => 0.05 ) if $] >= 5.019003; +-set_dep( CORE => CGI => 4.00 ) if $] > 5.019003; +- +-$deps{'MAILGATE'} = [ text_to_hash( << '.') ]; +-Crypt::SSLeay +-Getopt::Long +-LWP::Protocol::https +-LWP::UserAgent 6.0 +-Mozilla::CA +-Net::SSL +-Pod::Usage +-. +- +-$deps{'CLI'} = [ text_to_hash( << '.') ]; +-Getopt::Long 2.24 +-HTTP::Request::Common +-LWP +-Term::ReadKey +-Term::ReadLine +-Text::ParseWords +-. +- +-$deps{'DEVELOPER'} = [ text_to_hash( << '.') ]; +-Email::Abstract +-File::Find +-File::Which +-Locale::PO +-Log::Dispatch::Perl +-Mojo::DOM +-Plack::Middleware::Test::StashWarnings 0.08 +-Set::Tiny +-String::ShellQuote 0 # needed for gnupg-incoming.t +-Test::Builder 0.90 # needed for is_passing +-Test::Deep 0 # needed for shredder tests +-Test::Email +-Test::Expect 0.31 +-Test::LongString +-Test::MockTime +-Test::NoWarnings +-Test::Pod +-Test::Warn +-Test::WWW::Mechanize 1.30 +-Test::WWW::Mechanize::PSGI +-WWW::Mechanize 1.52 +-XML::Simple +-. +- +-$deps{'FASTCGI'} = [ text_to_hash( << '.') ]; +-FCGI 0.74 +-FCGI::ProcManager +-. +- +-$deps{'MODPERL1'} = [ text_to_hash( << '.') ]; +-Apache::DBI 0.92 +-Apache::Request +-. +- +-$deps{'MODPERL2'} = [ text_to_hash( << '.') ]; +-Apache::DBI +-. +- +-$deps{'MYSQL'} = [ text_to_hash( << '.') ]; +-DBD::mysql 2.1018 +-. +- +-$deps{'ORACLE'} = [ text_to_hash( << '.') ]; +-DBD::Oracle +-. +- +-$deps{'PG'} = [ text_to_hash( << '.') ]; +-DBIx::SearchBuilder 1.66 +-DBD::Pg 1.43 +-. +- +-$deps{'SQLITE'} = [ text_to_hash( << '.') ]; +-DBD::SQLite 1.00 +-. +- +-$deps{'GPG'} = [ text_to_hash( << '.') ]; +-File::Which +-GnuPG::Interface +-PerlIO::eol +-. +- +-$deps{'SMIME'} = [ text_to_hash( << '.') ]; +-Crypt::X509 +-File::Which +-String::ShellQuote +-. +- +-$deps{'ICAL'} = [ text_to_hash( << '.') ]; +-Data::ICal +-. +- +-$deps{'DASHBOARDS'} = [ text_to_hash( << '.') ]; +-MIME::Types +-URI 1.59 +-URI::QueryParam +-. +- +-$deps{'GRAPHVIZ'} = [ text_to_hash( << '.') ]; +-GraphViz +-IPC::Run 0.90 +-. +- +-$deps{'GD'} = [ text_to_hash( << '.') ]; +-GD +-GD::Graph 1.47 +-GD::Text +-. +- +-$deps{'USERLOGO'} = [ text_to_hash( << '.') ]; +-Convert::Color +-. +- +-$deps{'HTML-DOC'} = [ text_to_hash( <<'.') ]; +-HTML::Entities +-Pod::Simple 3.24 +-. +- +-my %AVOID = ( +- 'DBD::Oracle' => [qw(1.23)], +- 'Devel::StackTrace' => [qw(1.28 1.29)], +-); +- +-if ($args{'download'}) { +- download_mods(); +-} +- +- +-check_perl_version(); +- +-check_users(); +- +-my %Missing_By_Type = (); +-foreach my $type (sort grep $args{$_}, keys %args) { +- next unless ($type =~ /^with-(.*?)$/) and $deps{$1}; +- +- $type = $1; +- section("$type dependencies"); +- +- my @missing; +- my @deps = @{ $deps{$type} }; +- +- my %missing = test_deps(@deps); +- +- if ( $args{'install'} ) { +- for my $module (keys %missing) { +- resolve_dep($module, $missing{$module}{version}); +- my $m = $module . '.pm'; +- $m =~ s!::!/!g; +- if ( delete $INC{$m} ) { +- my $symtab = $module . '::'; +- no strict 'refs'; +- for my $symbol ( keys %{$symtab} ) { +- next if substr( $symbol, -2, 2 ) eq '::'; +- delete $symtab->{$symbol}; +- } +- } +- delete $missing{$module} +- if test_dep($module, $missing{$module}{version}, $AVOID{$module}); +- } +- } +- +- $Missing_By_Type{$type} = \%missing if keys %missing; +-} +- +-if ( $args{'install'} && keys %Missing_By_Type ) { +- exec($script_path, @orig_argv, '--no-install'); +-} +-else { +- conclude(%Missing_By_Type); +-} +- +-sub test_deps { +- my @deps = @_; +- +- my %missing; +- while(@deps) { +- my $module = shift @deps; +- my $version = shift @deps; +- my($test, $error) = test_dep($module, $version, $AVOID{$module}); +- my $msg = $module . ($version && !$error ? " >= $version" : ''); +- print_found($msg, $test, $error); +- +- $missing{$module} = { version => $version, error => $error } unless $test; +- } +- +- return %missing; +-} +- +-sub test_dep { +- my $module = shift; +- my $version = shift; +- my $avoid = shift; +- +- if ( $args{'list-deps'} ) { +- print $module, ': ', $version || 0, "\n"; +- } +- else { +- no warnings 'deprecated'; +- eval "{ local \$ENV{__WARN__}; use $module $version () }"; +- if ( my $error = $@ ) { +- return 0 unless wantarray; +- +- $error =~ s/\n(.*)$//s; +- $error =~ s/at \(eval \d+\) line \d+\.$//; +- undef $error if $error =~ /this is only/; +- +- my $path = $module; +- $path =~ s{::}{/}g; +- undef $error if defined $error and $error =~ /^Can't locate $path\.pm in \@INC/; +- +- return ( 0, $error ); +- } +- +- if ( $avoid ) { +- my $version = $module->VERSION; +- if ( grep $version eq $_, @$avoid ) { +- return 0 unless wantarray; +- return (0, "It's known that there are problems with RT and version '$version' of '$module' module. If it's the latest available version of the module then you have to downgrade manually."); +- } +- } +- +- return 1; +- } +-} +- +-sub resolve_dep { +- my $module = shift; +- my $version = shift; +- +- print "\nInstall module $module\n"; +- +- my $ext = $ENV{'RT_FIX_DEPS_CMD'} || $ENV{'PERL_PREFER_CPAN_CLIENT'}; +- unless( $ext ) { +- my $configured = 1; +- { +- local @INC = @INC; +- if ( $ENV{'HOME'} ) { +- unshift @INC, "$ENV{'HOME'}/.cpan"; +- } +- $configured = eval { require CPAN::MyConfig } || eval { require CPAN::Config }; +- } +- unless ( $configured ) { +- print <<END; +-You haven't configured the CPAN shell yet. +-Please run `/usr/bin/perl -MCPAN -e shell` to configure it. +-END +- exit(1); +- } +- my $rv = eval { require CPAN; CPAN::Shell->install($module) }; +- return $rv unless $@; +- +- print <<END; +-Failed to load module CPAN. +- +--------- Error --------- +-$@ +------------------------- +- +-When we tried to start installing RT's perl dependencies, +-we were unable to load the CPAN client. This module is usually distributed +-with Perl. This usually indicates that your vendor has shipped an unconfigured +-or incorrectly configured CPAN client. +-The error above may (or may not) give you a hint about what went wrong +- +-You have several choices about how to install dependencies in +-this situatation: +- +-1) use a different tool to install dependencies by running setting the following +- shell environment variable and rerunning this tool: +- RT_FIX_DEPS_CMD='/usr/bin/perl -MCPAN -e"install %s"' +-2) Attempt to configure CPAN by running: +- `/usr/bin/perl -MCPAN -e shell` program from shell. +- If this fails, you may have to manually upgrade CPAN (see below) +-3) Try to update the CPAN client. Download it from: +- http://search.cpan.org/dist/CPAN and try again +-4) Install each dependency manually by downloading them one by one from +- http://search.cpan.org +- +-END +- exit(1); +- } +- +- if( $ext =~ /\%s/) { +- $ext =~ s/\%s/$module/g; # sprintf( $ext, $module ); +- } else { +- $ext .= " $module"; +- } +- print "\t\tcommand: '$ext'\n"; +- return scalar `$ext 1>&2`; +-} +- +-sub check_perl_version { +- section("perl"); +- eval {require 5.010_001}; +- if ($@) { +- print_found("5.10.1", 0, sprintf("RT requires Perl v5.10.1 or newer. Your current Perl is v%vd", $^V)); +- exit(1); +- } else { +- print_found( sprintf(">=5.10.1(%vd)", $^V), 1 ); +- } +-} +- +-sub check_users { +- section("users"); +- print_found("rt group (www-data)", defined getgrnam("www-data")); +- print_found("bin owner (root)", defined getpwnam("root")); +- print_found("libs owner (root)", defined getpwnam("root")); +- print_found("libs group (bin)", defined getgrnam("bin")); +- print_found("web owner (www-data)", defined getpwnam("www-data")); +- print_found("web group (www-data)", defined getgrnam("www-data")); +-} +- +-1; +- +-__END__ +- +-=head1 NAME +- +-rt-test-dependencies - test rt's dependencies +- +-=head1 SYNOPSIS +- +- rt-test-dependencies +- rt-test-dependencies --install +- rt-test-dependencies --with-mysql --with-fastcgi +- +-=head1 DESCRIPTION +- +-by default, C<rt-test-dependencies> determines whether you have installed all +-the perl modules RT needs to run. +- +-the "RT_FIX_DEPS_CMD" environment variable, if set, will be used instead of +-the standard CPAN shell by --install to install any required modules. it will +-be called with the module name, or, if "RT_FIX_DEPS_CMD" contains a "%s", will +-replace the "%s" with the module name before calling the program. +- +-=head1 OPTIONS +- +-=over +- +-=item install +- +- install missing modules +- +-=item verbose +- +-list the status of all dependencies, rather than just the missing ones. +- +--v is equal to --verbose +- +-=item specify dependencies +- +-=over +- +-=item --with-mysql +- +-database interface for mysql +- +-=item --with-pg +- +-database interface for postgresql +- +-=item --with-oracle +- +-database interface for oracle +- +-=item --with-sqlite +- +-database interface and driver for sqlite (unsupported) +- +-=item --with-fastcgi +- +-libraries needed to support the fastcgi handler +- +-=item --with-modperl1 +- +-libraries needed to support the modperl 1 handler +- +-=item --with-modperl2 +- +-libraries needed to support the modperl 2 handler +- +-=item --with-developer +- +-tools needed for RT development +- +-=back +- +-=back +- +diff --git a/sbin/rt-validate-aliases b/sbin/rt-validate-aliases +deleted file mode 100755 +index 25d4cc3..0000000 +--- a/sbin/rt-validate-aliases ++++ /dev/null +@@ -1,373 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +-use Text::ParseWords qw//; +-use Getopt::Long; +- +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-require RT; +-RT::LoadConfig(); +-RT::Init(); +- +-my ($PREFIX, $URL, $HOST) = (""); +-GetOptions( +- "prefix|p=s" => \$PREFIX, +- "url|u=s" => \$URL, +- "host|h=s" => \$HOST, +-); +- +-unless (@ARGV) { +- @ARGV = grep {-f} ("/etc/aliases", +- "/etc/mail/aliases", +- "/etc/postfix/aliases"); +- die "Can't determine aliases file to parse!" +- unless @ARGV; +-} +- +-my %aliases = parse_lines(); +-unless (%aliases) { +- warn "No mailgate aliases found in @ARGV"; +- exit; +-} +- +-my %seen; +-my $global_mailgate; +-for my $address (sort keys %aliases) { +- my ($mailgate, $opts, $extra) = @{$aliases{$address}}; +- my %opts = %{$opts}; +- +- next if $opts{url} and $URL and $opts{url} !~ /\Q$URL\E/; +- +- if ($mailgate !~ /^\|/) { +- warn "Missing the leading | on alias $address\n"; +- $mailgate = "|$mailgate"; +- } +- if (($global_mailgate ||= $mailgate) ne $mailgate) { +- warn "Unexpected mailgate for alias $address -- expected $global_mailgate, got $mailgate\n"; +- } +- +- if (not defined $opts{action}) { +- warn "Missing --action parameter for alias $address\n"; +- } elsif ($opts{action} !~ /^(correspond|comment)$/) { +- warn "Invalid --action parameter for alias $address: $opts{action}\n" +- } +- +- my $queue = RT::Queue->new( RT->SystemUser ); +- if (not defined $opts{queue}) { +- warn "Missing --queue parameter for alias $address\n"; +- } else { +- $queue->Load( $opts{queue} ); +- if (not $queue->id) { +- warn "Invalid --queue parameter for alias $address: $opts{queue}\n"; +- } elsif ($queue->Disabled) { +- warn "Disabled --queue given for alias $address: $opts{queue}\n"; +- } +- } +- +- if (not defined $opts{url}) { +- warn "Missing --url parameter for alias $address\n"; +- } #XXX: Test connectivity and/or https certs? +- +- if ($queue->id and $opts{action} =~ /^(correspond|comment)$/) { +- push @{$seen{lc $queue->Name}{$opts{action}}}, $address; +- } +- +- warn "Unknown extra arguments for alias $address: @{$extra}\n" +- if @{$extra}; +-} +- +-# Check the global settings +-my %global; +-for my $action (qw/correspond comment/) { +- my $setting = ucfirst($action) . "Address"; +- my $value = RT->Config->Get($setting); +- if (not defined $value) { +- warn "$setting is not set!\n"; +- next; +- } +- my ($local,$host) = lc($value) =~ /(.*?)\@(.*)/; +- next if $HOST and $host !~ /\Q$HOST\E/; +- $local = "$PREFIX$local" unless exists $aliases{$local}; +- +- $global{$setting} = $local; +- if (not exists $aliases{$local}) { +- warn "$setting $value does not exist in aliases!\n" +- } elsif ($aliases{$local}[1]{action} ne $action) { +- warn "$setting $value is a $aliases{$local}[1]{action} in aliases!" +- } +-} +-warn "CorrespondAddress and CommentAddress are the same!\n" +- if RT->Config->Get("CorrespondAddress") eq RT->Config->Get("CommentAddress"); +- +- +-# Go through the queues, one at a time +-my $queues = RT::Queues->new( RT->SystemUser ); +-$queues->UnLimit; +-while (my $q = $queues->Next) { +- my $qname = $q->Name; +- for my $action (qw/correspond comment/) { +- my $setting = ucfirst($action) . "Address"; +- my $value = $q->$setting; +- +- if (not $value) { +- my @other = grep {$_ ne $global{$setting}} @{$seen{lc $q->Name}{$action} || []}; +- warn "$setting not set on $qname, but in aliases as " +- .join(" and ", @other) . "\n" if @other; +- next; +- } +- +- if ($action eq "comment" and $q->CorrespondAddress +- and $q->CorrespondAddress eq $q->CommentAddress) { +- warn "CorrespondAddress and CommentAddress are set the same on $qname\n"; +- next; +- } +- +- my ($local, $host) = lc($value) =~ /(.*?)\@(.*)/; +- next if $HOST and $host !~ /\Q$HOST\E/; +- $local = "$PREFIX$local" unless exists $aliases{$local}; +- +- my @other = @{$seen{lc $q->Name}{$action} || []}; +- if (not exists $aliases{$local}) { +- if (@other) { +- warn "$setting $value on $qname does not exist in aliases -- typo'd as " +- .join(" or ", @other) . "?\n"; +- } else { +- warn "$setting $value on $qname does not exist in aliases!\n" +- } +- next; +- } +- +- my %opt = %{$aliases{$local}[1]}; +- if ($opt{action} ne $action) { +- warn "$setting address $value on $qname is a $opt{action} in aliases!\n" +- } +- if (lc $opt{queue} ne lc $q->Name and $action ne "comment") { +- warn "$setting address $value on $qname points to queue $opt{queue} in aliases!\n"; +- } +- +- @other = grep {$_ ne $local} @other; +- warn "Extra aliases for queue $qname: ".join(",",@other)."\n" +- if @other; +- } +-} +- +- +-sub parse_lines { +- local @ARGV = @ARGV; +- +- my %aliases; +- my $line = ""; +- for (<>) { +- next unless /\S/; +- next if /^#/; +- chomp; +- if (/^\s+/) { +- $line .= $_; +- } else { +- add_line($line, \%aliases); +- $line = $_; +- } +- } +- add_line($line, \%aliases); +- +- expand(\%aliases); +- filter_mailgate(\%aliases); +- +- return %aliases; +-} +- +-sub expand { +- my ($data) = @_; +- +- for (1..100) { +- my $expanded = 0; +- for my $address (sort keys %{$data}) { +- my @new; +- for my $part (@{$data->{$address}}) { +- if (m!^[|/]! or not $data->{$part}) { +- push @new, $part; +- } else { +- $expanded++; +- push @new, @{$data->{$part}}; +- } +- } +- $data->{$address} = \@new; +- } +- return unless $expanded; +- } +- warn "Recursion limit exceeded -- cycle in aliases?\n"; +-} +- +-sub filter_mailgate { +- my ($data) = @_; +- +- for my $address (sort keys %{$data}) { +- my @parts = @{delete $data->{$address}}; +- +- my @pipes = grep {m!^\|?.*?/rt-mailgate\b!} @parts; +- next unless @pipes; +- +- my $pipe = shift @pipes; +- warn "More than one rt-mailgate pipe for alias: $address\n" +- if @pipes; +- +- my @args = Text::ParseWords::shellwords($pipe); +- +- # We allow "|/random-other-command /opt/rt4/bin/rt-mailgate ...", +- # we just need to strip off enough +- my $index = 0; +- $index++ while $args[$index] !~ m!/rt-mailgate!; +- my $mailgate = join(' ', splice(@args,0,$index+1)); +- +- my %opts; +- local @ARGV = @args; +- Getopt::Long::Configure( "pass_through" ); # Allow unknown options +- my $ret = eval { +- GetOptions( \%opts, "queue=s", "action=s", "url=s", +- "jar=s", "debug", "extension=s", +- "timeout=i", "verify-ssl!", "ca-file=s", +- ); +- 1; +- }; +- warn "Failed to parse options for $address: $@" unless $ret; +- next unless %opts; +- +- $data->{lc $address} = [$mailgate, \%opts, [@ARGV]]; +- } +-} +- +-sub add_line { +- my ($line, $data) = @_; +- return unless $line =~ /\S/; +- +- my ($name, $parts) = parse_line($line); +- return unless defined $name; +- +- if (defined $data->{$name}) { +- warn "Duplicate definition for alias $name\n"; +- return; +- } +- +- $data->{lc $name} = $parts; +-} +- +-sub parse_line { +- my $re_name = qr/\S+/; +- # Intentionally accept pipe-like aliases with a missing | -- we deal with them later +- my $re_quoted_pipe = qr/"\|?[^\\"]*(?:\\[\\"][^\\"]*)*"/; +- my $re_nonquoted_pipe = qr/\|[^\s,]+/; +- my $re_pipe = qr/(?:$re_quoted_pipe|$re_nonquoted_pipe)/; +- my $re_path = qr!/[^,\s]+!; +- my $re_address = qr![^|/,\s][^,\s]*!; +- my $re_value = qr/(?:$re_pipe|$re_path|$re_address)/; +- my $re_values = qr/(?:$re_value(?:\s*,\s*$re_value)*)/; +- +- my ($line) = @_; +- if ($line =~ /^($re_name):\s*($re_values)/) { +- my ($name, $all_parts) = ($1, $2); +- my @parts; +- while ($all_parts =~ s/^(?:\s*,\s*)?($re_value)//) { +- my $part = $1; +- if ($part =~ /^"/) { +- $part =~ s/^"//; $part =~ s/"$//; +- $part =~ s/\\(.)/$1/g; +- } +- push @parts, $part; +- } +- return $name, [@parts]; +- } else { +- warn "Parse failure, line $. of $ARGV: $line\n"; +- return (); +- } +-} +- +-__END__ +- +-=head1 NAME +- +-rt-validate-aliases - Check an MTA alias file against RT queue configuration +- +-=head1 SYNOPSIS +- +-rt-validate-aliases [options] /etc/aliases +- +-=head1 OPTIONS +- +-=over +- +-=item C<--prefix> +- +-An expected address prefix used in the alias file +- +-=item C<--url> +- +-The root URL of your RT server (the same URL you expect to be passed to +-rt-mailgate) +- +-=item C<--host> +- +-The host part of your RT email addresses +- +-=back +diff --git a/sbin/rt-validator b/sbin/rt-validator +deleted file mode 100755 +index 6ebfc0d..0000000 +--- a/sbin/rt-validator ++++ /dev/null +@@ -1,1465 +0,0 @@ +-#!/usr/bin/perl +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use strict; +-use warnings; +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long; +-my %opt = (); +-GetOptions( +- \%opt, +- 'check|c', +- 'resolve', +- 'force', +- 'verbose|v', +- 'help|h', +- 'links-only', +-); +- +-if ( $opt{help} || !$opt{check} ) { +- require Pod::Usage; +- print Pod::Usage::pod2usage( { verbose => 2 } ); +- exit 2; +-} +- +-usage_warning() if $opt{'resolve'} && !$opt{'force'}; +- +-sub usage_warning { +- print <<END; +-This utility can fix some issues with DB by creating or updating. In some +-cases there is not enough data to resurect a missing record, but records which +-refer to a missing record can be deleted. It's up to you to decide what to do. +- +-In any case it's highly recommended to have a backup before resolving anything. +- +-Press enter to continue. +-END +-# Read a line of text, any line of text +- <STDIN>; +-} +- +-use RT; +-RT::LoadConfig(); +-RT::Init(); +- +-my $dbh = $RT::Handle->dbh; +-my $db_type = RT->Config->Get('DatabaseType'); +- +-my %TYPE = ( +- 'Transactions.Field' => 'text', +- 'Transactions.OldValue' => 'text', +- 'Transactions.NewValue' => 'text', +-); +- +-my @models = qw( +- ACE +- Article +- Attachment +- Attribute +- CachedGroupMember +- CustomField +- CustomFieldValue +- GroupMember +- Group +- Link +- ObjectCustomField +- ObjectCustomFieldValue +- Principal +- Queue +- ScripAction +- ScripCondition +- Scrip +- ObjectScrip +- Template +- Ticket +- Transaction +- User +-); +- +-my %redo_on; +-$redo_on{'Delete'} = { +- ACL => [], +- +- Attributes => [], +- +- Links => [], +- +- CustomFields => [], +- CustomFieldValues => [], +- ObjectCustomFields => [], +- ObjectCustomFieldValues => [], +- +- Queues => [], +- +- Scrips => [], +- ObjectScrips => [], +- ScripActions => [], +- ScripConditions => [], +- Templates => [], +- +- Tickets => [ 'Tickets -> other', 'Tickets <-> Role Groups' ], +- Transactions => [ 'Attachments -> other' ], +- +- Principals => ['User <-> ACL equivalence group', 'GMs -> Groups, Members' ], +- Users => ['User <-> ACL equivalence group', 'GMs -> Groups, Members', 'Principals -> Users' ], +- Groups => ['User <-> ACL equivalence group', 'GMs -> Groups, Members', 'CGM vs. GM', 'Principals -> Groups' ], +- +- GroupMembers => [ 'CGM vs. GM' ], +- CachedGroupMembers => [ 'CGM vs. GM' ], +-}; +-$redo_on{'Create'} = { +- Principals => ['User <-> ACL equivalence group', 'GMs -> Groups, Members' ], +- Groups => ['User <-> ACL equivalence group', 'GMs -> Groups, Members', 'CGM vs. GM' ], +- GroupMembers => [ 'CGM vs. GM' ], +- CachedGroupMembers => [ 'CGM vs. GM' ], +-}; +-$redo_on{'Update'} = { +- Groups => ['User Defined Group Name uniqueness'], +-}; +- +-my %describe_cb; +-%describe_cb = ( +- Attachments => sub { +- my $row = shift; +- my $txn_id = $row->{transactionid}; +- my $res = 'Attachment #'. $row->{id} .' -> Txn #'. $txn_id; +- return $res .', '. describe( 'Transactions', $txn_id ); +- }, +- Transactions => sub { +- my $row = shift; +- return 'Transaction #'. $row->{id} .' -> object '. $row->{objecttype} .' #'. $row->{objectid}; +- }, +-); +- +-{ my %cache = (); +-sub m2t($) { +- my $model = shift; +- return $cache{$model} if $cache{$model}; +- my $class = "RT::$model"; +- my $object = $class->new( RT->SystemUser ); +- return $cache{$model} = $object->Table; +-} } +- +-my (@do_check, %redo_check); +- +-my @CHECKS; +-foreach my $table ( qw(Users Groups) ) { +- push @CHECKS, "$table -> Principals" => sub { +- my $msg = "A record in $table refers to a nonexistent record in Principals." +- ." The script can either create the missing record in Principals" +- ." or delete the record in $table."; +- my ($type) = ($table =~ /^(.*)s$/); +- return check_integrity( +- $table, 'id' => 'Principals', 'id', +- join_condition => 't.PrincipalType = ?', +- bind_values => [ $type ], +- action => sub { +- my $id = shift; +- return unless my $a = prompt_action( ['Create', 'delete'], $msg ); +- +- if ( $a eq 'd' ) { +- delete_record( $table, $id ); +- } +- elsif ( $a eq 'c' ) { +- my $principal_id = create_record( 'Principals', +- id => $id, PrincipalType => $type, ObjectId => $id, Disabled => 0 +- ); +- } +- else { +- die "Unknown action '$a'"; +- } +- }, +- ); +- }; +- +- push @CHECKS, "Principals -> $table" => sub { +- my $msg = "A record in Principals refers to a nonexistent record in $table." +- ." In some cases it's possible to manually resurrect such records," +- ." but this utility can only delete records."; +- +- return check_integrity( +- 'Principals', 'id' => $table, 'id', +- condition => 's.PrincipalType = ?', +- bind_values => [ $table =~ /^(.*)s$/ ], +- action => sub { +- my $id = shift; +- return unless prompt( 'Delete', $msg ); +- +- delete_record( 'Principals', $id ); +- }, +- ); +- }; +-} +- +-push @CHECKS, 'User <-> ACL equivalence group' => sub { +- my $res = 1; +- # from user to group +- $res *= check_integrity( +- 'Users', 'id' => 'Groups', 'Instance', +- join_condition => 't.Domain = ? AND t.Type = ?', +- bind_values => [ 'ACLEquivalence', 'UserEquiv' ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Create', "Found a user that has no ACL equivalence group." +- ); +- +- my $gid = create_record( 'Groups', +- Domain => 'ACLEquivalence', Type => 'UserEquiv', Instance => $id, +- ); +- }, +- ); +- # from group to user +- $res *= check_integrity( +- 'Groups', 'Instance' => 'Users', 'id', +- condition => 's.Domain = ? AND s.Type = ?', +- bind_values => [ 'ACLEquivalence', 'UserEquiv' ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a user ACL equivalence group, but there is no user." +- ); +- +- delete_record( 'Groups', $id ); +- }, +- ); +- # one ACL equiv group for each user +- $res *= check_uniqueness( +- 'Groups', +- columns => ['Instance'], +- condition => '.Domain = ? AND .Type = ?', +- bind_values => [ 'ACLEquivalence', 'UserEquiv' ], +- ); +- return $res; +-}; +- +-# check integrity of Queue role groups +-push @CHECKS, 'Queues <-> Role Groups' => sub { +- # XXX: we check only that there is at least one group for a queue +- # from queue to group +- my $res = 1; +- $res *= check_integrity( +- 'Queues', 'id' => 'Groups', 'Instance', +- join_condition => 't.Domain = ?', +- bind_values => [ 'RT::Queue-Role' ], +- ); +- # from group to queue +- $res *= check_integrity( +- 'Groups', 'Instance' => 'Queues', 'id', +- condition => 's.Domain = ?', +- bind_values => [ 'RT::Queue-Role' ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a role group of a nonexistent queue." +- ); +- +- delete_record( 'Groups', $id ); +- }, +- ); +- return $res; +-}; +- +-# check integrity of Ticket role groups +-push @CHECKS, 'Tickets <-> Role Groups' => sub { +- # XXX: we check only that there is at least one group for a queue +- # from queue to group +- my $res = 1; +- $res *= check_integrity( +- 'Tickets', 'id' => 'Groups', 'Instance', +- join_condition => 't.Domain = ?', +- bind_values => [ 'RT::Ticket-Role' ], +- ); +- # from group to ticket +- $res *= check_integrity( +- 'Groups', 'Instance' => 'Tickets', 'id', +- condition => 's.Domain = ?', +- bind_values => [ 'RT::Ticket-Role' ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a role group of a nonexistent ticket." +- ); +- +- delete_record( 'Groups', $id ); +- }, +- ); +- return $res; +-}; +- +-# additional CHECKS on groups +-push @CHECKS, 'Role Groups (Instance, Type) uniqueness' => sub { +- # Check that Domain, Instance and Type are unique +- return check_uniqueness( +- 'Groups', +- columns => ['Domain', 'Instance', 'Type'], +- condition => '.Domain LIKE ?', +- bind_values => [ '%-Role' ], +- ); +-}; +- +-push @CHECKS, 'System internal group uniqueness' => sub { +- return check_uniqueness( +- 'Groups', +- columns => ['Instance', 'Type'], +- condition => '.Domain = ?', +- bind_values => [ 'SystemInternal' ], +- ); +-}; +- +-# CHECK that user defined group names are unique +-push @CHECKS, 'User Defined Group Name uniqueness' => sub { +- return check_uniqueness( +- 'Groups', +- columns => ['Name'], +- condition => '.Domain = ?', +- bind_values => [ 'UserDefined' ], +- extra_tables => ['Principals sp', 'Principals tp'], +- extra_condition => join(" and ", map { "$_.id = ${_}p.ObjectId and ${_}p.PrincipalType = ? and ${_}p.Disabled != 1" } qw(s t)), +- extra_values => ['Group', 'Group'], +- action => sub { +- return unless prompt( +- 'Rename', "Found a user defined group with a non-unique Name." +- ); +- +- my $id = shift; +- my %cols = @_; +- update_records('Groups', { id => $id }, { Name => join('-', $cols{'Name'}, $id) }); +- }, +- ); +-}; +- +-push @CHECKS, 'GMs -> Groups, Members' => sub { +- my $msg = "A record in GroupMembers references an object that doesn't exist." +- ." Maybe you deleted a group or principal directly from the database?" +- ." Usually it's OK to delete such records."; +- my $res = 1; +- $res *= check_integrity( +- 'GroupMembers', 'GroupId' => 'Groups', 'id', +- action => sub { +- my $id = shift; +- return unless prompt( 'Delete', $msg ); +- +- delete_record( 'GroupMembers', $id ); +- }, +- ); +- $res *= check_integrity( +- 'GroupMembers', 'MemberId' => 'Principals', 'id', +- action => sub { +- my $id = shift; +- return unless prompt( 'Delete', $msg ); +- +- delete_record( 'GroupMembers', $id ); +- }, +- ); +- return $res; +-}; +- +-# CGM and GM +-push @CHECKS, 'CGM vs. GM' => sub { +- my $res = 1; +- # all GM record should be duplicated in CGM +- $res *= check_integrity( +- GroupMembers => ['GroupId', 'MemberId'], +- CachedGroupMembers => ['GroupId', 'MemberId'], +- join_condition => 't.ImmediateParentId = t.GroupId AND t.Via = t.id', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Create', +- "Found a record in GroupMembers that has no direct duplicate in CachedGroupMembers table." +- ); +- +- my $gm = RT::GroupMember->new( RT->SystemUser ); +- $gm->Load( $id ); +- die "Couldn't load GM record #$id" unless $gm->id; +- my $cgm = create_record( 'CachedGroupMembers', +- GroupId => $gm->GroupId, MemberId => $gm->MemberId, +- ImmediateParentId => $gm->GroupId, Via => undef, +- Disabled => 0, # XXX: we should check integrity of Disabled field +- ); +- update_records( "CachedGroupMembers", { id => $cgm }, { Via => $cgm } ); +- }, +- ); +- # all first level CGM records should have a GM record +- $res *= check_integrity( +- CachedGroupMembers => ['GroupId', 'MemberId'], +- GroupMembers => ['GroupId', 'MemberId'], +- condition => 's.ImmediateParentId = s.GroupId AND s.Via = s.id AND s.GroupId != s.MemberId', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', +- "Found a record in CachedGroupMembers for a (Group, Member) pair" +- ." that doesn't exist in the GroupMembers table." +- ); +- +- delete_record( 'CachedGroupMembers', $id ); +- }, +- ); +- # each group should have a CGM record where MemberId == GroupId +- $res *= check_integrity( +- Groups => ['id', 'id'], +- CachedGroupMembers => ['GroupId', 'MemberId'], +- join_condition => 't.ImmediateParentId = t.GroupId AND t.Via = t.id', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Create', +- "Found a record in Groups that has no direct" +- ." duplicate in CachedGroupMembers table." +- ); +- +- my $g = RT::Group->new( RT->SystemUser ); +- $g->Load( $id ); +- die "Couldn't load group #$id" unless $g->id; +- die "Loaded group by $id has id ". $g->id unless $g->id == $id; +- my $cgm = create_record( 'CachedGroupMembers', +- GroupId => $id, MemberId => $id, +- ImmediateParentId => $id, Via => undef, +- Disabled => $g->Disabled, +- ); +- update_records( "CachedGroupMembers", { id => $cgm }, { Via => $cgm } ); +- }, +- ); +- +- # and back, each record in CGM with MemberId == GroupId without exceptions +- # should reference a group +- $res *= check_integrity( +- CachedGroupMembers => ['GroupId', 'MemberId'], +- Groups => ['id', 'id'], +- condition => "s.GroupId = s.MemberId", +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', +- "Found a record in CachedGroupMembers for a group that doesn't exist." +- ); +- +- delete_record( 'CachedGroupMembers', $id ); +- }, +- ); +- # Via +- $res *= check_integrity( +- CachedGroupMembers => 'Via', +- CachedGroupMembers => 'id', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', +- "Found a record in CachedGroupMembers with Via that references a nonexistent record." +- ); +- +- delete_record( 'CachedGroupMembers', $id ); +- }, +- ); +- +- # for every CGM where ImmediateParentId != GroupId there should be +- # matching parent record (first level) +- $res *= check_integrity( +- CachedGroupMembers => ['ImmediateParentId', 'MemberId'], +- CachedGroupMembers => ['GroupId', 'MemberId'], +- join_condition => 't.Via = t.id', +- condition => 's.ImmediateParentId != s.GroupId', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', +- "Found a record in CachedGroupMembers that references a nonexistent record in CachedGroupMembers table." +- ); +- +- delete_record( 'CachedGroupMembers', $id ); +- }, +- ); +- +- # for every CGM where ImmediateParentId != GroupId there should be +- # matching "grand" parent record +- $res *= check_integrity( +- CachedGroupMembers => ['GroupId', 'ImmediateParentId', 'Via'], +- CachedGroupMembers => ['GroupId', 'MemberId', 'id'], +- condition => 's.ImmediateParentId != s.GroupId', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', +- "Found a record in CachedGroupMembers that references a nonexistent record in CachedGroupMembers table." +- ); +- +- delete_record( 'CachedGroupMembers', $id ); +- }, +- ); +- +- # CHECK recursive records: +- # if we have CGM1 (G1,M1,V1,IP1) then for every GM2(G2, M2), where G2 == M1, +- # we should have CGM3 where G3 = G1, M3 = M2, V3 = ID1, IP3 = M1 +- { +- my $query = <<END; +-SELECT cgm1.GroupId, gm2.MemberId, cgm1.id AS Via, +- cgm1.MemberId AS ImmediateParentId, cgm1.Disabled +-FROM +- CachedGroupMembers cgm1 +- CROSS JOIN GroupMembers gm2 +- LEFT JOIN CachedGroupMembers cgm3 ON ( +- cgm3.GroupId = cgm1.GroupId +- AND cgm3.MemberId = gm2.MemberId +- AND cgm3.Via = cgm1.id +- AND cgm3.ImmediateParentId = cgm1.MemberId ) +-WHERE cgm1.GroupId != cgm1.MemberId +-AND gm2.GroupId = cgm1.MemberId +-AND cgm3.id IS NULL +-END +- +- my $action = sub { +- my %props = @_; +- return unless prompt( +- 'Create', +- "Found records in CachedGroupMembers table without recursive duplicates." +- ); +- my $cgm = create_record( 'CachedGroupMembers', %props ); +- }; +- +- my $sth = execute_query( $query ); +- while ( my ($g, $m, $via, $ip, $dis) = $sth->fetchrow_array ) { +- $res = 0; +- print STDERR "Principal #$m is member of #$ip when #$ip is member of #$g,"; +- print STDERR " but there is no cached GM record that $m is member of #$g.\n"; +- $action->( +- GroupId => $g, MemberId => $m, Via => $via, +- ImmediateParentId => $ip, Disabled => $dis, +- ); +- } +- } +- +- return $res; +-}; +- +-# Tickets +-push @CHECKS, 'Tickets -> other' => sub { +- my $res = 1; +- $res *= check_integrity( +- 'Tickets', 'EffectiveId' => 'Tickets', 'id', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', +- "Found a ticket that's been merged into a ticket that no longer exists." +- ); +- +- delete_record( 'Tickets', $id ); +- }, +- ); +- $res *= check_integrity( +- 'Tickets', 'Queue' => 'Queues', 'id', +- ); +- $res *= check_integrity( +- 'Tickets', 'Owner' => 'Users', 'id', +- action => sub { +- my ($id, %prop) = @_; +- return unless my $replace_with = prompt_integer( +- 'Replace', +- "Column Owner should point to a user, but there is record #$id in Tickets\n" +- ."where it's not true. It's ok to replace these wrong references with id of any user.\n" +- ."Note that id you enter is not checked. You can pick any user from your DB, but it's\n" +- ."may be better to create a special user for this, for example 'user_that_has_been_deleted'\n" +- ."or something like that.", +- "Tickets.Owner -> user #$prop{Owner}" +- ); +- update_records( 'Tickets', { id => $id, Owner => $prop{Owner} }, { Owner => $replace_with } ); +- }, +- ); +- # XXX: check that owner is only member of owner role group +- return $res; +-}; +- +- +-push @CHECKS, 'Transactions -> other' => sub { +- my $res = 1; +- foreach my $model ( @models ) { +- $res *= check_integrity( +- 'Transactions', 'ObjectId' => m2t($model), 'id', +- condition => 's.ObjectType = ?', +- bind_values => [ "RT::$model" ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a transaction without object." +- ); +- +- delete_record( 'Transactions', $id ); +- }, +- ); +- } +- # type = CustomField +- $res *= check_integrity( +- 'Transactions', 'Field' => 'CustomFields', 'id', +- condition => 's.Type = ?', +- bind_values => [ 'CustomField' ], +- ); +- # type = Take, Untake, Force, Steal or Give +- $res *= check_integrity( +- 'Transactions', 'OldValue' => 'Users', 'id', +- condition => 's.Type IN (?, ?, ?, ?, ?)', +- bind_values => [ qw(Take Untake Force Steal Give) ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a transaction regarding Owner changes," +- ." but the User with id stored in OldValue column doesn't exist anymore." +- ); +- +- delete_record( 'Transactions', $id ); +- }, +- ); +- $res *= check_integrity( +- 'Transactions', 'NewValue' => 'Users', 'id', +- condition => 's.Type IN (?, ?, ?, ?, ?)', +- bind_values => [ qw(Take Untake Force Steal Give) ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a transaction regarding Owner changes," +- ." but the User with id stored in NewValue column doesn't exist anymore." +- ); +- +- delete_record( 'Transactions', $id ); +- }, +- ); +- # type = DelWatcher +- $res *= check_integrity( +- 'Transactions', 'OldValue' => 'Principals', 'id', +- condition => 's.Type = ?', +- bind_values => [ 'DelWatcher' ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a transaction describing watcher changes," +- ." but the User with id stored in OldValue column doesn't exist anymore." +- ); +- +- delete_record( 'Transactions', $id ); +- }, +- ); +- # type = AddWatcher +- $res *= check_integrity( +- 'Transactions', 'NewValue' => 'Principals', 'id', +- condition => 's.Type = ?', +- bind_values => [ 'AddWatcher' ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a transaction describing watcher changes," +- ." but the User with id stored in NewValue column doesn't exist anymore." +- ); +- +- delete_record( 'Transactions', $id ); +- }, +- ); +- +-# type = DeleteLink or AddLink +-# handled in 'Links: *' checks as {New,Old}Value store URIs +- +- # type = Set, Field = Queue +- $res *= check_integrity( +- 'Transactions', 'NewValue' => 'Queues', 'id', +- condition => 's.Type = ? AND s.Field = ?', +- bind_values => [ 'Set', 'Queue' ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a transaction describing a queue change," +- ." but the Queue with id stored in the NewValue column doesn't exist anymore." +- ); +- +- delete_record( 'Transactions', $id ); +- }, +- ); +- $res *= check_integrity( +- 'Transactions', 'OldValue' => 'Queues', 'id', +- condition => 's.Type = ? AND s.Field = ?', +- bind_values => [ 'Set', 'Queue' ], +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found a transaction describing a queue change," +- ." but the Queue with id stored in the OldValue column doesn't exist anymore." +- ); +- +- delete_record( 'Transactions', $id ); +- }, +- ); +- # Reminders +- $res *= check_integrity( +- 'Transactions', 'NewValue' => 'Tickets', 'id', +- join_condition => 't.Type = ?', +- condition => 's.Type IN (?, ?, ?)', +- bind_values => [ 'reminder', 'AddReminder', 'OpenReminder', 'ResolveReminder' ], +- ); +- return $res; +-}; +- +-# Attachments +-push @CHECKS, 'Attachments -> other' => sub { +- my $res = 1; +- $res *= check_integrity( +- Attachments => 'TransactionId', Transactions => 'id', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found an attachment without a transaction." +- ); +- delete_record( 'Attachments', $id ); +- }, +- ); +- $res *= check_integrity( +- Attachments => 'Parent', Attachments => 'id', +- action => sub { +- my $id = shift; +- return unless prompt( +- 'Delete', "Found an sub-attachment without its parent attachment." +- ); +- delete_record( 'Attachments', $id ); +- }, +- ); +- $res *= check_integrity( +- Attachments => 'Parent', +- Attachments => 'id', +- join_condition => 's.TransactionId = t.TransactionId', +- ); +- return $res; +-}; +- +-push @CHECKS, 'CustomFields and friends' => sub { +- my $res = 1; +- #XXX: ObjectCustomFields needs more love +- $res *= check_integrity( +- 'CustomFieldValues', 'CustomField' => 'CustomFields', 'id', +- ); +- $res *= check_integrity( +- 'ObjectCustomFieldValues', 'CustomField' => 'CustomFields', 'id', +- ); +- foreach my $model ( @models ) { +- $res *= check_integrity( +- 'ObjectCustomFieldValues', 'ObjectId' => m2t($model), 'id', +- condition => 's.ObjectType = ?', +- bind_values => [ "RT::$model" ], +- ); +- } +- return $res; +-}; +- +-push @CHECKS, Templates => sub { +- return check_integrity( +- 'Templates', 'Queue' => 'Queues', 'id', +- ); +-}; +- +-push @CHECKS, Scrips => sub { +- my $res = 1; +- $res *= check_integrity( +- 'Scrips', 'ScripCondition' => 'ScripConditions', 'id', +- ); +- $res *= check_integrity( +- 'Scrips', 'ScripAction' => 'ScripActions', 'id', +- ); +- $res *= check_integrity( +- 'Scrips', 'Template' => 'Templates', 'Name', +- ); +- $res *= check_integrity( +- 'ObjectScrips', 'Scrip' => 'Scrips', 'id', +- ); +- $res *= check_integrity( +- 'ObjectScrips', 'ObjectId' => 'Queues', 'id', +- ); +- return $res; +-}; +- +-push @CHECKS, Attributes => sub { +- my $res = 1; +- foreach my $model ( @models ) { +- $res *= check_integrity( +- 'Attributes', 'ObjectId' => m2t($model), 'id', +- condition => 's.ObjectType = ?', +- bind_values => [ "RT::$model" ], +- ); +- } +- return $res; +-}; +- +-# Fix situations when Creator or LastUpdatedBy references ACL equivalence +-# group of a user instead of user +-push @CHECKS, 'FIX: LastUpdatedBy and Creator' => sub { +- my $res = 1; +- my %fix = (); +- foreach my $model ( @models ) { +- my $class = "RT::$model"; +- my $object = $class->new( RT->SystemUser ); +- foreach my $column ( qw(LastUpdatedBy Creator) ) { +- next unless $object->_Accessible( $column, 'auto' ); +- +- my $table = m2t($model); +- my $query = <<END; +-SELECT m.id, g.id, g.Instance +-FROM +- Groups g JOIN $table m ON g.id = m.$column +-WHERE +- g.Domain = ? +- AND g.Type = ? +-END +- my $action = sub { +- my ($gid, $uid) = @_; +- return unless prompt( +- 'Update', +- "Looks like there were a bug in old versions of RT back in 2006\n" +- ."that has been fixed. If other checks are ok then it's ok to update\n" +- ."these records to point them to users instead of groups" +- ); +- $fix{ $table }{ $column }{ $gid } = $uid; +- }; +- +- my $sth = execute_query( $query, 'ACLEquivalence', 'UserEquiv' ); +- while ( my ($rid, $gid, $uid) = $sth->fetchrow_array ) { +- $res = 0; +- print STDERR "Record #$rid in $table refers to ACL equivalence group #$gid of user #$uid"; +- print STDERR " when must reference user.\n"; +- $action->( $gid, $uid ); +- if ( keys( %fix ) > 1000 ) { +- $sth->finish; +- last; +- } +- } +- } +- } +- +- if ( keys %fix ) { +- foreach my $table ( keys %fix ) { +- foreach my $column ( keys %{ $fix{ $table } } ) { +- my $query = "UPDATE $table SET $column = ? WHERE $column = ?"; +- while ( my ($gid, $uid) = each %{ $fix{ $table }{ $column } } ) { +- update_records( $table, { $column => $gid }, { $column => $uid } ); +- } +- } +- } +- $redo_check{'FIX: LastUpdatedBy and Creator'} = 1; +- } +- return $res; +-}; +- +-push @CHECKS, 'LastUpdatedBy and Creator' => sub { +- my $res = 1; +- foreach my $model ( @models ) { +- my $class = "RT::$model"; +- my $object = $class->new( RT->SystemUser ); +- my $table = $object->Table; +- foreach my $column ( qw(LastUpdatedBy Creator) ) { +- next unless $object->_Accessible( $column, 'auto' ); +- $res *= check_integrity( +- $table, $column => 'Users', 'id', +- action => sub { +- my ($id, %prop) = @_; +- return unless my $replace_with = prompt_integer( +- 'Replace', +- "Column $column should point to a user, but there is record #$id in table $table\n" +- ."where it's not true. It's ok to replace these wrong references with id of any user.\n" +- ."Note that id you enter is not checked. You can pick any user from your DB, but it's\n" +- ."may be better to create a special user for this, for example 'user_that_has_been_deleted'\n" +- ."or something like that.", +- "$table.$column -> user #$prop{$column}" +- ); +- update_records( $table, { id => $id, $column => $prop{$column} }, { $column => $replace_with } ); +- }, +- ); +- } +- } +- return $res; +-}; +- +-push @CHECKS, 'Links: wrong organization' => sub { +- my $res = 1; +- my @URI_USES = ( +- { model => 'Transaction', column => 'OldValue', Additional => { Type => 'DeleteLink' } }, +- { model => 'Transaction', column => 'NewValue', Additional => { Type => 'AddLink' } }, +- { model => 'Link', column => 'Target' }, +- { model => 'Link', column => 'Base' }, +- ); +- +- my $right_org = RT->Config->Get('Organization'); +- my @rt_uris = rt_uri_modules(); +- foreach my $package (@rt_uris) { +- +- my $rt_uri = $package->new( $RT::SystemUser ); +- my $scheme = $rt_uri->Scheme; +- my $prefix = $rt_uri->LocalURIPrefix; +- +- foreach my $use ( @URI_USES ) { +- my $table = m2t( $use->{'model'} ); +- my $column = $use->{'column'}; +- +- my $query = "SELECT id, $column FROM $table WHERE" +- . " $column LIKE ? AND $column NOT LIKE ?"; +- my @binds = ($scheme ."://%", $prefix ."%"); +- +- while ( my ($k, $v) = each %{ $use->{'Additional'} || {} } ) { +- $query .= " AND $k = ?"; +- push @binds, $v; +- } +- my $sth = execute_query( $query, @binds ); +- while ( my ($id, $value) = $sth->fetchrow_array ) { +- $res = 0; +- print STDERR "Record #$id in $table. Value of $column column most probably is an incorrect link\n"; +- my ($wrong_org) = ( $value =~ m{^\Q$scheme\E://(.+)/[^/]+/[0-9]*$} ); +- next unless my $replace_with = prompt( +- 'Replace', +- "Column $column in $table is a link. There is record #$id that has a" +- ." local scheme of '$scheme', but its organization is '$wrong_org'" +- ." instead of '$right_org'. Most probably the Organization was" +- ." changed from '$wrong_org' to '$right_org' at some point. It is" +- ." generally a good idea to replace these wrong links.\n", +- "Links: wrong organization $wrong_org" +- ); +- +- print "Updating record(s) in $table\n" if $opt{'verbose'}; +- my $wrong_prefix = $scheme . '://'. $wrong_org; +- my $query = "UPDATE $table SET $column = ". sql_concat('?', "SUBSTR($column, ?)") +- ." WHERE $column LIKE ?"; +- execute_query( $query, $prefix, length($wrong_prefix)+1, $wrong_prefix .'/%' ); +- +- $redo_check{'Links: wrong organization'} = 1; +- $redo_check{'Links: LocalX for non-ticket'} = 1; +- last; # plenty of chances we covered all cases with one update +- } +- } +- } # end foreach my $package (@rt_uris) +- return $res; +-}; +- +-push @CHECKS, 'Links: LocalX for non-ticket' => sub { +- my $res = 1; +- my $rt_uri = RT::URI::fsck_com_rt->new( $RT::SystemUser ); +- my $scheme = $rt_uri->Scheme; +- my $prefix = $rt_uri->LocalURIPrefix; +- my $table = m2t('Link'); +- +- foreach my $dir ( 'Target', 'Base' ) { +- # we look only at links with correct organization, previouse check deals +- # with incorrect orgs +- my $where = "Local$dir > 0 AND $dir LIKE ? AND $dir NOT LIKE ?"; +- my @binds = ($prefix ."/%", $prefix ."/ticket/%"); +- +- my $sth = execute_query( "SELECT id FROM $table WHERE $where", @binds ); +- while ( my ($id, $value) = $sth->fetchrow_array ) { +- $res = 0; +- print STDERR "Record #$id in $table. Value of Local$dir is not 0\n"; +- next unless my $replace_with = prompt( +- 'Replace', +- "Column Local$dir in $table should be 0 if $dir column is not link" +- ." to a ticket. It's ok to replace with 0.\n", +- ); +- +- print "Updating record(s) in $table\n" if $opt{'verbose'}; +- execute_query( "UPDATE $table SET Local$dir = 0 WHERE $where", @binds ); +- $redo_check{'Links: wrong organization'} = 1; +- +- last; # we covered all cases with one update +- } +- } +- return $res; +-}; +- +-push @CHECKS, 'Links: LocalX != X' => sub { +- my $res = 1; +- my $rt_uri = RT::URI::fsck_com_rt->new( $RT::SystemUser ); +- my $scheme = $rt_uri->Scheme; +- my $prefix = $rt_uri->LocalURIPrefix .'/ticket/'; +- my $table = m2t('Link'); +- +- foreach my $dir ( 'Target', 'Base' ) { +- # we limit to $dir = */ticket/* so it doesn't conflict with previouse check +- # previouse check is more important as there was a bug in RT when Local$dir +- # was set for not tickets +- # XXX: we have issue with MergedInto links - "LocalX !~ X" +- my $where = "Local$dir > 0 AND $dir LIKE ? AND $dir != ". sql_concat('?', "Local$dir") +- ." AND Type != ?"; +- my @binds = ($prefix ."%", $prefix, 'MergedInto'); +- +- my $sth = execute_query( "SELECT id FROM $table WHERE $where", @binds ); +- while ( my ($id, $value) = $sth->fetchrow_array ) { +- $res = 0; +- print STDERR "Record #$id in $table. Value of $dir doesn't match ticket id in Local$dir\n"; +- next unless my $replace_with = prompt( +- 'Replace', +- "For ticket links column $dir in $table table should end with" +- ." ticket id from Local$dir. It's probably ok to fix $dir column.\n", +- ); +- +- print "Updating record(s) in $table\n" if $opt{'verbose'}; +- execute_query( +- "UPDATE $table SET $dir = ". sql_concat('?', "Local$dir") ." WHERE $where", +- $prefix, @binds +- ); +- +- last; # we covered all cases with one update +- } +- } +- return $res; +-}; +- +-push @CHECKS, 'Links: missing object' => sub { +- my $res = 1; +- my @URI_USES = ( +- { model => 'Transaction', column => 'OldValue', Additional => { Type => 'DeleteLink' } }, +- { model => 'Transaction', column => 'NewValue', Additional => { Type => 'AddLink' } }, +- { model => 'Link', column => 'Target' }, +- { model => 'Link', column => 'Base' }, +- ); +- +- my @rt_uris = rt_uri_modules(); +- foreach my $package (@rt_uris) { +- +- my $rt_uri = $package->new( $RT::SystemUser ); +- my $scheme = $rt_uri->Scheme; +- my $prefix = $rt_uri->LocalURIPrefix; +- +- foreach my $use ( @URI_USES ) { +- my $stable = m2t( $use->{'model'} ); +- my $scolumn = $use->{'column'}; +- +- foreach my $tmodel ( @models ) { +- my $tclass = 'RT::'. $tmodel; +- my $ttable = m2t($tmodel); +- +- my $tprefix = $prefix .'/'. ($tclass eq 'RT::Ticket'? 'ticket' : $tclass) .'/'; +- +- $tprefix = $prefix . '/article/' if $tclass eq 'RT::Article'; +- +- my $query = "SELECT s.id FROM $stable s LEFT JOIN $ttable t " +- ." ON t.id = ". sql_str2int("SUBSTR(s.$scolumn, ?)") +- ." WHERE s.$scolumn LIKE ? AND t.id IS NULL"; +- my @binds = (length($tprefix) + 1, $tprefix.'%'); +- +- while ( my ($k, $v) = each %{ $use->{'Additional'} || {} } ) { +- $query .= " AND s.$k = ?"; +- push @binds, $v; +- } +- +- my $sth = execute_query( $query, @binds ); +- while ( my ($sid) = $sth->fetchrow_array ) { +- $res = 0; +- print STDERR "Link in $scolumn column in record #$sid in $stable table points" +- ." to not existing object.\n"; +- next unless prompt( +- 'Delete', +- "Column $scolumn in $stable table is a link to an object that doesn't exist." +- ." You can delete such records, however make sure there is no other" +- ." errors with links.\n", +- 'Link to a missing object in $ttable' +- ); +- +- delete_record($stable, $sid); +- } +- } +- } +- } # end foreach my $package (@rt_uris) +- return $res; +-}; +- +- +-my %CHECKS = @CHECKS; +- +-@do_check = do { my $i = 1; grep $i++%2, @CHECKS }; +- +-if ($opt{'links-only'}) { +- @do_check = grep { /^Links:/ } @do_check; +-} +- +-my $status = 1; +-while ( my $check = shift @do_check ) { +- $status *= $CHECKS{ $check }->(); +- +- foreach my $redo ( keys %redo_check ) { +- die "check $redo doesn't exist" unless $CHECKS{ $redo }; +- delete $redo_check{ $redo }; +- next if grep $_ eq $redo, @do_check; # don't do twice +- push @do_check, $redo; +- } +-} +-exit 1 unless $status; +-exit 0; +- +-=head2 check_integrity +- +-Takes two (table name, column(s)) pairs. First pair +-is reference we check and second is destination that +-must exist. Array reference can be used for multiple +-columns. +- +-Returns 0 if a record is missing or 1 otherwise. +- +-=cut +- +-sub check_integrity { +- my ($stable, @scols) = (shift, shift); +- my ($ttable, @tcols) = (shift, shift); +- my %args = @_; +- +- @scols = @{ $scols[0] } if ref $scols[0]; +- @tcols = @{ $tcols[0] } if ref $tcols[0]; +- +- print "Checking integrity of $stable.{". join(', ', @scols) ."} => $ttable.{". join(', ', @tcols) ."}\n" +- if $opt{'verbose'}; +- +- my $query = "SELECT s.id, ". join(', ', map "s.$_", @scols) +- ." FROM $stable s LEFT JOIN $ttable t" +- ." ON (". join( +- ' AND ', map columns_eq_cond('s', $stable, $scols[$_] => 't', $ttable, $tcols[$_]), (0..(@scols-1)) +- ) .")" +- . ($args{'join_condition'}? " AND ( $args{'join_condition'} )": "") +- ." WHERE t.id IS NULL" +- ." AND ". join(' AND ', map "s.$_ IS NOT NULL", @scols); +- +- $query .= " AND ( $args{'condition'} )" if $args{'condition'}; +- +- my @binds = @{ $args{'bind_values'} || [] }; +- if ( $tcols[0] eq 'id' && @tcols == 1 ) { +- my $type = $TYPE{"$stable.$scols[0]"} || 'number'; +- if ( $type eq 'number' ) { +- $query .= " AND s.$scols[0] != ?" +- } +- elsif ( $type eq 'text' ) { +- $query .= " AND s.$scols[0] NOT LIKE ?" +- } +- push @binds, 0; +- } +- +- my $res = 1; +- +- my $sth = execute_query( $query, @binds ); +- while ( my ($sid, @set) = $sth->fetchrow_array ) { +- $res = 0; +- +- print STDERR "Record #$sid in $stable references a nonexistent record in $ttable\n"; +- for ( my $i = 0; $i < @scols; $i++ ) { +- print STDERR "\t$scols[$i] => '$set[$i]' => $tcols[$i]\n"; +- } +- print STDERR "\t". describe( $stable, $sid ) ."\n"; +- $args{'action'}->( $sid, map { $scols[$_] => $set[$_] } (0 .. (@scols-1)) ) +- if $args{'action'}; +- } +- return $res; +-} +- +-sub describe { +- my ($table, $id) = @_; +- return '' unless my $cb = $describe_cb{ $table }; +- +- my $row = load_record( $table, $id ); +- unless ( $row->{id} ) { +- $table =~ s/s$//; +- return "$table doesn't exist"; +- } +- return $cb->( $row ); +-} +- +-sub columns_eq_cond { +- my ($la, $lt, $lc, $ra, $rt, $rc) = @_; +- my $ltype = $TYPE{"$lt.$lc"} || 'number'; +- my $rtype = $TYPE{"$rt.$rc"} || 'number'; +- return "$la.$lc = $ra.$rc" if $db_type ne 'Pg' || $ltype eq $rtype; +- +- if ( $rtype eq 'text' ) { +- return "$ra.$rc LIKE CAST($la.$lc AS text)"; +- } +- elsif ( $ltype eq 'text' ) { +- return "$la.$lc LIKE CAST($ra.$rc AS text)"; +- } +- else { die "don't know how to cast" } +-} +- +-sub check_uniqueness { +- my $on = shift; +- my %args = @_; +- +- my @columns = @{ $args{'columns'} }; +- +- print "Checking uniqueness of ( ", join(', ', map "'$_'", @columns )," ) in table '$on'\n" +- if $opt{'verbose'}; +- +- my ($scond, $tcond); +- if ( $scond = $tcond = $args{'condition'} ) { +- $scond =~ s/(\s|^)\./$1s./g; +- $tcond =~ s/(\s|^)\./$1t./g; +- } +- +- my $query = "SELECT s.id, t.id, ". join(', ', map "s.$_", @columns) +- ." FROM $on s LEFT JOIN $on t " +- ." ON s.id != t.id AND ". join(' AND ', map "s.$_ = t.$_", @columns) +- . ($tcond? " AND ( $tcond )": "") +- . ($args{'extra_tables'} ? join(", ", "", @{$args{'extra_tables'}}) : "") +- ." WHERE t.id IS NOT NULL " +- ." AND ". join(' AND ', map "s.$_ IS NOT NULL", @columns); +- $query .= " AND ( $scond )" if $scond; +- $query .= " AND ( $args{'extra_condition'} )" if $args{'extra_condition'}; +- +- my $sth = execute_query( +- $query, +- $args{'bind_values'}? (@{ $args{'bind_values'} }, @{ $args{'bind_values'} }): (), +- $args{'extra_values'}? (@{ $args{'extra_values'} }): () +- ); +- my $res = 1; +- while ( my ($sid, $tid, @set) = $sth->fetchrow_array ) { +- $res = 0; +- print STDERR "Record #$tid in $on has the same set of values as $sid\n"; +- for ( my $i = 0; $i < @columns; $i++ ) { +- print STDERR "\t$columns[$i] => '$set[$i]'\n"; +- } +- $args{'action'}->( $tid, map { $columns[$_] => $set[$_] } (0 .. (@columns-1)) ) if $args{'action'}; +- } +- return $res; +-} +- +-sub load_record { +- my ($table, $id) = @_; +- my $sth = execute_query( "SELECT * FROM $table WHERE id = ?", $id ); +- return $sth->fetchrow_hashref('NAME_lc'); +-} +- +-sub delete_record { +- my ($table, $id) = (@_); +- print "Deleting record #$id in $table\n" if $opt{'verbose'}; +- my $query = "DELETE FROM $table WHERE id = ?"; +- $redo_check{ $_ } = 1 foreach @{ $redo_on{'Delete'}{ $table } || [] }; +- return execute_query( $query, $id ); +-} +- +-sub create_record { +- print "Creating a record in $_[0]\n" if $opt{'verbose'}; +- $redo_check{ $_ } = 1 foreach @{ $redo_on{'Create'}{ $_[0] } || [] }; +- return $RT::Handle->Insert( @_ ); +-} +- +-sub update_records { +- my $table = shift; +- my $where = shift; +- my $what = shift; +- +- my (@where_cols, @where_binds); +- while ( my ($k, $v) = each %$where ) { push @where_cols, $k; push @where_binds, $v; } +- +- my (@what_cols, @what_binds); +- while ( my ($k, $v) = each %$what ) { push @what_cols, $k; push @what_binds, $v; } +- +- print "Updating record(s) in $table\n" if $opt{'verbose'}; +- my $query = "UPDATE $table SET ". join(', ', map "$_ = ?", @what_cols) +- ." WHERE ". join(' AND ', map "$_ = ?", @where_cols); +- $redo_check{ $_ } = 1 foreach @{ $redo_on{'Update'}{ $table } || [] }; +- return execute_query( $query, @what_binds, @where_binds ); +-} +- +-sub execute_query { +- my ($query, @binds) = @_; +- +- print "Executing query: $query\n\n" if $opt{'verbose'}; +- +- my $sth = $dbh->prepare( $query ) or die "couldn't prepare $query\n\tError: ". $dbh->errstr; +- $sth->execute( @binds ) or die "couldn't execute $query\n\tError: ". $sth->errstr; +- return $sth; +-} +- +-sub sql_concat { +- return $_[0] if @_ <= 1; +- +- my $db_type = RT->Config->Get('DatabaseType'); +- if ( $db_type eq 'Pg' || $db_type eq 'SQLite' ) { +- return '('. join( ' || ', @_ ) .')'; +- } +- return sql_concat('CONCAT('. join( ', ', splice @_, 0, 2 ).')', @_); +-} +- +-sub sql_str2int { +- my $db_type = RT->Config->Get('DatabaseType'); +- if ( $db_type eq 'Pg' ) { +- return "($_[0])::integer"; +- } +- return $_[0]; +-} +- +-{ my %cached_answer; +-sub prompt { +- my $action = shift; +- my $msg = shift; +- my $token = shift || join ':', caller; +- +- return 0 unless $opt{'resolve'}; +- return 1 if $opt{'force'}; +- +- return $cached_answer{ $token } if exists $cached_answer{ $token }; +- +- print $msg, "\n"; +- print "$action ALL records with the same defect? [N]: "; +- my $a = <STDIN>; +- return $cached_answer{ $token } = 1 if $a =~ /^(y|yes)$/i; +- return $cached_answer{ $token } = 0; +-} } +- +-{ my %cached_answer; +-sub prompt_action { +- my $actions = shift; +- my $msg = shift; +- my $token = shift || join ':', caller; +- +- return '' unless $opt{'resolve'}; +- return lc substr $actions->[0], 0, 1 if $opt{'force'}; +- return $cached_answer{ $token } if exists $cached_answer{ $token }; +- +- print $msg, "\n"; +- print join( ' or ', @$actions ) ." ALL records with the same defect? [do nothing]: "; +- my $a = <STDIN>; +- chomp $a; +- return $cached_answer{ $token } = '' unless $a; +- foreach ( grep rindex(lc $_, lc $a, 0) == 0, @$actions ) { +- return $cached_answer{ $token } = lc substr $a, 0, 1; +- } +- return $cached_answer{ $token } = ''; +-} } +- +-{ my %cached_answer; +-sub prompt_integer { +- my $action = shift; +- my $msg = shift; +- my $token = shift || join ':', caller; +- +- return 0 unless $opt{'resolve'}; +- return 0 if $opt{'force'}; +- +- return $cached_answer{ $token } if exists $cached_answer{ $token }; +- +- print $msg, "\n"; +- print "$action ALL records with the same defect? [0]: "; +- my $a = <STDIN>; chomp $a; $a = int($a); +- return $cached_answer{ $token } = $a; +-} } +- +-# Find all RT::URI modules RT has loaded +- +-sub rt_uri_modules { +- my @uris = grep /^RT\/URI\/.+\.pm$/, keys %INC; +- my @uri_modules; +- foreach my $uri_path (@uris){ +- next if $uri_path =~ /base\.pm$/; # Skip base RT::URI object +- $uri_path = substr $uri_path, 0, -3; # chop off .pm +- push @uri_modules, join '::', split '/', $uri_path; +- } +- +- return @uri_modules; +-} +- +-1; +- +-__END__ +- +-=head1 NAME +- +-rt-validator - check and correct validity of records in RT's database +- +-=head1 SYNOPSIS +- +- rt-validator --check +- rt-validator --check --verbose +- rt-validator --check --verbose --resolve +- rt-validator --check --verbose --resolve --force +- +-=head1 DESCRIPTION +- +-This script checks integrity of records in RT's DB. May delete some invalid +-records or ressurect accidentally deleted. +- +-=head1 OPTIONS +- +-=over +- +-=item check +- +- mandatory. +- +- it's equal to -c +- +-=item verbose +- +- print additional info to STDOUT +- it's equal to -v +- +-=item resolve +- +- enable resolver that can delete or create some records +- +-=item force +- +- resolve without asking questions +- +-=item links-only +- +- only run the Link validation routines, useful if you changed your Organization +- +-=back +- +diff --git a/sbin/standalone_httpd b/sbin/standalone_httpd +deleted file mode 100755 +index d3711f1..0000000 +--- a/sbin/standalone_httpd ++++ /dev/null +@@ -1,181 +0,0 @@ +-#!/usr/bin/perl -w +-# BEGIN BPS TAGGED BLOCK {{{ +-# +-# COPYRIGHT: +-# +-# This software is Copyright (c) 1996-2015 Best Practical Solutions, LLC +-# <sales@xxxxxxxxxxxxxxxxx> +-# +-# (Except where explicitly superseded by other copyright notices) +-# +-# +-# LICENSE: +-# +-# This work is made available to you under the terms of Version 2 of +-# the GNU General Public License. A copy of that license should have +-# been provided with this software, but in any event can be snarfed +-# from www.gnu.org. +-# +-# This work is distributed in the hope that it will be useful, but +-# WITHOUT ANY WARRANTY; without even the implied warranty of +-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +-# General Public License for more details. +-# +-# You should have received a copy of the GNU General Public License +-# along with this program; if not, write to the Free Software +-# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +-# 02110-1301 or visit their web page on the internet at +-# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +-# +-# +-# CONTRIBUTION SUBMISSION POLICY: +-# +-# (The following paragraph is not intended to limit the rights granted +-# to you to modify and distribute this software under the terms of +-# the GNU General Public License and is only of importance to you if +-# you choose to contribute your changes and enhancements to the +-# community by submitting them to Best Practical Solutions, LLC.) +-# +-# By intentionally submitting any modifications, corrections or +-# derivatives to this work, or any other work intended for use with +-# Request Tracker, to Best Practical Solutions, LLC, you confirm that +-# you are the copyright holder for those contributions and you grant +-# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +-# royalty-free, perpetual, license to use, copy, create derivative +-# works based on those contributions, and sublicense and distribute +-# those contributions and any derivatives thereof. +-# +-# END BPS TAGGED BLOCK }}} +-use warnings; +-use strict; +- +-BEGIN { +- die <<EOT if ${^TAINT}; +-RT does not run under Perl's "taint mode". Remove -T from the command +-line, or remove the PerlTaintCheck parameter from your mod_perl +-configuration. +-EOT +-} +- +-# fix lib paths, some may be relative +-BEGIN { # BEGIN RT CMD BOILERPLATE +- require File::Spec; +- require Cwd; +- my @libs = ("lib", "local/lib"); +- my $bin_path; +- +- for my $lib (@libs) { +- unless ( File::Spec->file_name_is_absolute($lib) ) { +- $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; +- $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); +- } +- unshift @INC, $lib; +- } +- +-} +- +-use Getopt::Long; +-no warnings 'once'; +- +-if (grep { m/help/ } @ARGV) { +- require Pod::Usage; +- print Pod::Usage::pod2usage( { verbose => 2 } ); +- exit; +-} +- +-require RT; +-die "Wrong version of RT $RT::VERSION found; need 4.2.*" +- unless $RT::VERSION =~ /^4\.2\./; +- +-RT->LoadConfig(); +-RT->InitPluginPaths(); +-RT->InitLogging(); +- +-require RT::Handle; +-my ($integrity, $state, $msg) = RT::Handle->CheckIntegrity; +- +-unless ( $integrity ) { +- print STDERR <<EOF; +- +-RT couldn't connect to the database where tickets are stored. +-If this is a new installation of RT, you should visit the URL below +-to configure RT and initialize your database. +- +-If this is an existing RT installation, this may indicate a database +-connectivity problem. +- +-The error RT got back when trying to connect to your database was: +- +-$msg +- +-EOF +- +- require RT::Installer; +- # don't enter install mode if the file exists but is unwritable +- if (-e RT::Installer->ConfigFile && !-w _) { +- die 'Since your configuration exists (' +- . RT::Installer->ConfigFile +- . ") but is not writable, I'm refusing to do anything.\n"; +- } +- +- RT->Config->Set( 'LexiconLanguages' => '*' ); +- RT::I18N->Init; +- +- RT->InstallMode(1); +-} else { +- RT->Init( Heavy => 1 ); +- +- my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'post'); +- unless ( $status ) { +- print STDERR $msg, "\n\n"; +- exit -1; +- } +-} +- +-# we must disconnect DB before fork +-if ($RT::Handle) { +- $RT::Handle->dbh->disconnect if $RT::Handle->dbh; +- $RT::Handle->dbh(undef); +- undef $RT::Handle; +-} +- +-require RT::PlackRunner; +-# when used as a psgi file +-if (caller) { +- return RT::PlackRunner->app; +-} +- +- +-my $r = RT::PlackRunner->new( RT->InstallMode ? ( server => 'Standalone' ) : +- $0 =~ /standalone/ ? ( server => 'Standalone' ) : +- $0 =~ /fcgi$/ ? ( server => 'FCGI', env => "deployment" ) +- : ( server => 'Starlet', env => "deployment" ) ); +-$r->parse_options(@ARGV); +- +-# Try to clean up wrong-permissions var/ +-$SIG{INT} = sub { +- local $@; +- system("chown", "-R", "www-data:www-data", "/opt/rt4/var"); +- exit 0; +-} if $> == 0; +- +-$r->run; +- +-__END__ +- +-=head1 NAME +- +-rt-server - RT standalone server +- +-=head1 SYNOPSIS +- +- # runs prefork server listening on port 8080, requires Starlet +- rt-server --port 8080 +- +- # runs server listening on port 8080 +- rt-server --server Standalone --port 8080 +- # or +- standalone_httpd --port 8080 +- +- # runs other PSGI server on port 8080 +- rt-server --server Starman --port 8080 +diff --git a/t/data/configs/apache2.2+fastcgi.conf b/t/data/configs/apache2.2+fastcgi.conf +deleted file mode 100644 +index bc6056f..0000000 +--- a/t/data/configs/apache2.2+fastcgi.conf ++++ /dev/null +@@ -1,49 +0,0 @@ +-ServerRoot %%SERVER_ROOT%% +-PidFile %%PID_FILE%% +-LockFile %%LOCK_FILE%% +-ServerAdmin root@localhost +- +-%%LOAD_MODULES%% +- +-<IfModule !mpm_netware_module> +-<IfModule !mpm_winnt_module> +-User www-data +-Group www-data +-</IfModule> +-</IfModule> +- +-ServerName localhost +-Listen %%LISTEN%% +- +-ErrorLog "%%LOG_FILE%%" +-LogLevel debug +- +-<Directory /> +- Options FollowSymLinks +- AllowOverride None +- Order deny,allow +- Deny from all +-</Directory> +- +-AddDefaultCharset UTF-8 +- +-FastCgiServer %%RT_SBIN_PATH%%/rt-server.fcgi \ +- -socket %%TMP_DIR%%/socket \ +- -processes 1 \ +- -idle-timeout 180 \ +- -initial-env RT_SITE_CONFIG=%%RT_SITE_CONFIG%% \ +- -initial-env RT_TESTING=1 +- +-ScriptAlias / %%RT_SBIN_PATH%%/rt-server.fcgi/ +- +-DocumentRoot "%%DOCUMENT_ROOT%%" +-<Location /> +- Order allow,deny +- Allow from all +- +-%%BASIC_AUTH%% +- +- Options +ExecCGI +- AddHandler fastcgi-script fcgi +-</Location> +- +diff --git a/t/data/configs/apache2.2+mod_perl.conf b/t/data/configs/apache2.2+mod_perl.conf +deleted file mode 100644 +index e0ce14c..0000000 +--- a/t/data/configs/apache2.2+mod_perl.conf ++++ /dev/null +@@ -1,67 +0,0 @@ +-<IfModule mpm_prefork_module> +- StartServers 1 +- MinSpareServers 1 +- MaxSpareServers 1 +- MaxClients 1 +- MaxRequestsPerChild 0 +-</IfModule> +- +-<IfModule mpm_worker_module> +- StartServers 1 +- MinSpareThreads 1 +- MaxSpareThreads 1 +- ThreadLimit 1 +- ThreadsPerChild 1 +- MaxClients 1 +- MaxRequestsPerChild 0 +-</IfModule> +- +-ServerRoot %%SERVER_ROOT%% +-PidFile %%PID_FILE%% +-LockFile %%LOCK_FILE%% +-ServerAdmin root@localhost +- +-%%LOAD_MODULES%% +- +-<IfModule !mpm_netware_module> +-<IfModule !mpm_winnt_module> +-User www-data +-Group www-data +-</IfModule> +-</IfModule> +- +-ServerName localhost +-Listen %%LISTEN%% +- +-ErrorLog "%%LOG_FILE%%" +-LogLevel debug +- +-<Directory /> +- Options FollowSymLinks +- AllowOverride None +- Order deny,allow +- Deny from all +-</Directory> +- +-AddDefaultCharset UTF-8 +-PerlSetEnv RT_SITE_CONFIG %%RT_SITE_CONFIG%% +- +-DocumentRoot "%%DOCUMENT_ROOT%%" +-<Location /> +- Order allow,deny +- Allow from all +- +-%%BASIC_AUTH%% +- +- SetHandler modperl +- +- PerlResponseHandler Plack::Handler::Apache2 +- PerlSetVar psgi_app %%RT_SBIN_PATH%%/rt-server +-</Location> +- +-<Perl> +- $ENV{RT_TESTING}=1; +- use Plack::Handler::Apache2; +- Plack::Handler::Apache2->preload("%%RT_SBIN_PATH%%/rt-server"); +-</Perl> +- +-- +2.1.0 + diff --git a/0004-Use-usr-bin-perl-instead-of-usr-bin-env-perl.patch b/0004-Use-usr-bin-perl-instead-of-usr-bin-env-perl.patch index e6d893b..e5eb235 100644 --- a/0004-Use-usr-bin-perl-instead-of-usr-bin-env-perl.patch +++ b/0004-Use-usr-bin-perl-instead-of-usr-bin-env-perl.patch @@ -1,24 +1,25 @@ -From 9be38aa4c1524a287684f5fa1cd8841385a95722 Mon Sep 17 00:00:00 2001 +From b39008e2f83274e4298da24b1ee4d00bea30c47b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralf=20Cors=C3=A9pius?= <corsepiu@xxxxxxxxxxxxxxxxx> Date: Tue, 18 Mar 2014 14:55:18 +0100 Subject: [PATCH 4/8] Use /usr/bin/perl instead of /usr/bin/env perl. --- - devel/tools/change-loc-msgstr | 2 +- - devel/tools/cmd-boilerplate | 2 +- - devel/tools/extract-message-catalog | 2 +- - devel/tools/license_tag | 2 +- - devel/tools/rt-apache | 2 +- - devel/tools/rt-attributes-editor | 2 +- - devel/tools/rt-static-docs | 2 +- - devel/tools/tweak-template-locstring | 2 +- - etc/upgrade/upgrade-mysql-schema.pl | 2 +- - lib/RT/Interface/Web/QueryBuilder.pm | 1 + - t/mail/fake-sendmail | 2 +- - 14 files changed, 14 insertions(+), 13 deletions(-) + devel/tools/change-loc-msgstr | 2 +- + devel/tools/cmd-boilerplate | 2 +- + devel/tools/extract-message-catalog | 2 +- + devel/tools/license_tag | 2 +- + devel/tools/rt-apache | 2 +- + devel/tools/rt-attributes-editor | 2 +- + devel/tools/rt-message-catalog | 2 +- + devel/tools/rt-static-docs | 2 +- + devel/tools/tweak-template-locstring | 2 +- + etc/upgrade/upgrade-mysql-schema.pl | 2 +- + lib/RT/Interface/Web/QueryBuilder.pm | 1 + + t/mail/fake-sendmail | 2 +- + 12 files changed, 12 insertions(+), 11 deletions(-) diff --git a/devel/tools/change-loc-msgstr b/devel/tools/change-loc-msgstr -index bd1892a..ffeb5e1 100755 +index 85780de..68fa249 100755 --- a/devel/tools/change-loc-msgstr +++ b/devel/tools/change-loc-msgstr @@ -1,4 +1,4 @@ @@ -28,7 +29,7 @@ index bd1892a..ffeb5e1 100755 # # COPYRIGHT: diff --git a/devel/tools/cmd-boilerplate b/devel/tools/cmd-boilerplate -index 2c6463d..8fa596c 100755 +index 3e8c1bf..e865822 100755 --- a/devel/tools/cmd-boilerplate +++ b/devel/tools/cmd-boilerplate @@ -1,4 +1,4 @@ @@ -38,7 +39,7 @@ index 2c6463d..8fa596c 100755 # BEGIN BPS TAGGED BLOCK {{{ diff --git a/devel/tools/extract-message-catalog b/devel/tools/extract-message-catalog -index ad9e5f5..9e74b53 100755 +index 5dd89b8..5dbbe79 100755 --- a/devel/tools/extract-message-catalog +++ b/devel/tools/extract-message-catalog @@ -1,4 +1,4 @@ @@ -48,7 +49,7 @@ index ad9e5f5..9e74b53 100755 # # COPYRIGHT: diff --git a/devel/tools/license_tag b/devel/tools/license_tag -index 4cf0917..0d1e5f3 100755 +index b9d7192..4a4f20c 100755 --- a/devel/tools/license_tag +++ b/devel/tools/license_tag @@ -1,4 +1,4 @@ @@ -58,7 +59,7 @@ index 4cf0917..0d1e5f3 100755 # BEGIN BPS TAGGED BLOCK {{{ diff --git a/devel/tools/rt-apache b/devel/tools/rt-apache -index 71b420b..545b3ba 100755 +index dfbd477..942946c 100755 --- a/devel/tools/rt-apache +++ b/devel/tools/rt-apache @@ -1,4 +1,4 @@ @@ -68,7 +69,7 @@ index 71b420b..545b3ba 100755 # BEGIN BPS TAGGED BLOCK {{{ # diff --git a/devel/tools/rt-attributes-editor b/devel/tools/rt-attributes-editor -index 92998a4..117f167 100755 +index 15436ac..6890e8b 100755 --- a/devel/tools/rt-attributes-editor +++ b/devel/tools/rt-attributes-editor @@ -1,4 +1,4 @@ @@ -77,8 +78,18 @@ index 92998a4..117f167 100755 # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: +diff --git a/devel/tools/rt-message-catalog b/devel/tools/rt-message-catalog +index f1a3158..1488993 100755 +--- a/devel/tools/rt-message-catalog ++++ b/devel/tools/rt-message-catalog +@@ -1,4 +1,4 @@ +-#!/usr/bin/env perl ++#!/usr/bin/perl + # BEGIN BPS TAGGED BLOCK {{{ + # + # COPYRIGHT: diff --git a/devel/tools/rt-static-docs b/devel/tools/rt-static-docs -index ef3dc50..6c6514d 100755 +index 93362e4..4f5ba33 100755 --- a/devel/tools/rt-static-docs +++ b/devel/tools/rt-static-docs @@ -1,4 +1,4 @@ @@ -88,7 +99,7 @@ index ef3dc50..6c6514d 100755 # # COPYRIGHT: diff --git a/devel/tools/tweak-template-locstring b/devel/tools/tweak-template-locstring -index ca44d39..b31570b 100755 +index c2741ab..51fec1e 100755 --- a/devel/tools/tweak-template-locstring +++ b/devel/tools/tweak-template-locstring @@ -1,4 +1,4 @@ @@ -98,7 +109,7 @@ index ca44d39..b31570b 100755 # # COPYRIGHT: diff --git a/etc/upgrade/upgrade-mysql-schema.pl b/etc/upgrade/upgrade-mysql-schema.pl -index 8d6615d..204676c 100755 +index 92d18e3..451a37c 100755 --- a/etc/upgrade/upgrade-mysql-schema.pl +++ b/etc/upgrade/upgrade-mysql-schema.pl @@ -1,4 +1,4 @@ @@ -108,7 +119,7 @@ index 8d6615d..204676c 100755 # # COPYRIGHT: diff --git a/lib/RT/Interface/Web/QueryBuilder.pm b/lib/RT/Interface/Web/QueryBuilder.pm -index a1b0662..4af51f4 100644 +index b551424..8859bb2 100644 --- a/lib/RT/Interface/Web/QueryBuilder.pm +++ b/lib/RT/Interface/Web/QueryBuilder.pm @@ -1,3 +1,4 @@ diff --git a/0006-Fix-permissions.patch b/0006-Fix-permissions.patch index fffe753..f0280b9 100644 --- a/0006-Fix-permissions.patch +++ b/0006-Fix-permissions.patch @@ -1,86 +1,16 @@ -From 83b48cfbc9dbb68e50055b6c51ec3582238e8fb1 Mon Sep 17 00:00:00 2001 +From 85c2c1c33757a68a248437272b082e0106098b70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralf=20Cors=C3=A9pius?= <corsepiu@xxxxxxxxxxxxxxxxx> Date: Tue, 18 Mar 2014 10:00:00 +0100 Subject: [PATCH 6/8] Fix permissions. --- - Makefile.in | 0 - aclocal.m4 | 0 - config.layout | 0 - configure.ac | 0 - docs/schema.dot | 0 - etc/RT_Config.pm.in | 0 - etc/RT_SiteConfig.pm | 0 - etc/acl.Oracle | 0 - etc/acl.Pg | 0 - etc/acl.mysql | 0 - etc/schema.Oracle | 0 - etc/schema.Pg | 0 - etc/schema.SQLite | 0 - etc/schema.mysql | 0 - etc/upgrade/vulnerable-passwords.in | 0 - 15 files changed, 0 insertions(+), 0 deletions(-) - mode change 100755 => 100644 Makefile.in - mode change 100755 => 100644 aclocal.m4 - mode change 100755 => 100644 config.layout + configure.ac | 0 + 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 configure.ac - mode change 100755 => 100644 docs/schema.dot - mode change 100755 => 100644 etc/RT_Config.pm.in - mode change 100755 => 100644 etc/RT_SiteConfig.pm - mode change 100755 => 100644 etc/acl.Oracle - mode change 100755 => 100644 etc/acl.Pg - mode change 100755 => 100644 etc/acl.mysql - mode change 100755 => 100644 etc/schema.Oracle - mode change 100755 => 100644 etc/schema.Pg - mode change 100755 => 100644 etc/schema.SQLite - mode change 100755 => 100644 etc/schema.mysql - mode change 100755 => 100644 etc/upgrade/vulnerable-passwords.in -diff --git a/Makefile.in b/Makefile.in -old mode 100755 -new mode 100644 -diff --git a/aclocal.m4 b/aclocal.m4 -old mode 100755 -new mode 100644 -diff --git a/config.layout b/config.layout -old mode 100755 -new mode 100644 diff --git a/configure.ac b/configure.ac old mode 100755 new mode 100644 -diff --git a/docs/schema.dot b/docs/schema.dot -old mode 100755 -new mode 100644 -diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in -old mode 100755 -new mode 100644 -diff --git a/etc/RT_SiteConfig.pm b/etc/RT_SiteConfig.pm -old mode 100755 -new mode 100644 -diff --git a/etc/acl.Oracle b/etc/acl.Oracle -old mode 100755 -new mode 100644 -diff --git a/etc/acl.Pg b/etc/acl.Pg -old mode 100755 -new mode 100644 -diff --git a/etc/acl.mysql b/etc/acl.mysql -old mode 100755 -new mode 100644 -diff --git a/etc/schema.Oracle b/etc/schema.Oracle -old mode 100755 -new mode 100644 -diff --git a/etc/schema.Pg b/etc/schema.Pg -old mode 100755 -new mode 100644 -diff --git a/etc/schema.SQLite b/etc/schema.SQLite -old mode 100755 -new mode 100644 -diff --git a/etc/schema.mysql b/etc/schema.mysql -old mode 100755 -new mode 100644 -diff --git a/etc/upgrade/vulnerable-passwords.in b/etc/upgrade/vulnerable-passwords.in -old mode 100755 -new mode 100644 -- 2.1.0 diff --git a/0007-Fix-translation.patch b/0007-Fix-translation.patch index 790bdca..d2a7c16 100644 --- a/0007-Fix-translation.patch +++ b/0007-Fix-translation.patch @@ -1,4 +1,4 @@ -From 08352fe91e355ee16c8531b3ea335c060a005b17 Mon Sep 17 00:00:00 2001 +From d0d5d1b779a1c503bd9739c5b2245f6f842cd85d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralf=20Cors=C3=A9pius?= <corsepiu@xxxxxxxxxxxxxxxxx> Date: Fri, 23 Jan 2015 04:36:45 +0100 Subject: [PATCH 7/8] Fix translation. @@ -7,9 +7,10 @@ Subject: [PATCH 7/8] Fix translation. share/po/de.po | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) -diff -up ./share/po/de.po.orig ./share/po/de.po ---- ./share/po/de.po.orig 2015-02-25 15:30:21.000000000 -0600 -+++ ./share/po/de.po 2015-03-09 13:54:09.094706168 -0500 +diff --git a/share/po/de.po b/share/po/de.po +index 45c1977..ff47768 100644 +--- a/share/po/de.po ++++ b/share/po/de.po @@ -1006,7 +1006,7 @@ msgstr "Alle Tickets" #: share/html/User/Prefs.html:173 @@ -19,3 +20,6 @@ diff -up ./share/po/de.po.orig ./share/po/de.po #: share/html/Admin/Queues/index.html:99 msgid "All queues matching search criteria" +-- +2.1.0 + diff --git a/0008-Work-around-testsuite-failure.patch b/0008-Work-around-testsuite-failure.patch new file mode 100644 index 0000000..25973b0 --- /dev/null +++ b/0008-Work-around-testsuite-failure.patch @@ -0,0 +1,51 @@ +From 8c6ede102c5a1ce1f7214abf7ac90ca72f2f2538 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Ralf=20Cors=C3=A9pius?= <corsepiu@xxxxxxxxxxxxxxxxx> +Date: Tue, 24 Mar 2015 08:39:06 +0100 +Subject: [PATCH 8/8] Work-around testsuite failure. cf. + https://bugzilla.redhat.com/show_bug.cgi?id=1121601#c36. + +--- + t/mail/html-outgoing.t | 27 --------------------------- + 1 file changed, 27 deletions(-) + +diff --git a/t/mail/html-outgoing.t b/t/mail/html-outgoing.t +index a37f52c..f6b3fac 100644 +--- a/t/mail/html-outgoing.t ++++ b/t/mail/html-outgoing.t +@@ -81,33 +81,6 @@ mail_ok { + 'Content-Type' => qr{multipart}, + }; + +-SKIP: { +- skip "Only fails on core HTMLFormatter", 9 +- unless RT->Config->Get("HTMLFormatter") eq "core"; +- diag "Failing HTML -> Text conversion"; +- warnings_like { +- my $body = '<table><tr><td><table><tr><td>Foo</td></tr></table></td></tr></table>'; +- mail_ok { +- ($ok, $tmsg) = $t->Correspond( +- MIMEObj => HTML::Mason::Commands::MakeMIMEEntity( +- Body => $body, +- Type => 'text/html', +- ), +- ); +- } { from => qr/RT System/, +- bcc => 'root@localhost', +- subject => qr/\Q[example.com #1] The internet is broken\E/, +- body => qr{Ticket URL: <a href="(http://localhost:\d+/Ticket/Display\.html\?id=1)">\1</a>.+?$body}s, +- 'Content-Type' => qr{text/html}, # TODO +- },{ from => qr/RT System/, +- to => 'enduser@xxxxxxxxxxx', +- subject => qr/\Q[example.com #1] The internet is broken\E/, +- body => qr{$body}, +- 'Content-Type' => qr{text/html}, # TODO +- }; +- } [(qr/uninitialized value/, qr/Failed to downgrade HTML/)x3]; +-} +- + + diag "Admin Comment in HTML"; + mail_ok { +-- +2.1.0 + diff --git a/rt.spec b/rt.spec index ec5a640..d8a798a 100644 --- a/rt.spec +++ b/rt.spec @@ -39,7 +39,7 @@ Name: rt Version: 4.2.10 -Release: 1%{?dist} +Release: 2%{?dist} Summary: Request tracker Group: Applications/Internet @@ -55,12 +55,14 @@ Source3: README.fedora.in # rt's logrotate configuration Source4: rt.logrotate.in +Patch1: 0001-Remove-configure-time-generated-files.patch Patch2: 0002-Add-Fedora-configuration.patch Patch3: 0003-Broken-test-dependencies.patch Patch4: 0004-Use-usr-bin-perl-instead-of-usr-bin-env-perl.patch Patch5: 0005-Remove-fixperms-font-install.patch Patch6: 0006-Fix-permissions.patch Patch7: 0007-Fix-translation.patch +Patch8: 0008-Work-around-testsuite-failure.patch BuildArch: noarch @@ -245,6 +247,7 @@ Requires: perl(Plack::Middleware::Test::StashWarnings) >= 0.06 Requires: perl(Plack::Handler::Starlet) Requires: perl(Text::Quoted) Requires: perl(Text::WikiFormat) +Requires: perl(Time::ParseDate) Requires: perl(URI::URL) Requires: perl(XML::RSS) @@ -305,12 +308,12 @@ Requires: perl(GnuPG::Interface) # Bug: The testsuite unconditionally depends upon perl(GraphViz) Requires: perl(GraphViz) Requires: perl(Plack::Handler::Apache2) -Requires: perl(Set::Tiny) -Requires: perl(String::ShellQuote) -Requires: perl(Test::Deep) -Requires: perl(Test::Expect) +Requires: perl(Set::Tiny) +Requires: perl(String::ShellQuote) +Requires: perl(Test::Deep) +Requires: perl(Test::Expect) Requires: perl(Test::MockTime) -Requires: perl(Test::Warn) +Requires: perl(Test::Warn) Obsoletes: rt3-tests < %{version}-%{release} Provides: rt3-tests = %{version}-%{release} @@ -349,49 +352,14 @@ sed -e 's,@RT_CACHEDIR@,%{RT_CACHEDIR},' %{SOURCE3} \ sed -e 's,@RT_LOGDIR@,%{RT_LOGDIR},' %{SOURCE4} \ > rt.logrotate -# Remove configure-time generated files -rm Makefile -rm bin/rt -rm bin/rt-crontool -rm bin/rt-mailgate -rm etc/RT_Config.pm -rm etc/upgrade/3.8-ical-extension -rm etc/upgrade/4.0-customfield-checkbox-extension -rm etc/upgrade/generate-rtaddressregexp -rm etc/upgrade/split-out-cf-categories -rm etc/upgrade/switch-templates-to -rm etc/upgrade/upgrade-articles -rm etc/upgrade/vulnerable-passwords -rm lib/RT/Generated.pm -rm sbin/rt-attributes-viewer -rm sbin/rt-clean-sessions -rm sbin/rt-dump-metadata -rm sbin/rt-email-dashboards -rm sbin/rt-email-digest -rm sbin/rt-email-group-admin -rm sbin/rt-fulltext-indexer -rm sbin/rt-importer -rm sbin/rt-preferences-viewer -rm sbin/rt-serializer -rm sbin/rt-server -rm sbin/rt-server.fcgi -rm sbin/rt-session-viewer -rm sbin/rt-setup-database -rm sbin/rt-setup-fulltext-index -rm sbin/rt-shredder -rm sbin/rt-test-dependencies -rm sbin/rt-validate-aliases -rm sbin/rt-validator -rm sbin/standalone_httpd -rm t/data/configs/apache2.2+fastcgi.conf -rm t/data/configs/apache2.2+mod_perl.conf - +%patch1 -p1 %patch2 -p1 %patch3 -p1 %patch4 -p1 %patch5 -p1 %patch6 -p1 %patch7 -p1 +%patch8 -p1 # Propagate rpm's directories to config.layout cat << \EOF >> config.layout @@ -407,7 +375,7 @@ cat << \EOF >> config.layout localstatedir: %{RT_LOCALSTATEDIR} htmldir: %{RT_WWWDIR} fontdir: %{RT_FONTSDIR} - staticdir: %{RT_STATICDIR} + staticdir: %{RT_STATICDIR} logfiledir: %{RT_LOGDIR} masonstatedir: %{RT_CACHEDIR}/mason_data sessionstatedir: %{RT_CACHEDIR}/session_data @@ -519,6 +487,7 @@ ln -s %{_bindir} ${RPM_BUILD_ROOT}%{perl_testdir}/%{name}/bin ln -s %{_sbindir} ${RPM_BUILD_ROOT}%{perl_testdir}/%{name}/sbin ln -s %{_sysconfdir}/%{name} ${RPM_BUILD_ROOT}%{perl_testdir}/%{name}/etc ln -s %{RT_LIBDIR} ${RPM_BUILD_ROOT}%{perl_testdir}/%{name}/lib +ln -s %{_pkgdocdir}/docs ${RPM_BUILD_ROOT}%{perl_testdir}/%{name}/docs # These files should not be installed @@ -559,7 +528,8 @@ fi %files -%doc COPYING README README.fedora +%doc README README.fedora +%license COPYING %{_bindir}/* %{_sbindir}/* %exclude %{_bindir}/rt-mailgate @@ -601,7 +571,7 @@ fi %attr(0770,apache,apache) %{RT_CACHEDIR}/session_data %files mailgate -%doc COPYING +%license COPYING %{_bindir}/rt-mailgate %{_mandir}/man1/rt-mailgate* @@ -615,12 +585,19 @@ fi %{_sysconfdir}/%{name}/*.SQLite %files -n perl-RT-Test -%doc COPYING +%license COPYING %dir %{RT_LIBDIR}/RT %{RT_LIBDIR}/RT/Test* %endif %changelog +* Tue Mar 24 2015 Ralf Corsépius <corsepiu@xxxxxxxxxxxxxxxxx> - 4.2.10-2 +- Update patches. +- R: perl(Time::ParseDate). +- Add docs symlink. +- Add %%license. +- Spec cleanup. + * Mon Mar 09 2015 Jason L Tibbitts III <tibbs@xxxxxxxxxxx> - 4.2.10-1 - Update to 4.2.10. - Remove 0001-Remove-configure-time-generated-files.patch and delete the files -- Fedora Extras Perl SIG http://www.fedoraproject.org/wiki/Extras/SIGs/Perl perl-devel mailing list perl-devel@xxxxxxxxxxxxxxxxxxxxxxx https://admin.fedoraproject.org/mailman/listinfo/perl-devel