rt-4.4.7/0000755000076500000240000000000014514267701011550 5ustar sunnavystaffrt-4.4.7/devel/0000755000076500000240000000000014514237602012644 5ustar sunnavystaffrt-4.4.7/devel/tools/0000755000076500000240000000000014514237602014004 5ustar sunnavystaffrt-4.4.7/devel/tools/localhost.crt0000644000076500000240000000172514514237602016513 0ustar sunnavystaff-----BEGIN CERTIFICATE----- MIICpjCCAY4CCQDLtMptx45HuDANBgkqhkiG9w0BAQUFADAUMRIwEAYDVQQDEwls b2NhbGhvc3QwIBcNMTIwMjE3MjIxMTU3WhgPMjExMjAxMjQyMjExNTdaMBQxEjAQ BgNVBAMTCWxvY2FsaG9zdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB AKokK5sAKbNkJDoOInDQpwRxDDfanXKUR7MK761G2gWmUpxy+hlUn457VLgDKgDp s3gSUk0x3rsXcMxpsSDQ+E37kz5DnbPGSGdiS5tJD6VoQ2NsMfvrY1pZFWNv8wHu c4MDtStxsIxvZHjqguWeVUsXLKSfGEMTQ/MbKbn4d/7FSRpQDum2o3AsxHi4VbrS aWXRgCfcPlwaoOSc73lCD0kuXIl66wO8DBQOqqBtkuS59BcH+cq1T5wwKzMdJNfp Rx0TXISGUa4DSbTjqfAAJe4TzavH73PgNjXBl6+GsGb5/pf8Zad+t62xRcocDfOQ 5e2ASmInsDtlSX0pfLfBHg0CAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAUuiDKlBN RcR/YYkk/hCgDB4ronO3AO+d264Y3vDK+JsH2lI6/kwxpmJj+bA2IVM+eM5NrcFh zEm+LKnyz4EvmxXTI4gI1iFPhOP4NJYmMtyKGavlZP3gNW4JQRYOiA0vQ2Egcngo uW2k7xUaNPPkpHptkI0P1jLVl4bX/qKA6tzrmwsmdwNOW9j9zk9BOq8HVvduBDeU XFsrdmN4EgD0nU39olaArg/RqMacIfCfKqYdRo9OSbBfQ7x2di9HgI1h2VVfPGi5 cDRyLlpAY9KNuuStutcFMoQbdwKU/0GFkRuguFPJbIcDg7nhZDXRMU+XugQ8dsZ/ 0VgszAIRc510nA== -----END CERTIFICATE----- rt-4.4.7/devel/tools/cmd-boilerplate0000755000076500000240000000600514514237602016776 0ustar sunnavystaff#!/usr/bin/env perl # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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 File::Find; File::Find::find({ no_chdir => 1, wanted => \&tag_it}, 'sbin', 'bin'); sub tag_it { my $file = $_; return unless (-f $file); return if $file !~ /.in$/; open( FILE, '<', $file ) or die "Failed to open $file"; my $content = (join "", ); close (FILE); my $new = q'BEGIN { # BEGIN RT CMD BOILERPLATE require File::Spec; require Cwd; my @libs = ("@RT_LIB_PATH@", "@LOCAL_LIB_PATH@"); my $bin_path; for my $lib (@libs) { unless ( File::Spec->file_name_is_absolute($lib) ) { $bin_path ||= ( File::Spec->splitpath(Cwd::abs_path(__FILE__)) )[1]; $lib = File::Spec->catfile( $bin_path, File::Spec->updir, $lib ); } unshift @INC, $lib; } }'; unless ($content =~ s/^BEGIN \{ # BEGIN RT CMD BOILERPLATE.*?^\}$/$new/ms) { return; } warn $file; open( FILE, '>', $file ) or die "couldn't write new file"; print FILE $content; close FILE; } rt-4.4.7/devel/tools/rt-parse-mail-log0000755000076500000240000000475614514237602017202 0ustar sunnavystaff#!/bin/bash # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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 }}} function usage() { if [[ -n $1 ]]; then echo "$1" echo fi echo "usage: $0 ticket " } logfile=$1 what=$2 id=$3 if [[ -z $logfile || -z $what || -z $id ]]; then usage exit 1 fi case "$what" in ticket) pattern='\[info\]: # # (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 File::Copy; die "Usage: $0 'pattern to match msgid' 'code that changes \$_ for the msgstr' [files]" if @ARGV < 2; my $msgid_pattern = shift; my $msgid_regex = qr#$msgid_pattern#o; my $code_str = shift; @ARGV = ( , , , ) unless @ARGV; my @files = @ARGV; for my $file (@files) { my ($src, $dest) = ($file, "$file.new"); open(my $fh_in, '<', $src) or die $!; open(my $fh_out, '>', $dest) or die $!; my $mark_to_change = 0; while (<$fh_in>) { if (/^msgid\s+"(.+?)"$/ and $1 =~ $msgid_regex) { # we're at the msgid in question $mark_to_change = 1; } elsif ($mark_to_change) { # we're at the line after the msgid in question eval $code_str; $mark_to_change = 0; } print $fh_out $_; } close $_ for $fh_in, $fh_out; # copy back to source move($dest => $src); } rt-4.4.7/devel/tools/tweak-template-locstring0000755000076500000240000000432314514237602020662 0ustar sunnavystaff#!/usr/bin/env perl # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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; # run this script with: # perl -0pi sbin/tweak-template-locstring `ack -f share/html -G 'html$'` s!\<\&\|\/l([^&]*)\&\>[\n\s]+(.*?)[\n\s]*\<\/\&\>!;my ($arg, $x) = ($1, $2); $x =~ s/\s*\n\s*/ /g;"<&|/l$arg&>$x"!smge; 1; rt-4.4.7/devel/tools/rt-message-catalog0000755000076500000240000001564614514237602017425 0ustar sunnavystaff#!/usr/bin/env perl # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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 Locale::PO; use Getopt::Long; use File::Temp 'tempdir'; use constant PO_DIR => 'share/po'; use constant BOUNDARY => 20; sub usage { warn @_, "\n\n" if @_; warn <<' USAGE'; usages: rt-message-catalog stats [po-directory] rt-message-catalog clean rt-message-catalog rosetta download-url rt-message-catalog extract [po-file ...] stats: Print stats for each translation. clean: Remove unused and identity translations rosetta: Merge translations from Launchpad's Rosetta; Requires a Launchpad translations export url. extract: Extract message catalogs from source code and report common errors. If passed a specific translation file, only that file is updated. (Not recommended except for debugging.) USAGE exit 1; } my $command = shift; usage() unless $command; usage("Unknown command '$command'") unless main->can($command); main->can($command)->( @ARGV ); exit; sub stats { my $dir = shift || PO_DIR; my $max = 0; my %res = (); foreach my $po_file (<$dir/*.po>) { my $array = Locale::PO->load_file_asarray( $po_file, "utf-8" ); $res{$po_file} = 0; my $size = 0; foreach my $entry ( splice @$array, 1 ) { next if $entry->obsolete; next if $entry->reference && $entry->reference =~ /NOT FOUND IN SOURCE/; $size++; next unless length $entry->dequote( $entry->msgstr ); $res{$po_file}++; } $max = $size if $max < $size; } my $width = length($max); foreach my $po_file ( sort { $res{$b} <=> $res{$a} } keys %res ) { my $tr = $res{$po_file}; my $perc = int($tr*1000/$max)/10; printf "%-20s %${width}d/%${width}d (%.1f%%)\n", "$po_file:", $tr, $max, $perc; } } sub clean { my $dir = shift || PO_DIR; foreach my $po_file (<$dir/*.po>) { my $array = Locale::PO->load_file_asarray( $po_file, "utf-8" ); foreach my $entry ( splice @$array, 1 ) { # Replace identical translations with the empty string $entry->msgstr("") if $entry->msgstr eq $entry->msgid; # Skip NOT FOUND IN SOURCE entries next if $entry->obsolete; next if $entry->reference && $entry->reference =~ /NOT FOUND IN SOURCE/; push @$array, $entry; } Locale::PO->save_file_fromarray($po_file, $array, "utf-8"); } } sub rosetta { my $url = shift or die 'must provide Rosetta download url or directory with new po files'; my $dir; if ( $url =~ m{^[a-z]+://} ) { $dir = tempdir(); my ($fname) = $url =~ m{([^/]+)$}; print "Downloading $url\n"; require LWP::Simple; LWP::Simple::getstore($url => "$dir/$fname"); print "Extracting $dir/$fname\n"; require Archive::Extract; my $ae = Archive::Extract->new(archive => "$dir/$fname"); my $ok = $ae->extract( to => $dir ); } elsif ( -e $url && -d _ ) { $dir = $url; } else { die "Is not URL or directory: '$url'"; } my @files = ( <$dir/*/*/*.po>, <$dir/*/*.po>, <$dir/*.po> ); unless ( @files ) { print STDERR "No files in $dir/rt/*.po and $dir/*.po\n"; exit; } for my $file ( @files ) { my ($lang) = $file =~ m/([\w_]+)\.po/; my $fn_orig = PO_DIR . "/$lang.po"; my $load_from = $fn_orig; $load_from = PO_DIR . "/rt.pot" unless -e $load_from; my $orig = Locale::PO->load_file_ashash( $fn_orig, "utf-8" ); print "$file -> $fn_orig\n"; my $rosetta = Locale::PO->load_file_asarray( $file, "utf-8" ); # We're merging in the current hash as fallbacks for the rosetta hash my $translated = 0; foreach my $entry ( splice @$rosetta, 1 ) { # Skip no longer in source entries next if $entry->obsolete; next if $entry->reference && $entry->reference =~ /NOT FOUND IN SOURCE/; # Update to what the old po file had, if we have nothing my $oldval = $orig->{$entry->msgid}; if (not length $entry->dequote($entry->msgstr) and $oldval) { $entry->msgstr($oldval->dequote($oldval->msgstr)); } # Replace identical translations with the empty string $entry->msgstr("") if $entry->msgstr eq $entry->msgid; # Drop "fuzzy" information $entry->fuzzy_msgctxt(undef); $entry->fuzzy_msgid(undef); $entry->fuzzy_msgid_plural(undef); $translated++ if length $entry->dequote($entry->msgstr); push @$rosetta, $entry; } my $perc = int($translated/(@$rosetta - 1) * 100 + 0.5); if ( $perc < BOUNDARY and $lang !~ /^en(_[A-Z]{2})?$/) { unlink $fn_orig; next; } Locale::PO->save_file_fromarray($fn_orig, $rosetta, "utf-8"); } extract(); } sub extract { system($^X, 'devel/tools/extract-message-catalog', @_); } rt-4.4.7/devel/tools/rt-attributes-editor0000755000076500000240000000731514514237602020035 0ustar sunnavystaff#!/usr/bin/env perl # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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 Term::EditorEdit; use RT::Interface::CLI qw(Init); my ($key, $id); Init('key|k=s' => \$key, 'id=i' => \$id); Pod::Usage::pod2usage({ verbose => 2 }) unless $id; require RT::Attribute; my $attr = RT::Attribute->new( RT->SystemUser ); $attr->Load( $id ); unless ( $attr->id ) { print STDERR "Couldn't load attribute #$id\n"; exit 1; } my $orig; if ($key) { if (ref($attr->Content) ne 'HASH') { print STDERR "The attribute's content must be a hashref for editing keys\n"; exit 1; } $orig = $attr->Content->{$key} || ''; } else { use Data::Dumper; $orig = Dumper( $attr->Content ); } my $edit = Term::EditorEdit->edit(document => $orig); if ($edit ne $orig) { if ($key) { my $content = $attr->Content; $content->{$key} = $edit; $attr->SetContent($content); print "Attribute key saved.\n"; } else { my $VAR1; eval $edit; if ($@) { print STDERR "Your change had an error: $@"; exit 1; } $attr->SetContent($VAR1); print "Attribute saved.\n"; } } else { print "Aborted.\n" } __END__ =head1 NAME rt-attributes-editor - edit the content of an attribute =head1 SYNOPSIS # edit the Perl dump of attribute 2's content rt-attributes-editor --id 2 # edit the dump of attribute 2's content (hash key: Query) # note: this will error if the attribute content is not a hashref rt-attributes-editor --id 2 --key Query =head1 DESCRIPTION This script deserializes and puts the content of an attribute defined by into the preferred editor set in C<$EDITOR>. May be useful for developers to editing attributes by hand if there is any trouble editing it from the UI. rt-4.4.7/devel/tools/rt-static-docs0000755000076500000240000001615614514237602016603 0ustar sunnavystaff#!/usr/bin/env perl # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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 Getopt::Long; use File::Temp; use File::Spec; use File::Path qw(make_path rmtree); use File::Copy qw(copy); use Encode qw(decode_utf8); use HTML::Entities qw(encode_entities); use List::Util qw(first); use RT::Pod::HTMLBatch; my %opts; GetOptions( \%opts, "help|h", "source=s", "to=s", "extension=s", ); if ( $opts{'help'} ) { require Pod::Usage; print Pod::Usage::pod2usage( -verbose => 2 ); exit; } die "--to=DIRECTORY is required\n" unless $opts{to}; $opts{to} = File::Spec->rel2abs($opts{to}); make_path( $opts{to} ) unless -e $opts{to}; die "--to MUST be a directory\n" unless -d $opts{to}; # Unpack the tarball, if that's what we're given. my $tmpdir; if (($opts{source} || '') =~ /\.tar\.gz$/ and -f $opts{source}) { $tmpdir = File::Temp->newdir(); system("tar", "xzpf", $opts{source}, "-C", $tmpdir); $opts{source} = first { -d $_ } <$tmpdir/*>; die "Can't find directory to chdir into after unpacking tarball" unless $opts{source}; } chdir $opts{source} if $opts{source}; my @dirs = ( qw( docs etc lib bin sbin devel/tools local/lib local/sbin local/bin ), glob("local/plugins/*/{lib,sbin,bin}"), glob("docs/UPGRADING*"), ); my $converter = RT::Pod::HTMLBatch->new; sub generate_configure_help { my $configure = shift; my $help = `./$configure --help`; my $dest = "$opts{to}/configure.html"; if ($help and open my $html, ">", $dest) { print $html join "\n", "
", encode_entities($help), "
", "\n"; close $html; $converter->note_for_contents_file(["configure options"], $configure, $dest); } else { warn "Can't open $dest: $!"; } } # Generate a page for ./configure --help if we can if (-x "configure.ac" and -d ".git") { rmtree("autom4te.cache") if -d "autom4te.cache"; generate_configure_help("configure.ac"); } elsif (-x "configure") { generate_configure_help("configure"); } else { warn "Unable to generate a page for ./configure --help!\n" unless $opts{extension}; } # Manually "convert" README* and 3.8-era UPGRADING* to HTML and push them into # the known contents. for my $file () { (my $name = $file) =~ s{^.+/}{}; my $dest = "$opts{to}/$name.html"; open my $source, "<", $file or warn "Can't open $file: $!", next; my $str = ""; $str .= encode_entities(decode_utf8($_)) while <$source>; close $source; $str = "
$str
"; $str =~ s{\bdocs/([a-z_-]+)\.pod\b}{docs/$1.pod}ig; $str =~ s{\betc/(RT_Config)\.pm\b}{etc/$1.pm}g; $str =~ s{\betc/(UPRGADING\.mysql)\b}{etc/$1}g; $str =~ s{\b(https?://(?!rt\.example\.com)[.a-z0-9/_:-]+(?$1}ig; $str =~ s{\b([\w-]+\@(lists\.)?bestpractical.com)\b}{$1}g; open my $html, ">", $dest or warn "Can't open $dest: $!", next; print $html $str; close $html; $converter->note_for_contents_file([$name], $file, $dest); } # Copy images into place make_path("$opts{to}/images/"); copy($_, "$opts{to}/images/") for ; # Temporarily set executable bits on upgrading doc to work around # Pod::Simple::Search limitation/bug: # https://rt.cpan.org/Ticket/Display.html?id=80082 sub system_chmod { system("chmod", @_) == 0 or die "Unable to chmod: $! (exit $?)"; } system_chmod("+x", $_) for ; # Convert each POD file to HTML $converter->batch_convert( \@dirs, $opts{to} ); # Run it again to make sure local links are linked correctly $converter->contents_file(undef); $converter->batch_convert( \@dirs, $opts{to} ); # Remove execution bit from workaround above system_chmod("-x", $_) for ; # Need to chdir back out, if we are in the tmpdir, to let it clean up chdir "/" if $tmpdir; exit 0; __END__ =head1 NAME rt-static-docs - generate doc shipped with RT =head1 SYNOPSIS rt-static-docs --to /path/to/output [--source /path/to/rt] =head1 DESCRIPTION RT ships with documentation (written in POD) embedded in library files, at the end of utility scripts, and in standalone files. This script finds all of that documentation, collects and converts it into a nice set of HTML files, and tops it off with a helpful index. Best Practical uses this to publish documentation under L. =head1 OPTIONS =over =item --to Set the destination directory for the output files. =item --source Set the RT base directory to search under. Defaults to the current working directory, which is fine if you're running this script as C. May also point to a tarball (a file ending in C<.tar.gz>) which will be unpacked into a temporary directory and used as the RT base directory. =item --extension=RTx::Foo Indicates when C<--source> is an RT extension, such as RT::IR. Takes an extension name for future use, but currently it only acts as a flag to suppress a warning about not finding ./configure. =item --help Print this help. =back =cut rt-4.4.7/devel/tools/rt-apache0000755000076500000240000003437314514237602015610 0ustar sunnavystaff#!/usr/bin/env perl # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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 Getopt::Long; use FindBin; use Pod::Usage; use File::Spec::Functions qw(rel2abs); my %opt = ( root => ($ENV{RTHOME} || "/opt/rt4"), fcgid => 0, fastcgi => 0, perl => 0, modules => "/usr/lib/apache2/modules", ); GetOptions( \%opt, "root=s", "rt3|3!", "fcgid!", "fastcgi!", "perl!", "port|p=i", "ssl:i", "single|X", "auth|A:s", "modules=s", "help|h|?", ) or pod2usage( 1 ); pod2usage( {verbose => 2} ) if $opt{help}; # All paths must be absolute $opt{$_} = rel2abs($opt{$_}) for qw(root modules); # Determine what module to use my $mod; if ($opt{fcgid} + $opt{fastcgi} + $opt{perl} > 1) { die "Can only supply one of fcgid, fastcgi, or perl\n"; } elsif ($opt{fcgid} + $opt{fastcgi} + $opt{perl} == 0) { my @guess = qw(fastcgi fcgid perl); @guess = grep {-f "$opt{modules}/mod_$_.so"} @guess; die "Neither mod_fcgid, mod_fastcgi, nor mod_perl are installed; aborting\n" unless @guess; warn "No deployment given -- assuming mod_$guess[0] deployment\n"; $mod = $guess[0]; } else { $mod = (grep {$opt{$_}} qw(fastcgi fcgid perl))[0]; } # Sanity check that the root contains an RT install die "$opt{root} doesn't look like an RT install\n" unless -e "$opt{root}/lib/RT.pm"; # Detect if we are actually rt3 if (not -e "$opt{root}/sbin/rt-server.fcgi" and -e "$opt{root}/bin/mason_handler.fcgi") { $opt{rt3}++; warn "RT3 install detected!\n"; } # Parse etc/RT_SiteConfig.pm for the default port my $RTCONF; $opt{port} ||= parseconf( "WebPort" ); unless ($opt{port}) { warn "Defaulting to port 8888\n"; $opt{port} = 8888; } # Set ssl port if they want it but didn't provide a number $opt{ssl} = 4430 if defined $opt{ssl} and not $opt{ssl}; # Default auth to on if they set $WebRemoteUserAuth $opt{auth} = '' if not exists $opt{auth} and parseconf( "WebRemoteUserAuth" ); # Set an auth path if they want it but didn't pass a path if (defined $opt{auth} and not $opt{auth}) { $opt{auth} = "$opt{root}/var/htpasswd"; unless (-f $opt{auth}) { open(my $fh, ">", $opt{auth}) or die "Can't create default htpasswd: $!"; print $fh 'root:$apr1$TZA4Y0DL$DS5ZhDH8QrhB.uAtvNJmh.' . "\n"; close $fh or die "Can't create default htpasswd: $!"; } } elsif ($opt{auth} and not -f $opt{auth}) { die "Can't read htpasswd file $opt{auth}!"; } # Parse out the WebPath my $path = parseconf( "WebPath" ) || ""; my $template = join("", ); $template =~ s/\$PORT/$opt{port}/g; $template =~ s!\$PATH/!$path/!g; $template =~ s!\$PATH!$path || "/"!ge; $template =~ s/\$SSL/$opt{ssl} || 0/ge; $template =~ s/\$AUTH/$opt{auth}/ge; $template =~ s/\$RTHOME/$opt{root}/g; $template =~ s/\$MODULES/$opt{modules}/g; $template =~ s/\$TOOLS/$FindBin::Bin/g; $template =~ s/\$PROCESSES/$opt{single} ? 1 : 3/ge; my $conf = "$opt{root}/var/apache.conf"; open(CONF, ">", $conf) or die "Can't write $conf: $!"; print CONF $template; close CONF; my @opts = ("-f", $conf, "-D" . uc($mod) ); push @opts, "-DSSL" if $opt{ssl}; push @opts, "-DRT3" if $opt{rt3}; push @opts, "-DSINGLE" if $opt{single}; push @opts, "-DREDIRECT" if $path; push @opts, "-DAUTH" if $opt{auth}; # Wait for a previous run to terminate if ( open( PIDFILE, "<", "$opt{root}/var/apache2.pid") ) { my $pid = ; chomp $pid; close PIDFILE; if ($pid and kill 0, $pid) { warn "Waiting for previous run (pid $pid) to finish...\n"; sleep 1 while kill 0, $pid; } } # Clean out the log in preparation my $log = "$opt{root}/var/log/apache-error.log"; unlink($log); # Start 'er up warn "Starting apache server on http://localhost:$opt{port}$path/" . ($opt{ssl} ? " and https://localhost:$opt{ssl}$path/" : "") . "\n"; !system("apache2", @opts, "-k", "start") or die "Can't exec apache2: $@"; # Ignore the return value, as we expect it to be ^C'd system("tail", "-f", $log); warn "Shutting down apache...\n"; !system("apache2", @opts, "-k", "stop") or die "Can't exec apache2: $@"; sub parseconf { my ($optname) = @_; # We're going to be evil, and try to parse the config unless (defined $RTCONF) { unless ( open(CONF, "<", "$opt{root}/etc/RT_SiteConfig.pm") ) { warn "Can't open $opt{root}/etc/RT_SiteConfig.pm: $!\n"; $RTCONF = ""; return; } $RTCONF = join("", ); close CONF; } return unless $RTCONF =~ /^\s*Set\(\s*\$$optname\s*(?:,|=>)\s*['"]?(.*?)['"]?\s*\)/m; return $1; } =head1 NAME rt-apache - Wrapper to start Apache running RT =head1 DESCRIPTION This script exists to make it easier to run RT under Apache for testing. It is not intended as a way to deploy RT, or to provide example Apache configuration for RT. For instructions on how to deploy RT with Apache, please read the provided F file. Running this script will start F with a custom-built configuration file, built based on command-line options and the contents of your F. It will work with either RT 3.8.x or RT 4.0.x. As it is primarily for simple testing, it runs Apache as the current user. =head1 OPTIONS C will parse your F for its C and C configuration, and adjust its defaults accordingly. =over =item --root B The path to the RT install to serve. This defaults to the C environment variable, or C. =item --fastcgi, --fcgid, --perl Determines the Apache module which is used. By default, the first one of that list which exists will be used. See also L. =item --port B, -p Choses the port to listen on. By default, this is parsed from the F, and falling back to 8888. =item --ssl [B] Also listens on the provided port with HTTPS, using a self-signed certificate for C. If the port number is not specified, defaults to port 4430. =item --auth [F], -A Turns on HTTP Basic Authentication; this is done automatically if C<$WebRemoteUserAuth> is set in the F. The provided path should be to a F file; if not given, defaults to a file containing only user C with password C. =item --single, -X Run only one process or thread, for ease of debugging. =item --rt3, -3 Declares that the RT install in question is RT 3.8.x. C can usually detect this for you, however. =item --modules B The path to the Apache2 modules directory, which is expected to contain at least one of F, F, or F. Defaults to F. =back =cut __DATA__ Listen $PORT Listen $SSL ServerName localhost ServerRoot $RTHOME/var PidFile $RTHOME/var/apache2.pid LockFile $RTHOME/var/apache2.lock ServerAdmin root@localhost = 2.4> LoadModule mpm_prefork_module $MODULES/mod_mpm_prefork.so LoadModule authz_core_module $MODULES/mod_authz_core.so LoadModule authz_host_module $MODULES/mod_authz_host.so LoadModule env_module $MODULES/mod_env.so LoadModule alias_module $MODULES/mod_alias.so LoadModule mime_module $MODULES/mod_mime.so TypesConfig $TOOLS/mime.types StartServers 1 MinSpareServers 1 MaxSpareServers 1 MaxClients 1 MaxRequestsPerChild 0 StartServers 1 MinSpareThreads 1 MaxSpareThreads 1 ThreadLimit 1 ThreadsPerChild 1 MaxClients 1 MaxRequestsPerChild 0 LoadModule perl_module $MODULES/mod_perl.so LoadModule fastcgi_module $MODULES/mod_fastcgi.so LoadModule fcgid_module $MODULES/mod_fcgid.so LoadModule ssl_module $MODULES/mod_ssl.so = 2.4> LoadModule socache_shmcb_module $MODULES/mod_socache_shmcb.so LoadModule log_config_module $MODULES/mod_log_config.so ErrorLog "$RTHOME/var/log/apache-error.log" TransferLog "$RTHOME/var/log/apache-access.log" LogLevel notice Options FollowSymLinks AllowOverride None = 2.4> Require all denied Order deny,allow Deny from all AddDefaultCharset UTF-8 LoadModule rewrite_module $MODULES/mod_rewrite.so RewriteEngine on RewriteRule ^(?!\Q$PATH\E) - [R=404] = 2.4> LoadModule authn_core_module $MODULES/mod_authn_core.so LoadModule auth_basic_module $MODULES/mod_auth_basic.so LoadModule authn_file_module $MODULES/mod_authn_file.so LoadModule authz_user_module $MODULES/mod_authz_user.so Require valid-user AuthType basic AuthName "RT access" AuthBasicProvider file AuthUserFile $AUTH = 2.4> Require local Order deny,allow Deny from all Allow from localhost Satisfy any = 2.4> Require all granted Order allow,deny Allow from all ########## 4.0 mod_perl PerlSetEnv RT_SITE_CONFIG $RTHOME/etc/RT_SiteConfig.pm SetHandler modperl PerlResponseHandler Plack::Handler::Apache2 PerlSetVar psgi_app $RTHOME/sbin/rt-server use Plack::Handler::Apache2; Plack::Handler::Apache2->preload("$RTHOME/sbin/rt-server"); ########## 4.0 mod_fastcgi FastCgiIpcDir $RTHOME/var FastCgiServer $RTHOME/sbin/rt-server.fcgi -processes $PROCESSES -idle-timeout 300 ScriptAlias $PATH $RTHOME/sbin/rt-server.fcgi/ Options +ExecCGI AddHandler fastcgi-script fcgi ########## 4.0 mod_fcgid FcgidProcessTableFile $RTHOME/var/fcgid_shm FcgidIPCDir $RTHOME/var FcgidMaxRequestLen 1073741824 ScriptAlias $PATH $RTHOME/sbin/rt-server.fcgi/ Options +ExecCGI AddHandler fcgid-script fcgi ########## 3.8 mod_perl PerlSetEnv RT_SITE_CONFIG $RTHOME/etc/RT_SiteConfig.pm PerlRequire "$RTHOME/bin/webmux.pl" SetHandler default SetHandler perl-script PerlResponseHandler RT::Mason ########## 3.8 mod_fastcgi FastCgiIpcDir $RTHOME/var FastCgiServer $RTHOME/bin/mason_handler.fcgi -processes $PROCESSES -idle-timeout 300 ScriptAlias $PATH $RTHOME/bin/mason_handler.fcgi/ Options +ExecCGI AddHandler fastcgi-script fcgi ########## 3.8 mod_fcgid FcgidProcessTableFile $RTHOME/var/fcgid_shm FcgidIPCDir $RTHOME/var FcgidMaxRequestLen 1073741824 ScriptAlias $PATH $RTHOME/bin/mason_handler.fcgi/ Options +ExecCGI AddHandler fcgid-script fcgi SSLRandomSeed startup builtin SSLRandomSeed startup file:/dev/urandom 512 SSLRandomSeed connect builtin SSLRandomSeed connect file:/dev/urandom 512 SSLSessionCache shmcb:$RTHOME/var/ssl_scache(512000) SSLMutex file:$RTHOME/var/ssl_mutex SSLEngine on SSLCertificateFile $TOOLS/localhost.crt SSLCertificateKeyFile $TOOLS/localhost.key rt-4.4.7/devel/tools/mime.types0000644000076500000240000000022314514237602016016 0ustar sunnavystaff# This is a mime.types for only the file types which we serve # statically (those that Apache might care about). image/gif gif image/png png rt-4.4.7/devel/tools/localhost.key0000644000076500000240000000321314514237602016505 0ustar sunnavystaff-----BEGIN RSA PRIVATE KEY----- MIIEogIBAAKCAQEAqiQrmwAps2QkOg4icNCnBHEMN9qdcpRHswrvrUbaBaZSnHL6 GVSfjntUuAMqAOmzeBJSTTHeuxdwzGmxIND4TfuTPkOds8ZIZ2JLm0kPpWhDY2wx ++tjWlkVY2/zAe5zgwO1K3GwjG9keOqC5Z5VSxcspJ8YQxND8xspufh3/sVJGlAO 6bajcCzEeLhVutJpZdGAJ9w+XBqg5JzveUIPSS5ciXrrA7wMFA6qoG2S5Ln0Fwf5 yrVPnDArMx0k1+lHHRNchIZRrgNJtOOp8AAl7hPNq8fvc+A2NcGXr4awZvn+l/xl p363rbFFyhwN85Dl7YBKYiewO2VJfSl8t8EeDQIDAQABAoIBAGeZsrulM786QRzg snQDeU+pDomMIsc8JxSMmjjmpac/CZqeIFAASU/XJVUPCCqaI1//uAGtVjSSJ2sx CFw1Ip1JjPUi8woeuMPLBMK/kDll7XLC1QTS5iKDkBSGfHA2pDuorE6R4bEBuyot khsDeGhK6jIrdfiR6JRFe/jzpQ2KUV7PDKhcGjWdCCGoss7s2d0Gx4UdlYn456Dr atPLXU9Aspg7uIUSO44Zwal03k25S0EW4WjdFCx3+1WqXs8l+XNXlqowZSL8qjOy cL2H5bpElE+NjSsHtTZdzC8jcDhbIRp8cZD32t+BRY5gqodKw+Z3MmblL2b3/qPi xNMaq8ECgYEA3ACjPUhRb7kYMgmowOXR/HL9Aht+4uCM+UM/pz0S4rn4MooBuCwv Nc0oFi5wFNJpFsOsiJwik7re1/olPPneZWgZWgBoiQl4+OB5hzvLc56B9Ez3Z84X 19BxKcUaf5gXjxVAAAeKxn8ZbL/OHB3WvYP4zsIO1J+ijOe2LZJFEpUCgYEAxfr4 RsK8avAdgOC0e/uB007rtiErCIaVnK/1WMPwWb5FxDkkl31MTB6oLO/JU5zfCsE1 ROtnehB69c73sokWzAqMCuVFs+M0Owq1Kdm63b1k0wtUZL7v3wfGoUgZFL/65LDg RQ2Grntul5H7XS9c9v7Tn9GSo8VIbej6fvPPN5kCgYAqbL0N7ko1/z2ZOJ+gQzFR O2Nq6p53ZdIJp1w5BeAEdNRV+qMGPw8DkwJt9JqMiV7WkvlMhr9sOZcLkyNnNNAc QgzRfE6sTnVTmQYWfANp0mFBGS6EiAu1BG8uHOJVRKEWaISk/M9YI95lSD+Y0HA+ r5plVKrDed1AytYox5ImWQKBgC/VNQsTnaZQoTA0GiciWvmMxdJZLSaALcGPmb16 iaWFHSINlFOtiDOT7Jn+zSuQaSsWByLBpVyOgsbE3H+cM4/UtIUlY7PUnxfsvFyC KG3Ohn+e6yL0JsxB+rGY08Z5o8qBGY5VeEbLt6qTMKIRAWsDommonr9GuPslIPBv Q49xAoGAI7LBHEJtPTJx56EcKicST++NzUYha7E8nkqogs9oTTpdT6n+viHDCNud YUUK2slnEvgOPtNEkf1kHTqcajKZmIVpQi1cZqKzPCgk49JM+2OU+98qFR8UKe8i s5t09zDVhy9Hy+MaASqbU1AQT9bWbyfsgormjQ5jzadDdP5zovE= -----END RSA PRIVATE KEY----- rt-4.4.7/devel/tools/css_tidy0000755000076500000240000000450714514237602015561 0ustar sunnavystaff#!/bin/bash # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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 }}} set -e tmpfile=$(mktemp) curl -F css=@$1 -F source=file \ -F property_formatting=newline -F braces=default -F indent_size=4 \ -F blank_line_rules_chk=1 -F blank_line_rules=1 \ -F safe_chk=1 -F safe=1 \ http://procssor.com/process > $tmpfile xml_grep --text_only '*/textarea[@id="download_me"]' --html $tmpfile | expand -t4 | tail -n +2 > $1 rt-4.4.7/devel/tools/extract-message-catalog0000755000076500000240000001257314514237602020446 0ustar sunnavystaff#!/usr/bin/env perl # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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 }}} # Portions Copyright 2002 Autrijus Tang use strict; use warnings; use open qw/ :std :encoding(UTF-8) /; use Locale::PO; use lib 'lib'; use RT::I18N::Extract; $| = 1; # po dir is for extensions @ARGV = (, , , ) unless @ARGV; # extract all strings and stuff them into %POT # scan html dir for extensions my $extract = RT::I18N::Extract->new; our %POT = $extract->all; print "$_\n" for $extract->errors; # update all language dictionaries foreach my $dict (@ARGV) { $dict = "share/po/$dict.pot" if ( $dict eq 'rt' ); $dict = "share/po/$dict.po" unless -f $dict or $dict =~ m!/!; my $lang = $dict; $lang =~ s|.*/||; $lang =~ s|\.po$||; $lang =~ s|\.pot$||; update($lang, $dict); } sub uniq { my %seen; return grep { !$seen{$_}++ } @_; } sub update { my $lang = shift; my $file = shift; unless (!-e $file or -w $file) { warn "Can't write to $lang, skipping...\n"; return; } my $is_english = ( $lang =~ /^en(?:[^A-Za-z]|$)/ ); print "Updating $lang"; my $lexicon = Locale::PO->load_file_ashash( $file, "utf-8" ); # Default to the empty string for new ones $lexicon->{$_->msgid} ||= $_ for values %POT; my $errors = 0; for my $msgid ( keys %{$lexicon} ) { my $entry = $lexicon->{$msgid}; # Don't output empty translations for english if (not length $entry->dequote($entry->msgstr) and $is_english) { delete $lexicon->{$msgid}; next; } # The PO properties at the top are always fine to leave as-is next if not length $entry->dequote($msgid); # Not found in source? Drop it my $source = $POT{$msgid}; if (not $source) { delete $lexicon->{$msgid}; next; } # Pull in the properties from the source $entry->reference( $source->reference ); $entry->automatic( $source->automatic ); my $fail = validate_msgstr($lang, map {$entry->dequote($_)} $entry->msgid, $entry->msgstr); next unless $fail; print "\n" unless $errors++; print $fail; } my @order = map {$_->[0]} sort {$a->[1] cmp $b->[1]} map {[$_, $_->dequote($_->msgid)]} values %{$lexicon}; Locale::PO->save_file_fromarray($file, \@order, "utf-8") or die "Couldn't update '$file': $!"; if ($errors) { print "\n"; } else { print "\r", " "x100, "\r"; } return 1; } sub validate_msgstr { my $lang = shift; my $msgid = shift; my $msgstr = shift; return if not defined $msgstr or $msgstr eq ''; # no translation for this string # we uniq because a string can use a placeholder more than once # (eg %1 %quant(%1, ...) like in our czech localization my @expected_variables = uniq($msgid =~ /%\d+/g); my @got_variables = uniq($msgstr =~ /%\d+/g); # this catches the case where expected uses %1,%2 and got uses %1,%3 # unlike a simple @expected_variables == @got_variables my $expected = join ", ", sort @expected_variables; my $got = join ", ", sort @got_variables; return if $expected eq $got; return " expected (" . $expected . ") in msgid: $msgid\n" . " got (" . $got . ") in msgstr: $msgstr\n"; } rt-4.4.7/devel/tools/license_tag0000755000076500000240000002172514514237602016216 0ustar sunnavystaff#!/usr/bin/env perl # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (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; my $LICENSE = <<'EOL'; COPYRIGHT: This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC (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. EOL use File::Find; my @MAKE = qw(Makefile); File::Find::find({ no_chdir => 1, wanted => \&tag_pm}, 'lib'); for my $masondir (qw( html share/html )) { next unless -d $masondir; File::Find::find({ no_chdir => 1, wanted => \&tag_mason}, $masondir); } for my $bindir (qw( sbin bin etc/upgrade devel/tools )) { next unless -d $bindir; File::Find::find({ no_chdir => 1, wanted => \&tag_script}, $bindir); } tag_makefile ('Makefile.in') if -f 'Makefile.in'; tag_makefile ('README'); sub tag_mason { my $pm = $_; return unless (-f $pm); return if $pm =~ /\.(?:png|jpe?g|gif)$/; open( FILE, '<', $pm ) or die "Failed to open $pm"; my $file = (join "", ); close (FILE); print "$pm - "; return if another_license($pm => $file) && print "has different license\n"; my $pmlic = $LICENSE; $pmlic =~ s/^/%# /mg; $pmlic =~ s/\s*$//mg; if ($file =~ /^%# BEGIN BPS TAGGED BLOCK \{\{\{/ms) { print "has license section"; $file =~ s/^%# BEGIN BPS TAGGED BLOCK \{\{\{(.*?)%# END BPS TAGGED BLOCK \}\}\}/%# BEGIN BPS TAGGED BLOCK {{{\n$pmlic\n%# END BPS TAGGED BLOCK }}}/ms; } else { print "no license section"; $file ="%# BEGIN BPS TAGGED BLOCK {{{\n$pmlic\n%# END BPS TAGGED BLOCK }}}\n". $file; } $file =~ s/%# END BPS TAGGED BLOCK \}\}\}(\n+)/%# END BPS TAGGED BLOCK }}}\n/mg; print "\n"; open( FILE, '>', $pm ) or die "couldn't write new file"; print FILE $file; close FILE; } sub tag_makefile { my $pm = shift; open( FILE, '<', $pm ) or die "Failed to open $pm"; my $file = (join "", ); close (FILE); print "$pm - "; return if another_license($pm => $file) && print "has different license\n"; my $pmlic = $LICENSE; $pmlic =~ s/^/# /mg; $pmlic =~ s/\s*$//mg; if ($file =~ /^# BEGIN BPS TAGGED BLOCK \{\{\{/ms) { print "has license section"; $file =~ s/^# BEGIN BPS TAGGED BLOCK \{\{\{(.*?)# END BPS TAGGED BLOCK \}\}\}/# BEGIN BPS TAGGED BLOCK {{{\n$pmlic\n# END BPS TAGGED BLOCK }}}/ms; } else { print "no license section"; $file ="# BEGIN BPS TAGGED BLOCK {{{\n$pmlic\n# END BPS TAGGED BLOCK }}}\n". $file; } $file =~ s/# END BPS TAGGED BLOCK \}\}\}(\n+)/# END BPS TAGGED BLOCK }}}\n/mg; print "\n"; open( FILE, '>', $pm ) or die "couldn't write new file"; print FILE $file; close FILE; } sub tag_pm { my $pm = $_; return unless $pm =~ /\.pm/s; open( FILE, '<', $pm ) or die "Failed to open $pm"; my $file = (join "", ); close (FILE); print "$pm - "; return if another_license($pm => $file) && print "has different license\n"; my $pmlic = $LICENSE; $pmlic =~ s/^/# /mg; $pmlic =~ s/\s*$//mg; if ($file =~ /^# BEGIN BPS TAGGED BLOCK \{\{\{/ms) { print "has license section"; $file =~ s/^# BEGIN BPS TAGGED BLOCK \{\{\{(.*?)# END BPS TAGGED BLOCK \}\}\}/# BEGIN BPS TAGGED BLOCK {{{\n$pmlic\n# END BPS TAGGED BLOCK }}}/ms; } else { print "no license section"; $file ="# BEGIN BPS TAGGED BLOCK {{{\n$pmlic\n# END BPS TAGGED BLOCK }}}\n". $file; } $file =~ s/# END BPS TAGGED BLOCK \}\}\}(\n+)/# END BPS TAGGED BLOCK }}}\n\n/mg; print "\n"; open( FILE, '>', $pm ) or die "couldn't write new file $pm"; print FILE $file; close FILE; } sub tag_script { my $pm = $_; return unless (-f $pm); open( FILE, '<', $pm ) or die "Failed to open $pm"; my $file = (join "", ); close (FILE); print "$pm - "; return if another_license($pm => $file) && print "has different license\n"; my $pmlic = $LICENSE; $pmlic =~ s/^/# /msg; $pmlic =~ s/\s*$//mg; if ($file =~ /^# BEGIN BPS TAGGED BLOCK \{\{\{/ms) { print "has license section"; $file =~ s/^# BEGIN BPS TAGGED BLOCK \{\{\{(.*?)# END BPS TAGGED BLOCK \}\}\}/# BEGIN BPS TAGGED BLOCK {{{\n$pmlic\n# END BPS TAGGED BLOCK }}}/ms; } else { print "no license section"; if ($file =~ /^(#!.*?)\n/) { my $lic ="# BEGIN BPS TAGGED BLOCK {{{\n$pmlic\n# END BPS TAGGED BLOCK }}}\n"; $file =~ s/^(#!.*?)\n/$1\n$lic/; } } $file =~ s/# END BPS TAGGED BLOCK \}\}\}(\n+)/# END BPS TAGGED BLOCK }}}\n/mg; print "\n"; open( FILE, '>', $pm ) or die "couldn't write new file"; print FILE $file; close FILE; } sub another_license { my $name = shift; my $file = shift; return 1 if ($name =~ /(?:ckeditor|scriptaculous|superfish|tablesorter|farbtastic)/i); return 0 if $file =~ /Copyright\s+\(c\)\s+\d\d\d\d-\d\d\d\d Best Practical Solutions/i; return 1 if $file =~ /\b(copyright|GPL|Public Domain)\b/i; # common return 1 if $file =~ /\(c\)\s+\d\d\d\d(?:-\d\d\d\d)?/i; # prototype return 0; } rt-4.4.7/devel/docs/0000755000076500000240000000000014514237602013574 5ustar sunnavystaffrt-4.4.7/devel/docs/UPGRADING-4.20000644000076500000240000000366614514237602015353 0ustar sunnavystaff=head1 UPGRADING FROM RT 4.0.0 and greater This documentation notes internals changes between the 4.0 and 4.2 series that are primarily of interest to developers writing extensions or local customizations. It is not an exhaustive list. =over =item * The link direction and type maps are consolidated into RT::Link. If you wrote local customizations or extensions utilizing C<%RT::Ticket::LINKDIRMAP>, C<%RT::Ticket::LINKTYPEMAP>, CLINKDIRMAP>, CLINKTYPEMAP>, or C<%RT::Record::LINKDIRMAP>, you will need to switch to C<%RT::Link::DIRMAP> and C<%RT::Link::TYPEMAP>. =item * MakeClicky handlers added via a callback are now passed an "object" key in the parameter hash instead of "ticket". The object may be any L subclass. =item * C handlers (C) have moved out of Mason components and into C methods. Any custom username formats will need to be reimplemented as C methods. Renaming should follow that of the core components: /Elements/ShowUserConcise => RT::User->_FormatUserConcise /Elements/ShowUserVerbose => RT::User->_FormatUserVerbose The C<_FormatUser*> methods are passed a hash containing the keys C and C
, which have the same properties as before. =item * CSS is no longer processed through Mason; it's served by a proper static file handler. If you used the C or C callbacks of C in the aileron, web2, or ballard themes, you should transition to the L config option. If you need to target specific themes, you can use the class set on the ECE element (for example: body.aileron). See F for more information on custom styles. =item * The C and C methods on L, L, L, L, and L were refactored into L. =back rt-4.4.7/devel/docs/UPGRADING-4.40000644000076500000240000000606114514237602015345 0ustar sunnavystaff=head1 UPGRADING FROM RT 4.2.0 and greater This documentation notes internals changes between the 4.2 and 4.4 series that are primarily of interest to developers writing extensions or local customizations. It is not an exhaustive list. =over =item * The mailgate has been completely redesigned in a backwards-incompatible way. See F. =item * jQuery has been updated from 1.9.1 to 1.11.3 and jQuery UI from 1.10.0 to 1.11.4. https://jqueryui.com/upgrade-guide/1.10/ https://jqueryui.com/upgrade-guide/1.11/ =item * Customizations which link to attachments should take L into account. See L and F for an example. =item * We've added support for more object types in C-like files: C<@Classes>, C<@Categories>, C<@CustomRoles>, etc. =item * We've added a L method for displaying groups in the UI, as L was not overrideable. =item * We've dropped a number of unused fields: For Users, we've dropped C, C, C, C, C, C, and C. (Note: GPG keys have always been stored in attributes on the User record in RT, never in the C column) For Tickets, we've dropped C, C, and C. For Groups, we've dropped C. Use L. For Principals, we've dropped C. Use L. =item * The JOIN from tickets to watcher groups has changed from INNER to LEFT to support lazily-created watcher groups for custom roles. =item * Previously we've called the L method on classes, like so: RT::Queue->Roles Now that custom roles can be applied to individual objects it's important to switch such cases to $QueueObj->Roles (C<< RT::Queue->Roles >> will continue to function, but may include roles that are not applied to the specific queue you're dealing with) =item * The C page was moved to C for consistency. Callbacks may need to be adjusted. =item * TicketSQL now supports C and C. =item * We removed the C, C, and C columns from the Queues table. In their stead we have a more general C<< ->DefaultValue >> call, e.g. $queue->DefaultValue('InitialPriority') $queue->DefaultValue('FinalPriority') $queue->DefaultValue('Due') Note that "Due" can now be anything that can be parsed as a date. With this, we've also added the ability to add default values for "Starts" and custom fields. All of them may be set on a queue's DefaultValues admin page. =item * You can now split settings from F into separate files under an F directory. All files ending in C<.pm> will be parsed, in alphabetical order, after the main F is loaded. You also no longer need the C<1;> at the end of site config files. =back rt-4.4.7/devel/docs/UPGRADING-4.00000644000076500000240000000127414514237602015342 0ustar sunnavystaff=head1 UPGRADING FROM BEFORE 4.0.0 This documentation notes internals changes between the 3.8 and 4.0 series that are primarily of interest to developers writing extensions or local customizations. It is not an exhaustive list. =over =item * The deprecated classes RT::Action::Generic, RT::Condition::Generic and RT::Search::Generic have been removed, but you shouldn't have been using them anyway. You should have been using RT::Action, RT::Condition and RT::Search, respectively. =item * The menu system has been reworked significantly; the C and C callbacks in C, along with L, comprise the new menuing hooks. =back =cut rt-4.4.7/install-sh0000755000076500000240000003246414514237602013562 0ustar sunnavystaff#!/bin/sh # install - install a program, script, or datafile scriptversion=2006-12-25.00 # This originates from X11R5 (mit/util/scripts/install.sh), which was # later released in X11R6 (xc/config/util/install.sh) with the # following copyright and license. # # Copyright (C) 1994 X Consortium # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to # deal in the Software without restriction, including without limitation the # rights to use, copy, modify, merge, publish, distribute, sublicense, and/or # sell copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # X CONSORTIUM BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNEC- # TION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # # Except as contained in this notice, the name of the X Consortium shall not # be used in advertising or otherwise to promote the sale, use or other deal- # ings in this Software without prior written authorization from the X Consor- # tium. # # # FSF changes to this file are in the public domain. # # Calling this script install-sh is preferred over install.sh, to prevent # `make' implicit rules from creating a file called install from it # when there is no Makefile. # # This script is compatible with the BSD install script, but was written # from scratch. nl=' ' IFS=" "" $nl" # set DOITPROG to echo to test this script # Don't use :- since 4.3BSD and earlier shells don't like it. doit=${DOITPROG-} if test -z "$doit"; then doit_exec=exec else doit_exec=$doit fi # Put in absolute file names if you don't have them in your path; # or use environment vars. chgrpprog=${CHGRPPROG-chgrp} chmodprog=${CHMODPROG-chmod} chownprog=${CHOWNPROG-chown} cmpprog=${CMPPROG-cmp} cpprog=${CPPROG-cp} mkdirprog=${MKDIRPROG-mkdir} mvprog=${MVPROG-mv} rmprog=${RMPROG-rm} stripprog=${STRIPPROG-strip} posix_glob='?' initialize_posix_glob=' test "$posix_glob" != "?" || { if (set -f) 2>/dev/null; then posix_glob= else posix_glob=: fi } ' posix_mkdir= # Desired mode of installed file. mode=0755 chgrpcmd= chmodcmd=$chmodprog chowncmd= mvcmd=$mvprog rmcmd="$rmprog -f" stripcmd= src= dst= dir_arg= dst_arg= copy_on_change=false no_target_directory= usage="\ Usage: $0 [OPTION]... [-T] SRCFILE DSTFILE or: $0 [OPTION]... SRCFILES... DIRECTORY or: $0 [OPTION]... -t DIRECTORY SRCFILES... or: $0 [OPTION]... -d DIRECTORIES... In the 1st form, copy SRCFILE to DSTFILE. In the 2nd and 3rd, copy all SRCFILES to DIRECTORY. In the 4th, create DIRECTORIES. Options: --help display this help and exit. --version display version info and exit. -c (ignored) -C install only if different (preserve the last data modification time) -d create directories instead of installing files. -g GROUP $chgrpprog installed files to GROUP. -m MODE $chmodprog installed files to MODE. -o USER $chownprog installed files to USER. -s $stripprog installed files. -t DIRECTORY install into DIRECTORY. -T report an error if DSTFILE is a directory. Environment variables override the default commands: CHGRPPROG CHMODPROG CHOWNPROG CMPPROG CPPROG MKDIRPROG MVPROG RMPROG STRIPPROG " while test $# -ne 0; do case $1 in -c) ;; -C) copy_on_change=true;; -d) dir_arg=true;; -g) chgrpcmd="$chgrpprog $2" shift;; --help) echo "$usage"; exit $?;; -m) mode=$2 case $mode in *' '* | *' '* | *' '* | *'*'* | *'?'* | *'['*) echo "$0: invalid mode: $mode" >&2 exit 1;; esac shift;; -o) chowncmd="$chownprog $2" shift;; -s) stripcmd=$stripprog;; -t) dst_arg=$2 shift;; -T) no_target_directory=true;; --version) echo "$0 $scriptversion"; exit $?;; --) shift break;; -*) echo "$0: invalid option: $1" >&2 exit 1;; *) break;; esac shift done if test $# -ne 0 && test -z "$dir_arg$dst_arg"; then # When -d is used, all remaining arguments are directories to create. # When -t is used, the destination is already specified. # Otherwise, the last argument is the destination. Remove it from $@. for arg do if test -n "$dst_arg"; then # $@ is not empty: it contains at least $arg. set fnord "$@" "$dst_arg" shift # fnord fi shift # arg dst_arg=$arg done fi if test $# -eq 0; then if test -z "$dir_arg"; then echo "$0: no input file specified." >&2 exit 1 fi # It's OK to call `install-sh -d' without argument. # This can happen when creating conditional directories. exit 0 fi if test -z "$dir_arg"; then trap '(exit $?); exit' 1 2 13 15 # Set umask so as not to create temps with too-generous modes. # However, 'strip' requires both read and write access to temps. case $mode in # Optimize common cases. *644) cp_umask=133;; *755) cp_umask=22;; *[0-7]) if test -z "$stripcmd"; then u_plus_rw= else u_plus_rw='% 200' fi cp_umask=`expr '(' 777 - $mode % 1000 ')' $u_plus_rw`;; *) if test -z "$stripcmd"; then u_plus_rw= else u_plus_rw=,u+rw fi cp_umask=$mode$u_plus_rw;; esac fi for src do # Protect names starting with `-'. case $src in -*) src=./$src;; esac if test -n "$dir_arg"; then dst=$src dstdir=$dst test -d "$dstdir" dstdir_status=$? else # Waiting for this to be detected by the "$cpprog $src $dsttmp" command # might cause directories to be created, which would be especially bad # if $src (and thus $dsttmp) contains '*'. if test ! -f "$src" && test ! -d "$src"; then echo "$0: $src does not exist." >&2 exit 1 fi if test -z "$dst_arg"; then echo "$0: no destination specified." >&2 exit 1 fi dst=$dst_arg # Protect names starting with `-'. case $dst in -*) dst=./$dst;; esac # If destination is a directory, append the input filename; won't work # if double slashes aren't ignored. if test -d "$dst"; then if test -n "$no_target_directory"; then echo "$0: $dst_arg: Is a directory" >&2 exit 1 fi dstdir=$dst dst=$dstdir/`basename "$src"` dstdir_status=0 else # Prefer dirname, but fall back on a substitute if dirname fails. dstdir=` (dirname "$dst") 2>/dev/null || expr X"$dst" : 'X\(.*[^/]\)//*[^/][^/]*/*$' \| \ X"$dst" : 'X\(//\)[^/]' \| \ X"$dst" : 'X\(//\)$' \| \ X"$dst" : 'X\(/\)' \| . 2>/dev/null || echo X"$dst" | sed '/^X\(.*[^/]\)\/\/*[^/][^/]*\/*$/{ s//\1/ q } /^X\(\/\/\)[^/].*/{ s//\1/ q } /^X\(\/\/\)$/{ s//\1/ q } /^X\(\/\).*/{ s//\1/ q } s/.*/./; q' ` test -d "$dstdir" dstdir_status=$? fi fi obsolete_mkdir_used=false if test $dstdir_status != 0; then case $posix_mkdir in '') # Create intermediate dirs using mode 755 as modified by the umask. # This is like FreeBSD 'install' as of 1997-10-28. umask=`umask` case $stripcmd.$umask in # Optimize common cases. *[2367][2367]) mkdir_umask=$umask;; .*0[02][02] | .[02][02] | .[02]) mkdir_umask=22;; *[0-7]) mkdir_umask=`expr $umask + 22 \ - $umask % 100 % 40 + $umask % 20 \ - $umask % 10 % 4 + $umask % 2 `;; *) mkdir_umask=$umask,go-w;; esac # With -d, create the new directory with the user-specified mode. # Otherwise, rely on $mkdir_umask. if test -n "$dir_arg"; then mkdir_mode=-m$mode else mkdir_mode= fi posix_mkdir=false case $umask in *[123567][0-7][0-7]) # POSIX mkdir -p sets u+wx bits regardless of umask, which # is incompatible with FreeBSD 'install' when (umask & 300) != 0. ;; *) tmpdir=${TMPDIR-/tmp}/ins$RANDOM-$$ trap 'ret=$?; rmdir "$tmpdir/d" "$tmpdir" 2>/dev/null; exit $ret' 0 if (umask $mkdir_umask && exec $mkdirprog $mkdir_mode -p -- "$tmpdir/d") >/dev/null 2>&1 then if test -z "$dir_arg" || { # Check for POSIX incompatibilities with -m. # HP-UX 11.23 and IRIX 6.5 mkdir -m -p sets group- or # other-writeable bit of parent directory when it shouldn't. # FreeBSD 6.1 mkdir -m -p sets mode of existing directory. ls_ld_tmpdir=`ls -ld "$tmpdir"` case $ls_ld_tmpdir in d????-?r-*) different_mode=700;; d????-?--*) different_mode=755;; *) false;; esac && $mkdirprog -m$different_mode -p -- "$tmpdir" && { ls_ld_tmpdir_1=`ls -ld "$tmpdir"` test "$ls_ld_tmpdir" = "$ls_ld_tmpdir_1" } } then posix_mkdir=: fi rmdir "$tmpdir/d" "$tmpdir" else # Remove any dirs left behind by ancient mkdir implementations. rmdir ./$mkdir_mode ./-p ./-- 2>/dev/null fi trap '' 0;; esac;; esac if $posix_mkdir && ( umask $mkdir_umask && $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir" ) then : else # The umask is ridiculous, or mkdir does not conform to POSIX, # or it failed possibly due to a race condition. Create the # directory the slow way, step by step, checking for races as we go. case $dstdir in /*) prefix='/';; -*) prefix='./';; *) prefix='';; esac eval "$initialize_posix_glob" oIFS=$IFS IFS=/ $posix_glob set -f set fnord $dstdir shift $posix_glob set +f IFS=$oIFS prefixes= for d do test -z "$d" && continue prefix=$prefix$d if test -d "$prefix"; then prefixes= else if $posix_mkdir; then (umask=$mkdir_umask && $doit_exec $mkdirprog $mkdir_mode -p -- "$dstdir") && break # Don't fail if two instances are running concurrently. test -d "$prefix" || exit 1 else case $prefix in *\'*) qprefix=`echo "$prefix" | sed "s/'/'\\\\\\\\''/g"`;; *) qprefix=$prefix;; esac prefixes="$prefixes '$qprefix'" fi fi prefix=$prefix/ done if test -n "$prefixes"; then # Don't fail if two instances are running concurrently. (umask $mkdir_umask && eval "\$doit_exec \$mkdirprog $prefixes") || test -d "$dstdir" || exit 1 obsolete_mkdir_used=true fi fi fi if test -n "$dir_arg"; then { test -z "$chowncmd" || $doit $chowncmd "$dst"; } && { test -z "$chgrpcmd" || $doit $chgrpcmd "$dst"; } && { test "$obsolete_mkdir_used$chowncmd$chgrpcmd" = false || test -z "$chmodcmd" || $doit $chmodcmd $mode "$dst"; } || exit 1 else # Make a couple of temp file names in the proper directory. dsttmp=$dstdir/_inst.$$_ rmtmp=$dstdir/_rm.$$_ # Trap to clean up those temp files at exit. trap 'ret=$?; rm -f "$dsttmp" "$rmtmp" && exit $ret' 0 # Copy the file name to the temp name. (umask $cp_umask && $doit_exec $cpprog "$src" "$dsttmp") && # and set any options; do chmod last to preserve setuid bits. # # If any of these fail, we abort the whole thing. If we want to # ignore errors from any of these, just make sure not to ignore # errors from the above "$doit $cpprog $src $dsttmp" command. # { test -z "$chowncmd" || $doit $chowncmd "$dsttmp"; } && { test -z "$chgrpcmd" || $doit $chgrpcmd "$dsttmp"; } && { test -z "$stripcmd" || $doit $stripcmd "$dsttmp"; } && { test -z "$chmodcmd" || $doit $chmodcmd $mode "$dsttmp"; } && # If -C, don't bother to copy if it wouldn't change the file. if $copy_on_change && old=`LC_ALL=C ls -dlL "$dst" 2>/dev/null` && new=`LC_ALL=C ls -dlL "$dsttmp" 2>/dev/null` && eval "$initialize_posix_glob" && $posix_glob set -f && set X $old && old=:$2:$4:$5:$6 && set X $new && new=:$2:$4:$5:$6 && $posix_glob set +f && test "$old" = "$new" && $cmpprog "$dst" "$dsttmp" >/dev/null 2>&1 then rm -f "$dsttmp" else # Rename the file to the real destination. $doit $mvcmd -f "$dsttmp" "$dst" 2>/dev/null || # The rename failed, perhaps because mv can't rename something else # to itself, or perhaps because mv is so ancient that it does not # support -f. { # Now remove or move aside any old file at destination location. # We try this two ways since rm can't unlink itself on some # systems and the destination file might be busy for other # reasons. In this case, the final cleanup might fail but the new # file should still install successfully. { test ! -f "$dst" || $doit $rmcmd -f "$dst" 2>/dev/null || { $doit $mvcmd -f "$dst" "$rmtmp" 2>/dev/null && { $doit $rmcmd -f "$rmtmp" 2>/dev/null; :; } } || { echo "$0: cannot unlink or rename $dst" >&2 (exit 1); exit 1 } } && # Now rename the file to the real destination. $doit $mvcmd "$dsttmp" "$dst" } fi || exit 1 trap '' 0 fi done # Local variables: # eval: (add-hook 'write-file-hooks 'time-stamp) # time-stamp-start: "scriptversion=" # time-stamp-format: "%:y-%02m-%02d.%02H" # time-stamp-end: "$" # End: rt-4.4.7/configure.ac0000755000076500000240000004255614514237602014052 0ustar sunnavystaffautoconf; exec ./configure $@ dnl dnl Process this file with autoconf to produce a configure script dnl Setup autoconf AC_PREREQ([2.59]) AC_INIT(RT, m4_esyscmd([( git describe --tags || cat ./.tag 2> /dev/null || echo "rt-3.9.EXPORTED" )| tr -d "\n"]), [rt-bugs@bestpractical.com]) AC_CONFIG_SRCDIR([lib/RT.pm]) dnl Save our incant early since $@ gets overwritten by some macros. dnl ${ac_configure_args} is available later, but it's quoted differently dnl and undocumented. See http://www.spinics.net/lists/ac/msg10022.html. AC_SUBST(CONFIGURE_INCANT, "$0 $@") dnl Extract RT version number components AC_SUBST([rt_version_major], m4_bregexp(AC_PACKAGE_VERSION,[^rt-\(\w+\)\.\(\w+\)\.\(.+\)$],[\1])) AC_SUBST([rt_version_minor], m4_bregexp(AC_PACKAGE_VERSION,[^rt-\(\w+\)\.\(\w+\)\.\(.+\)$],[\2])) AC_SUBST([rt_version_patch], m4_bregexp(AC_PACKAGE_VERSION,[^rt-\(\w+\)\.\(\w+\)\.\(.+\)$],[\3])) test "x$rt_version_major" = 'x' && rt_version_major=0 test "x$rt_version_minor" = 'x' && rt_version_minor=0 test "x$rt_version_patch" = 'x' && rt_version_patch=0 dnl Check for programs AC_PROG_INSTALL AC_ARG_VAR([PERL],[Perl interpreter command]) AC_PATH_PROG([PERL], [perl], [not found]) if test "$PERL" = 'not found'; then AC_MSG_ERROR([cannot use $PACKAGE_NAME without perl]) fi dnl BSD find uses -perm +xxxx, GNU find has deprecated this syntax in favour of dnl -perm /xxx. AC_MSG_CHECKING([checking version of find]) AS_IF([find --version 2>&1 | grep 'GNU'], [ FINDPERM="/" AC_MSG_RESULT([configuring for GNU find]) ], [ FINDPERM="+" AC_MSG_RESULT([configuring for BSD find]) ]) AC_SUBST([FINDPERM]) dnl WEB_HANDLER AC_ARG_WITH(web-handler, AS_HELP_STRING([--with-web-handler=LIST], [comma separated list of web-handlers RT will be able to use. Default is fastcgi. Valid values are modperl2, fastcgi and standalone. To successfully run RT you need only one. ]), WEB_HANDLER=$withval, WEB_HANDLER=fastcgi) my_web_handler_test=$($PERL -e 'print "ok" unless grep $_ !~ /^(modperl2|fastcgi|fcgid|standalone)$/i, grep defined && length, split /\s*,\s*/, $ARGV@<:@0@:>@' $WEB_HANDLER) if test "$my_web_handler_test" != "ok"; then AC_MSG_ERROR([Only modperl2, fastcgi, fcgid and standalone are valid web-handlers]) fi AC_SUBST(WEB_HANDLER) dnl Defaults paths for installation AC_PREFIX_DEFAULT([/opt/rt4]) RT_ENABLE_LAYOUT # ACRT_USER_EXISTS( users, variable, default ) # - users is a list of users [www apache www-docs] # from highest to lowest priority to high priority (i.e. first match) # - variable is what you set with the result # AC_DEFUN([ACRT_USER_GUESS], [ $2=$3 for x in $1; do AC_MSG_CHECKING([if user $x exists]) AS_IF([ $PERL -e"exit( defined getpwnam('$x') ? 0 : 1)" ], [ AC_MSG_RESULT([found]); $2=$x ; break], [ AC_MSG_RESULT([not found]) ]) done ]) AC_DEFUN([ACRT_GROUP_GUESS], [ $2=$3 for x in $1; do AC_MSG_CHECKING([if group $x exists]) AS_IF([ $PERL -e"exit( defined getgrnam('$x') ? 0 : 1)" ], [ AC_MSG_RESULT([found]); $2=$x ; break], [ AC_MSG_RESULT([not found]) ]) done ]) dnl BIN_OWNER AC_ARG_WITH(bin-owner, AS_HELP_STRING([--with-bin-owner=OWNER], [user that will own RT binaries (default root)]), BIN_OWNER=$withval, BIN_OWNER=root) AC_SUBST(BIN_OWNER) dnl LIBS_OWNER AC_ARG_WITH(libs-owner, AS_HELP_STRING([--with-libs-owner=OWNER], [user that will own RT libraries (default root)]), LIBS_OWNER=$withval, LIBS_OWNER=root) AC_SUBST(LIBS_OWNER) dnl LIBS_GROUP AC_ARG_WITH(libs-group, AS_HELP_STRING([--with-libs-group=GROUP], [group that will own RT libraries (default root)]), LIBS_GROUP=$withval, LIBS_GROUP=root) AC_SUBST(LIBS_GROUP) dnl DB_TYPE AC_ARG_WITH(db-type, AS_HELP_STRING([--with-db-type=TYPE], [sort of database RT will use (default: mysql) (mysql, Pg, Oracle and SQLite are valid)]), DB_TYPE=$withval, DB_TYPE=mysql) if test "$DB_TYPE" != 'mysql' -a "$DB_TYPE" != 'Pg' -a "$DB_TYPE" != 'SQLite' -a "$DB_TYPE" != 'Oracle' ; then AC_MSG_ERROR([Only Oracle, Pg, mysql and SQLite are valid db types]) fi AC_SUBST(DB_TYPE) dnl DATABASE_ENV_PREF if test "$DB_TYPE" = 'Oracle'; then test "x$ORACLE_HOME" = 'x' && AC_MSG_ERROR([Please declare the ORACLE_HOME environment variable]) DATABASE_ENV_PREF="\$ENV{'ORACLE_HOME'} = '$ORACLE_HOME';" fi AC_SUBST(DATABASE_ENV_PREF) dnl DB_HOST AC_ARG_WITH(db-host, AS_HELP_STRING([--with-db-host=HOSTNAME], [FQDN of database server (default: localhost)]), DB_HOST=$withval, DB_HOST=localhost) AC_SUBST(DB_HOST) dnl DB_PORT AC_ARG_WITH(db-port, AS_HELP_STRING([--with-db-port=PORT], [port on which the database listens on]), DB_PORT=$withval, DB_PORT=) AC_SUBST(DB_PORT) dnl DB_RT_HOST AC_ARG_WITH(db-rt-host, AS_HELP_STRING([--with-db-rt-host=HOSTNAME], [FQDN of RT server which talks to the database server (default: localhost)]), DB_RT_HOST=$withval, DB_RT_HOST=localhost) AC_SUBST(DB_RT_HOST) dnl DB_DATABASE_ADMIN if test "$DB_TYPE" = "Pg" ; then DB_DBA="postgres" else DB_DBA="root" fi AC_ARG_WITH(db-dba, AS_HELP_STRING([--with-db-dba=DBA], [name of database administrator (default: root or postgres)]), DB_DBA=$withval, DB_DBA="$DB_DBA") AC_SUBST(DB_DBA) dnl DB_DATABASE AC_ARG_WITH(db-database, AS_HELP_STRING([--with-db-database=DBNAME], [name of the database to use (default: rt4)]), DB_DATABASE=$withval, DB_DATABASE=rt4) AC_SUBST(DB_DATABASE) dnl DB_RT_USER AC_ARG_WITH(db-rt-user, AS_HELP_STRING([--with-db-rt-user=DBUSER], [name of database user (default: rt_user)]), DB_RT_USER=$withval, DB_RT_USER=rt_user) AC_SUBST(DB_RT_USER) dnl DB_RT_PASS AC_ARG_WITH(db-rt-pass, AS_HELP_STRING([--with-db-rt-pass=PASSWORD], [password for database user (default: rt_pass)]), DB_RT_PASS=$withval, DB_RT_PASS=rt_pass) AC_SUBST(DB_RT_PASS) dnl WEB_USER AC_ARG_WITH(web-user, AS_HELP_STRING([--with-web-user=USER], [user the web server runs as (default: www)]), WEB_USER=$withval, ACRT_USER_GUESS([www www-data apache httpd nobody],[WEB_USER],[www]) ) AC_SUBST(WEB_USER) dnl WEB_GROUP AC_ARG_WITH(web-group, AS_HELP_STRING([--with-web-group=GROUP], [group the web server runs as (default: www)]), WEB_GROUP=$withval, ACRT_GROUP_GUESS([www www-data apache httpd nogroup nobody],[WEB_GROUP], [www])) AC_SUBST(WEB_GROUP) dnl RTGROUP AC_ARG_WITH(rt-group, AS_HELP_STRING([--with-rt-group=GROUP], [group to own all files (default: rt)]), RTGROUP=$withval, ACRT_GROUP_GUESS([rt $WEB_GROUP],[RTGROUP], [rt])) AC_SUBST(RTGROUP) dnl INSTALL AS ME my_group=$($PERL -MPOSIX=getgid -le 'print scalar getgrgid getgid') my_user=${USER:-$LOGNAME} AC_ARG_WITH(my-user-group, AS_HELP_STRING([--with-my-user-group], [set all users and groups to current user/group]), RTGROUP=$my_group BIN_OWNER=$my_user LIBS_OWNER=$my_user LIBS_GROUP=$my_group WEB_USER=$my_user WEB_GROUP=$my_group) # Test for valid database names AC_MSG_CHECKING([if database name is set]) AS_IF([ echo $DB_DATABASE | $PERL -e 'exit(1) unless <> =~ /\S/' ], [ AC_MSG_RESULT([yes]) ], [ AC_MSG_ERROR([no. database name is not set]) ] ) dnl Dependencies for testing and developing RT AC_ARG_WITH(developer,[],RT_DEVELOPER=$withval,RT_DEVELOPER="0") AC_ARG_ENABLE(developer, AS_HELP_STRING([--enable-developer], [Add dependencies needed for testing and developing RT]), RT_DEVELOPER=$enableval, RT_DEVELOPER=$RT_DEVELOPER) if test "$RT_DEVELOPER" = yes; then RT_DEVELOPER="1" else RT_DEVELOPER="0" fi AC_SUBST(RT_DEVELOPER) dnl RT's GraphViz dependency charts AC_CHECK_PROG([RT_GRAPHVIZ], [dot], "yes", "no") AC_ARG_WITH(graphviz,[],RT_GRAPHVIZ=$withval) AC_ARG_ENABLE(graphviz, AS_HELP_STRING([--enable-graphviz], [Turns on support for RT's GraphViz dependency charts]), RT_GRAPHVIZ=$enableval) if test "$RT_GRAPHVIZ" = yes; then RT_GRAPHVIZ="1" else RT_GRAPHVIZ="0" fi AC_SUBST(RT_GRAPHVIZ) dnl RT's GD pie and bar charts AC_CHECK_PROG([RT_GD], [gdlib-config], "yes", "no") AC_ARG_WITH(gd,[],RT_GD=$withval) AC_ARG_ENABLE(gd, AS_HELP_STRING([--enable-gd], [Turns on support for RT's GD pie and bar charts]), RT_GD=$enableval) if test "$RT_GD" = yes; then RT_GD="1" else RT_GD="0" fi AC_SUBST(RT_GD) dnl RT's GPG support AC_CHECK_PROG([RT_GPG_DEPS], [gpg], "yes", "no") if test "$RT_GPG_DEPS" = yes; then RT_GPG_DEPS="1" else RT_GPG_DEPS="0" fi AC_ARG_ENABLE(gpg, AS_HELP_STRING([--enable-gpg], [Turns on GNU Privacy Guard (GPG) support]), RT_GPG=$enableval) if test "$RT_GPG" = yes; then RT_GPG="1" RT_GPG_DEPS="1" else if test "$RT_GPG" = no; then RT_GPG="0" RT_GPG_DEPS="0" else RT_GPG="0" fi fi AC_SUBST(RT_GPG_DEPS) AC_SUBST(RT_GPG) dnl RT's SMIME support AC_CHECK_PROG([RT_SMIME_DEPS], [openssl], "yes", "no") if test "$RT_SMIME_DEPS" = yes; then RT_SMIME_DEPS="1" else RT_SMIME_DEPS="0" fi AC_ARG_ENABLE(smime, AS_HELP_STRING([--enable-smime], [Turns on Secure MIME (SMIME) support]), RT_SMIME=$enableval) if test "$RT_SMIME" = yes; then RT_SMIME="1" RT_SMIME_DEPS="1" else if test "$RT_SMIME" = no; then RT_SMIME="0" RT_SMIME_DEPS="0" else RT_SMIME="0" fi fi AC_SUBST(RT_SMIME_DEPS) AC_SUBST(RT_SMIME) dnl Dependencies for external auth AC_ARG_WITH(externalauth,[],RT_EXTERNALAUTH=$withval,RT_EXTERNALAUTH="0") AC_ARG_ENABLE(externalauth, AS_HELP_STRING([--enable-externalauth], [Add dependencies needed for external auth]), RT_EXTERNALAUTH=$enableval, RT_EXTERNALAUTH=$RT_EXTERNALAUTH) if test "$RT_EXTERNALAUTH" = yes; then RT_EXTERNALAUTH="1" else RT_EXTERNALAUTH="0" fi AC_SUBST(RT_EXTERNALAUTH) dnl ExternalStorage AC_ARG_WITH(attachment-store, AS_HELP_STRING([--with-attachment-store=TYPE], [which attachment storage RT will use for attachments (default: database) (database, disk, S3 and Dropbox are valid)]), ATTACHMENT_STORE=$withval, ATTACHMENT_STORE=database) if test "$ATTACHMENT_STORE" != 'database' -a "$ATTACHMENT_STORE" != 'disk' -a "$ATTACHMENT_STORE" != 'S3' -a "$ATTACHMENT_STORE" != 'Dropbox' ; then AC_MSG_ERROR([Only database, disk, S3 and Dropbox are valid db types]) fi AC_SUBST(ATTACHMENT_STORE) dnl This section maps the variable names this script 'natively' generates dnl to their existing names. They should be removed from here as the .in dnl files are changed to use the new names. dnl version numbers AC_SUBST(RT_VERSION_MAJOR, ${rt_version_major}) AC_SUBST(RT_VERSION_MINOR, ${rt_version_minor}) AC_SUBST(RT_VERSION_PATCH, ${rt_version_patch}) dnl layout paths AC_SUBST([RT_PATH], ${exp_prefix}) AC_SUBST([RT_DOC_PATH], ${exp_manualdir}) AC_SUBST([RT_LOCAL_PATH], ${exp_customdir}) AC_SUBST([RT_LIB_PATH], ${exp_libdir}) AC_SUBST([RT_LEXICON_PATH], ${exp_lexdir}) AC_SUBST([RT_STATIC_PATH], ${exp_staticdir}) AC_SUBST([RT_ETC_PATH], ${exp_sysconfdir}) AC_SUBST([CONFIG_FILE_PATH], ${exp_sysconfdir}) AC_SUBST([RT_BIN_PATH], ${exp_bindir}) AC_SUBST([RT_SBIN_PATH], ${exp_sbindir}) AC_SUBST([RT_VAR_PATH], ${exp_localstatedir}) AC_SUBST([RT_MAN_PATH], ${exp_mandir}) AC_SUBST([RT_FONT_PATH], ${exp_fontdir}) AC_SUBST([RT_PLUGIN_PATH], ${exp_plugindir}) AC_SUBST([MASON_DATA_PATH], ${exp_masonstatedir}) AC_SUBST([MASON_SESSION_PATH], ${exp_sessionstatedir}) AC_SUBST([MASON_HTML_PATH], ${exp_htmldir}) AC_SUBST([LOCAL_ETC_PATH], ${exp_custometcdir}) AC_SUBST([MASON_LOCAL_HTML_PATH], ${exp_customhtmldir}) AC_SUBST([LOCAL_LEXICON_PATH], ${exp_customlexdir}) AC_SUBST([LOCAL_STATIC_PATH], ${exp_customstaticdir}) AC_SUBST([LOCAL_LIB_PATH], ${exp_customlibdir}) AC_SUBST([LOCAL_PLUGIN_PATH], ${exp_customplugindir}) AC_SUBST([RT_LOG_PATH], ${exp_logfiledir}) if test ${exp_sysconfdir} = "etc" -o ${exp_sysconfdir} = "etc/rt"; then AC_SUBST([RT_PATH_R], ${exp_prefix}) AC_SUBST([RT_DOC_PATH_R], ${exp_prefix}/${exp_manualdir}) AC_SUBST([RT_LOCAL_PATH_R], ${exp_prefix}/${exp_customdir}) AC_SUBST([RT_LIB_PATH_R], ${exp_prefix}/${exp_libdir}) AC_SUBST([RT_ETC_PATH_R], ${exp_prefix}/${exp_sysconfdir}) AC_SUBST([CONFIG_FILE_PATH_R], ${exp_prefix}/${exp_sysconfdir}) AC_SUBST([RT_BIN_PATH_R], ${exp_prefix}/${exp_bindir}) AC_SUBST([RT_SBIN_PATH_R], ${exp_prefix}/${exp_sbindir}) AC_SUBST([RT_VAR_PATH_R], ${exp_prefix}/${exp_localstatedir}) AC_SUBST([RT_MAN_PATH_R], ${exp_prefix}/${exp_mandir}) AC_SUBST([RT_FONT_PATH_R], ${exp_prefix}/${exp_fontdir}) AC_SUBST([RT_LEXICON_PATH_R], ${exp_prefix}/${exp_lexdir}) AC_SUBST([RT_STATIC_PATH_R], ${exp_prefix}/${exp_staticdir}) AC_SUBST([RT_PLUGIN_PATH_R], ${exp_prefix}/${exp_plugindir}) AC_SUBST([MASON_DATA_PATH_R], ${exp_prefix}/${exp_masonstatedir}) AC_SUBST([MASON_SESSION_PATH_R], ${exp_prefix}/${exp_sessionstatedir}) AC_SUBST([MASON_HTML_PATH_R], ${exp_prefix}/${exp_htmldir}) AC_SUBST([LOCAL_ETC_PATH_R], ${exp_prefix}/${exp_custometcdir}) AC_SUBST([MASON_LOCAL_HTML_PATH_R], ${exp_prefix}/${exp_customhtmldir}) AC_SUBST([LOCAL_LEXICON_PATH_R], ${exp_prefix}/${exp_customlexdir}) AC_SUBST([LOCAL_STATIC_PATH_R], ${exp_prefix}/${exp_customstaticdir}) AC_SUBST([LOCAL_LIB_PATH_R], ${exp_prefix}/${exp_customlibdir}) AC_SUBST([LOCAL_PLUGIN_PATH_R], ${exp_prefix}/${exp_customplugindir}) AC_SUBST([RT_LOG_PATH_R], ${exp_prefix}/${exp_logfiledir}) else AC_SUBST([RT_PATH_R], ${exp_prefix}) AC_SUBST([RT_DOC_PATH_R], ${exp_manualdir}) AC_SUBST([RT_LOCAL_PATH_R], ${exp_customdir}) AC_SUBST([RT_LIB_PATH_R], ${exp_libdir}) AC_SUBST([RT_LEXICON_PATH_R], ${exp_lexdir}) AC_SUBST([RT_STATIC_PATH_R], ${exp_staticdir}) AC_SUBST([RT_ETC_PATH_R], ${exp_sysconfdir}) AC_SUBST([RT_PLUGIN_PATH_R], ${exp_plugindir}) AC_SUBST([CONFIG_FILE_PATH_R], ${exp_sysconfdir}) AC_SUBST([RT_BIN_PATH_R], ${exp_bindir}) AC_SUBST([RT_SBIN_PATH_R], ${exp_sbindir}) AC_SUBST([RT_VAR_PATH_R], ${exp_localstatedir}) AC_SUBST([RT_MAN_PATH_R], ${exp_mandir}) AC_SUBST([RT_FONT_PATH_R], ${exp_fontdir}) AC_SUBST([MASON_DATA_PATH_R], ${exp_masonstatedir}) AC_SUBST([MASON_SESSION_PATH_R], ${exp_sessionstatedir}) AC_SUBST([MASON_HTML_PATH_R], ${exp_htmldir}) AC_SUBST([LOCAL_ETC_PATH_R], ${exp_custometcdir}) AC_SUBST([MASON_LOCAL_HTML_PATH_R], ${exp_customhtmldir}) AC_SUBST([LOCAL_LEXICON_PATH_R], ${exp_customlexdir}) AC_SUBST([LOCAL_STATIC_PATH_R], ${exp_customstaticdir}) AC_SUBST([LOCAL_PLUGIN_PATH_R], ${exp_customplugindir}) AC_SUBST([LOCAL_LIB_PATH_R], ${exp_customlibdir}) AC_SUBST([RT_LOG_PATH_R], ${exp_logfiledir}) fi dnl Configure the output files, and generate them. dnl Binaries that should be +x AC_CONFIG_FILES([ etc/upgrade/3.8-ical-extension etc/upgrade/4.0-customfield-checkbox-extension etc/upgrade/generate-rtaddressregexp etc/upgrade/reset-sequences etc/upgrade/sanity-check-stylesheets etc/upgrade/shrink-cgm-table etc/upgrade/shrink-transactions-table etc/upgrade/switch-templates-to etc/upgrade/time-worked-history etc/upgrade/upgrade-articles etc/upgrade/upgrade-assets etc/upgrade/vulnerable-passwords etc/upgrade/upgrade-sla sbin/rt-ldapimport sbin/rt-attributes-viewer sbin/rt-preferences-viewer sbin/rt-session-viewer sbin/rt-dump-metadata sbin/rt-setup-database sbin/rt-test-dependencies sbin/rt-email-digest sbin/rt-email-dashboards sbin/rt-externalize-attachments sbin/rt-clean-sessions sbin/rt-shredder sbin/rt-validator sbin/rt-validate-aliases sbin/rt-email-group-admin sbin/rt-search-attributes sbin/rt-server sbin/rt-server.fcgi sbin/standalone_httpd sbin/rt-setup-fulltext-index sbin/rt-fulltext-indexer sbin/rt-serializer sbin/rt-importer sbin/rt-passwd sbin/rt-munge-attachments bin/rt-crontool bin/rt-mailgate bin/rt], [chmod ug+x $ac_file] ) dnl All other generated files AC_CONFIG_FILES([ Makefile etc/RT_Config.pm lib/RT/Generated.pm t/data/configs/apache2.4+mod_perl.conf t/data/configs/apache2.4+fcgid.conf], ) AC_OUTPUT rt-4.4.7/bin/0000755000076500000240000000000014514267701012320 5ustar sunnavystaffrt-4.4.7/bin/rt-mailgate.in0000644000076500000240000003234014514237602015055 0ustar sunnavystaff#!@PERL@ -w # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) # # # LICENSE: # # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. # # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. # # # CONTRIBUTION SUBMISSION POLICY: # # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) # # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that # you are the copyright holder for those contributions and you grant # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} =head1 NAME rt-mailgate - Mail interface to RT. =cut use strict; use warnings; use Getopt::Long; my $opts = { }; GetOptions( $opts, "queue=s", "action=s", "url=s", "jar=s", "help", "debug", "extension=s", "timeout=i", "verify-ssl!", "ca-file=s", ); my $gateway = RT::Client::MailGateway->new(); $gateway->run($opts); package RT::Client::MailGateway; use LWP::UserAgent; use HTTP::Request::Common qw($DYNAMIC_FILE_UPLOAD); use File::Temp qw(tempfile tempdir); $DYNAMIC_FILE_UPLOAD = 1; use constant EX_TEMPFAIL => 75; use constant BUFFER_SIZE => 8192; sub new { my $class = shift; my $self = bless {}, $class; return $self; } sub run { my $self = shift; my $opts = shift; if ( $opts->{running_in_test_harness} ) { $self->{running_in_test_harness} = 1; } $self->validate_cli_flags($opts); my $ua = $self->get_useragent($opts); my $post_params = $self->setup_session($opts); $self->upload_message( $ua => $post_params ); $self->exit_with_success(); } sub exit_with_success { my $self = shift; if ( $self->{running_in_test_harness} ) { return 1; } else { exit 0; } } sub tempfail { my $self = shift; if ( $self->{running_in_test_harness} ) { die "tempfail"; } else { exit EX_TEMPFAIL; } } sub permfail { my $self = shift; if ( $self->{running_in_test_harness} ) { die "permfail"; } else { exit 1; } } sub validate_cli_flags { my $self = shift; my $opts = shift; if ( $opts->{'help'} ) { require Pod::Usage; Pod::Usage::pod2usage( { verbose => 2 } ); return $self->permfail() ; # Don't want to succeed if this is really an email! } unless ( $opts->{'url'} ) { print STDERR "$0 invoked improperly\n\nNo 'url' provided to mail gateway!\n"; return $self->permfail(); } $opts->{"verify-ssl"} = 1 unless defined $opts->{"verify-ssl"}; } sub get_useragent { my $self = shift; my $opts = shift; my $ua = LWP::UserAgent->new(); $ua->agent("rt-mailgate/@RT_VERSION_MAJOR@.@RT_VERSION_MINOR@.@RT_VERSION_PATCH@ "); $ua->cookie_jar( { file => $opts->{'jar'} } ) if $opts->{'jar'}; $ua->ssl_opts( verify_hostname => $opts->{'verify-ssl'}, SSL_verify_mode => $opts->{'verify-ssl'} ); $ua->ssl_opts( SSL_ca_file => $opts->{'ca-file'} ) if $opts->{'ca-file'}; return $ua; } sub setup_session { my $self = shift; my $opts = shift; my %post_params; foreach (qw(queue action)) { $post_params{$_} = $opts->{$_} if defined $opts->{$_}; } if ( ( $opts->{'extension'} || '' ) =~ /^(?:action|queue|ticket)$/i ) { $post_params{ lc $opts->{'extension'} } = $ENV{'EXTENSION'} || $opts->{ $opts->{'extension'} }; } elsif ( $opts->{'extension'} && $ENV{'EXTENSION'} ) { print STDERR "Value of the --extension argument is not action, queue or ticket" . ", but environment variable EXTENSION is also defined. The former is ignored.\n"; } # add ENV{'EXTENSION'} as X-RT-MailExtension to the message header if ( my $value = ( $ENV{'EXTENSION'} || $opts->{'extension'} ) ) { # prepare value to avoid MIME format breakage # strip trailing newline symbols $value =~ s/(\r*\n)+$//; # make a correct multiline header field, # with tabs in the beginning of each line $value =~ s/(\r*\n)/$1\t/g; $opts->{'headers'} .= "X-RT-Mail-Extension: $value\n"; } # Read the message in from STDIN # _raw_message is used for testing my $message = $opts->{'_raw_message'} || $self->slurp_message(); unless ( $message->{'filename'} ) { $post_params{'message'} = [ undef, '', 'Content-Type' => 'application/octet-stream', Content => ${ $message->{'content'} }, ]; } else { $post_params{'message'} = [ $message->{'filename'}, '', 'Content-Type' => 'application/octet-stream', ]; } return \%post_params; } sub upload_message { my $self = shift; my $ua = shift; my $post_params = shift; my $full_url = $opts->{'url'} . "/REST/1.0/NoAuth/mail-gateway"; print STDERR "$0: connecting to $full_url\n" if $opts->{'debug'}; $ua->timeout( exists( $opts->{'timeout'} ) ? $opts->{'timeout'} : 180 ); my $r = $ua->post( $full_url, $post_params, Content_Type => 'form-data' ); # Follow 3 redirects my $n = 0; while ($n++ < 3 and $r->is_redirect) { $full_url = $r->header( "Location" ); $r = $ua->post( $full_url, $post_params, Content_Type => 'form-data' ); } $self->check_failure($r); my $content = $r->content; print STDERR $content . "\n" if $opts->{'debug'}; return if ( $content =~ /^(ok|not ok)/ ); # It's not the server's fault if the mail is bogus. We just want to know that # *something* came out of the server. print STDERR <tempfail(); } sub check_failure { my $self = shift; my $r = shift; return if $r->is_success; print STDERR "HTTP request failed: @{[ $r->status_line ]}. " ."Your webserver logs may have more information or there may be a network problem.\n"; print STDERR "\n$0: undefined server error\n" if $opts->{'debug'}; return $self->tempfail(); } sub slurp_message { my $self = shift; local $@; my %message; my ( $fh, $filename ) = eval { tempfile( DIR => tempdir( CLEANUP => 1 ) ) }; if ( !$fh || $@ ) { print STDERR "$0: Couldn't create temp file, using memory\n"; print STDERR "error: $@\n" if $@; my $message = \do { local ( @ARGV, $/ ); }; unless ( $$message =~ /\S/ ) { print STDERR "$0: no message passed on STDIN\n"; $self->exit_with_success; } $$message = $opts->{'headers'} . $$message if $opts->{'headers'}; return ( { content => $message } ); } binmode $fh; binmode \*STDIN; print $fh $opts->{'headers'} if $opts->{'headers'}; my $buf; my $empty = 1; while (1) { my $status = read \*STDIN, $buf, BUFFER_SIZE; unless ( defined $status ) { print STDERR "$0: couldn't read message: $!\n"; return $self->tempfail(); } elsif ( !$status ) { last; } $empty = 0 if $buf =~ /\S/; print $fh $buf; } close $fh; if ($empty) { print STDERR "$0: no message passed on STDIN\n"; $self->exit_with_success; } print STDERR "$0: temp file is '$filename'\n" if $opts->{'debug'}; return ( { filename => $filename } ); } =head1 SYNOPSIS rt-mailgate --help : this text Usual invocation (from MTA): rt-mailgate --action (correspond|comment|...) --queue queuename --url http://your.rt.server/ [ --debug ] [ --extension (queue|action|ticket) ] [ --timeout seconds ] =head1 OPTIONS =over 3 =item C<--action> Specifies what happens to email sent to this alias. The avaliable basic actions are: C, C. Additional actions, such as C or C, may be available depending on your local C<@MailPlugins> configuration. You can execute two or more actions on a single message using a C<-> separated list. RT will execute the actions in the listed order. For example you can use C, C or C as actions. Note that C and C actions ignore message text if used alone. Include a C or C action if you want RT to record the incoming message. The default action is C. =item C<--queue> This flag determines which queue this alias should create a ticket in if no ticket identifier is found. =item C<--url> This flag tells the mail gateway where it can find your RT server. You should probably use the same URL that users use to log into RT. If you have a self-signed SSL certificate, you may also need to pass C<--ca-file> or C<--no-verify-ssl>, below. =item C<--ca-file> I Specifies the path to the public SSL certificate for the certificate authority that should be used to verify the website's SSL certificate. If your webserver uses a self-signed certificate, you should preferentially use this option over C<--no-verify-ssl>, as it will ensure that the self-signed certificate that the mailgate is seeing the I self-signed certificate. =item C<--no-verify-ssl> This flag tells the mail gateway to trust all SSL certificates, regardless of if their hostname matches the certificate, and regardless of CA. This is required if you have a self-signed certificate, or some other certificate which is not traceable back to an certificate your system ultimitely trusts. =item C<--extension> OPTIONAL Some MTAs will route mail sent to user-foo@host or user+foo@host to user@host and present "foo" in the environment variable $EXTENSION. By specifying the value "queue" for this parameter, the queue this message should be submitted to will be set to the value of $EXTENSION. By specifying "ticket", $EXTENSION will be interpreted as the id of the ticket this message is related to. "action" will allow the user to specify either "comment" or "correspond" in the address extension. =item C<--debug> OPTIONAL Print debugging output to standard error =item C<--timeout> OPTIONAL Configure the timeout for posting the message to the web server. The default timeout is 3 minutes (180 seconds). =back =head1 DESCRIPTION The RT mail gateway is the primary mechanism for communicating with RT via email. This program simply directs the email to the RT web server, which handles filing correspondence and sending out any required mail. It is designed to be run as part of the mail delivery process, either called directly by the MTA or C, or in a F<.forward> or equivalent. =head1 SETUP Much of the set up of the mail gateway depends on your MTA and mail routing configuration. You need to route mail to C for the queues you're monitoring. For instance, if you're using F and you have a "bugs" queue, you will want something like this: bugs: "|@RT_BIN_PATH_R@/rt-mailgate --queue bugs --action correspond --url http://rt.mycorp.com/" bugs-comment: "|@RT_BIN_PATH_R@/rt-mailgate --queue bugs --action comment --url http://rt.mycorp.com/" Note that you don't have to run your RT server on your mail server, as the mail gateway will happily relay to a different machine. =head1 ENVIRONMENT =over 4 =item EXTENSION Some MTAs will route mail sent to user-foo@host or user+foo@host to user@host and present "foo" in the environment variable C. Mailgate adds value of this variable to message in the C field of the message header. See also C<--extension> option. Note that value of the environment variable is always added to the message header when it's not empty even if C<--extension> option is not provided. =back =cut rt-4.4.7/bin/rt.in0000644000076500000240000023426214514237602013303 0ustar sunnavystaff#!@PERL@ -w # BEGIN BPS TAGGED BLOCK {{{ # # COPYRIGHT: # # This software is Copyright (c) 1996-2023 Best Practical Solutions, LLC # # # (Except where explicitly superseded by other copyright notices) # # # LICENSE: # # This work is made available to you under the terms of Version 2 of # the GNU General Public License. A copy of that license should have # been provided with this software, but in any event can be snarfed # from www.gnu.org. # # This work is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA # 02110-1301 or visit their web page on the internet at # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. # # # CONTRIBUTION SUBMISSION POLICY: # # (The following paragraph is not intended to limit the rights granted # to you to modify and distribute this software under the terms of # the GNU General Public License and is only of importance to you if # you choose to contribute your changes and enhancements to the # community by submitting them to Best Practical Solutions, LLC.) # # By intentionally submitting any modifications, corrections or # derivatives to this work, or any other work intended for use with # Request Tracker, to Best Practical Solutions, LLC, you confirm that # you are the copyright holder for those contributions and you grant # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, # royalty-free, perpetual, license to use, copy, create derivative # works based on those contributions, and sublicense and distribute # those contributions and any derivatives thereof. # # END BPS TAGGED BLOCK }}} # Designed and implemented for Best Practical Solutions, LLC by # Abhijit Menon-Sen use strict; use warnings; if ( $ARGV[0] && $ARGV[0] =~ /^(?:--help|-h)$/ ) { require Pod::Usage; print Pod::Usage::pod2usage( { verbose => 2 } ); exit; } # This program is intentionally written to have as few non-core module # dependencies as possible. It should stay that way. use Cwd; use LWP; use Text::ParseWords; use HTTP::Request::Common; use HTTP::Headers; use Term::ReadLine; use Time::Local; # used in prettyshow use File::Temp; # We derive configuration information from hardwired defaults, dotfiles, # and the RT* environment variables (in increasing order of precedence). # Session information is stored in ~/.rt_sessions. my $VERSION = 0.02; my $HOME = eval{(getpwuid($<))[7]} || $ENV{HOME} || $ENV{LOGDIR} || $ENV{HOMEPATH} || "."; my %config = ( ( debug => 0, user => eval{(getpwuid($<))[0]} || $ENV{USER} || $ENV{USERNAME}, passwd => undef, server => 'http://localhost/', query => "Status!='resolved' and Status!='rejected'", orderby => 'id', queue => undef, # to protect against unlimited searches a better choice would be # queue => 'Unknown_Queue', auth => "rt", ), config_from_file($ENV{RTCONFIG} || ".rtrc"), config_from_env() ); $config{auth} = "basic" if delete $config{externalauth}; my $session = Session->new("$HOME/.rt_sessions"); my $REST = "$config{server}/REST/1.0"; my $prompt = 'rt> '; sub whine; sub DEBUG { warn @_ if $config{debug} >= shift } # These regexes are used by command handlers to parse arguments. # (XXX: Ask Autrijus how i18n changes these definitions.) my $name = '[\w.-]+'; my $CF_name = '[^,]+?'; my $field = '(?i:[a-z][a-z0-9_-]*|C(?:ustom)?F(?:ield)?-'.$CF_name.'|CF\.\{'.$CF_name.'\})'; my $label = '[^,\\/]+'; my $labels = "(?:$label,)*$label"; my $idlist = '(?:(?:\d+-)?\d+,)*(?:\d+-)?\d+'; # Our command line looks like this: # # rt [options] [arguments] # # We'll parse just enough of it to decide upon an action to perform, and # leave the rest to per-action handlers to interpret appropriately. my %handlers = ( # handler => [ ...aliases... ], version => ["version", "ver"], shell => ["shell"], logout => ["logout"], help => ["help", "man"], show => ["show", "cat"], edit => ["create", "edit", "new", "ed"], list => ["search", "list", "ls"], comment => ["comment", "correspond"], link => ["link", "ln"], merge => ["merge"], grant => ["grant", "revoke"], take => ["take", "steal", "untake"], quit => ["quit", "exit"], setcommand => ["del", "delete", "give", "res", "resolve", "subject"], ); my %actions; foreach my $fn (keys %handlers) { foreach my $alias (@{ $handlers{$fn} }) { $actions{$alias} = \&{"$fn"}; } } # Once we find and call an appropriate handler, we're done. sub handler { my $action; push @ARGV, 'shell' if (!@ARGV); # default to shell mode shift @ARGV if ($ARGV[0] eq 'rt'); # ignore a leading 'rt' if (@ARGV && exists $actions{$ARGV[0]}) { $action = shift @ARGV; return $actions{$action}->($action); } else { print STDERR "rt: Unknown command '@ARGV'.\n"; print STDERR "rt: For help, run 'rt help'.\n"; return 1; } } exit handler(); # Handler functions. # ------------------ # # The following subs are handlers for each entry in %actions. sub shell { $|=1; my $term = Term::ReadLine->new('RT CLI'); while ( defined ($_ = $term->readline($prompt)) ) { next if /^#/ || /^\s*$/; @ARGV = shellwords($_); handler(); } } sub version { print "rt $VERSION\n"; return 0; } sub logout { submit("$REST/logout") if defined $session->cookie; return 0; } sub quit { logout(); exit; } my %help; sub help { my ($action, $type, $rv) = @_; $rv = defined $rv ? $rv : 0; my $key; # What help topics do we know about? if (!%help) { local $/ = undef; foreach my $item (@{ Form::parse() }) { my $title = $item->[2]{Title}; my @titles = ref $title eq 'ARRAY' ? @$title : $title; foreach $title (grep $_, @titles) { $help{$title} = $item->[2]{Text}; } } } # What does the user want help with? undef $action if ($action && $actions{$action} eq \&help); unless ($action || $type) { # If we don't know, we'll look for clues in @ARGV. foreach (@ARGV) { if (exists $help{$_}) { $key = $_; last; } } unless ($key) { # Tolerate possibly plural words. foreach (@ARGV) { if ($_ =~ s/s$// && exists $help{$_}) { $key = $_; last; } } } } if ($type && $action) { $key = "$type.$action"; } $key ||= $type || $action || "introduction"; # Find a suitable topic to display. while (!exists $help{$key}) { if ($type && $action) { if ($key eq "$type.$action") { $key = $action; } elsif ($key eq $action) { $key = $type; } else { $key = "introduction"; } } else { $key = "introduction"; } } print STDERR $help{$key}, "\n\n"; return $rv; } # Displays a list of objects that match some specified condition. sub list { my ($q, $type, %data); my $orderby = $config{orderby}; if ($config{orderby}) { $data{orderby} = $config{orderby}; } my $bad = 0; my $rawprint = 0; my $reverse_sort = 0; my $queue = $config{queue}; while (@ARGV) { $_ = shift @ARGV; if (/^-t$/) { $bad = 1, last unless defined($type = get_type_argument()); } elsif (/^-S$/) { $bad = 1, last unless get_var_argument(\%data); } elsif (/^-o$/) { $data{'orderby'} = shift @ARGV; } elsif (/^-([isl])$/) { $data{format} = $1; $rawprint = 1; } elsif (/^-q$/) { $queue = shift @ARGV; } elsif (/^-r$/) { $reverse_sort = 1; } elsif (/^-f$/) { if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) { whine "No valid field list in '-f $ARGV[0]'."; $bad = 1; last; } $data{fields} = shift @ARGV; $data{format} = 's' if ! $data{format}; $rawprint = 1; } elsif (!defined $q && !/^-/) { $q = $_; } else { my $datum = /^-/ ? "option" : "argument"; whine "Unrecognised $datum '$_'."; $bad = 1; last; } } if ( ! $rawprint and ! exists $data{format} ) { $data{format} = 'l'; $data{fields} = 'subject,status,queue,created,told,owner,requestors'; } if ( $reverse_sort and $data{orderby} =~ /^-/ ) { $data{orderby} =~ s/^-/+/; } elsif ($reverse_sort) { $data{orderby} =~ s/^\+?(.*)/-$1/; } $type ||= "ticket"; if (!defined $q ) { if ( $type eq 'ticket' ) { $q = $config{query}; } else { $q = ''; } } if ( $type ne 'ticket' ) { $rawprint = 1; } unless (defined $q) { my $item = $type ? "query string" : "object type"; whine "No $item specified."; $bad = 1; } $q =~ s/^#//; # get rid of leading hash if ( $type eq 'ticket' ) { if ( $q =~ /^\d+$/ ) { # only digits, must be an id, formulate a correct query $q = "id=$q" if $q =~ /^\d+$/; } else { # a string only, take it as an owner or requestor (quoting done later) $q = "(Owner=$q or Requestor like $q) and $config{query}" if $q =~ /^[\w\-]+$/; # always add a query for a specific queue or (comma separated) queues $queue =~ s/,/ or Queue=/g if $queue; $q .= " and (Queue=$queue)" if $queue and $q and $q !~ /Queue\s*=/i and $q !~ /id\s*=/i; } # correctly quote strings in a query $q =~ s/(=|like\s)\s*([^'\d\s]\S*)\b/$1\'$2\'/g; } #return help("list", $type) if $bad; return suggest_help("list", $type, $bad) if $bad; print "Query:$q\n" if ! $rawprint; my $r = submit("$REST/search/$type", { query => $q, %data }); if ( $rawprint ) { print $r->content; } else { my $forms = Form::parse($r->content); prettylist ($forms); } return 0; } # Displays selected information about a single object. sub show { my ($type, @objects, %data); my $slurped = 0; my $bad = 0; my $rawprint = 0; my $histspec; while (@ARGV) { $_ = shift @ARGV; s/^#// if /^#\d+/; # get rid of leading hash if (/^-t$/) { $bad = 1, last unless defined($type = get_type_argument()); } elsif (/^-S$/) { $bad = 1, last unless get_var_argument(\%data); } elsif (/^-([isl])$/) { $data{format} = $1; $rawprint = 1; } elsif (/^-$/ && !$slurped) { chomp(my @lines = ); foreach (@lines) { unless (is_object_spec($_, $type)) { whine "Invalid object on STDIN: '$_'."; $bad = 1; last; } push @objects, $_; } $slurped = 1; } elsif (/^-f$/) { if ($ARGV[0] !~ /^(?:(?:$field,)*$field)$/) { whine "No valid field list in '-f $ARGV[0]'."; $bad = 1; last; } $data{fields} = shift @ARGV; # option f requires short raw listing format $data{format} = 's'; $rawprint = 1; } elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) { push @objects, $spc2; $histspec = is_object_spec("ticket/$_/history", $type); } elsif (/^\d+\// and my $spc3 = is_object_spec("ticket/$_", $type)) { push @objects, $spc3; $rawprint = 1 if $_ =~ /\/content$/; } elsif (my $spec = is_object_spec($_, $type)) { push @objects, $spec; $rawprint = 1 if $_ =~ /\/content$/ or $_ =~ /\/links/ or $_ !~ /^ticket/; } else { my $datum = /^-/ ? "option" : "argument"; whine "Unrecognised $datum '$_'."; $bad = 1; last; } } if ( ! $rawprint ) { push @objects, $histspec if $histspec; $data{format} = 'l' if ! exists $data{format}; } unless (@objects) { whine "No objects specified."; $bad = 1; } #return help("show", $type) if $bad; return suggest_help("show", $type, $bad) if $bad; my $r = submit("$REST/show", { id => \@objects, %data }); my $c = $r->content; # if this isn't a text reply, remove the trailing newline so we # don't corrupt things like tarballs when people do # show ticket/id/attachments/id/content > foo.tar.gz if ($r->content_type !~ /^text\//) { chomp($c); $rawprint = 1; } if ( $rawprint ) { print $c; } else { # I do not know how to get more than one form correctly returned $c =~ s!^RT/[\d\.]+ 200 Ok$!--!mg; my $forms = Form::parse($c); prettyshow ($forms); } return 0; } # To create a new object, we ask the server for a form with the defaults # filled in, allow the user to edit it, and send the form back. # # To edit an object, we must ask the server for a form representing that # object, make changes requested by the user (either on the command line # or interactively via $EDITOR), and send the form back. sub edit { my ($action) = @_; my (%data, $type, @objects); my ($cl, $text, $edit, $input, $output, $content_type); use vars qw(%set %add %del); %set = %add = %del = (); my $slurped = 0; my $bad = 0; while (@ARGV) { $_ = shift @ARGV; s/^#// if /^#\d+/; # get rid of leading hash if (/^-e$/) { $edit = 1 } elsif (/^-i$/) { $input = 1 } elsif (/^-o$/) { $output = 1 } elsif (/^-ct$/) { $content_type = shift @ARGV } elsif (/^-t$/) { $bad = 1, last unless defined($type = get_type_argument()); } elsif (/^-S$/) { $bad = 1, last unless get_var_argument(\%data); } elsif (/^-$/ && !($slurped || $input)) { chomp(my @lines = ); foreach (@lines) { unless (is_object_spec($_, $type)) { whine "Invalid object on STDIN: '$_'."; $bad = 1; last; } push @objects, $_; } $slurped = 1; } elsif (/^set$/i) { my $vars = 0; while (@ARGV && $ARGV[0] =~ /^($field)([+-]?=)(.*)$/s) { my ($key, $op, $val) = ($1, $2, $3); my $hash = ($op eq '=') ? \%set : ($op =~ /^\+/) ? \%add : \%del; vpush($hash, lc $key, $val); shift @ARGV; $vars++; } unless ($vars) { whine "No variables to set."; $bad = 1; last; } $cl = $vars; } elsif (/^(?:add|del)$/i) { my $vars = 0; my $hash = ($_ eq "add") ? \%add : \%del; while (@ARGV && $ARGV[0] =~ /^($field)=(.*)$/s) { my ($key, $val) = ($1, $2); vpush($hash, lc $key, $val); shift @ARGV; $vars++; } unless ($vars) { whine "No variables to set."; $bad = 1; last; } $cl = $vars; } elsif (/^\d+$/ and my $spc2 = is_object_spec("ticket/$_", $type)) { push @objects, $spc2; } elsif (my $spec = is_object_spec($_, $type)) { push @objects, $spec; } else { my $datum = /^-/ ? "option" : "argument"; whine "Unrecognised $datum '$_'."; $bad = 1; last; } } if ($action =~ /^ed(?:it)?$/) { unless (@objects) { whine "No objects specified."; $bad = 1; } } else { if (@objects) { whine "You shouldn't specify objects as arguments to $action."; $bad = 1; } unless ($type) { whine "What type of object do you want to create?"; $bad = 1; } @objects = ("$type/new") if defined($type); } #return help($action, $type) if $bad; return suggest_help($action, $type, $bad) if $bad; # We need a form to make changes to. We usually ask the server for # one, but we can avoid that if we are fed one on STDIN, or if the # user doesn't want to edit the form by hand, and the command line # specifies only simple variable assignments. We *should* get a # form if we're creating a new ticket, so that the default values # get filled in properly. my @new_objects = grep /\/new$/, @objects; if ($input) { local $/ = undef; $text = ; } elsif ($edit || %add || %del || !$cl || @new_objects) { my $r = submit("$REST/show", { id => \@objects, format => 'l' }); $text = $r->content; } # If any changes were specified on the command line, apply them. if ($cl) { if ($text) { # We're updating forms from the server. my $forms = Form::parse($text); foreach my $form (@$forms) { my ($c, $o, $k, $e) = @$form; my ($key, $val); next if ($e || !@$o); local %add = %add; local %del = %del; local %set = %set; # Make changes to existing fields. foreach $key (@$o) { if (exists $add{lc $key}) { $val = delete $add{lc $key}; vpush($k, $key, $val); $k->{$key} = vsplit($k->{$key}) if $val =~ /[,\n]/; } if (exists $del{lc $key}) { $val = delete $del{lc $key}; my %val = map {$_=>1} @{ vsplit($val) }; $k->{$key} = vsplit($k->{$key}); @{$k->{$key}} = grep {!exists $val{$_}} @{$k->{$key}}; } if (exists $set{lc $key}) { $k->{$key} = delete $set{lc $key}; } } # Then update the others. foreach $key (keys %set) { vpush($k, $key, $set{$key}) } foreach $key (keys %add) { vpush($k, $key, $add{$key}); $k->{$key} = vsplit($k->{$key}); } push @$o, (keys %add, keys %set); } $text = Form::compose($forms); } else { # We're rolling our own set of forms. my @forms; foreach (@objects) { my ($type, $ids, $args) = m{^($name)/($idlist|$labels)(?:(/.*))?$}o; $args ||= ""; foreach my $obj (expand_list($ids)) { my %set = (%set, id => "$type/$obj$args"); push @forms, ["", [keys %set], \%set]; } } $text = Form::compose(\@forms); } } if ($output) { print $text; return 0; } my @files; @files = @{ vsplit($set{'attachment'}) } if exists $set{'attachment'}; my $synerr = 0; EDIT: # We'll let the user edit the form before sending it to the server, # unless we have enough information to submit it non-interactively. if ( $type && $type eq 'ticket' && $text !~ /^Content-Type:/m ) { $text .= "Content-Type: $content_type\n" if $content_type and $content_type ne "text/plain"; } if ($edit || (!$input && !$cl)) { my ($newtext) = vi_form_while( $text, sub { my ($text, $form) = @_; return 1 unless exists $form->[2]{'Attachment'}; foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) { return (0, "File '$f' doesn't exist") unless -f $f; } @files = @{ vsplit($form->[2]{'Attachment'}) }; return 1; }, ); return $newtext unless $newtext; # We won't resubmit a bad form unless it was changed. $text = ($synerr && $newtext eq $text) ? undef : $newtext; } delete @data{ grep /^attachment_\d+$/, keys %data }; my $i = 1; foreach my $file (@files) { $data{"attachment_$i"} = bless([ $file ], "Attachment"); $i++; } if ($text) { my $r = submit("$REST/edit", {content => $text, %data}); if ($r->code == 409) { # If we submitted a bad form, we'll give the user a chance # to correct it and resubmit. if ($edit || (!$input && !$cl)) { my $content = $r->content . "\n"; $content =~ s/^(?!#)/# /mg; $text = $content . $text; $synerr = 1; goto EDIT; } else { print $r->content; return 0; } } print $r->content; } return 0; } # handler for special edit commands. A valid edit command is constructed and # further work is delegated to the edit handler sub setcommand { my ($action) = @_; my ($id, $bad, $what); if ( @ARGV ) { $_ = shift @ARGV; $id = $1 if (m|^(?:ticket/)?($idlist)$|); } if ( ! $id ) { $bad = 1; whine "No ticket number specified."; } if ( @ARGV ) { if ($action eq 'subject') { my $subject = '"'.join (" ", @ARGV).'"'; @ARGV = (); $what = "subject=$subject"; } elsif ($action eq 'give') { my $owner = shift @ARGV; $what = "owner=$owner"; } } else { if ( $action eq 'delete' or $action eq 'del' ) { $what = "status=deleted"; } elsif ($action eq 'resolve' or $action eq 'res' ) { $what = "status=resolved"; } elsif ($action eq 'take' ) { $what = "owner=$config{user}"; } elsif ($action eq 'untake') { $what = "owner=Nobody"; } } if (@ARGV) { $bad = 1; whine "Extraneous arguments for action $action: @ARGV."; } if ( ! $what ) { $bad = 1; whine "unrecognized action $action."; } return help("edit", undef, $bad) if $bad; @ARGV = ( $id, "set", $what ); print "Executing: rt edit @ARGV\n"; return edit("edit"); } # We roll "comment" and "correspond" into the same handler. sub comment { my ($action) = @_; my (%data, $id, @files, @bcc, @cc, $msg, $content_type, $wtime, $edit); my $bad = 0; my $status = ''; while (@ARGV) { $_ = shift @ARGV; if (/^-e$/) { $edit = 1; } elsif (/^-(?:[abcmws]|ct)$/) { unless (@ARGV) { whine "No argument specified with $_."; $bad = 1; last; } if (/-a/) { unless (-f $ARGV[0] && -r $ARGV[0]) { whine "Cannot read attachment: '$ARGV[0]'."; return 0; } push @files, shift @ARGV; } elsif (/-ct/) { $content_type = shift @ARGV; } elsif (/-s/) { $status = shift @ARGV; } elsif (/-([bc])/) { my $a = $_ eq "-b" ? \@bcc : \@cc; @$a = split /\s*,\s*/, shift @ARGV; } elsif (/-m/) { $msg = shift @ARGV; if ( $msg =~ /^-$/ ) { undef $msg; while () { $msg .= $_ } } } elsif (/-w/) { $wtime = shift @ARGV } } elsif (!$id && m|^(?:ticket/)?($idlist)$|) { $id = $1; } else { my $datum = /^-/ ? "option" : "argument"; whine "Unrecognised $datum '$_'."; $bad = 1; last; } } unless ($id) { whine "No object specified."; $bad = 1; } #return help($action, "ticket") if $bad; return suggest_help($action, "ticket") if $bad; my $form = [ "", [ "Ticket", "Action", "Cc", "Bcc", "Attachment", "TimeWorked", "Content-Type", "Text" ], { Ticket => $id, Action => $action, Cc => [ @cc ], Bcc => [ @bcc ], Attachment => [ @files ], TimeWorked => $wtime || '', 'Content-Type' => $content_type || 'text/plain', Text => $msg || '', Status => $status } ]; if ($status ne '') { push(@{$form->[1]}, "Status"); } my $text = Form::compose([ $form ]); if ($edit || !$msg) { my ($tmp) = vi_form_while( $text, sub { my ($text, $form) = @_; foreach my $f ( @{ vsplit($form->[2]{'Attachment'}) } ) { return (0, "File '$f' doesn't exist") unless -f $f; } @files = @{ vsplit($form->[2]{'Attachment'}) }; return 1; }, ); return $tmp unless $tmp; $text = $tmp; } my $i = 1; foreach my $file (@files) { $data{"attachment_$i"} = bless([ $file ], "Attachment"); $i++; } $data{content} = $text; my $r = submit("$REST/ticket/$id/comment", \%data); print $r->content; return 0; } # Merge one ticket into another. sub merge { my @id; my $bad = 0; while (@ARGV) { $_ = shift @ARGV; s/^#// if /^#\d+/; # get rid of leading hash if (/^\d+$/) { push @id, $_; } else { whine "Unrecognised argument: '$_'."; $bad = 1; last; } } unless (@id == 2) { my $evil = @id > 2 ? "many" : "few"; whine "Too $evil arguments specified."; $bad = 1; } #return help("merge", "ticket") if $bad; return suggest_help("merge", "ticket", $bad) if $bad; my $r = submit("$REST/ticket/$id[0]/merge/$id[1]"); print $r->content; return 0; } # Link one ticket to another. sub link { my ($bad, $del, %data) = (0, 0, ()); my $type; my %ltypes = map { lc $_ => $_ } qw(DependsOn DependedOnBy RefersTo ReferredToBy HasMember MemberOf); while (@ARGV && $ARGV[0] =~ /^-/) { $_ = shift @ARGV; if (/^-d$/) { $del = 1; } elsif (/^-t$/) { $bad = 1, last unless defined($type = get_type_argument()); } else { whine "Unrecognised option: '$_'."; $bad = 1; last; } } $type = "ticket" unless $type; # default type to tickets if (@ARGV == 3) { my ($from, $rel, $to) = @ARGV; if (($type eq "ticket") && ( ! exists $ltypes{lc $rel})) { whine "Invalid link '$rel' for type $type specified."; $bad = 1; } %data = (id => $from, rel => $rel, to => $to, del => $del); } else { my $bad = @ARGV < 3 ? "few" : "many"; whine "Too $bad arguments specified."; $bad = 1; } return suggest_help("link", $type, $bad) if $bad; my $r = submit("$REST/$type/link", \%data); print $r->content; return 0; } # Take/steal a ticket sub take { my ($cmd) = @_; my ($bad, %data) = (0, ()); my $id; # get the ticket id if (@ARGV == 1) { ($id) = @ARGV; unless ($id =~ /^\d+$/) { whine "Invalid ticket ID $id specified."; $bad = 1; } my $form = [ "", [ "Ticket", "Action" ], { Ticket => $id, Action => $cmd, Status => '', } ]; my $text = Form::compose([ $form ]); $data{content} = $text; } else { $bad = @ARGV < 1 ? "few" : "many"; whine "Too $bad arguments specified."; $bad = 1; } return suggest_help("take", "ticket", $bad) if $bad; my $r = submit("$REST/ticket/$id/take", \%data); print $r->content; return 0; } # Grant/revoke a user's rights. sub grant { my ($cmd) = @_; whine "$cmd is unimplemented."; return 1; } # Client <-> Server communication. # -------------------------------- # # This function composes and sends an HTTP request to the RT server, and # interprets the response. It takes a request URI, and optional request # data (a string, or a reference to a set of key-value pairs). sub submit { my ($uri, $content) = @_; my ($req, $data); my $ua = LWP::UserAgent->new(agent => "RT/3.0b", env_proxy => 1); my $h = HTTP::Headers->new; # Did the caller specify any data to send with the request? $data = []; if (defined $content) { unless (ref $content) { # If it's just a string, make sure LWP handles it properly. # (By pretending that it's a file!) $content = [ content => [undef, "", Content => $content] ]; } elsif (ref $content eq 'HASH') { my @data; foreach my $k (keys %$content) { if (ref $content->{$k} eq 'ARRAY') { foreach my $v (@{ $content->{$k} }) { push @data, $k, $v; } } else { push @data, $k, $content->{$k} } } $content = \@data; } $data = $content; } # Should we send authentication information to start a new session? my $how = $config{server} =~ /^https/ ? 'over SSL' : 'unencrypted'; my($server) = $config{server} =~ m{^.*//([^/]+)}; if ($config{auth} eq "gssapi") { die "GSSAPI support not available; failed to load perl module GSSAPI:\n$@\n" unless eval { require GSSAPI; 1 }; die "GSSAPI support not available; failed to load perl module LWP::Authen::Negotiate:\n$@\n" unless eval { require LWP::Authen::Negotiate; 1 }; } elsif ($config{auth} eq "basic") { print " Password will be sent to $server $how\n", " Press CTRL-C now if you do not want to continue\n" if ! $config{passwd}; $h->authorization_basic($config{user}, $config{passwd} || read_passwd() ); } elsif ( !defined $session->cookie ) { print " Password will be sent to $server $how\n", " Press CTRL-C now if you do not want to continue\n" if ! $config{passwd}; push @$data, ( user => $config{user} ); push @$data, ( pass => $config{passwd} || read_passwd() ); } # Now, we construct the request. if (@$data) { $req = POST($uri, $data, Content_Type => 'form-data'); } else { $req = GET($uri); } $session->add_cookie_header($req); $req->header(%$h) if %$h; # Then we send the request and parse the response. DEBUG(3, $req->as_string); my $res = $ua->request($req); DEBUG(3, $res->as_string); if ($res->is_success) { # The content of the response we get from the RT server consists # of an HTTP-like status line followed by optional header lines, # a blank line, and arbitrary text. my ($head, $text) = split /\n\n/, $res->content, 2; my ($status, @headers) = split /\n/, $head; $text =~ s/\n*$/\n/ if ($text); # "RT/3.0.1 401 Credentials required" if ($status !~ m#^RT/\d+(?:\S+) (\d+) ([\w\s]+)$#) { warn "rt: Malformed RT response from $server.\n"; warn "(Rerun with RTDEBUG=3 for details.)\n" if $config{debug} < 3; exit -1; } # Our caller can pretend that the server returned a custom HTTP # response code and message. (Doing that directly is apparently # not sufficiently portable and uncomplicated.) $res->code($1); $res->message($2); $res->content($text); $session->update($res) if ($res->is_success || $res->code != 401); if (!$res->is_success) { # We can deal with authentication failures ourselves. Either # we sent invalid credentials, or our session has expired. if ($res->code == 401) { my %d = @$data; if (exists $d{user}) { warn "rt: Incorrect username or password.\n"; exit -1; } elsif ($req->header("Cookie")) { # We'll retry the request with credentials, unless # we only wanted to logout in the first place. $session->delete; return submit(@_) unless $uri eq "$REST/logout"; } } # Conflicts should be dealt with by the handler and user. # For anything else, we just die. elsif ($res->code != 409) { warn "rt: ", $res->content; #exit; } } } else { warn "rt: Server error: ", $res->message, " (", $res->code, ")\n"; exit -1; } return $res; } # Session management. # ------------------- # # Maintains a list of active sessions in the ~/.rt_sessions file. { package Session; my ($s, $u); # Initialises the session cache. sub new { my ($class, $file) = @_; my $self = { file => $file || "$HOME/.rt_sessions", sids => { } }; # The current session is identified by the currently configured # server and user. ($s, $u) = @config{"server", "user"}; bless $self, $class; $self->load(); return $self; } # Returns the current session cookie. sub cookie { my ($self) = @_; my $cookie = $self->{sids}{$s}{$u}; return defined $cookie ? "RT_SID_$cookie" : undef; } # Deletes the current session cookie. sub delete { my ($self) = @_; delete $self->{sids}{$s}{$u}; } # Adds a Cookie header to an outgoing HTTP request. sub add_cookie_header { my ($self, $request) = @_; my $cookie = $self->cookie(); $request->header(Cookie => $cookie) if defined $cookie; } # Extracts the Set-Cookie header from an HTTP response, and updates # session information accordingly. sub update { my ($self, $response) = @_; my $cookie = $response->header("Set-Cookie"); if (defined $cookie && $cookie =~ /^RT_SID_(.[^;,\s]+=[0-9A-Fa-f]+);/) { $self->{sids}{$s}{$u} = $1; } } # Loads the session cache from the specified file. sub load { my ($self, $file) = @_; $file ||= $self->{file}; open( my $handle, '<', $file ) or return 0; $self->{file} = $file; my $sids = $self->{sids} = {}; while (<$handle>) { chomp; next if /^$/ || /^#/; next unless m#^https?://[^ ]+ \w+ [^;,\s]+=[0-9A-Fa-f]+$#; my ($server, $user, $cookie) = split / /, $_; $sids->{$server}{$user} = $cookie; } return 1; } # Writes the current session cache to the specified file. sub save { my ($self, $file) = shift; $file ||= $self->{file}; open( my $handle, '>', "$file" ) or return 0; my $sids = $self->{sids}; foreach my $server (keys %$sids) { foreach my $user (keys %{ $sids->{$server} }) { my $sid = $sids->{$server}{$user}; if (defined $sid) { print $handle "$server $user $sid\n"; } } } close($handle); chmod 0600, $file; return 1; } sub DESTROY { my $self = shift; $self->save; } } # Form handling. # -------------- # # Forms are RFC822-style sets of (field, value) specifications with some # initial comments and interspersed blank lines allowed for convenience. # Sets of forms are separated by --\n (in a cheap parody of MIME). # # Each form is parsed into an array with four elements: commented text # at the start of the form, an array with the order of keys, a hash with # key/value pairs, and optional error text if the form syntax was wrong. # Returns a reference to an array of parsed forms. sub Form::parse { my $state = 0; my @forms = (); my @lines = split /\n/, $_[0] if $_[0]; my ($c, $o, $k, $e) = ("", [], {}, ""); LINE: while (@lines) { my $line = shift @lines; next LINE if $line eq ''; if ($line eq '--') { # We reached the end of one form. We'll ignore it if it was # empty, and store it otherwise, errors and all. if ($e || $c || @$o) { push @forms, [ $c, $o, $k, $e ]; $c = ""; $o = []; $k = {}; $e = ""; } $state = 0; } elsif ($state != -1) { if ($state == 0 && $line =~ /^#/) { # Read an optional block of comments (only) at the start # of the form. $state = 1; $c = $line; while (@lines && $lines[0] =~ /^#/) { $c .= "\n".shift @lines; } $c .= "\n"; } elsif ($state <= 1 && $line =~ /^($field):(?:\s+(.*))?$/) { # Read a field: value specification. my $f = $1; my @v = (defined $2 && length $2 ? $2 : ()); # Read continuation lines, if any. while (@lines && ($lines[0] eq '' || $lines[0] =~ /^\s+/)) { push @v, shift @lines; } pop @v while (@v && $v[-1] eq ''); # Strip longest common leading indent from text. my $ws = ""; foreach my $ls (map {/^(\s+)/} @v[1..$#v]) { $ws = $ls if (!$ws || length($ls) < length($ws)); } s/^$ws// foreach @v; push(@$o, $f) unless exists $k->{$f}; vpush($k, $f, join("\n", @v)); $state = 1; } elsif ($line !~ /^#/) { # We've found a syntax error, so we'll reconstruct the # form parsed thus far, and add an error marker. (>>) $state = -1; $e = Form::compose([[ "", $o, $k, "" ]]); $e.= $line =~ /^>>/ ? "$line\n" : ">> $line\n"; } } else { # We saw a syntax error earlier, so we'll accumulate the # contents of this form until the end. $e .= "$line\n"; } } push(@forms, [ $c, $o, $k, $e ]) if ($e || $c || @$o); foreach my $l (keys %$k) { $k->{$l} = vsplit($k->{$l}) if (ref $k->{$l} eq 'ARRAY'); } return \@forms; } # Returns text representing a set of forms. sub Form::compose { my ($forms) = @_; my @text; foreach my $form (@$forms) { my ($c, $o, $k, $e) = @$form; my $text = ""; if ($c) { $c =~ s/\n*$/\n/; $text = "$c\n"; } if ($e) { $text .= $e; } elsif ($o) { my @lines; foreach my $key (@$o) { my ($line, $sp); my $v = $k->{$key}; my @values = ref $v eq 'ARRAY' ? @$v : $v; $sp = " "x(length("$key: ")); $sp = " "x4 if length($sp) > 16; foreach $v (@values) { if ($v =~ /\n/) { $v =~ s/^/$sp/gm; $v =~ s/^$sp//; if ($line) { push @lines, "$line\n\n"; $line = ""; } elsif (@lines && $lines[-1] !~ /\n\n$/) { $lines[-1] .= "\n"; } push @lines, "$key: $v\n\n"; } elsif ($line && length($line)+length($v)-rindex($line, "\n") >= 70) { $line .= ",\n$sp$v"; } else { $line = $line ? "$line,$v" : "$key: $v"; } } $line = "$key:" unless @values; if ($line) { if ($line =~ /\n/) { if (@lines && $lines[-1] !~ /\n\n$/) { $lines[-1] .= "\n"; } $line .= "\n"; } push @lines, "$line\n"; } } $text .= join "", @lines; } else { chomp $text; } push @text, $text; } return join "\n--\n\n", @text; } # Configuration. # -------------- # Returns configuration information from the environment. sub config_from_env { my %env; foreach my $k (qw(EXTERNALAUTH AUTH DEBUG USER PASSWD SERVER QUERY ORDERBY)) { if (exists $ENV{"RT$k"}) { $env{lc $k} = $ENV{"RT$k"}; } } return %env; } # Finds a suitable configuration file and returns information from it. sub config_from_file { my ($rc) = @_; if ($rc =~ m#^/#) { # We'll use an absolute path if we were given one. return parse_config_file($rc); } else { # Otherwise we'll use the first file we can find in the current # directory, or in one of its (increasingly distant) ancestors. my @dirs = split /\//, cwd; while (@dirs) { my $file = join('/', @dirs, $rc); if (-r $file) { return parse_config_file($file); } # Remove the last directory component each time. pop @dirs; } # Still nothing? We'll fall back to some likely defaults. for ("$HOME/$rc", "@LOCAL_ETC_PATH@/rt.conf", "/etc/rt.conf") { return parse_config_file($_) if (-r $_); } } return (); } # Makes a hash of the specified configuration file. sub parse_config_file { my %cfg; my ($file) = @_; local $_; # $_ may be aliased to a constant, from line 1163 open( my $handle, '<', $file ) or return; while (<$handle>) { chomp; next if (/^#/ || /^\s*$/); if (/^(externalauth|auth|user|passwd|server|query|orderby|queue)\s+(.*)\s?$/) { $cfg{$1} = $2; } else { die "rt: $file:$.: unknown configuration directive.\n"; } } return %cfg; } # Helper functions. # ----------------- sub whine { my $sub = (caller(1))[3]; $sub =~ s/^main:://; warn "rt: $sub: @_\n"; return 0; } sub read_passwd { eval 'require Term::ReadKey'; if ($@) { die "No password specified (and Term::ReadKey not installed).\n"; } print "Password: "; Term::ReadKey::ReadMode('noecho'); chomp(my $passwd = Term::ReadKey::ReadLine(0)); Term::ReadKey::ReadMode('restore'); print "\n"; return $passwd; } sub vi_form_while { my $text = shift; my $cb = shift; my $error = 0; my ($c, $o, $k, $e); do { my $ntext = vi($text); return undef if ($error && $ntext eq $text); $text = $ntext; my $form = Form::parse($text); $error = 0; ($c, $o, $k, $e) = @{ $form->[0] }; if ( $e ) { $error = 1; $c = "# Syntax error."; goto NEXT; } elsif (!@$o) { return 0; } my ($status, $msg) = $cb->( $text, [$c, $o, $k, $e] ); unless ( $status ) { $error = 1; $c = "# $msg"; } NEXT: $text = Form::compose([[$c, $o, $k, $e]]); } while ($error); return $text; } sub vi { my ($text) = @_; my $editor = $ENV{EDITOR} || $ENV{VISUAL} || "vi"; local $/ = undef; my $handle = File::Temp->new; print $handle $text; close($handle); system($editor, $handle->filename) && die "Couldn't run $editor.\n"; open( $handle, '<', $handle->filename ) or die "$handle: $!\n"; $text = <$handle>; close($handle); return $text; } # Add a value to a (possibly multi-valued) hash key. sub vpush { my ($hash, $key, $val) = @_; my @val = ref $val eq 'ARRAY' ? @$val : $val; if (exists $hash->{$key}) { unless (ref $hash->{$key} eq 'ARRAY') { my @v = $hash->{$key} ne '' ? $hash->{$key} : (); $hash->{$key} = \@v; } push @{ $hash->{$key} }, @val; } else { $hash->{$key} = $val; } } # WARNING: this code is duplicated in lib/RT/Interface/REST.pm # If you change one, change both functions at once # "Normalise" a hash key that's known to be multi-valued. sub vsplit { my ($val, $strip) = @_; my @words; my @values = map {split /\n/} (ref $val eq 'ARRAY' ? @$val : $val); foreach my $line (@values) { while ($line =~ /\S/) { $line =~ s/^ \s* # Trim leading whitespace (?: (") # Quoted string ((?>[^\\"]*(?:\\.[^\\"]*)*))" | (') # Single-quoted string ((?>[^\\']*(?:\\.[^\\']*)*))' | q\{(.*?)\} # A perl-ish q{} string; this does # no paren balancing, however, and # only exists for back-compat | (.*?) # Anything else, until the next comma ) \s* # Trim trailing whitespace (?: \Z # Finish at end-of-line | , # Or a comma ) //xs or last; # There should be no way this match # fails, but add a failsafe to # prevent infinite-looping if it # somehow does. my ($quote, $quoted) = ($1 ? ($1, $2) : $3 ? ($3, $4) : ('', $5 || $6)); # Only unquote the quote character, or the backslash -- and # only if we were originally quoted.. if ($5) { $quoted =~ s/([\\'])/\\$1/g; $quote = "'"; } if ($strip) { $quoted =~ s/\\([\\$quote])/$1/g if $quote; push @words, $quoted; } else { push @words, "$quote$quoted$quote"; } } } return \@words; } # WARN: this code is duplicated in lib/RT/Interface/REST.pm # change both functions at once sub expand_list { my ($list) = @_; my @elts; foreach (split /\s*,\s*/, $list) { push @elts, /^(\d+)-(\d+)$/? ($1..$2): $_; } return map $_->[0], # schwartzian transform sort { defined $a->[1] && defined $b->[1]? # both numbers $a->[1] <=> $b->[1] :!defined $a->[1] && !defined $b->[1]? # both letters $a->[2] cmp $b->[2] # mix, number must be first :defined $a->[1]? -1: 1 } map [ $_, (defined( /^(\d+)$/ )? $1: undef), lc($_) ], @elts; } sub get_type_argument { my $type; if (@ARGV) { $type = shift @ARGV; unless ($type =~ /^[A-Za-z0-9_.-]+$/) { # We want whine to mention our caller, not us. @_ = ("Invalid type '$type' specified."); goto &whine; } } else { @_ = ("No type argument specified with -t."); goto &whine; } $type =~ s/s$//; # "Plural". Ugh. return $type; } sub get_var_argument { my ($data) = @_; if (@ARGV) { my $kv = shift @ARGV; if (my ($k, $v) = $kv =~ /^($field)=(.*)$/) { push @{ $data->{$k} }, $v; } else { @_ = ("Invalid variable specification: '$kv'."); goto &whine; } } else { @_ = ("No variable argument specified with -S."); goto &whine; } } sub is_object_spec { my ($spec, $type) = @_; $spec =~ s|^(?:$type/)?|$type/| if defined $type; return $spec if ($spec =~ m{^$name/(?:$idlist|$labels)(?:/.*)?$}o); return 0; } sub suggest_help { my ($action, $type, $rv) = @_; print STDERR "rt: For help, run 'rt help $action'.\n" if defined $action; print STDERR "rt: For help, run 'rt help $type'.\n" if defined $type; return $rv; } sub str2time { # simplified procedure for parsing date, avoid loading Date::Parse my %month = (Jan => 0, Feb => 1, Mar => 2, Apr => 3, May => 4, Jun => 5, Jul => 6, Aug => 7, Sep => 8, Oct => 9, Nov => 10, Dec => 11); $_ = shift; my ($mon, $day, $hr, $min, $sec, $yr, $monstr); if ( /(\w{3})\s+(\d\d?)\s+(\d\d):(\d\d):(\d\d)\s+(\d{4})/ ) { ($monstr, $day, $hr, $min, $sec, $yr) = ($1, $2, $3, $4, $5, $6); $mon = $month{$monstr} if exists $month{$monstr}; } elsif ( /(\d{4})-(\d\d)-(\d\d)\s+(\d\d):(\d\d):(\d\d)/ ) { ($yr, $mon, $day, $hr, $min, $sec) = ($1, $2-1, $3, $4, $5, $6); } if ( $yr and defined $mon and $day and defined $hr and defined $sec ) { return timelocal($sec,$min,$hr,$day,$mon,$yr); } else { print "Unknown date format in parsedate: $_\n"; return undef; } } sub date_diff { my ($old, $new) = @_; $new = time() if ! $new; $old = str2time($old) if $old !~ /^\d+$/; $new = str2time($new) if $new !~ /^\d+$/; return "???" if ! $old or ! $new; my %seconds = (min => 60, hr => 60*60, day => 60*60*24, wk => 60*60*24*7, mth => 60*60*24*30, yr => 60*60*24*365); my $diff = $new - $old; my $what = 'sec'; my $howmuch = $diff; for ( sort {$seconds{$a} <=> $seconds{$b}} keys %seconds) { last if $diff < $seconds{$_}; $what = $_; $howmuch = int($diff/$seconds{$_}); } return "$howmuch $what"; } sub prettyshow { my $forms = shift; my ($form) = grep { exists $_->[2]->{Queue} } @$forms; my $k = $form->[2]; # dates are in local time zone if ( $k ) { print "Date: $k->{Created}\n"; print "From: $k->{Requestors}\n"; print "Cc: $k->{Cc}\n" if $k->{Cc}; print "X-AdminCc: $k->{AdminCc}\n" if $k->{AdminCc}; print "X-Queue: $k->{Queue}\n"; print "Subject: [rt #$k->{id}] $k->{Subject}\n\n"; } # dates in these attributes are in GMT and will be converted foreach my $form (@$forms) { my ($c, $o, $k, $e) = @$form; next if ! $k->{id} or exists $k->{Queue}; if ( exists $k->{Created} ) { my ($y,$m,$d,$hh,$mm,$ss) = ($k->{Created} =~ /(\d\d\d\d)-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)/); $m--; my $created = localtime(timegm($ss,$mm,$hh,$d,$m,$y)); if ( exists $k->{Description} ) { print "===> $k->{Description} on $created\n"; } } print "$k->{Content}\n" if exists $k->{Content} and $k->{Content} !~ /to have no content$/ and ($k->{Type}||'') ne 'EmailRecord'; print "$k->{Attachments}\n" if exists $k->{Attachments} and $k->{Attachments}; } } sub prettylist { my $forms = shift; my $heading = "Ticket Owner Queue Age Told Status Requestor Subject\n"; $heading .= '-' x 80 . "\n"; my (@open, @me); foreach my $form (@$forms) { my ($c, $o, $k, $e) = @$form; next if ! $k->{id}; print $heading if $heading; $heading = ''; my $id = $k->{id}; $id =~ s!^ticket/!!; my $owner = $k->{Owner} eq 'Nobody' ? '' : $k->{Owner}; $owner = substr($owner, 0, 5); my $queue = substr($k->{Queue}, 0, 5); my $subject = substr($k->{Subject}, 0, 30); my $age = date_diff($k->{Created}); my $told = $k->{Told} eq 'Not set' ? '' : date_diff($k->{Told}); my $status = substr($k->{Status}, 0, 6); my $requestor = substr($k->{Requestors}, 0, 9); my $line = sprintf "%6s %5s %5s %6s %6s %-6s %-9s %-30s\n", $id, $owner, $queue, $age, $told, $status, $requestor, $subject; if ( $k->{Owner} eq 'Nobody' ) { push @open, $line; } elsif ($k->{Owner} eq $config{user} ) { push @me, $line; } else { print $line; } } print "No matches found\n" if $heading; printf "========== my %2d open tickets ==========\n", scalar @me if @me; print @me if @me; printf "========== %2d unowned tickets ==========\n", scalar @open if @open; print @open if @open; } __DATA__ Title: intro Title: introduction Text: This is a command-line interface to RT 3.0 or newer. It allows you to interact with an RT server over HTTP, and offers an interface to RT's functionality that is better-suited to automation and integration with other tools. In general, each invocation of this program should specify an action to perform on one or more objects, and any other arguments required to complete the desired action. For more information: - rt help usage (syntax information) - rt help objects (how to specify objects) - rt help actions (a list of possible actions) - rt help types (a list of object types) - rt help config (configuration details) - rt help examples (a few useful examples) - rt help topics (a list of help topics) -- Title: usage Title: syntax Text: Syntax: rt [options] [arguments] or rt shell Each invocation of this program must specify an action (e.g. "edit", "create"), options to modify behaviour, and other arguments required by the specified action. (For example, most actions expect a list of numeric object IDs to act upon.) The details of the syntax and arguments for each action are given by "rt help ". Some actions may be referred to by more than one name ("create" is the same as "new", for example). You may also call "rt shell", which will give you an 'rt>' prompt at which you can issue commands of the form " [options] [arguments]". See "rt help shell" for details. Objects are identified by a type and an ID (which can be a name or a number, depending on the type). For some actions, the object type is implied (you can only comment on tickets); for others, the user must specify it explicitly. See "rt help objects" for details. In syntax descriptions, mandatory arguments that must be replaced by appropriate value are enclosed in <>, and optional arguments are indicated by [] (for example, and [options] above). For more information: - rt help objects (how to specify objects) - rt help actions (a list of actions) - rt help types (a list of object types) - rt help shell (how to use the shell) -- Title: conf Title: config Title: configuration Text: This program has two major sources of configuration information: its configuration files, and the environment. The program looks for configuration directives in a file named .rtrc (or $RTCONFIG; see below) in the current directory, and then in more distant ancestors, until it reaches /. If no suitable configuration files are found, it will also check for ~/.rtrc, @LOCAL_ETC_PATH@/rt.conf and /etc/rt.conf. Configuration directives: The following directives may occur, one per line: - server URL to RT server. - user RT username. - passwd RT user's password. - query Default RT Query for list action - orderby Default RT order for list action - queue Default RT Queue for list action - auth Method to authenticate via; "basic" means HTTP Basic authentication, "gssapi" means Kerberos credentials, if your RT is configured with $WebRemoteUserAuth. For backwards compatibility, "externalauth 1" means "auth basic" Blank and #-commented lines are ignored. Sample configuration file contents: server https://rt.somewhere.com/ # more than one queue can be given (by adding a query expression) queue helpdesk or queue=support query Status != resolved and Owner=myaccount Environment variables: The following environment variables override any corresponding values defined in configuration files: - RTUSER - RTPASSWD - RTAUTH - RTSERVER - RTDEBUG Numeric debug level. (Set to 3 for full logs.) - RTCONFIG Specifies a name other than ".rtrc" for the configuration file. - RTQUERY Default RT Query for rt list - RTORDERBY Default order for rt list -- Title: objects Text: Syntax: /[/] Every object in RT has a type (e.g. "ticket", "queue") and a numeric ID. Some types of objects can also be identified by name (like users and queues). Furthermore, objects may have named attributes (such as "ticket/1/history"). An object specification is like a path in a virtual filesystem, with object types as top-level directories, object IDs as subdirectories, and named attributes as further subdirectories. A comma-separated list of names, numeric IDs, or numeric ranges can be used to specify more than one object of the same type. Note that the list must be a single argument (i.e., no spaces). For example, "user/root,1-3,5,7-10,ams" is a list of ten users; the same list can also be written as "user/ams,root,1,2,3,5,7,8-10". If just a number is given as object specification it will be interpreted as ticket/ Examples: 1 # the same as ticket/1 ticket/1 ticket/1/attachments ticket/1/attachments/3 ticket/1/attachments/3/content ticket/1-3/links ticket/1-3,5-7/history user/ams For more information: - rt help (action-specific details) - rt help (type-specific details) -- Title: actions Title: commands Text: You can currently perform the following actions on all objects: - list (list objects matching some condition) - show (display object details) - edit (edit object details) - create (create a new object) Each type may define actions specific to itself; these are listed in the help item about that type. For more information: - rt help (action-specific details) - rt help types (a list of possible types) The following actions on tickets are also possible: - comment Add comments to a ticket - correspond Add comments to a ticket - merge Merge one ticket into another - link Link one ticket to another - take Take a ticket (steal and untake are possible as well) For several edit set subcommands that are frequently used abbreviations have been introduced. These abbreviations are: - delete or del delete a ticket (edit set status=deleted) - resolve or res resolve a ticket (edit set status=resolved) - subject change subject of ticket (edit set subject=string) - give give a ticket to somebody (edit set owner=user) -- Title: types Text: You can currently operate on the following types of objects: - tickets - users - groups - queues For more information: - rt help (type-specific details) - rt help objects (how to specify objects) - rt help actions (a list of possible actions) -- Title: ticket Text: Tickets are identified by a numeric ID. The following generic operations may be performed upon tickets: - list - show - edit - create In addition, the following ticket-specific actions exist: - link - merge - comment - correspond - take - steal - untake - give - resolve - delete - subject Attributes: The following attributes can be used with "rt show" or "rt edit" to retrieve or edit other information associated with tickets: links A ticket's relationships with others. history All of a ticket's transactions. history/type/ Only a particular type of transaction. history/id/ Only the transaction of the specified id. attachments A list of attachments. attachments/ The metadata for an individual attachment. attachments//content The content of an individual attachment. -- Title: user Title: group Text: Users and groups are identified by name or numeric ID. The following generic operations may be performed upon them: - list - show - edit - create -- Title: queue Text: Queues are identified by name or numeric ID. Currently, they can be subjected to the following actions: - show - edit - create -- Title: subject Text: Syntax: rt subject Change the subject of a ticket whose ticket id is given. -- Title: give Text: Syntax: rt give Give a ticket whose ticket id is given to another user. -- Title: steal Text: rt steal Steal a ticket whose ticket id is given, i.e. set the owner to myself. -- Title: take Text: Syntax: rt take Take a ticket whose ticket id is given, i.e. set the owner to myself. -- Title: untake Text: Syntax: rt untake Untake a ticket whose ticket id is given, i.e. set the owner to Nobody. -- Title: resolve Title: res Text: Syntax: rt resolve Resolves a ticket whose ticket id is given. -- Title: delete Title: del Text: Syntax: rt delete Deletes a ticket whose ticket id is given. -- Title: logout Text: Syntax: rt logout Terminates the currently established login session. You will need to provide authentication credentials before you can continue using the server. (See "rt help config" for details about authentication.) -- Title: ls Title: list Title: search Text: Syntax: rt [options] "query string" Displays a list of objects matching the specified conditions. ("ls", "list", and "search" are synonyms.) The query string must be supplied as one argument. if on tickets, query is in the SQL-like syntax used internally by RT. (For more information, see "rt help query".), otherwise, query is plain string with format "FIELD OP VALUE", e.g. "Name = General". if query string is absent, we limit to privileged ones on users and user defined ones on groups automatically. Options: The following options control how much information is displayed about each matching object: -i Numeric IDs only. (Useful for |rt edit -; see examples.) -s Short description. -l Longer description. -f Orders the returned list by the specified field. -r reversed order (useful if a default was given) -q queue[s] restricts the query to the queue[s] given multiple queues are separated by comma -S var=val Submits the specified variable with the request. -t type Specifies the type of object to look for. (The default is "ticket".) Examples: rt ls "Priority > 5 and Status=new" rt ls -o +Subject "Priority > 5 and Status=new" rt ls -o -Created "Priority > 5 and Status=new" rt ls -i "Priority > 5"|rt edit - set status=resolved rt ls -t ticket "Subject like '[PATCH]%'" rt ls -q systems rt ls -f owner,subject rt ls -t queue 'Name = General' rt ls -t user 'EmailAddress like foo@bar.com' rt ls -t group 'Name like foo' -- Title: show Text: Syntax: rt show [options] Displays details of the specified objects. For some types, object information is further classified into named attributes (for example, "1-3/links" is a valid ticket specification that refers to the links for tickets 1-3). Consult "rt help " and "rt help objects" for further details. If only a number is given it will be interpreted as the objects ticket/number and ticket/number/history This command writes a set of forms representing the requested object data to STDOUT. Options: The following options control how much information is displayed about each matching object: Without any formatting options prettyprinted output is generated. Giving any of the two options below reverts to raw output. -s Short description (history and attachments only). -l Longer description (history and attachments only). In addition, - Read IDs from STDIN instead of the command-line. -t type Specifies object type. -f a,b,c Restrict the display to the specified fields. -S var=val Submits the specified variable with the request. Examples: rt show -t ticket -f id,subject,status 1-3 rt show ticket/3/attachments/29 rt show ticket/3/attachments/29/content rt show ticket/1-3/links rt show ticket/3/history rt show -l ticket/3/history rt show -t user 2 rt show 2 -- Title: new Title: edit Title: create Text: Syntax: rt edit [options] set field=value [field=value] ... add field=value [field=value] ... del field=value [field=value] ... Edits information corresponding to the specified objects. A purely numeric object id nnn is translated into ticket/nnn If, instead of "edit", an action of "new" or "create" is specified, then a new object is created. In this case, no numeric object IDs may be specified, but the syntax and behaviour remain otherwise unchanged. This command typically starts an editor to allow you to edit object data in a form for submission. If you specified enough information on the command-line, however, it will make the submission directly. The command line may specify field-values in three different ways. "set" sets the named field to the given value, "add" adds a value to a multi-valued field, and "del" deletes the corresponding value. Each "field=value" specification must be given as a single argument. For some types, object information is further classified into named attributes (for example, "1-3/links" is a valid ticket specification that refers to the links for tickets 1-3). These attributes may also be edited. Consult "rt help " and "rt help object" for further details. Options: - Read numeric IDs from STDIN instead of the command-line. (Useful with rt ls ... | rt edit -; see examples below.) -i Read a completed form from STDIN before submitting. -o Dump the completed form to STDOUT instead of submitting. -e Allows you to edit the form even if the command-line has enough information to make a submission directly. -S var=val Submits the specified variable with the request. -t type Specifies object type. -ct content-type Specifies content type of message(tickets only). Examples: # Interactive (starts $EDITOR with a form). rt edit ticket/3 rt create -t ticket rt create -t ticket -ct text/html # Non-interactive. rt edit ticket/1-3 add cc=foo@example.com set priority=3 due=tomorrow rt ls -t tickets -i 'Priority > 5' | rt edit - set status=resolved rt edit ticket/4 set priority=3 owner=bar@example.com \ add cc=foo@example.com bcc=quux@example.net rt create -t ticket set subject='new ticket' priority=10 \ add cc=foo@example.com -- Title: comment Title: correspond Text: Syntax: rt [options] Adds a comment (or correspondence) to the specified ticket (the only difference being that comments aren't sent to the requestors.) This command will typically start an editor and allow you to type a comment into a form. If, however, you specified all the necessary information on the command line, it submits the comment directly. (See "rt help forms" for more information about forms.) Options: -m Specify comment text. -ct Specify content-type of comment text. -a Attach a file to the comment. (May be used more than once to attach multiple files.) -c A comma-separated list of Cc addresses. -b A comma-separated list of Bcc addresses. -s Set a new status for the ticket (default will leave the status unchanged) -w