diff options
| author | Ivan Kohler <ivan@freeside.biz> | 2012-04-24 11:35:56 -0700 |
|---|---|---|
| committer | Ivan Kohler <ivan@freeside.biz> | 2012-04-24 11:35:56 -0700 |
| commit | 6587f6ba7d047ddc1686c080090afe7d53365bd4 (patch) | |
| tree | ec77342668e8865aca669c9b4736e84e3077b523 /rt/sbin | |
| parent | 47153aae5c2fc00316654e7277fccd45f72ff611 (diff) | |
first pass RT4 merge, RT#13852
Diffstat (limited to 'rt/sbin')
| -rwxr-xr-x | rt/sbin/rt-dump-metadata | 251 | ||||
| -rw-r--r-- | rt/sbin/rt-dump-metadata.in | 251 | ||||
| -rwxr-xr-x | rt/sbin/rt-fulltext-indexer | 453 | ||||
| -rw-r--r-- | rt/sbin/rt-fulltext-indexer.in | 453 | ||||
| -rwxr-xr-x | rt/sbin/rt-preferences-viewer | 149 | ||||
| -rw-r--r-- | rt/sbin/rt-preferences-viewer.in | 149 | ||||
| -rwxr-xr-x | rt/sbin/rt-server.fcgi | 282 | ||||
| -rw-r--r-- | rt/sbin/rt-server.fcgi.in | 282 | ||||
| -rwxr-xr-x | rt/sbin/rt-setup-database | 590 | ||||
| -rwxr-xr-x | rt/sbin/rt-setup-fulltext-index | 714 | ||||
| -rw-r--r-- | rt/sbin/rt-setup-fulltext-index.in | 714 | ||||
| -rwxr-xr-x | rt/sbin/rt-test-dependencies | 661 | ||||
| -rwxr-xr-x | rt/sbin/standalone_httpd | 282 | ||||
| -rw-r--r-- | rt/sbin/standalone_httpd.in | 282 |
14 files changed, 5513 insertions, 0 deletions
diff --git a/rt/sbin/rt-dump-metadata b/rt/sbin/rt-dump-metadata new file mode 100755 index 000000000..193ea85f4 --- /dev/null +++ b/rt/sbin/rt-dump-metadata @@ -0,0 +1,251 @@ +#!/usr/bin/perl -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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; + +# 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 { + require File::Spec; + my @libs = ( "/opt/rt3/lib", "/opt/rt3/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 Getopt::Long; +my %opt; +GetOptions( \%opt, "help|h" ); + +if ( $opt{help} ) { + require Pod::Usage; + Pod::Usage::pod2usage( { verbose => 2 } ); + exit; +} + +require RT; +require XML::Simple; + +RT::LoadConfig(); +RT::Init(); + +my $LocalOnly = @ARGV ? shift(@ARGV) : 1; + +my %RV; +my %Ignore = ( + All => [ + qw( + id Created Creator LastUpdated LastUpdatedBy + ) + ], + Templates => [ + qw( + TranslationOf + ) + ], +); + +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; + $objects->UnLimit; + + if ( $class eq 'CustomFields' ) { + $objects->OrderByCols( + { FIELD => 'LookupType' }, + { FIELD => 'SortOrder' }, + { FIELD => 'Id' }, + ); + } else { + $objects->OrderBy( FIELD => 'Id' ); + } + + if ($LocalOnly) { + 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'; + $objects->Limit( + FIELD => 'Domain', + OPERATOR => '=', + VALUE => 'UserDefined' + ) if $class eq 'Groups'; + } + + my %fields; + 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; + + # 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 ( eval { require RT::Attributes; 1 } ) { + my $attributes = $obj->Attributes; + while ( my $attribute = $attributes->Next ) { + my $content = $attribute->Content; + $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 [ 0 ] + +=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. + +When run without arguments, the metadata dump will only include 'local' +configuration changes, i.e. those done manually in the web interface. + +When run with the argument '0', the dump will include all configuration +metadata. + +This is NOT a tool for backing up an RT database. + diff --git a/rt/sbin/rt-dump-metadata.in b/rt/sbin/rt-dump-metadata.in new file mode 100644 index 000000000..f58371f5d --- /dev/null +++ b/rt/sbin/rt-dump-metadata.in @@ -0,0 +1,251 @@ +#!@PERL@ -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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; + +# 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 { + require File::Spec; + my @libs = ( "@RT_LIB_PATH@", "@LOCAL_LIB_PATH@" ); + 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 Getopt::Long; +my %opt; +GetOptions( \%opt, "help|h" ); + +if ( $opt{help} ) { + require Pod::Usage; + Pod::Usage::pod2usage( { verbose => 2 } ); + exit; +} + +require RT; +require XML::Simple; + +RT::LoadConfig(); +RT::Init(); + +my $LocalOnly = @ARGV ? shift(@ARGV) : 1; + +my %RV; +my %Ignore = ( + All => [ + qw( + id Created Creator LastUpdated LastUpdatedBy + ) + ], + Templates => [ + qw( + TranslationOf + ) + ], +); + +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; + $objects->UnLimit; + + if ( $class eq 'CustomFields' ) { + $objects->OrderByCols( + { FIELD => 'LookupType' }, + { FIELD => 'SortOrder' }, + { FIELD => 'Id' }, + ); + } else { + $objects->OrderBy( FIELD => 'Id' ); + } + + if ($LocalOnly) { + 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'; + $objects->Limit( + FIELD => 'Domain', + OPERATOR => '=', + VALUE => 'UserDefined' + ) if $class eq 'Groups'; + } + + my %fields; + 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; + + # 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 ( eval { require RT::Attributes; 1 } ) { + my $attributes = $obj->Attributes; + while ( my $attribute = $attributes->Next ) { + my $content = $attribute->Content; + $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 [ 0 ] + +=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. + +When run without arguments, the metadata dump will only include 'local' +configuration changes, i.e. those done manually in the web interface. + +When run with the argument '0', the dump will include all configuration +metadata. + +This is NOT a tool for backing up an RT database. + diff --git a/rt/sbin/rt-fulltext-indexer b/rt/sbin/rt-fulltext-indexer new file mode 100755 index 000000000..2a6b07e39 --- /dev/null +++ b/rt/sbin/rt-fulltext-indexer @@ -0,0 +1,453 @@ +#!/usr/bin/perl +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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 { + require File::Spec; + my @libs = ("/opt/rt3/lib", "/opt/rt3/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; + } +} + +BEGIN { + use RT; + RT::LoadConfig(); + RT::Init(); +}; +use RT::Interface::CLI (); + +my %OPT = ( + help => 0, + debug => 0, +); +my @OPT_LIST = qw(help|h! debug!); + +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, + xmlpipe2 => 0, + ); + push @OPT_LIST, 'limit=i', 'all!', 'xmlpipe2!'; +} +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), + ); +} + +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/rt3/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 ( $db_type eq 'mysql' ) { + unless ($OPT{'xmlpipe2'}) { + 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 ); + } + finalize( $type, $attachments ) if $found; + clean( $type ); + 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 finalize { + return goto_specific( + suffix => $db_type, + arguments => \@_, + ); +} + +sub clean { + return goto_specific( + suffix => $db_type, + arguments => \@_, + ); +} + +{ +sub last_indexed_mysql { + my $type = shift; + my $attr = $RT::System->FirstAttribute('LastIndexedAttachments'); + return 0 unless $attr; + return 0 unless exists $attr->{ $type }; + return $attr->{ $type } || 0; +} + +sub process_mysql { + my ($type, $attachment, $text) = (@_); + + my $doc = sphinx_template(); + + my $element = $doc->createElement('sphinx:document'); + $element->setAttribute( id => $attachment->id ); + $element->appendTextChild( content => $$text ); + + $doc->documentElement->appendChild( $element ); +} + +my $doc = undef; +sub sphinx_template { + return $doc if $doc; + + require XML::LibXML; + $doc = XML::LibXML::Document->new('1.0', 'UTF-8'); + my $root = $doc->createElement('sphinx:docset'); + $doc->setDocumentElement( $root ); + + my $schema = $doc->createElement('sphinx:schema'); + $root->appendChild( $schema ); + foreach ( qw(content) ) { + my $field = $doc->createElement('sphinx:field'); + $field->setAttribute( name => $_ ); + $schema->appendChild( $field ); + } + + return $doc; +} + +sub finalize_mysql { + my ($type, $attachments) = @_; + sphinx_template()->toFH(*STDOUT, 1); +} + +sub clean_mysql { + $doc = undef; +} + +} + +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->errstr =~ /string is too long for tsvector/) { + warn "Attachment @{[$attachment->id]} not indexed, as it contains too many unique words to be indexed"; + } else { + die "error: ". $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); +# TODO: html -> 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@bestpractical.comE<gt>, +Alex Vandiver E<lt>alexmv@bestpractical.comE<gt> + +=cut + diff --git a/rt/sbin/rt-fulltext-indexer.in b/rt/sbin/rt-fulltext-indexer.in new file mode 100644 index 000000000..7e31cac84 --- /dev/null +++ b/rt/sbin/rt-fulltext-indexer.in @@ -0,0 +1,453 @@ +#!@PERL@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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 { + require File::Spec; + my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@"); + 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; + } +} + +BEGIN { + use RT; + RT::LoadConfig(); + RT::Init(); +}; +use RT::Interface::CLI (); + +my %OPT = ( + help => 0, + debug => 0, +); +my @OPT_LIST = qw(help|h! debug!); + +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, + xmlpipe2 => 0, + ); + push @OPT_LIST, 'limit=i', 'all!', 'xmlpipe2!'; +} +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), + ); +} + +my $fts_config = RT->Config->Get('FullTextSearch') || {}; +unless ( $fts_config->{'Enable'} ) { + print STDERR <<EOT; + +Full text search is disabled in your RT configuration. Run +@RT_SBIN_PATH_R@/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 ( $db_type eq 'mysql' ) { + unless ($OPT{'xmlpipe2'}) { + 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 ); + } + finalize( $type, $attachments ) if $found; + clean( $type ); + 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 finalize { + return goto_specific( + suffix => $db_type, + arguments => \@_, + ); +} + +sub clean { + return goto_specific( + suffix => $db_type, + arguments => \@_, + ); +} + +{ +sub last_indexed_mysql { + my $type = shift; + my $attr = $RT::System->FirstAttribute('LastIndexedAttachments'); + return 0 unless $attr; + return 0 unless exists $attr->{ $type }; + return $attr->{ $type } || 0; +} + +sub process_mysql { + my ($type, $attachment, $text) = (@_); + + my $doc = sphinx_template(); + + my $element = $doc->createElement('sphinx:document'); + $element->setAttribute( id => $attachment->id ); + $element->appendTextChild( content => $$text ); + + $doc->documentElement->appendChild( $element ); +} + +my $doc = undef; +sub sphinx_template { + return $doc if $doc; + + require XML::LibXML; + $doc = XML::LibXML::Document->new('1.0', 'UTF-8'); + my $root = $doc->createElement('sphinx:docset'); + $doc->setDocumentElement( $root ); + + my $schema = $doc->createElement('sphinx:schema'); + $root->appendChild( $schema ); + foreach ( qw(content) ) { + my $field = $doc->createElement('sphinx:field'); + $field->setAttribute( name => $_ ); + $schema->appendChild( $field ); + } + + return $doc; +} + +sub finalize_mysql { + my ($type, $attachments) = @_; + sphinx_template()->toFH(*STDOUT, 1); +} + +sub clean_mysql { + $doc = undef; +} + +} + +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->errstr =~ /string is too long for tsvector/) { + warn "Attachment @{[$attachment->id]} not indexed, as it contains too many unique words to be indexed"; + } else { + die "error: ". $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); +# TODO: html -> 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@bestpractical.comE<gt>, +Alex Vandiver E<lt>alexmv@bestpractical.comE<gt> + +=cut + diff --git a/rt/sbin/rt-preferences-viewer b/rt/sbin/rt-preferences-viewer new file mode 100755 index 000000000..69a65a8bb --- /dev/null +++ b/rt/sbin/rt-preferences-viewer @@ -0,0 +1,149 @@ +#!/usr/bin/perl +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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 = ("/opt/rt3/lib", "/opt/rt3/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 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; +RT::LoadConfig(); +RT::Init(); + +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/rt/sbin/rt-preferences-viewer.in b/rt/sbin/rt-preferences-viewer.in new file mode 100644 index 000000000..1c32f879e --- /dev/null +++ b/rt/sbin/rt-preferences-viewer.in @@ -0,0 +1,149 @@ +#!@PERL@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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 = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@"); + 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 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; +RT::LoadConfig(); +RT::Init(); + +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/rt/sbin/rt-server.fcgi b/rt/sbin/rt-server.fcgi new file mode 100755 index 000000000..78533c6e5 --- /dev/null +++ b/rt/sbin/rt-server.fcgi @@ -0,0 +1,282 @@ +#!/usr/bin/perl -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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; + +# fix lib paths, some may be relative +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 + + require File::Spec; + my @libs = ("/opt/rt3/lib", "/opt/rt3/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 Getopt::Long; +no warnings 'once'; + +if (grep { m/help/ } @ARGV) { + require Pod::Usage; + print Pod::Usage::pod2usage( { verbose => 2 } ); + exit; +} + +require RT; +RT->LoadConfig(); +require Module::Refresh if RT->Config->Get('DevelMode'); + +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(); + + 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(undef); + undef $RT::Handle; +} + +require RT::Interface::Web::Handler; +my $app = RT::Interface::Web::Handler->PSGIApp; + +if ($ENV{RT_TESTING}) { + my $screen_logger = $RT::Logger->remove('screen'); + require Log::Dispatch::Perl; + $RT::Logger->add( + Log::Dispatch::Perl->new( + name => 'rttest', + min_level => $screen_logger->min_level, + action => { + error => 'warn', + critical => 'warn' + } + ) + ); + require Plack::Middleware::Test::StashWarnings; + $app = Plack::Middleware::Test::StashWarnings->wrap($app); +} + +# when used as a psgi file +if (caller) { + return $app; +} + + +# load appropriate server + +require Plack::Runner; + +my $is_fastcgi = $0 =~ m/fcgi$/; +my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) : + $is_fastcgi ? ( server => 'FCGI' ) + : (), + env => 'deployment' ); + +# figure out the port +my $port; + +# handle "rt-server 8888" for back-compat, but complain about it +if ($ARGV[0] && $ARGV[0] =~ m/^\d+$/) { + warn "Deprecated: please run $0 --port $ARGV[0] instead\n"; + unshift @ARGV, '--port'; +} + +my @args = @ARGV; + +use List::MoreUtils 'last_index'; +my $last_index = last_index { $_ eq '--port' } @args; + +my $explicit_port; + +if ( $last_index != -1 && $args[$last_index+1] =~ /^\d+$/ ) { + $explicit_port = $args[$last_index+1]; + $port = $explicit_port; + + # inform the rest of the system what port we manually chose + my $old_app = $app; + $app = sub { + my $env = shift; + + $env->{'rt.explicit_port'} = $port; + + $old_app->($env, @_); + }; +} +else { + # default to the configured WebPort and inform Plack::Runner + $port = RT->Config->Get('WebPort') || '8080'; + push @args, '--port', $port; +} + +push @args, '--server', 'Standalone' if RT->InstallMode; +push @args, '--server', 'Starlet' unless $r->{server} || grep { m/--server/ } @args; + +$r->parse_options(@args); + +delete $r->{options} if $is_fastcgi; ### mangle_host_port_socket ruins everything + +unless ($r->{env} eq 'development') { + push @{$r->{options}}, server_ready => sub { + my($args) = @_; + my $name = $args->{server_software} || ref($args); # $args is $server + my $host = $args->{host} || 0; + my $proto = $args->{proto} || 'http'; + print STDERR "$name: Accepting connections at $proto://$host:$args->{port}/\n"; + }; +} +eval { $r->run($app) }; +if (my $err = $@) { + handle_startup_error($err); +} + +exit 0; + +sub handle_startup_error { + my $err = shift; + if ( $err =~ /listen/ ) { + handle_bind_error(); + } else { + die + "Something went wrong while trying to run RT's standalone web server:\n\t" + . $err; + } +} + + +sub handle_bind_error { + + print STDERR <<EOF; +WARNING: RT couldn't start up a web server on port @{[$port]}. +This is often the case if the port is already in use or you're running @{[$0]} +as someone other than your system's "root" user. You may also specify a +temporary port with: $0 --port <port> +EOF + + if ($explicit_port) { + print STDERR + "Please check your system configuration or choose another port\n\n"; + } +} + +__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/rt/sbin/rt-server.fcgi.in b/rt/sbin/rt-server.fcgi.in new file mode 100644 index 000000000..b438202dd --- /dev/null +++ b/rt/sbin/rt-server.fcgi.in @@ -0,0 +1,282 @@ +#!@PERL@ -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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; + +# fix lib paths, some may be relative +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 + + require File::Spec; + my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@"); + 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 Getopt::Long; +no warnings 'once'; + +if (grep { m/help/ } @ARGV) { + require Pod::Usage; + print Pod::Usage::pod2usage( { verbose => 2 } ); + exit; +} + +require RT; +RT->LoadConfig(); +require Module::Refresh if RT->Config->Get('DevelMode'); + +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(); + + 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(undef); + undef $RT::Handle; +} + +require RT::Interface::Web::Handler; +my $app = RT::Interface::Web::Handler->PSGIApp; + +if ($ENV{RT_TESTING}) { + my $screen_logger = $RT::Logger->remove('screen'); + require Log::Dispatch::Perl; + $RT::Logger->add( + Log::Dispatch::Perl->new( + name => 'rttest', + min_level => $screen_logger->min_level, + action => { + error => 'warn', + critical => 'warn' + } + ) + ); + require Plack::Middleware::Test::StashWarnings; + $app = Plack::Middleware::Test::StashWarnings->wrap($app); +} + +# when used as a psgi file +if (caller) { + return $app; +} + + +# load appropriate server + +require Plack::Runner; + +my $is_fastcgi = $0 =~ m/fcgi$/; +my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) : + $is_fastcgi ? ( server => 'FCGI' ) + : (), + env => 'deployment' ); + +# figure out the port +my $port; + +# handle "rt-server 8888" for back-compat, but complain about it +if ($ARGV[0] && $ARGV[0] =~ m/^\d+$/) { + warn "Deprecated: please run $0 --port $ARGV[0] instead\n"; + unshift @ARGV, '--port'; +} + +my @args = @ARGV; + +use List::MoreUtils 'last_index'; +my $last_index = last_index { $_ eq '--port' } @args; + +my $explicit_port; + +if ( $last_index != -1 && $args[$last_index+1] =~ /^\d+$/ ) { + $explicit_port = $args[$last_index+1]; + $port = $explicit_port; + + # inform the rest of the system what port we manually chose + my $old_app = $app; + $app = sub { + my $env = shift; + + $env->{'rt.explicit_port'} = $port; + + $old_app->($env, @_); + }; +} +else { + # default to the configured WebPort and inform Plack::Runner + $port = RT->Config->Get('WebPort') || '8080'; + push @args, '--port', $port; +} + +push @args, '--server', 'Standalone' if RT->InstallMode; +push @args, '--server', 'Starlet' unless $r->{server} || grep { m/--server/ } @args; + +$r->parse_options(@args); + +delete $r->{options} if $is_fastcgi; ### mangle_host_port_socket ruins everything + +unless ($r->{env} eq 'development') { + push @{$r->{options}}, server_ready => sub { + my($args) = @_; + my $name = $args->{server_software} || ref($args); # $args is $server + my $host = $args->{host} || 0; + my $proto = $args->{proto} || 'http'; + print STDERR "$name: Accepting connections at $proto://$host:$args->{port}/\n"; + }; +} +eval { $r->run($app) }; +if (my $err = $@) { + handle_startup_error($err); +} + +exit 0; + +sub handle_startup_error { + my $err = shift; + if ( $err =~ /listen/ ) { + handle_bind_error(); + } else { + die + "Something went wrong while trying to run RT's standalone web server:\n\t" + . $err; + } +} + + +sub handle_bind_error { + + print STDERR <<EOF; +WARNING: RT couldn't start up a web server on port @{[$port]}. +This is often the case if the port is already in use or you're running @{[$0]} +as someone other than your system's "root" user. You may also specify a +temporary port with: $0 --port <port> +EOF + + if ($explicit_port) { + print STDERR + "Please check your system configuration or choose another port\n\n"; + } +} + +__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/rt/sbin/rt-setup-database b/rt/sbin/rt-setup-database new file mode 100755 index 000000000..d0b149b4c --- /dev/null +++ b/rt/sbin/rt-setup-database @@ -0,0 +1,590 @@ +#!/usr/bin/perl +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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 { + require File::Spec; + my @libs = ("/opt/rt3/lib", "/opt/rt3/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 Term::ReadKey; +use Getopt::Long; + +$| = 1; # unbuffer all output. + +my %args = ( + dba => 'freeside', +); +GetOptions( + \%args, + 'action=s', + 'force', 'debug', + 'dba=s', 'dba-password=s', 'prompt-for-dba-password', + 'datafile=s', 'datadir=s', 'skip-create', 'root-password-file=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( LogToScreen => 'warning') + unless ( RT->Config->Get( 'LogToScreen' ) + && RT->Config->Get( 'LogToScreen' ) =~ /^(debug|info|notice)$/ ); + +# 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|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_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'} || $db_user || ''; +my $dba_pass = exists($args{'dba-password'}) + ? $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); + } +} + +print "Working with:\n" + ."Type:\t$db_type\nHost:\t$db_host\nName:\t$db_name\n" + ."User:\t$db_user\nDBA:\t$dba_user" . ($args{'skip-create'} ? ' (No DBA)' : '') . "\n"; + +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, 'pre' ); + 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. +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, 'pre' ); + return ($status, $msg) unless $status; + + print "Now populating database schema.\n"; + return RT::Handle->InsertSchema( $dbh, $args{'datafile'} || $args{'datadir'} ); +} + +sub action_acl { + my %args = @_; + my $dbh = get_admin_dbh(); + my ($status, $msg) = RT::Handle->CheckCompatibility( $dbh, 'pre' ); + return ($status, $msg) unless $status; + + print "Now inserting database ACLs.\n"; + return RT::Handle->InsertACL( $dbh, $args{'datafile'} || $args{'datadir'} ); +} + +sub action_coredata { + my %args = @_; + $RT::Handle = RT::Handle->new; + $RT::Handle->dbh( undef ); + RT::ConnectToDatabase(); + RT::InitLogging(); + my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' ); + 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(); + my ($status, $msg) = RT::Handle->CheckCompatibility( $RT::Handle->dbh, 'pre' ); + 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"; + + # Slurp in backcompat + my %removed; + my @back = @{$args{backcompat} || []}; + if (@back) { + my @lines = do {local @ARGV = @back; <>}; + for (@lines) { + s/\#.*//; + next unless /\S/; + my ($class, @fields) = split; + $class->_BuildTableAttributes; + $RT::Logger->debug("Temporarily removing @fields from $class"); + $removed{$class}{$_} = delete $RT::Record::_TABLE_ATTR->{$class}{$_} + for @fields; + } + } + + my @ret = $RT::Handle->InsertData( $file, $root_password ); + + # Put back the fields we chopped off + for my $class (keys %removed) { + $RT::Record::_TABLE_ATTR->{$class}{$_} = $removed{$class}{$_} + for keys %{$removed{$class}}; + } + 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 RT version you're upgrading from: "; + } + $upgrading_from = scalar <STDIN>; + chomp $upgrading_from; + $upgrading_from =~ s/\s+//g; + } while $upgrading_from !~ /^\d+\.\d+\.\w+$/; + + 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 RT 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 = scalar <STDIN>; + chomp $custom_upgrading_to; + $custom_upgrading_to =~ s/\s+//g; + last unless $custom_upgrading_to; + } while $custom_upgrading_to !~ /^\d+\.\d+\.\w+$/; + + 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; + } + } + + print "\nIT'S VERY IMPORTANT TO BACK UP BEFORE THIS STEP\n\n"; + _yesno() or exit(-2) unless $args{'force'}; + + my ( $ret, $msg ); + foreach my $n ( 0..$#versions ) { + my $v = $versions[$n]; + my @back = grep {-e $_} map {"$base_dir/$versions[$_]/backcompat"} $n+1..$#versions; + print "Processing $v\n"; + 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/content" ) { + ( $ret, $msg ) = action_insert( %tmp ); + return ( $ret, $msg ) unless $ret; + } + } + 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/$_" && /\d+\.\d+\.\d+/, readdir $dh; + closedir $dh; + + 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 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. + +=back diff --git a/rt/sbin/rt-setup-fulltext-index b/rt/sbin/rt-setup-fulltext-index new file mode 100755 index 000000000..862581544 --- /dev/null +++ b/rt/sbin/rt-setup-fulltext-index @@ -0,0 +1,714 @@ +#!/usr/bin/perl +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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 { + require File::Spec; + my @libs = ("/opt/rt3/lib", "/opt/rt3/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; + } +} + +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 => 'freeside', + 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; + +if ( $DB{'type'} eq 'mysql' ) { + 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 = $OPT{'url'} || prompt( + message => "Enter URL of the sphinx search server; this should be of the form\n" + . "sphinx://<server>:<port>/<index name>", + default => 'sphinx://localhost:3312/rt', + 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 INTEGER UNSIGNED 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. + +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@bestpractical.comE<gt>, +Alex Vandiver E<lt>alexmv@bestpractical.comE<gt> + +=cut + diff --git a/rt/sbin/rt-setup-fulltext-index.in b/rt/sbin/rt-setup-fulltext-index.in new file mode 100644 index 000000000..da8089d94 --- /dev/null +++ b/rt/sbin/rt-setup-fulltext-index.in @@ -0,0 +1,714 @@ +#!@PERL@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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 { + require File::Spec; + my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@"); + 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; + } +} + +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 => '@DB_DBA@', + 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; + +if ( $DB{'type'} eq 'mysql' ) { + 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 = $OPT{'url'} || prompt( + message => "Enter URL of the sphinx search server; this should be of the form\n" + . "sphinx://<server>:<port>/<index name>", + default => 'sphinx://localhost:3312/rt', + 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 INTEGER UNSIGNED 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. + +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@bestpractical.comE<gt>, +Alex Vandiver E<lt>alexmv@bestpractical.comE<gt> + +=cut + diff --git a/rt/sbin/rt-test-dependencies b/rt/sbin/rt-test-dependencies new file mode 100755 index 000000000..f5eb584a3 --- /dev/null +++ b/rt/sbin/rt-test-dependencies @@ -0,0 +1,661 @@ +#!/usr/bin/perl +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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; +no warnings qw(numeric redefine); +use Getopt::Long; +my %args; +my %deps; +GetOptions( + \%args, 'v|verbose', + 'install', 'with-MYSQL', + 'with-POSTGRESQL|with-pg|with-pgsql', 'with-SQLITE', + 'with-ORACLE', 'with-FASTCGI', + 'with-MODPERL1', 'with-MODPERL2', + 'with-STANDALONE', + + 'with-DEV', + + 'with-GPG', + 'with-ICAL', + 'with-SMTP', + 'with-GRAPHVIZ', + 'with-GD', + 'with-DASHBOARDS', + 'with-USERLOGO', + 'with-SSL-MAILGATE', + + 'download=s', + 'repository=s', + 'list-deps', + 'help|h', +); + +if ( $args{help} ) { + require Pod::Usage; + Pod::Usage::pod2usage( { verbose => 2 } ); + exit; +} + +# Set up defaults +my %default = ( + 'with-MASON' => 1, + 'with-PSGI' => 0, + 'with-CORE' => 1, + 'with-CLI' => 1, + 'with-MAILGATE' => 1, + 'with-DEV' => 0, + 'with-GPG' => 1, + 'with-ICAL' => 1, + 'with-SMTP' => 1, + 'with-GRAPHVIZ' => 0, + 'with-GD' => 0, + 'with-DASHBOARDS' => 1, + 'with-USERLOGO' => 1, + 'with-SSL-MAILGATE' => 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} ); + } + } + 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; +} + +$deps{'CORE'} = [ text_to_hash( << '.') ]; +Class::Accessor 0.34 +DateTime 0.44 +DateTime::Locale 0.40 +Digest::base +Digest::MD5 2.27 +Digest::SHA +DBI 1.37 +Class::ReturnValue 0.40 +DBIx::SearchBuilder 1.59 +Text::Template 1.44 +File::ShareDir +File::Spec 0.8 +HTML::Quoted +HTML::Scrubber 0.08 +Log::Dispatch 2.23 +Sys::Syslog 0.16 +Locale::Maketext 1.06 +Locale::Maketext::Lexicon 0.32 +Locale::Maketext::Fuzzy +MIME::Entity 5.425 +Mail::Mailer 1.57 +Email::Address +Text::Wrapper +Time::ParseDate +Time::HiRes +File::Temp 0.19 +Text::Quoted 2.02 +Tree::Simple 1.04 +UNIVERSAL::require +Regexp::Common +Scalar::Util +Module::Versions::Report 1.05 +Cache::Simple::TimedExpiry +Encode 2.39 +CSS::Squish 0.06 +File::Glob +Devel::StackTrace 1.19 +Text::Password::Pronounceable +Devel::GlobalDestruction +List::MoreUtils +Net::CIDR +Regexp::Common::net::CIDR +Regexp::IPv6 +. + +$deps{'MASON'} = [ text_to_hash( << '.') ]; +HTML::Mason 1.43 +Errno +Digest::MD5 2.27 +CGI::Cookie 1.20 +Storable 2.08 +Apache::Session 1.53 +XML::RSS 1.05 +Text::WikiFormat 0.76 +CSS::Squish 0.06 +Devel::StackTrace 1.19 +JSON +IPC::Run3 +. + +$deps{'PSGI'} = [ text_to_hash( << '.') ]; +CGI 3.38 +CGI::PSGI 0.12 +HTML::Mason::PSGIHandler 0.52 +Plack 0.9971 +Plack::Handler::Starlet +CGI::Emulate::PSGI +. + +$deps{'MAILGATE'} = [ text_to_hash( << '.') ]; +HTML::TreeBuilder +HTML::FormatText +Getopt::Long +LWP::UserAgent +Pod::Usage +. + +$deps{'SSL-MAILGATE'} = [ text_to_hash( << '.') ]; +Crypt::SSLeay +Net::SSL +LWP::UserAgent 6.0 +LWP::Protocol::https +Mozilla::CA +. + +$deps{'CLI'} = [ text_to_hash( << '.') ]; +Getopt::Long 2.24 +LWP +HTTP::Request::Common +Text::ParseWords +Term::ReadLine +Term::ReadKey +. + +$deps{'DEV'} = [ text_to_hash( << '.') ]; +Email::Abstract +Test::Email +HTML::Form +HTML::TokeParser +WWW::Mechanize 1.52 +Test::WWW::Mechanize 1.30 +Module::Refresh 0.03 +Test::Expect 0.31 +XML::Simple +File::Find +Test::Deep 0 # needed for shredder tests +String::ShellQuote 0 # needed for gnupg-incoming.t +Log::Dispatch::Perl +Test::Warn +Test::Builder 0.90 # needed for is_passing +Test::MockTime +Log::Dispatch::Perl +Test::WWW::Mechanize::PSGI +Plack::Middleware::Test::StashWarnings +Test::LongString +. + +$deps{'FASTCGI'} = [ text_to_hash( << '.') ]; +FCGI +FCGI::ProcManager +. + +$deps{'MODPERL1'} = [ text_to_hash( << '.') ]; +Apache::Request +Apache::DBI 0.92 +. + +$deps{'MODPERL2'} = [ text_to_hash( << '.') ]; +Apache::DBI +HTML::Mason 1.36 +. + +$deps{'MYSQL'} = [ text_to_hash( << '.') ]; +DBD::mysql 2.1018 +. + +$deps{'ORACLE'} = [ text_to_hash( << '.') ]; +DBD::Oracle +. + +$deps{'POSTGRESQL'} = [ text_to_hash( << '.') ]; +DBD::Pg 1.43 +. + +$deps{'SQLITE'} = [ text_to_hash( << '.') ]; +DBD::SQLite 1.00 +. + +$deps{'GPG'} = [ text_to_hash( << '.') ]; +GnuPG::Interface +PerlIO::eol +. + +$deps{'ICAL'} = [ text_to_hash( << '.') ]; +Data::ICal +. + +$deps{'SMTP'} = [ text_to_hash( << '.') ]; +Net::SMTP +. + +$deps{'DASHBOARDS'} = [ text_to_hash( << '.') ]; +HTML::RewriteAttributes 0.04 +MIME::Types +. + +$deps{'GRAPHVIZ'} = [ text_to_hash( << '.') ]; +GraphViz +IPC::Run +. + +$deps{'GD'} = [ text_to_hash( << '.') ]; +GD +GD::Graph +GD::Text +. + +$deps{'USERLOGO'} = [ text_to_hash( << '.') ]; +Convert::Color +. + +my %AVOID = ( + 'DBD::Oracle' => [qw(1.23)], +); + +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; +} + +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 { + eval "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/; + + 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 download_mods { + my %modules; + use CPAN; + + foreach my $key (keys %deps) { + my @deps = (@{$deps{$key}}); + while (@deps) { + my $mod = shift @deps; + my $ver = shift @deps; + next if ($mod =~ /^(DBD-|Apache-Request)/); + $modules{$mod} = $ver; + } + } + my @mods = keys %modules; + CPAN::get(); + my $moddir = $args{'download'}; + foreach my $mod (@mods) { + $CPAN::Config->{'build_dir'} = $moddir; + CPAN::get($mod); + } + + opendir(DIR, $moddir); + while ( my $dir = readdir(DIR)) { + print "Dir is $dir\n"; + next if ( $dir =~ /^\.\.?$/); + + # Skip things we've previously tagged + my $out = `svn ls $args{'repository'}/tags/$dir`; + next if ($out); + + if ($dir =~ /^(.*)-(.*?)$/) { + `svn_load_dirs -no_user_input -t tags/$dir -v $args{'repository'} dists/$1 $moddir/$dir`; + `rm -rf $moddir/$dir`; + + } + + } + closedir(DIR); + exit; +} + +sub check_perl_version { + section("perl"); + eval {require 5.008003}; + if ($@) { + print_found("5.8.3", 0,"RT is known to be non-functional on versions of perl older than 5.8.3. Please upgrade to 5.8.3 or newer."); + exit(1); + } else { + print_found( sprintf(">=5.8.3(%vd)", $^V), 1 ); + } +} + +sub check_users { + section("users"); + print_found("rt group (freeside)", defined getgrnam("freeside")); + 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 (freeside)", defined getpwnam("freeside")); + print_found("web group (freeside)", defined getgrnam("freeside")); +} + +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-postgresql + + 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-dev + + tools needed for RT development + +=back + +=back + diff --git a/rt/sbin/standalone_httpd b/rt/sbin/standalone_httpd new file mode 100755 index 000000000..78533c6e5 --- /dev/null +++ b/rt/sbin/standalone_httpd @@ -0,0 +1,282 @@ +#!/usr/bin/perl -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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; + +# fix lib paths, some may be relative +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 + + require File::Spec; + my @libs = ("/opt/rt3/lib", "/opt/rt3/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 Getopt::Long; +no warnings 'once'; + +if (grep { m/help/ } @ARGV) { + require Pod::Usage; + print Pod::Usage::pod2usage( { verbose => 2 } ); + exit; +} + +require RT; +RT->LoadConfig(); +require Module::Refresh if RT->Config->Get('DevelMode'); + +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(); + + 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(undef); + undef $RT::Handle; +} + +require RT::Interface::Web::Handler; +my $app = RT::Interface::Web::Handler->PSGIApp; + +if ($ENV{RT_TESTING}) { + my $screen_logger = $RT::Logger->remove('screen'); + require Log::Dispatch::Perl; + $RT::Logger->add( + Log::Dispatch::Perl->new( + name => 'rttest', + min_level => $screen_logger->min_level, + action => { + error => 'warn', + critical => 'warn' + } + ) + ); + require Plack::Middleware::Test::StashWarnings; + $app = Plack::Middleware::Test::StashWarnings->wrap($app); +} + +# when used as a psgi file +if (caller) { + return $app; +} + + +# load appropriate server + +require Plack::Runner; + +my $is_fastcgi = $0 =~ m/fcgi$/; +my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) : + $is_fastcgi ? ( server => 'FCGI' ) + : (), + env => 'deployment' ); + +# figure out the port +my $port; + +# handle "rt-server 8888" for back-compat, but complain about it +if ($ARGV[0] && $ARGV[0] =~ m/^\d+$/) { + warn "Deprecated: please run $0 --port $ARGV[0] instead\n"; + unshift @ARGV, '--port'; +} + +my @args = @ARGV; + +use List::MoreUtils 'last_index'; +my $last_index = last_index { $_ eq '--port' } @args; + +my $explicit_port; + +if ( $last_index != -1 && $args[$last_index+1] =~ /^\d+$/ ) { + $explicit_port = $args[$last_index+1]; + $port = $explicit_port; + + # inform the rest of the system what port we manually chose + my $old_app = $app; + $app = sub { + my $env = shift; + + $env->{'rt.explicit_port'} = $port; + + $old_app->($env, @_); + }; +} +else { + # default to the configured WebPort and inform Plack::Runner + $port = RT->Config->Get('WebPort') || '8080'; + push @args, '--port', $port; +} + +push @args, '--server', 'Standalone' if RT->InstallMode; +push @args, '--server', 'Starlet' unless $r->{server} || grep { m/--server/ } @args; + +$r->parse_options(@args); + +delete $r->{options} if $is_fastcgi; ### mangle_host_port_socket ruins everything + +unless ($r->{env} eq 'development') { + push @{$r->{options}}, server_ready => sub { + my($args) = @_; + my $name = $args->{server_software} || ref($args); # $args is $server + my $host = $args->{host} || 0; + my $proto = $args->{proto} || 'http'; + print STDERR "$name: Accepting connections at $proto://$host:$args->{port}/\n"; + }; +} +eval { $r->run($app) }; +if (my $err = $@) { + handle_startup_error($err); +} + +exit 0; + +sub handle_startup_error { + my $err = shift; + if ( $err =~ /listen/ ) { + handle_bind_error(); + } else { + die + "Something went wrong while trying to run RT's standalone web server:\n\t" + . $err; + } +} + + +sub handle_bind_error { + + print STDERR <<EOF; +WARNING: RT couldn't start up a web server on port @{[$port]}. +This is often the case if the port is already in use or you're running @{[$0]} +as someone other than your system's "root" user. You may also specify a +temporary port with: $0 --port <port> +EOF + + if ($explicit_port) { + print STDERR + "Please check your system configuration or choose another port\n\n"; + } +} + +__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/rt/sbin/standalone_httpd.in b/rt/sbin/standalone_httpd.in new file mode 100644 index 000000000..b438202dd --- /dev/null +++ b/rt/sbin/standalone_httpd.in @@ -0,0 +1,282 @@ +#!@PERL@ -w +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (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; + +# fix lib paths, some may be relative +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 + + require File::Spec; + my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@"); + 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 Getopt::Long; +no warnings 'once'; + +if (grep { m/help/ } @ARGV) { + require Pod::Usage; + print Pod::Usage::pod2usage( { verbose => 2 } ); + exit; +} + +require RT; +RT->LoadConfig(); +require Module::Refresh if RT->Config->Get('DevelMode'); + +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(); + + 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(undef); + undef $RT::Handle; +} + +require RT::Interface::Web::Handler; +my $app = RT::Interface::Web::Handler->PSGIApp; + +if ($ENV{RT_TESTING}) { + my $screen_logger = $RT::Logger->remove('screen'); + require Log::Dispatch::Perl; + $RT::Logger->add( + Log::Dispatch::Perl->new( + name => 'rttest', + min_level => $screen_logger->min_level, + action => { + error => 'warn', + critical => 'warn' + } + ) + ); + require Plack::Middleware::Test::StashWarnings; + $app = Plack::Middleware::Test::StashWarnings->wrap($app); +} + +# when used as a psgi file +if (caller) { + return $app; +} + + +# load appropriate server + +require Plack::Runner; + +my $is_fastcgi = $0 =~ m/fcgi$/; +my $r = Plack::Runner->new( $0 =~ 'standalone' ? ( server => 'Standalone' ) : + $is_fastcgi ? ( server => 'FCGI' ) + : (), + env => 'deployment' ); + +# figure out the port +my $port; + +# handle "rt-server 8888" for back-compat, but complain about it +if ($ARGV[0] && $ARGV[0] =~ m/^\d+$/) { + warn "Deprecated: please run $0 --port $ARGV[0] instead\n"; + unshift @ARGV, '--port'; +} + +my @args = @ARGV; + +use List::MoreUtils 'last_index'; +my $last_index = last_index { $_ eq '--port' } @args; + +my $explicit_port; + +if ( $last_index != -1 && $args[$last_index+1] =~ /^\d+$/ ) { + $explicit_port = $args[$last_index+1]; + $port = $explicit_port; + + # inform the rest of the system what port we manually chose + my $old_app = $app; + $app = sub { + my $env = shift; + + $env->{'rt.explicit_port'} = $port; + + $old_app->($env, @_); + }; +} +else { + # default to the configured WebPort and inform Plack::Runner + $port = RT->Config->Get('WebPort') || '8080'; + push @args, '--port', $port; +} + +push @args, '--server', 'Standalone' if RT->InstallMode; +push @args, '--server', 'Starlet' unless $r->{server} || grep { m/--server/ } @args; + +$r->parse_options(@args); + +delete $r->{options} if $is_fastcgi; ### mangle_host_port_socket ruins everything + +unless ($r->{env} eq 'development') { + push @{$r->{options}}, server_ready => sub { + my($args) = @_; + my $name = $args->{server_software} || ref($args); # $args is $server + my $host = $args->{host} || 0; + my $proto = $args->{proto} || 'http'; + print STDERR "$name: Accepting connections at $proto://$host:$args->{port}/\n"; + }; +} +eval { $r->run($app) }; +if (my $err = $@) { + handle_startup_error($err); +} + +exit 0; + +sub handle_startup_error { + my $err = shift; + if ( $err =~ /listen/ ) { + handle_bind_error(); + } else { + die + "Something went wrong while trying to run RT's standalone web server:\n\t" + . $err; + } +} + + +sub handle_bind_error { + + print STDERR <<EOF; +WARNING: RT couldn't start up a web server on port @{[$port]}. +This is often the case if the port is already in use or you're running @{[$0]} +as someone other than your system's "root" user. You may also specify a +temporary port with: $0 --port <port> +EOF + + if ($explicit_port) { + print STDERR + "Please check your system configuration or choose another port\n\n"; + } +} + +__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 |
