diff options
Diffstat (limited to 'rt/lib/RT')
-rw-r--r-- | rt/lib/RT/Action/SetStatus.pm | 152 | ||||
-rw-r--r-- | rt/lib/RT/Article.pm | 870 | ||||
-rw-r--r-- | rt/lib/RT/Articles.pm | 925 | ||||
-rw-r--r-- | rt/lib/RT/Class.pm | 620 | ||||
-rw-r--r-- | rt/lib/RT/Classes.pm | 104 | ||||
-rw-r--r-- | rt/lib/RT/Dashboard/Mailer.pm | 577 | ||||
-rw-r--r-- | rt/lib/RT/Dashboards.pm | 112 | ||||
-rw-r--r-- | rt/lib/RT/Generated.pm | 81 | ||||
-rw-r--r-- | rt/lib/RT/Generated.pm.in | 81 | ||||
-rw-r--r-- | rt/lib/RT/Lifecycle.pm | 670 | ||||
-rw-r--r-- | rt/lib/RT/ObjectClass.pm | 222 | ||||
-rw-r--r-- | rt/lib/RT/ObjectClasses.pm | 87 | ||||
-rw-r--r-- | rt/lib/RT/ObjectTopic.pm | 214 | ||||
-rw-r--r-- | rt/lib/RT/ObjectTopics.pm | 115 | ||||
-rw-r--r-- | rt/lib/RT/SharedSettings.pm | 155 | ||||
-rw-r--r-- | rt/lib/RT/Squish.pm | 122 | ||||
-rw-r--r-- | rt/lib/RT/Squish/CSS.pm | 105 | ||||
-rw-r--r-- | rt/lib/RT/Squish/JS.pm | 127 | ||||
-rw-r--r-- | rt/lib/RT/Test/Apache.pm | 270 | ||||
-rw-r--r-- | rt/lib/RT/Test/GnuPG.pm | 360 | ||||
-rw-r--r-- | rt/lib/RT/Tickets_SQL.pm | 423 | ||||
-rw-r--r-- | rt/lib/RT/Topic.pm | 376 | ||||
-rw-r--r-- | rt/lib/RT/Topics.pm | 119 | ||||
-rw-r--r-- | rt/lib/RT/URI/a.pm | 92 | ||||
-rw-r--r-- | rt/lib/RT/URI/fsck_com_article.pm | 216 |
25 files changed, 7195 insertions, 0 deletions
diff --git a/rt/lib/RT/Action/SetStatus.pm b/rt/lib/RT/Action/SetStatus.pm new file mode 100644 index 000000000..f52d401cc --- /dev/null +++ b/rt/lib/RT/Action/SetStatus.pm @@ -0,0 +1,152 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Action::SetStatus; + +use strict; +use warnings; +use base qw(RT::Action); + +=head1 NAME + +RT::Action::SetStatus - RT's scrip action to set status of a ticket + +=head1 DESCRIPTION + +This action changes status to a new value according to the rules in L</ARGUMENT>. +Status is not changed if the transition is invalid or another error occurs. All +issues are logged at apropriate levels. + +=head1 ARGUMENT + +Argument can be one of the following: + +=over 4 + +=item status literally + +Status is changed from the current value to a new defined by the argument, +but only if it's valid status and allowed by transitions of the current lifecycle, +for example: + + * The current status is 'stalled' + * Argument of this action is 'open' + * The only possible transition in the scheam from 'stalled' is 'open' + * Status is changed + +However, in the example above Status is not changed if argument is anything +else as it's just not allowed by the lifecycle. + +=item 'initial', 'active' or 'inactive' + +Status is changed from the current value to first possible 'initial', +'active' or 'inactive' correspondingly. First possible value is figured +according to transitions to the target set, for example: + + * The current status is 'open' + * Argument of this action is 'inactive' + * Possible transitions from 'open' are 'resolved', 'rejected' or 'deleted' + * Status is changed to 'resolved' + +=back + +=cut + +sub Prepare { + my $self = shift; + + my $ticket = $self->TicketObj; + my $lifecycle = $ticket->QueueObj->Lifecycle; + my $status = $ticket->Status; + + my $argument = $self->Argument; + unless ( $argument ) { + $RT::Logger->error("Argument is mandatory for SetStatus action"); + return 0; + } + + my $next = ''; + if ( $argument =~ /^(initial|active|inactive)$/i ) { + my $method = 'Is'. ucfirst lc $argument; + ($next) = grep $lifecycle->$method($_), $lifecycle->Transitions($status); + unless ( $next ) { + $RT::Logger->info("No transition from '$status' to $argument set"); + return 1; + } + } + elsif ( $lifecycle->IsValid( $argument ) ) { + unless ( $lifecycle->IsTransition( $status => $argument ) ) { + $RT::Logger->warning("Transition '$status -> $argument' is not valid"); + return 1; + } + $next = $argument; + } + else { + $RT::Logger->error("Argument for SetStatus action is not valid status or one of set"); + return 0; + } + + $self->{'set_status_to'} = $next; + + return 1; +} + +sub Commit { + my $self = shift; + + return 1 unless my $new_status = $self->{'set_status_to'}; + + my ($val, $msg) = $self->TicketObj->SetStatus( $new_status ); + unless ( $val ) { + $RT::Logger->error( "Couldn't set status: ". $msg ); + return 0; + } + return 1; +} + +1; diff --git a/rt/lib/RT/Article.pm b/rt/lib/RT/Article.pm new file mode 100644 index 000000000..7310241ee --- /dev/null +++ b/rt/lib/RT/Article.pm @@ -0,0 +1,870 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Article; + +use base 'RT::Record'; + +use RT::Articles; +use RT::ObjectTopics; +use RT::Classes; +use RT::Links; +use RT::CustomFields; +use RT::URI::fsck_com_article; +use RT::Transactions; + + +sub Table {'Articles'} + +# This object takes custom fields + +use RT::CustomField; +RT::CustomField->_ForObjectType( CustomFieldLookupType() => 'Articles' ) + ; #loc + +# {{{ Create + +=head2 Create PARAMHASH + +Create takes a hash of values and creates a row in the database: + + varchar(200) 'Name'. + varchar(200) 'Summary'. + int(11) 'Content'. + Class ID 'Class' + + A paramhash called 'CustomFields', which contains + arrays of values for each custom field you want to fill in. + Arrays aRe ordered. + + + + +=cut + +sub Create { + my $self = shift; + my %args = ( + Name => '', + Summary => '', + Class => '0', + CustomFields => {}, + Links => {}, + Topics => [], + @_ + ); + + my $class = RT::Class->new($RT::SystemUser); + $class->Load( $args{'Class'} ); + unless ( $class->Id ) { + return ( 0, $self->loc('Invalid Class') ); + } + + unless ( $class->CurrentUserHasRight('CreateArticle') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + return ( undef, $self->loc('Name in use') ) + unless $self->ValidateName( $args{'Name'} ); + + $RT::Handle->BeginTransaction(); + my ( $id, $msg ) = $self->SUPER::Create( + Name => $args{'Name'}, + Class => $class->Id, + Summary => $args{'Summary'}, + ); + unless ($id) { + $RT::Handle->Rollback(); + return ( undef, $msg ); + } + + # {{{ Add custom fields + + foreach my $key ( keys %args ) { + next unless ( $key =~ /CustomField-(.*)$/ ); + my $cf = $1; + my @vals = ref( $args{$key} ) eq 'ARRAY' ? @{ $args{$key} } : ( $args{$key} ); + foreach my $value (@vals) { + + my ( $cfid, $cfmsg ) = $self->_AddCustomFieldValue( + (UNIVERSAL::isa( $value => 'HASH' ) + ? %$value + : (Value => $value) + ), + Field => $cf, + RecordTransaction => 0 + ); + + unless ($cfid) { + $RT::Handle->Rollback(); + return ( undef, $cfmsg ); + } + } + + } + + # }}} + # {{{ Add topics + + foreach my $topic ( @{ $args{Topics} } ) { + my ( $cfid, $cfmsg ) = $self->AddTopic( Topic => $topic ); + + unless ($cfid) { + $RT::Handle->Rollback(); + return ( undef, $cfmsg ); + } + } + + # }}} + # {{{ Add relationships + + foreach my $type ( keys %args ) { + next unless ( $type =~ /^(RefersTo-new|new-RefersTo)$/ ); + my @vals = + ref( $args{$type} ) eq 'ARRAY' ? @{ $args{$type} } : ( $args{$type} ); + foreach my $val (@vals) { + my ( $base, $target ); + if ( $type =~ /^new-(.*)$/ ) { + $type = $1; + $base = undef; + $target = $val; + } + elsif ( $type =~ /^(.*)-new$/ ) { + $type = $1; + $base = $val; + $target = undef; + } + + my ( $linkid, $linkmsg ) = $self->AddLink( + Type => $type, + Target => $target, + Base => $base, + RecordTransaction => 0 + ); + + unless ($linkid) { + $RT::Handle->Rollback(); + return ( undef, $linkmsg ); + } + } + + } + + # }}} + + # We override the URI lookup. the whole reason + # we have a URI column is so that joins on the links table + # aren't expensive and stupid + $self->__Set( Field => 'URI', Value => $self->URI ); + + my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' ); + unless ($txn_id) { + $RT::Handle->Rollback(); + return ( undef, $self->loc( 'Internal error: [_1]', $txn_msg ) ); + } + $RT::Handle->Commit(); + + return ( $id, $self->loc('Article [_1] created',$self->id )); +} + +# }}} + +# {{{ ValidateName + +=head2 ValidateName NAME + +Takes a string name. Returns true if that name isn't in use by another article + +Empty names are permitted. + + +=cut + +sub ValidateName { + my $self = shift; + my $name = shift; + + if ( !$name ) { + return (1); + } + + my $temp = RT::Article->new($RT::SystemUser); + $temp->LoadByCols( Name => $name ); + if ( $temp->id && + (!$self->id || ($temp->id != $self->id ))) { + return (undef); + } + + return (1); + +} + +# }}} + +# {{{ Delete + +=head2 Delete + +Delete all its transactions +Delete all its custom field values +Delete all its relationships +Delete this article. + +=cut + +sub Delete { + my $self = shift; + unless ( $self->CurrentUserHasRight('DeleteArticle') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + $RT::Handle->BeginTransaction(); + my $linksto = $self->_Links( 'Target' ); + my $linksfrom = $self->_Links( 'Base' ); + my $cfvalues = $self->CustomFieldValues; + my $txns = $self->Transactions; + my $topics = $self->Topics; + + while ( my $item = $linksto->Next ) { + my ( $val, $msg ) = $item->Delete(); + unless ($val) { + $RT::Logger->crit( ref($item) . ": $msg" ); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Internal Error') ); + } + } + + while ( my $item = $linksfrom->Next ) { + my ( $val, $msg ) = $item->Delete(); + unless ($val) { + $RT::Logger->crit( ref($item) . ": $msg" ); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Internal Error') ); + } + } + + while ( my $item = $txns->Next ) { + my ( $val, $msg ) = $item->Delete(); + unless ($val) { + $RT::Logger->crit( ref($item) . ": $msg" ); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Internal Error') ); + } + } + + while ( my $item = $cfvalues->Next ) { + my ( $val, $msg ) = $item->Delete(); + unless ($val) { + $RT::Logger->crit( ref($item) . ": $msg" ); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Internal Error') ); + } + } + + while ( my $item = $topics->Next ) { + my ( $val, $msg ) = $item->Delete(); + unless ($val) { + $RT::Logger->crit( ref($item) . ": $msg" ); + $RT::Handle->Rollback(); + return ( 0, $self->loc('Internal Error') ); + } + } + + $self->SUPER::Delete(); + $RT::Handle->Commit(); + return ( 1, $self->loc('Article Deleted') ); + +} + +# }}} + +# {{{ Children + +=head2 Children + +Returns an RT::Articles object which contains +all articles which have this article as their parent. This +routine will not recurse and will not find grandchildren, great-grandchildren, uncles, aunts, nephews or any other such thing. + +=cut + +sub Children { + my $self = shift; + my $kids = RT::Articles->new( $self->CurrentUser ); + + unless ( $self->CurrentUserHasRight('ShowArticle') ) { + $kids->LimitToParent( $self->Id ); + } + return ($kids); +} + +# }}} + +# {{{ sub AddLink + +=head2 AddLink + +Takes a paramhash of Type and one of Base or Target. Adds that link to this tick +et. + +=cut + +sub DeleteLink { + my $self = shift; + my %args = ( + Target => '', + Base => '', + Type => '', + Silent => undef, + @_ + ); + + unless ( $self->CurrentUserHasRight('ModifyArticle') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + $self->_DeleteLink(%args); +} + +sub AddLink { + my $self = shift; + my %args = ( + Target => '', + Base => '', + Type => '', + Silent => undef, + @_ + ); + + unless ( $self->CurrentUserHasRight('ModifyArticle') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + # Disallow parsing of plain numbers in article links. If they are + # allowed, they default to being tickets instead of articles, which + # is counterintuitive. + if ( $args{'Target'} && $args{'Target'} =~ /^\d+$/ + || $args{'Base'} && $args{'Base'} =~ /^\d+$/ ) + { + return ( 0, $self->loc("Cannot add link to plain number") ); + } + + # Check that we're actually getting a valid URI + my $uri_obj = RT::URI->new( $self->CurrentUser ); + $uri_obj->FromURI( $args{'Target'}||$args{'Base'} ); + unless ( $uri_obj->Resolver && $uri_obj->Scheme ) { + my $msg = $self->loc( "Couldn't resolve '[_1]' into a Link.", $args{'Target'} ); + $RT::Logger->warning( $msg ); + return( 0, $msg ); + } + + + $self->_AddLink(%args); +} + +sub URI { + my $self = shift; + + unless ( $self->CurrentUserHasRight('ShowArticle') ) { + return $self->loc("Permission Denied"); + } + + my $uri = RT::URI::fsck_com_article->new( $self->CurrentUser ); + return ( $uri->URIForObject($self) ); +} + +# }}} + +# {{{ sub URIObj + +=head2 URIObj + +Returns this article's URI + + +=cut + +sub URIObj { + my $self = shift; + my $uri = RT::URI->new( $self->CurrentUser ); + if ( $self->CurrentUserHasRight('ShowArticle') ) { + $uri->FromObject($self); + } + + return ($uri); +} + +# }}} +# }}} + +# {{{ Topics + +# {{{ Topics +sub Topics { + my $self = shift; + + my $topics = RT::ObjectTopics->new( $self->CurrentUser ); + if ( $self->CurrentUserHasRight('ShowArticle') ) { + $topics->LimitToObject($self); + } + return $topics; +} + +# }}} + +# {{{ AddTopic +sub AddTopic { + my $self = shift; + my %args = (@_); + + unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + my $t = RT::ObjectTopic->new( $self->CurrentUser ); + my ($tid) = $t->Create( + Topic => $args{'Topic'}, + ObjectType => ref($self), + ObjectId => $self->Id + ); + if ($tid) { + return ( $tid, $self->loc("Topic membership added") ); + } + else { + return ( 0, $self->loc("Unable to add topic membership") ); + } +} + +# }}} + +sub DeleteTopic { + my $self = shift; + my %args = (@_); + + unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + my $t = RT::ObjectTopic->new( $self->CurrentUser ); + $t->LoadByCols( + Topic => $args{'Topic'}, + ObjectId => $self->Id, + ObjectType => ref($self) + ); + if ( $t->Id ) { + my $del = $t->Delete; + unless ($del) { + return ( + undef, + $self->loc( + "Unable to delete topic membership in [_1]", + $t->TopicObj->Name + ) + ); + } + else { + return ( 1, $self->loc("Topic membership removed") ); + } + } + else { + return ( + undef, + $self->loc( + "Couldn't load topic membership while trying to delete it") + ); + } +} + +=head2 CurrentUserHasRight + +Returns true if the current user has the right for this article, for the whole system or for this article's class + +=cut + +sub CurrentUserHasRight { + my $self = shift; + my $right = shift; + + return ( + $self->CurrentUser->HasRight( + Right => $right, + Object => $self, + EquivObjects => [ $RT::System, $RT::System, $self->ClassObj ] + ) + ); + +} + +# }}} + +# {{{ _Set + +=head2 _Set { Field => undef, Value => undef + +Internal helper method to record a transaction as we update some core field of the article + + +=cut + +sub _Set { + my $self = shift; + my %args = ( + Field => undef, + Value => undef, + @_ + ); + + unless ( $self->CurrentUserHasRight('ModifyArticle') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + $self->_NewTransaction( + Type => 'Set', + Field => $args{'Field'}, + NewValue => $args{'Value'}, + OldValue => $self->__Value( $args{'Field'} ) + ); + + return ( $self->SUPER::_Set(%args) ); + +} + +=head2 _Value PARAM + +Return "PARAM" for this object. if the current user doesn't have rights, returns undef + +=cut + +sub _Value { + my $self = shift; + my $arg = shift; + unless ( ( $arg eq 'Class' ) + || ( $self->CurrentUserHasRight('ShowArticle') ) ) + { + return (undef); + } + return $self->SUPER::_Value($arg); +} + +# }}} + +sub CustomFieldLookupType { + "RT::Class-RT::Article"; +} + +# _LookupId is the id of the toplevel type object the customfield is joined to +# in this case, that's an RT::Class. + +sub _LookupId { + my $self = shift; + return $self->ClassObj->id; + +} + +=head2 LoadByInclude Field Value + +Takes the name of a form field from "Include Article" +and the value submitted by the browser and attempts to load an Article. + +This handles Articles included by searching, by the Name and via +the hotlist. + +If you optionaly pass an id as the Queue argument, this will check that +the Article's Class is applied to that Queue. + +=cut + +sub LoadByInclude { + my $self = shift; + my %args = @_; + my $Field = $args{Field}; + my $Value = $args{Value}; + my $Queue = $args{Queue}; + + return unless $Field; + + my ($ok, $msg); + if ( $Field eq 'Articles-Include-Article' && $Value ) { + ($ok, $msg) = $self->Load( $Value ); + } elsif ( $Field =~ /^Articles-Include-Article-(\d+)$/ ) { + ($ok, $msg) = $self->Load( $1 ); + } elsif ( $Field =~ /^Articles-Include-Article-Named/ && $Value ) { + if ( $Value =~ /\D/ ) { + ($ok, $msg) = $self->LoadByCols( Name => $Value ); + } else { + ($ok, $msg) = $self->LoadByCols( id => $Value ); + } + } + + unless ($ok) { # load failed, don't check Class + return ($ok, $msg); + } + + unless ($Queue) { # we haven't requested extra sanity checking + return ($ok, $msg); + } + + # ensure that this article is available for the Queue we're + # operating under. + my $class = $self->ClassObj; + unless ($class->IsApplied(0) || $class->IsApplied($Queue)) { + $self->LoadById(0); + return (0, $self->loc("The Class of the Article identified by [_1] is not applied to the current Queue",$Value)); + } + + return ($ok, $msg); + +} + + +=head2 id + +Returns the current value of id. +(In the database, id is stored as int(11).) + + +=cut + + +=head2 Name + +Returns the current value of Name. +(In the database, Name is stored as varchar(255).) + + + +=head2 SetName VALUE + + +Set Name to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Name will be stored as a varchar(255).) + + +=cut + + +=head2 Summary + +Returns the current value of Summary. +(In the database, Summary is stored as varchar(255).) + + + +=head2 SetSummary VALUE + + +Set Summary to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Summary will be stored as a varchar(255).) + + +=cut + + +=head2 SortOrder + +Returns the current value of SortOrder. +(In the database, SortOrder is stored as int(11).) + + + +=head2 SetSortOrder VALUE + + +Set SortOrder to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, SortOrder will be stored as a int(11).) + + +=cut + + +=head2 Class + +Returns the current value of Class. +(In the database, Class is stored as int(11).) + + + +=head2 SetClass VALUE + + +Set Class to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Class will be stored as a int(11).) + + +=cut + + +=head2 ClassObj + +Returns the Class Object which has the id returned by Class + + +=cut + +sub ClassObj { + my $self = shift; + my $Class = RT::Class->new($self->CurrentUser); + $Class->Load($self->Class()); + return($Class); +} + +=head2 Parent + +Returns the current value of Parent. +(In the database, Parent is stored as int(11).) + + + +=head2 SetParent VALUE + + +Set Parent to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Parent will be stored as a int(11).) + + +=cut + + +=head2 URI + +Returns the current value of URI. +(In the database, URI is stored as varchar(255).) + + + +=head2 SetURI VALUE + + +Set URI to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, URI will be stored as a varchar(255).) + + +=cut + + +=head2 Creator + +Returns the current value of Creator. +(In the database, Creator is stored as int(11).) + + +=cut + + +=head2 Created + +Returns the current value of Created. +(In the database, Created is stored as datetime.) + + +=cut + + +=head2 LastUpdatedBy + +Returns the current value of LastUpdatedBy. +(In the database, LastUpdatedBy is stored as int(11).) + + +=cut + + +=head2 LastUpdated + +Returns the current value of LastUpdated. +(In the database, LastUpdated is stored as datetime.) + + +=cut + + + +sub _CoreAccessible { + { + + id => + {read => 1, type => 'int(11)', default => ''}, + Name => + {read => 1, write => 1, type => 'varchar(255)', default => ''}, + Summary => + {read => 1, write => 1, type => 'varchar(255)', default => ''}, + SortOrder => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Class => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Parent => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + URI => + {read => 1, write => 1, type => 'varchar(255)', default => ''}, + Creator => + {read => 1, auto => 1, type => 'int(11)', default => '0'}, + Created => + {read => 1, auto => 1, type => 'datetime', default => ''}, + LastUpdatedBy => + {read => 1, auto => 1, type => 'int(11)', default => '0'}, + LastUpdated => + {read => 1, auto => 1, type => 'datetime', default => ''}, + + } +}; + +RT::Base->_ImportOverlays(); + +1; + + +1; diff --git a/rt/lib/RT/Articles.pm b/rt/lib/RT/Articles.pm new file mode 100644 index 000000000..8dd661d2e --- /dev/null +++ b/rt/lib/RT/Articles.pm @@ -0,0 +1,925 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Articles; + +use base 'RT::SearchBuilder'; + +sub Table {'Articles'} + +sub _Init { + my $self = shift; + $self->OrderByCols( + { FIELD => 'SortOrder', ORDER => 'ASC' }, + { FIELD => 'Name', ORDER => 'ASC' }, + ); + return $self->SUPER::_Init( @_ ); +} + +=head2 Next + +Returns the next article that this user can see. + +=cut + +sub Next { + my $self = shift; + + my $Object = $self->SUPER::Next(); + if ( ( defined($Object) ) and ( ref($Object) ) ) { + + if ( $Object->CurrentUserHasRight('ShowArticle') ) { + return ($Object); + } + + #If the user doesn't have the right to show this Object + else { + return ( $self->Next() ); + } + } + + #if there never was any queue + else { + return (undef); + } + +} + +=head2 Limit { FIELD => undef, OPERATOR => '=', VALUE => 'undef'} + +Limit the result set. See DBIx::SearchBuilder docs +In addition to the "normal" stuff, value can be an array. + +=cut + +sub Limit { + my $self = shift; + my %ARGS = ( + OPERATOR => '=', + @_ + ); + + if ( ref( $ARGS{'VALUE'} ) ) { + my @values = $ARGS{'VALUE'}; + delete $ARGS{'VALUE'}; + foreach my $v (@values) { + $self->SUPER::Limit( %ARGS, VALUE => $v ); + } + } + else { + $self->SUPER::Limit(%ARGS); + } +} + +=head2 LimitName { OPERATOR => 'LIKE', VALUE => undef } + +Find all articles with Name fields which satisfy OPERATOR for VALUE + +=cut + +sub LimitName { + my $self = shift; + + my %args = ( + FIELD => 'Name', + OPERATOR => 'LIKE', + CASESENSITIVE => 0, + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +=head2 LimitSummary { OPERATOR => 'LIKE', VALUE => undef } + +Find all articles with summary fields which satisfy OPERATOR for VALUE + +=cut + +sub LimitSummary { + my $self = shift; + + my %args = ( + FIELD => 'Summary', + OPERATOR => 'LIKE', + CASESENSITIVE => 0, + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +sub LimitCreated { + my $self = shift; + my %args = ( + FIELD => 'Created', + OPERATOR => undef, + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +sub LimitCreatedBy { + my $self = shift; + my %args = ( + FIELD => 'CreatedBy', + OPERATOR => '=', + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +sub LimitUpdated { + + my $self = shift; + my %args = ( + FIELD => 'Updated', + OPERATOR => undef, + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +sub LimitUpdatedBy { + my $self = shift; + my %args = ( + FIELD => 'UpdatedBy', + OPERATOR => '=', + VALUE => undef, + @_ + ); + + $self->Limit(%args); + +} + +# {{{ LimitToParent ID + +=head2 LimitToParent ID + +Limit the returned set of articles to articles which are children +of article ID. +This does not recurse. + +=cut + +sub LimitToParent { + my $self = shift; + my $parent = shift; + $self->Limit( + FIELD => 'Parent', + OPERATOR => '=', + VALUE => $parent + ); + +} + +# }}} +# {{{ LimitCustomField + +=head2 LimitCustomField HASH + +Limit the result set to articles which have or do not have the custom field +value listed, using a left join to catch things where no rows match. + +HASH needs the following fields: + FIELD (A custom field id) or undef for any custom field + ENTRYAGGREGATOR => (AND, OR) + OPERATOR ('=', 'LIKE', '!=', 'NOT LIKE') + VALUE ( a single scalar value or a list of possible values to be concatenated with ENTRYAGGREGATOR) + +The subclause that the LIMIT statement(s) should be done in can also +be passed in with a SUBCLAUSE parameter. + +=cut + +sub LimitCustomField { + my $self = shift; + my %args = ( + FIELD => undef, + ENTRYAGGREGATOR => 'OR', + OPERATOR => '=', + QUOTEVALUE => 1, + VALUE => undef, + SUBCLAUSE => undef, + @_ + ); + + my $value = $args{'VALUE'}; + # XXX: this work in a different way than RT + return unless $value; #strip out total blank wildcards + + my $ObjectValuesAlias = $self->Join( + TYPE => 'left', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'ObjectCustomFieldValues', + FIELD2 => 'ObjectId', + EXPRESSION => 'main.id' + ); + + $self->Limit( + LEFTJOIN => $ObjectValuesAlias, + FIELD => 'Disabled', + VALUE => '0' + ); + + if ( $args{'FIELD'} ) { + + my $field_id; + if (UNIVERSAL::isa($args{'FIELD'} ,'RT::CustomField')) { + $field_id = $args{'FIELD'}->id; + } elsif($args{'FIELD'} =~ /^\d+$/) { + $field_id = $args{'FIELD'}; + } + if ($field_id) { + $self->Limit( LEFTJOIN => $ObjectValuesAlias, + FIELD => 'CustomField', + VALUE => $field_id, + ENTRYAGGREGATOR => 'AND'); + # Could convert the above to a non-left join and also enable the thing below + # $self->SUPER::Limit( ALIAS => $ObjectValuesAlias, + # FIELD => 'CustomField', + # OPERATOR => 'IS', + # VALUE => 'NULL', + # QUOTEVALUE => 0, + # ENTRYAGGREGATOR => 'OR',); + } else { + # Search for things by name if the cf was specced by name. + my $fields = $self->NewAlias('CustomFields'); + $self->Join( TYPE => 'left', + ALIAS1 => $ObjectValuesAlias , FIELD1 => 'CustomField', + ALIAS2 => $fields, FIELD2=> 'id'); + $self->Limit( ALIAS => $fields, + FIELD => 'Name', + VALUE => $args{'FIELD'}, + ENTRYAGGREGATOR => 'OR'); + $self->Limit( + ALIAS => $fields, + FIELD => 'LookupType', + VALUE => + RT::Article->new($RT::SystemUser)->CustomFieldLookupType() + ); + + } + } + # If we're trying to find articles where a custom field value + # doesn't match something, be sure to find things where it's null + + # basically, we do a left join on the value being applicable to + # the article and then we turn around and make sure that it's + # actually null in practise + + # TODO this should deal with starts with and ends with + + my $fix_op = sub { + my $op = shift; + return $op unless RT->Config->Get('DatabaseType') eq 'Oracle'; + return 'MATCHES' if $op eq '='; + return 'NOT MATCHES' if $op eq '!='; + return $op; + }; + + my $clause = $args{'SUBCLAUSE'} || $ObjectValuesAlias; + + if ( $args{'OPERATOR'} eq '!=' || $args{'OPERATOR'} =~ /^not like$/i ) { + my $op; + if ( $args{'OPERATOR'} eq '!=' ) { + $op = "="; + } + elsif ( $args{'OPERATOR'} =~ /^not like$/i ) { + $op = 'LIKE'; + } + + $self->SUPER::Limit( + LEFTJOIN => $ObjectValuesAlias, + FIELD => 'Content', + OPERATOR => $op, + VALUE => $value, + QUOTEVALUE => $args{'QUOTEVALUE'}, + ENTRYAGGREGATOR => 'AND', #$args{'ENTRYAGGREGATOR'}, + SUBCLAUSE => $clause, + ); + $self->SUPER::Limit( + ALIAS => $ObjectValuesAlias, + FIELD => 'Content', + OPERATOR => 'IS', + VALUE => 'NULL', + QUOTEVALUE => 0, + ENTRYAGGREGATOR => 'AND', + SUBCLAUSE => $clause, + ); + } + else { + $self->SUPER::Limit( + ALIAS => $ObjectValuesAlias, + FIELD => 'LargeContent', + OPERATOR => $fix_op->($args{'OPERATOR'}), + VALUE => $value, + QUOTEVALUE => $args{'QUOTEVALUE'}, + ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'}, + SUBCLAUSE => $clause, + ); + $self->SUPER::Limit( + ALIAS => $ObjectValuesAlias, + FIELD => 'Content', + OPERATOR => $args{'OPERATOR'}, + VALUE => $value, + QUOTEVALUE => $args{'QUOTEVALUE'}, + ENTRYAGGREGATOR => $args{'ENTRYAGGREGATOR'}, + SUBCLAUSE => $clause, + ); + } +} + +# }}} + +# {{{ LimitTopics +sub LimitTopics { + my $self = shift; + my @topics = @_; + + my $topics = $self->NewAlias('ObjectTopics'); + $self->Limit( + ALIAS => $topics, + FIELD => 'Topic', + VALUE => $_, + ENTRYAGGREGATOR => 'OR' + ) + for @topics; + + $self->Limit( + ALIAS => $topics, + FIELD => 'ObjectType', + VALUE => 'RT::Article', + ); + $self->Join( + ALIAS1 => 'main', + FIELD1 => 'id', + ALIAS2 => $topics, + FIELD2 => 'ObjectId', + ); +} + +# }}} + +# {{{ LimitRefersTo URI + +=head2 LimitRefersTo URI + +Limit the result set to only articles which are referred to by the URI passed in. + +=cut + +sub LimitRefersTo { + my $self = shift; + my $uri = shift; + + my $uri_obj = RT::URI->new($self->CurrentUser); + $uri_obj->FromURI($uri); + my $links = $self->NewAlias('Links'); + $self->Limit( + ALIAS => $links, + FIELD => 'Target', + VALUE => $uri_obj->URI + ); + + $self->Join( + ALIAS1 => 'main', + FIELD1 => 'URI', + ALIAS2 => $links, + FIELD2 => 'Base' + ); + +} + +# }}} + +# {{{ LimitReferredToBy URI + +=head2 LimitReferredToBy URI + +Limit the result set to only articles which are referred to by the URI passed in. + +=cut + +sub LimitReferredToBy { + my $self = shift; + my $uri = shift; + + my $uri_obj = RT::URI->new($self->CurrentUser); + $uri_obj->FromURI($uri); + my $links = $self->NewAlias('Links'); + $self->Limit( + ALIAS => $links, + FIELD => 'Base', + VALUE => $uri_obj->URI + ); + + $self->Join( + ALIAS1 => 'main', + FIELD1 => 'URI', + ALIAS2 => $links, + FIELD2 => 'Target' + ); + +} + +# }}} + +=head2 LimitHostlistClasses + +Only fetch Articles from classes where Hotlist is true. + +=cut + +sub LimitHotlistClasses { + my $self = shift; + + my $classes = $self->Join( + ALIAS1 => 'main', + FIELD1 => 'Class', + TABLE2 => 'Classes', + FIELD2 => 'id', + ); + $self->Limit( ALIAS => $classes, FIELD => 'HotList', VALUE => 1 ); +} + +=head2 LimitAppliedClasses Queue => QueueObj + +Takes a Queue and limits articles returned to classes which are applied to that Queue + +Accepts either a Queue obj or a Queue id + +=cut + +sub LimitAppliedClasses { + my $self = shift; + my %args = @_; + + unless (ref $args{Queue} || $args{Queue} =~/^[0-9]+$/) { + $RT::Logger->error("Not a valid Queue: $args{Queue}"); + return; + } + + my $queue = ( ref $args{Queue} ? $args{Queue}->Id : $args{Queue} ); + + my $oc_alias = $self->Join( + ALIAS1 => 'main', + FIELD1 => 'Class', + TABLE2 => 'ObjectClasses', + FIELD2 => 'Class' + ); + + my $subclause = "possibleobjectclasses"; + $self->_OpenParen($subclause); + $self->Limit( ALIAS => $oc_alias, + FIELD => 'ObjectId', + VALUE => $queue, + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'OR' ); + $self->Limit( ALIAS => $oc_alias, + FIELD => 'ObjectType', + VALUE => 'RT::Queue', + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'AND' ); + $self->_CloseParen($subclause); + + $self->_OpenParen($subclause); + $self->Limit( ALIAS => $oc_alias, + FIELD => 'ObjectId', + VALUE => 0, + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'OR' ); + $self->Limit( ALIAS => $oc_alias, + FIELD => 'ObjectType', + VALUE => 'RT::System', + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'AND' ); + $self->_CloseParen($subclause); + + return $self; + +} + +sub Search { + my $self = shift; + my %args = @_; + my $customfields = $args{CustomFields} + || RT::CustomFields->new( $self->CurrentUser ); + my $dates = $args{Dates} || {}; + my $order_by = $args{OrderBy}; + my $order = $args{Order}; + if ( $args{'q'} ) { + $self->Limit( + FIELD => 'Name', + SUBCLAUSE => 'NameOrSummary', + OPERATOR => 'LIKE', + ENTRYAGGREGATOR => 'OR', + CASESENSITIVE => 0, + VALUE => $args{'q'} + ); + $self->Limit( + FIELD => 'Summary', + SUBCLAUSE => 'NameOrSummary', + OPERATOR => 'LIKE', + ENTRYAGGREGATOR => 'OR', + CASESENSITIVE => 0, + VALUE => $args{'q'} + ); + } + + + require Time::ParseDate; + foreach my $date (qw(Created< Created> LastUpdated< LastUpdated>)) { + next unless ( $args{$date} ); + my $seconds = Time::ParseDate::parsedate( $args{$date}, FUZZY => 1, PREFER_PAST => 1 ); + my $date_obj = RT::Date->new( $self->CurrentUser ); + $date_obj->Set( Format => 'unix', Value => $seconds ); + $dates->{$date} = $date_obj; + + if ( $date =~ /^(.*?)<$/i ) { + $self->Limit( + FIELD => $1, + OPERATOR => "<=", + ENTRYAGGREGATOR => "AND", + VALUE => $date_obj->ISO + ); + } + + if ( $date =~ /^(.*?)>$/i ) { + $self->Limit( + FIELD => $1, + OPERATOR => ">=", + ENTRYAGGREGATOR => "AND", + VALUE => $date_obj->ISO + ); + } + + } + + if ($args{'RefersTo'}) { + foreach my $link ( split( /\s+/, $args{'RefersTo'} ) ) { + next unless ($link); + $self->LimitRefersTo($link); + } + } + + if ($args{'ReferredToBy'}) { + foreach my $link ( split( /\s+/, $args{'ReferredToBy'} ) ) { + next unless ($link); + $self->LimitReferredToBy($link); + } + } + + if ( $args{'Topics'} ) { + my @Topics = + ( ref $args{'Topics'} eq 'ARRAY' ) + ? @{ $args{'Topics'} } + : ( $args{'Topics'} ); + @Topics = map { split } @Topics; + if ( $args{'ExpandTopics'} ) { + my %topics; + while (@Topics) { + my $id = shift @Topics; + next if $topics{$id}; + my $Topics = + RT::Topics->new( $self->CurrentUser ); + $Topics->Limit( FIELD => 'Parent', VALUE => $id ); + push @Topics, $_->Id while $_ = $Topics->Next; + $topics{$id}++; + } + @Topics = keys %topics; + $args{'Topics'} = \@Topics; + } + $self->LimitTopics(@Topics); + } + + my %cfs; + $customfields->LimitToLookupType( + RT::Article->new( $self->CurrentUser ) + ->CustomFieldLookupType ); + if ( $args{'Class'} ) { + my @Classes = + ( ref $args{'Class'} eq 'ARRAY' ) + ? @{ $args{'Class'} } + : ( $args{'Class'} ); + foreach my $class (@Classes) { + $customfields->LimitToGlobalOrObjectId($class); + } + } + else { + $customfields->LimitToGlobalOrObjectId(); + } + while ( my $cf = $customfields->Next ) { + $cfs{ $cf->Name } = $cf->Id; + } + + # reset the iterator because we use this to build the UI + $customfields->GotoFirstItem; + + foreach my $field ( keys %cfs ) { + + my @MatchLike = + ( ref $args{ $field . "~" } eq 'ARRAY' ) + ? @{ $args{ $field . "~" } } + : ( $args{ $field . "~" } ); + my @NoMatchLike = + ( ref $args{ $field . "!~" } eq 'ARRAY' ) + ? @{ $args{ $field . "!~" } } + : ( $args{ $field . "!~" } ); + + my @Match = + ( ref $args{$field} eq 'ARRAY' ) + ? @{ $args{$field} } + : ( $args{$field} ); + my @NoMatch = + ( ref $args{ $field . "!" } eq 'ARRAY' ) + ? @{ $args{ $field . "!" } } + : ( $args{ $field . "!" } ); + + foreach my $val (@MatchLike) { + next unless $val; + push @Match, "~" . $val; + } + + foreach my $val (@NoMatchLike) { + next unless $val; + push @NoMatch, "~" . $val; + } + + foreach my $value (@Match) { + next unless $value; + my $op; + if ( $value =~ /^~(.*)$/ ) { + $value = "%$1%"; + $op = 'LIKE'; + } + else { + $op = '='; + } + $self->LimitCustomField( + FIELD => $cfs{$field}, + VALUE => $value, + CASESENSITIVE => 0, + ENTRYAGGREGATOR => 'OR', + OPERATOR => $op + ); + } + foreach my $value (@NoMatch) { + next unless $value; + my $op; + if ( $value =~ /^~(.*)$/ ) { + $value = "%$1%"; + $op = 'NOT LIKE'; + } + else { + $op = '!='; + } + $self->LimitCustomField( + FIELD => $cfs{$field}, + VALUE => $value, + CASESENSITIVE => 0, + ENTRYAGGREGATOR => 'OR', + OPERATOR => $op + ); + } + } + +### Searches for any field + + if ( $args{'Article~'} ) { + $self->LimitCustomField( + VALUE => $args{'Article~'}, + ENTRYAGGREGATOR => 'OR', + OPERATOR => 'LIKE', + CASESENSITIVE => 0, + SUBCLAUSE => 'SearchAll' + ); + $self->Limit( + SUBCLAUSE => 'SearchAll', + FIELD => "Name", + VALUE => $args{'Article~'}, + ENTRYAGGREGATOR => 'OR', + CASESENSITIVE => 0, + OPERATOR => 'LIKE' + ); + $self->Limit( + SUBCLAUSE => 'SearchAll', + FIELD => "Summary", + VALUE => $args{'Article~'}, + ENTRYAGGREGATOR => 'OR', + CASESENSITIVE => 0, + OPERATOR => 'LIKE' + ); + } + + if ( $args{'Article!~'} ) { + $self->LimitCustomField( + VALUE => $args{'Article!~'}, + OPERATOR => 'NOT LIKE', + CASESENSITIVE => 0, + SUBCLAUSE => 'SearchAll' + ); + $self->Limit( + SUBCLAUSE => 'SearchAll', + FIELD => "Name", + VALUE => $args{'Article!~'}, + ENTRYAGGREGATOR => 'AND', + CASESENSITIVE => 0, + OPERATOR => 'NOT LIKE' + ); + $self->Limit( + SUBCLAUSE => 'SearchAll', + FIELD => "Summary", + VALUE => $args{'Article!~'}, + ENTRYAGGREGATOR => 'AND', + CASESENSITIVE => 0, + OPERATOR => 'NOT LIKE' + ); + } + + foreach my $field (qw(Name Summary Class)) { + + my @MatchLike = + ( ref $args{ $field . "~" } eq 'ARRAY' ) + ? @{ $args{ $field . "~" } } + : ( $args{ $field . "~" } ); + my @NoMatchLike = + ( ref $args{ $field . "!~" } eq 'ARRAY' ) + ? @{ $args{ $field . "!~" } } + : ( $args{ $field . "!~" } ); + + my @Match = + ( ref $args{$field} eq 'ARRAY' ) + ? @{ $args{$field} } + : ( $args{$field} ); + my @NoMatch = + ( ref $args{ $field . "!" } eq 'ARRAY' ) + ? @{ $args{ $field . "!" } } + : ( $args{ $field . "!" } ); + + foreach my $val (@MatchLike) { + next unless $val; + push @Match, "~" . $val; + } + + foreach my $val (@NoMatchLike) { + next unless $val; + push @NoMatch, "~" . $val; + } + + my $op; + foreach my $value (@Match) { + if ( $value && $value =~ /^~(.*)$/ ) { + $value = "%$1%"; + $op = 'LIKE'; + } + else { + $op = '='; + } + + # preprocess Classes, so we can search on class + if ( $field eq 'Class' && $value ) { + my $class = RT::Class->new($RT::SystemUser); + $class->Load($value); + $value = $class->Id; + } + + # now that we've pruned the value, get out if it's different. + next unless $value; + + $self->Limit( + SUBCLAUSE => $field . 'Match', + FIELD => $field, + OPERATOR => $op, + CASESENSITIVE => 0, + VALUE => $value, + ENTRYAGGREGATOR => 'OR' + ); + + } + foreach my $value (@NoMatch) { + + # preprocess Classes, so we can search on class + if ( $value && $value =~ /^~(.*)/ ) { + $value = "%$1%"; + $op = 'NOT LIKE'; + } + else { + $op = '!='; + } + if ( $field eq 'Class' ) { + my $class = RT::Class->new($RT::SystemUser); + $class->Load($value); + $value = $class->Id; + } + + # now that we've pruned the value, get out if it's different. + next unless $value; + + $self->Limit( + SUBCLAUSE => $field . 'NoMatch', + OPERATOR => $op, + VALUE => $value, + CASESENSITIVE => 0, + FIELD => $field, + ENTRYAGGREGATOR => 'AND' + ); + + } + } + + if ($order_by && @$order_by) { + if ( $order_by->[0] && $order_by->[0] =~ /\|/ ) { + @$order_by = split '|', $order_by->[0]; + @$order = split '|', $order->[0]; + } + my @tmp = + map { { FIELD => $order_by->[$_], ORDER => $order->[$_] } } 0 .. $#{$order_by}; + $self->OrderByCols(@tmp); + } + + return 1; +} + + +=head2 NewItem + +Returns an empty new RT::Article item + +=cut + +sub NewItem { + my $self = shift; + return(RT::Article->new($self->CurrentUser)); +} + + + +RT::Base->_ImportOverlays(); + +1; + +1; diff --git a/rt/lib/RT/Class.pm b/rt/lib/RT/Class.pm new file mode 100644 index 000000000..bb694ce9c --- /dev/null +++ b/rt/lib/RT/Class.pm @@ -0,0 +1,620 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Class; + +use strict; +use warnings; +use base 'RT::Record'; + + +use RT::System; +use RT::CustomFields; +use RT::ACL; +use RT::Articles; +use RT::ObjectClass; +use RT::ObjectClasses; + +sub Table {'Classes'} + +=head2 Load IDENTIFIER + +Loads a class, either by name or by id + +=cut + +sub Load { + my $self = shift; + my $id = shift ; + + return unless $id; + if ( $id =~ /^\d+$/ ) { + $self->SUPER::Load($id); + } + else { + $self->LoadByCols( Name => $id ); + } +} + +# {{{ This object provides ACLs + +use vars qw/$RIGHTS/; +$RIGHTS = { + SeeClass => 'See that this class exists', #loc_pair + CreateArticle => 'Create articles in this class', #loc_pair + ShowArticle => 'See articles in this class', #loc_pair + ShowArticleHistory => 'See changes to articles in this class', #loc_pair + ModifyArticle => 'Modify or delete articles in this class', #loc_pair + ModifyArticleTopics => 'Modify topics for articles in this class', #loc_pair + AdminClass => 'Modify metadata and custom fields for this class', #loc_pair + AdminTopics => 'Modify topic hierarchy associated with this class', #loc_pair + ShowACL => 'Display Access Control List', #loc_pair + ModifyACL => 'Modify Access Control List', #loc_pair + DeleteArticle => 'Delete articles in this class', #loc_pair +}; + +our $RIGHT_CATEGORIES = { + SeeClass => 'Staff', + CreateArticle => 'Staff', + ShowArticle => 'General', + ShowArticleHistory => 'Staff', + ModifyArticle => 'Staff', + ModifyArticleTopics => 'Staff', + AdminClass => 'Admin', + AdminTopics => 'Admin', + ShowACL => 'Admin', + ModifyACL => 'Admin', + DeleteArticle => 'Staff', +}; + +# TODO: This should be refactored out into an RT::ACLedObject or something +# stuff the rights into a hash of rights that can exist. + +# Tell RT::ACE that this sort of object can get acls granted +$RT::ACE::OBJECT_TYPES{'RT::Class'} = 1; + +# TODO this is ripe for a refacor, since this is stolen from Queue +__PACKAGE__->AddRights(%$RIGHTS); +__PACKAGE__->AddRightCategories(%$RIGHT_CATEGORIES); + +=head2 AddRights C<RIGHT>, C<DESCRIPTION> [, ...] + +Adds the given rights to the list of possible rights. This method +should be called during server startup, not at runtime. + +=cut + +sub AddRights { + my $self = shift; + my %new = @_; + $RIGHTS = { %$RIGHTS, %new }; + %RT::ACE::LOWERCASERIGHTNAMES = ( %RT::ACE::LOWERCASERIGHTNAMES, + map { lc($_) => $_ } keys %new); +} + +=head2 AddRightCategories C<RIGHT>, C<CATEGORY> [, ...] + +Adds the given right and category pairs to the list of right categories. This +method should be called during server startup, not at runtime. + +=cut + +sub AddRightCategories { + my $self = shift if ref $_[0] or $_[0] eq __PACKAGE__; + my %new = @_; + $RIGHT_CATEGORIES = { %$RIGHT_CATEGORIES, %new }; +} + +=head2 AvailableRights + +Returns a hash of available rights for this object. The keys are the right names and the values are a description of what t +he rights do + +=cut + +sub AvailableRights { + my $self = shift; + return ($RIGHTS); +} + +sub RightCategories { + return $RIGHT_CATEGORIES; +} + + +# }}} + + +# {{{ Create + +=head2 Create PARAMHASH + +Create takes a hash of values and creates a row in the database: + + varchar(255) 'Name'. + varchar(255) 'Description'. + int(11) 'SortOrder'. + +=cut + +sub Create { + my $self = shift; + my %args = ( + Name => '', + Description => '', + SortOrder => '0', + HotList => 0, + @_ + ); + + unless ( + $self->CurrentUser->HasRight( + Right => 'AdminClass', + Object => $RT::System + ) + ) + { + return ( 0, $self->loc('Permission Denied') ); + } + + $self->SUPER::Create( + Name => $args{'Name'}, + Description => $args{'Description'}, + SortOrder => $args{'SortOrder'}, + HotList => $args{'HotList'}, + ); + +} + +sub ValidateName { + my $self = shift; + my $newval = shift; + + return undef unless ($newval); + my $obj = RT::Class->new($RT::SystemUser); + $obj->Load($newval); + return undef if ( $obj->Id ); + return $self->SUPER::ValidateName($newval); + +} + +# }}} + +# }}} + +# {{{ ACCESS CONTROL + +# {{{ sub _Set +sub _Set { + my $self = shift; + + unless ( $self->CurrentUserHasRight('AdminClass') ) { + return ( 0, $self->loc('Permission Denied') ); + } + return ( $self->SUPER::_Set(@_) ); +} + +# }}} + +# {{{ sub _Value + +sub _Value { + my $self = shift; + + unless ( $self->CurrentUserHasRight('SeeClass') ) { + return (undef); + } + + return ( $self->__Value(@_) ); +} + +# }}} + +sub CurrentUserHasRight { + my $self = shift; + my $right = shift; + + return ( + $self->CurrentUser->HasRight( + Right => $right, + Object => ( $self->Id ? $self : $RT::System ), + EquivObjects => [ $RT::System, $RT::System ] + ) + ); + +} + +sub ArticleCustomFields { + my $self = shift; + + + my $cfs = RT::CustomFields->new( $self->CurrentUser ); + if ( $self->CurrentUserHasRight('SeeClass') ) { + $cfs->LimitToGlobalOrObjectId( $self->Id ); + $cfs->LimitToLookupType( RT::Article->CustomFieldLookupType ); + $cfs->ApplySortOrder; + } + return ($cfs); +} + + +=head1 AppliedTo + +Returns collection of Queues this Class is applied to. +Doesn't takes into account if object is applied globally. + +=cut + +sub AppliedTo { + my $self = shift; + + my ($res, $ocfs_alias) = $self->_AppliedTo; + return $res unless $res; + + $res->Limit( + ALIAS => $ocfs_alias, + FIELD => 'id', + OPERATOR => 'IS NOT', + VALUE => 'NULL', + ); + + return $res; +} + +=head1 NotAppliedTo + +Returns collection of Queues this Class is not applied to. + +Doesn't takes into account if object is applied globally. + +=cut + +sub NotAppliedTo { + my $self = shift; + + my ($res, $ocfs_alias) = $self->_AppliedTo; + return $res unless $res; + + $res->Limit( + ALIAS => $ocfs_alias, + FIELD => 'id', + OPERATOR => 'IS', + VALUE => 'NULL', + ); + + return $res; +} + +sub _AppliedTo { + my $self = shift; + + my $res = RT::Queues->new( $self->CurrentUser ); + + $res->OrderBy( FIELD => 'Name' ); + my $ocfs_alias = $res->Join( + TYPE => 'LEFT', + ALIAS1 => 'main', + FIELD1 => 'id', + TABLE2 => 'ObjectClasses', + FIELD2 => 'ObjectId', + ); + $res->Limit( + LEFTJOIN => $ocfs_alias, + ALIAS => $ocfs_alias, + FIELD => 'Class', + VALUE => $self->id, + ); + return ($res, $ocfs_alias); +} + +=head2 IsApplied + +Takes object id and returns corresponding L<RT::ObjectClass> +record if this Class is applied to the object. Use 0 to check +if Class is applied globally. + +=cut + +sub IsApplied { + my $self = shift; + my $id = shift; + return unless defined $id; + my $oc = RT::ObjectClass->new( $self->CurrentUser ); + $oc->LoadByCols( Class=> $self->id, ObjectId => $id, + ObjectType => ( $id ? 'RT::Queue' : 'RT::System' )); + return undef unless $oc->id; + return $oc; +} + +=head2 AddToObject OBJECT + +Apply this Class to a single object, to start with we support Queues + +Takes an object + +=cut + + +sub AddToObject { + my $self = shift; + my $object = shift; + my $id = $object->Id || 0; + + unless ( $object->CurrentUserHasRight('AdminClass') ) { + return ( 0, $self->loc('Permission Denied') ); + } + + my $queue = RT::Queue->new( $self->CurrentUser ); + if ( $id ) { + my ($ok, $msg) = $queue->Load( $id ); + unless ($ok) { + return ( 0, $self->loc('Invalid Queue, unable to apply Class: [_1]',$msg ) ); + } + + } + + if ( $self->IsApplied( $id ) ) { + return ( 0, $self->loc("Class is already applied to [_1]",$queue->Name) ); + } + + if ( $id ) { + # applying locally + return (0, $self->loc("Class is already applied Globally") ) + if $self->IsApplied( 0 ); + } + else { + my $applied = RT::ObjectClasses->new( $self->CurrentUser ); + $applied->LimitToClass( $self->id ); + while ( my $record = $applied->Next ) { + $record->Delete; + } + } + + my $oc = RT::ObjectClass->new( $self->CurrentUser ); + my ( $oid, $msg ) = $oc->Create( + ObjectId => $id, Class => $self->id, + ObjectType => ( $id ? 'RT::Queue' : 'RT::System' ), + ); + return ( $oid, $msg ); +} + + +=head2 RemoveFromObject OBJECT + +Remove this class from a single queue object + +=cut + +sub RemoveFromObject { + my $self = shift; + my $object = shift; + my $id = $object->Id || 0; + + unless ( $object->CurrentUserHasRight('AdminClass') ) { + return ( 0, $self->loc('Permission Denied') ); + } + + my $ocf = $self->IsApplied( $id ); + unless ( $ocf ) { + return ( 0, $self->loc("This class does not apply to that object") ); + } + + # XXX: Delete doesn't return anything + my ( $oid, $msg ) = $ocf->Delete; + return ( $oid, $msg ); +} + + + +=head2 id + +Returns the current value of id. +(In the database, id is stored as int(11).) + + +=cut + + +=head2 Name + +Returns the current value of Name. +(In the database, Name is stored as varchar(255).) + + + +=head2 SetName VALUE + + +Set Name to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Name will be stored as a varchar(255).) + + +=cut + + +=head2 Description + +Returns the current value of Description. +(In the database, Description is stored as varchar(255).) + + + +=head2 SetDescription VALUE + + +Set Description to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Description will be stored as a varchar(255).) + + +=cut + + +=head2 SortOrder + +Returns the current value of SortOrder. +(In the database, SortOrder is stored as int(11).) + + + +=head2 SetSortOrder VALUE + + +Set SortOrder to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, SortOrder will be stored as a int(11).) + + +=cut + + +=head2 Disabled + +Returns the current value of Disabled. +(In the database, Disabled is stored as int(2).) + + + +=head2 SetDisabled VALUE + + +Set Disabled to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Disabled will be stored as a int(2).) + + +=cut + + +=head2 HotList + +Returns the current value of HotList. +(In the database, HotList is stored as int(2).) + + + +=head2 SetHotList VALUE + + +Set HotList to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, HotList will be stored as a int(2).) + + +=cut + + +=head2 Creator + +Returns the current value of Creator. +(In the database, Creator is stored as int(11).) + + +=cut + + +=head2 Created + +Returns the current value of Created. +(In the database, Created is stored as datetime.) + + +=cut + + +=head2 LastUpdatedBy + +Returns the current value of LastUpdatedBy. +(In the database, LastUpdatedBy is stored as int(11).) + + +=cut + + +=head2 LastUpdated + +Returns the current value of LastUpdated. +(In the database, LastUpdated is stored as datetime.) + + +=cut + + + +sub _CoreAccessible { + { + + id => + {read => 1, type => 'int(11)', default => ''}, + Name => + {read => 1, write => 1, type => 'varchar(255)', default => ''}, + Description => + {read => 1, write => 1, type => 'varchar(255)', default => ''}, + SortOrder => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Disabled => + {read => 1, write => 1, type => 'int(2)', default => '0'}, + HotList => + {read => 1, write => 1, type => 'int(2)', default => '0'}, + Creator => + {read => 1, auto => 1, type => 'int(11)', default => '0'}, + Created => + {read => 1, auto => 1, type => 'datetime', default => ''}, + LastUpdatedBy => + {read => 1, auto => 1, type => 'int(11)', default => '0'}, + LastUpdated => + {read => 1, auto => 1, type => 'datetime', default => ''}, + + } +}; + +RT::Base->_ImportOverlays(); + +1; + diff --git a/rt/lib/RT/Classes.pm b/rt/lib/RT/Classes.pm new file mode 100644 index 000000000..37dc411e8 --- /dev/null +++ b/rt/lib/RT/Classes.pm @@ -0,0 +1,104 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::Classes; +use base 'RT::SearchBuilder'; + +sub Table {'Classes'} + + +=head2 Next + +Returns the next Object that this user can see. + +=cut + +sub Next { + my $self = shift; + + + my $Object = $self->SUPER::Next(); + if ((defined($Object)) and (ref($Object))) { + if ( $Object->CurrentUserHasRight('SeeClass') ) { + return($Object); + } + + #If the user doesn't have the right to show this Object + else { + return($self->Next()); + } + } + #if there never was any Object + else { + return(undef); + } + +} + +sub ColumnMapClassName { + return 'RT__Class'; +} + +=head2 NewItem + +Returns an empty new RT::Class item + +=cut + +sub NewItem { + my $self = shift; + return(RT::Class->new($self->CurrentUser)); +} + + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/Dashboard/Mailer.pm b/rt/lib/RT/Dashboard/Mailer.pm new file mode 100644 index 000000000..85589787e --- /dev/null +++ b/rt/lib/RT/Dashboard/Mailer.pm @@ -0,0 +1,577 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Dashboard::Mailer; +use strict; +use warnings; + +use HTML::Mason; +use HTML::RewriteAttributes::Links; +use HTML::RewriteAttributes::Resources; +use MIME::Types; +use POSIX 'tzset'; +use RT::Dashboard; +use RT::Interface::Web::Handler; +use RT::Interface::Web; +use File::Temp 'tempdir'; + +sub MailDashboards { + my $self = shift; + my %args = ( + All => 0, + DryRun => 0, + Time => time, + @_, + ); + + $RT::Logger->debug("Using time $args{Time} for dashboard generation"); + + my $from = $self->GetFrom(); + $RT::Logger->debug("Sending email from $from"); + + # look through each user for her subscriptions + my $Users = RT::Users->new(RT->SystemUser); + $Users->LimitToPrivileged; + + while (defined(my $user = $Users->Next)) { + if ($user->PrincipalObj->Disabled) { + $RT::Logger->debug("Skipping over " . $user->Name . " due to having a disabled account."); + next; + } + + my ($hour, $dow, $dom) = HourDowDomIn($args{Time}, $user->Timezone || RT->Config->Get('Timezone')); + $hour .= ':00'; + $RT::Logger->debug("Checking ".$user->Name."'s subscriptions: hour $hour, dow $dow, dom $dom"); + + my $currentuser = RT::CurrentUser->new; + $currentuser->LoadByName($user->Name); + + # look through this user's subscriptions, are any supposed to be generated + # right now? + for my $subscription ($user->Attributes->Named('Subscription')) { + next unless $self->IsSubscriptionReady( + %args, + Subscription => $subscription, + User => $user, + LocalTime => [$hour, $dow, $dom], + ); + + my $email = $subscription->SubValue('Recipient') + || $user->EmailAddress; + + eval { + $self->SendDashboard( + %args, + CurrentUser => $currentuser, + Email => $email, + Subscription => $subscription, + From => $from, + ) + }; + if ( $@ ) { + $RT::Logger->error("Caught exception: $@"); + } + else { + my $counter = $subscription->SubValue('Counter') || 0; + $subscription->SetSubValues(Counter => $counter + 1) + unless $args{DryRun}; + } + } + } +} + +sub IsSubscriptionReady { + my $self = shift; + my %args = ( + All => 0, + Subscription => undef, + User => undef, + LocalTime => [0, 0, 0], + @_, + ); + + return 1 if $args{All}; + + my $subscription = $args{Subscription}; + + my $counter = $subscription->SubValue('Counter') || 0; + + my $sub_frequency = $subscription->SubValue('Frequency'); + my $sub_hour = $subscription->SubValue('Hour'); + my $sub_dow = $subscription->SubValue('Dow'); + my $sub_dom = $subscription->SubValue('Dom'); + my $sub_fow = $subscription->SubValue('Fow'); + + my ($hour, $dow, $dom) = @{ $args{LocalTime} }; + + $RT::Logger->debug("Checking against subscription " . $subscription->Id . " for " . $args{User}->Name . " with frequency $sub_frequency, hour $sub_hour, dow $sub_dow, dom $sub_dom, fow $sub_fow, counter $counter"); + + return 0 if $sub_frequency eq 'never'; + + # correct hour? + return 0 if $sub_hour ne $hour; + + # all we need is the correct hour for daily dashboards + return 1 if $sub_frequency eq 'daily'; + + if ($sub_frequency eq 'weekly') { + # correct day of week? + return 0 if $sub_dow ne $dow; + + # does it match the "every N weeks" clause? + $sub_fow = 1 if !$sub_fow; + + return 1 if $counter % $sub_fow == 0; + + $subscription->SetSubValues(Counter => $counter + 1) + unless $args{DryRun}; + return 0; + } + + # if monthly, correct day of month? + if ($sub_frequency eq 'monthly') { + return $sub_dom == $dom; + } + + # monday through friday + if ($sub_frequency eq 'm-f') { + return 0 if $dow eq 'Sunday' || $dow eq 'Saturday'; + return 1; + } + + $RT::Logger->debug("Invalid subscription frequency $sub_frequency for " . $args{User}->Name); + + # unknown frequency type, bail out + return 0; +} + +sub GetFrom { + RT->Config->Get('DashboardAddress') || RT->Config->Get('OwnerEmail') +} + +sub SendDashboard { + my $self = shift; + my %args = ( + CurrentUser => undef, + Email => undef, + Subscription => undef, + DryRun => 0, + @_, + ); + + my $currentuser = $args{CurrentUser}; + my $subscription = $args{Subscription}; + + my $rows = $subscription->SubValue('Rows'); + + my $DashboardId = $subscription->SubValue('DashboardId'); + + my $dashboard = RT::Dashboard->new($currentuser); + my ($ok, $msg) = $dashboard->LoadById($DashboardId); + + # failed to load dashboard. perhaps it was deleted or it changed privacy + if (!$ok) { + $RT::Logger->warning("Unable to load dashboard $DashboardId of subscription ".$subscription->Id." for user ".$currentuser->Name.": $msg"); + return $self->ObsoleteSubscription( + %args, + Subscription => $subscription, + ); + } + + $RT::Logger->debug('Generating dashboard "'.$dashboard->Name.'" for user "'.$currentuser->Name.'":'); + + if ($args{DryRun}) { + print << "SUMMARY"; + Dashboard: @{[ $dashboard->Name ]} + User: @{[ $currentuser->Name ]} <$args{Email}> +SUMMARY + return; + } + + local $HTML::Mason::Commands::session{CurrentUser} = $currentuser; + local $HTML::Mason::Commands::r = RT::Dashboard::FakeRequest->new; + + my $content = RunComponent( + '/Dashboards/Render.html', + id => $dashboard->Id, + Preview => 0, + ); + + if ( RT->Config->Get('EmailDashboardRemove') ) { + for ( RT->Config->Get('EmailDashboardRemove') ) { + $content =~ s/$_//g; + } + } + + $RT::Logger->debug("Got ".length($content)." characters of output."); + + $content = HTML::RewriteAttributes::Links->rewrite( + $content, + RT->Config->Get('WebURL') . '/Dashboards/Render.html', + ); + + $self->EmailDashboard( + %args, + Dashboard => $dashboard, + Content => $content, + ); +} + +sub ObsoleteSubscription { + my $self = shift; + my %args = ( + From => undef, + To => undef, + Subscription => undef, + CurrentUser => undef, + @_, + ); + + my $subscription = $args{Subscription}; + + my $ok = RT::Interface::Email::SendEmailUsingTemplate( + From => $args{From}, + To => $args{Email}, + Template => 'Error: Missing dashboard', + Arguments => { + SubscriptionObj => $subscription, + }, + ExtraHeaders => { + 'X-RT-Dashboard-Subscription-Id' => $subscription->Id, + 'X-RT-Dashboard-Id' => $subscription->SubValue('DashboardId'), + }, + ); + + # only delete the subscription if the email looks like it went through + if ($ok) { + my ($deleted, $msg) = $subscription->Delete(); + if ($deleted) { + $RT::Logger->debug("Deleted an obsolete subscription: $msg"); + } + else { + $RT::Logger->warning("Unable to delete an obsolete subscription: $msg"); + } + } + else { + $RT::Logger->warning("Unable to notify ".$args{CurrentUser}->Name." of an obsolete subscription"); + } +} + +sub EmailDashboard { + my $self = shift; + my %args = ( + CurrentUser => undef, + Email => undef, + Dashboard => undef, + Subscription => undef, + Content => undef, + @_, + ); + + my $subscription = $args{Subscription}; + my $dashboard = $args{Dashboard}; + my $currentuser = $args{CurrentUser}; + my $email = $args{Email}; + + my $frequency = $subscription->SubValue('Frequency'); + + my %frequency_lookup = ( + 'm-f' => 'Weekday', # loc + 'daily' => 'Daily', # loc + 'weekly' => 'Weekly', # loc + 'monthly' => 'Monthly', # loc + 'never' => 'Never', # loc + ); + + my $frequency_display = $frequency_lookup{$frequency} + || $frequency; + + my $subject = sprintf '[%s] ' . RT->Config->Get('DashboardSubject'), + RT->Config->Get('rtname'), + $currentuser->loc($frequency_display), + $dashboard->Name; + + my $entity = $self->BuildEmail( + %args, + To => $email, + Subject => $subject, + ); + + $entity->head->replace('X-RT-Dashboard-Id', $dashboard->Id); + $entity->head->replace('X-RT-Dashboard-Subscription-Id', $subscription->Id); + + $RT::Logger->debug('Mailing dashboard "'.$dashboard->Name.'" to user '.$currentuser->Name." <$email>"); + + my $ok = RT::Interface::Email::SendEmail( + Entity => $entity, + ); + + if (!$ok) { + $RT::Logger->error("Failed to email dashboard to user ".$currentuser->Name." <$email>"); + return; + } + + $RT::Logger->debug("Done sending dashboard to ".$currentuser->Name." <$email>"); +} + +sub BuildEmail { + my $self = shift; + my %args = ( + Content => undef, + From => undef, + To => undef, + Subject => undef, + @_, + ); + + my @parts; + my %cid_of; + + my $content = HTML::RewriteAttributes::Resources->rewrite($args{Content}, sub { + my $uri = shift; + + # already attached this object + return "cid:$cid_of{$uri}" if $cid_of{$uri}; + + $cid_of{$uri} = time() . $$ . int(rand(1e6)); + my ($data, $filename, $mimetype, $encoding) = GetResource($uri); + + # downgrade non-text strings, because all strings are utf8 by + # default, which is wrong for non-text strings. + if ( $mimetype !~ m{text/} ) { + utf8::downgrade( $data, 1 ) or $RT::Logger->warning("downgrade $data failed"); + } + + push @parts, MIME::Entity->build( + Top => 0, + Data => $data, + Type => $mimetype, + Encoding => $encoding, + Disposition => 'inline', + Name => $filename, + 'Content-Id' => $cid_of{$uri}, + ); + + return "cid:$cid_of{$uri}"; + }, + inline_css => sub { + my $uri = shift; + my ($content) = GetResource($uri); + return $content; + }, + inline_imports => 1, + ); + + my $entity = MIME::Entity->build( + From => $args{From}, + To => $args{To}, + Subject => $args{Subject}, + Type => "multipart/mixed", + ); + + $entity->attach( + Data => Encode::encode_utf8($content), + Type => 'text/html', + Charset => 'UTF-8', + Disposition => 'inline', + ); + + for my $part (@parts) { + $entity->add_part($part); + } + + return $entity; +} + +{ + my $mason; + my $outbuf = ''; + my $data_dir = ''; + + sub _mason { + unless ($mason) { + $RT::Logger->debug("Creating Mason object."); + + # user may not have permissions on the data directory, so create a + # new one + $data_dir = tempdir(CLEANUP => 1); + + $mason = HTML::Mason::Interp->new( + RT::Interface::Web::Handler->DefaultHandlerArgs, + out_method => \$outbuf, + autohandler_name => '', # disable forced login and more + data_dir => $data_dir, + ); + } + return $mason; + } + + sub RunComponent { + _mason->exec(@_); + my $ret = $outbuf; + $outbuf = ''; + return $ret; + } +} + +{ + my %cache; + + sub HourDowDomIn { + my $now = shift; + my $tz = shift; + + my $key = "$now $tz"; + return @{$cache{$key}} if exists $cache{$key}; + + my ($hour, $dow, $dom); + + { + local $ENV{'TZ'} = $tz; + ## Using POSIX::tzset fixes a bug where the TZ environment variable + ## is cached. + tzset(); + (undef, undef, $hour, $dom, undef, undef, $dow) = localtime($now); + } + tzset(); # return back previous value + + $hour = "0$hour" + if length($hour) == 1; + $dow = (qw/Sunday Monday Tuesday Wednesday Thursday Friday Saturday/)[$dow]; + + return @{$cache{$key}} = ($hour, $dow, $dom); + } +} + +sub GetResource { + my $uri = URI->new(shift); + my ($content, $filename, $mimetype, $encoding); + + $RT::Logger->debug("Getting resource $uri"); + + # strip out the equivalent of WebURL, so we start at the correct / + my $path = $uri->path; + my $webpath = RT->Config->Get('WebPath'); + $path =~ s/^\Q$webpath//; + + # add a leading / if needed + $path = "/$path" + unless $path =~ m{^/}; + + $HTML::Mason::Commands::r->path_info($path); + + # grab the query arguments + my %args; + for (split /&/, ($uri->query||'')) { + my ($k, $v) = /^(.*?)=(.*)$/ + or die "Unable to parse query parameter '$_'"; + + for ($k, $v) { s/%(..)/chr hex $1/ge } + + # no value yet, simple key=value + if (!exists $args{$k}) { + $args{$k} = $v; + } + # already have key=value, need to upgrade it to key=[value1, value2] + elsif (!ref($args{$k})) { + $args{$k} = [$args{$k}, $v]; + } + # already key=[value1, value2], just add the new value + else { + push @{ $args{$k} }, $v; + } + } + + $RT::Logger->debug("Running component '$path'"); + $content = RunComponent($path, %args); + + # guess at the filename from the component name + $filename = $1 if $path =~ m{^.*/(.*?)$}; + + # the rest of this was taken from Email::MIME::CreateHTML::Resolver::LWP + ($mimetype, $encoding) = MIME::Types::by_suffix($filename); + + my $content_type = $HTML::Mason::Commands::r->content_type; + if ($content_type) { + $mimetype = $content_type; + + # strip down to just a MIME type + $mimetype = $1 if $mimetype =~ /(\S+);\s*charset=(.*)$/; + } + + #If all else fails then some conservative and general-purpose defaults are: + $mimetype ||= 'application/octet-stream'; + $encoding ||= 'base64'; + + $RT::Logger->debug("Resource $uri: length=".length($content)." filename='$filename' mimetype='$mimetype', encoding='$encoding'"); + + return ($content, $filename, $mimetype, $encoding); +} + + +{ + package RT::Dashboard::FakeRequest; + sub new { bless {}, shift } + sub header_out { shift } + sub headers_out { shift } + sub content_type { + my $self = shift; + $self->{content_type} = shift if @_; + return $self->{content_type}; + } + sub path_info { + my $self = shift; + $self->{path_info} = shift if @_; + return $self->{path_info}; + } +} + +RT::Base->_ImportOverlays(); + +1; + diff --git a/rt/lib/RT/Dashboards.pm b/rt/lib/RT/Dashboards.pm new file mode 100644 index 000000000..5d10205a3 --- /dev/null +++ b/rt/lib/RT/Dashboards.pm @@ -0,0 +1,112 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 NAME + + RT::Dashboards - a pseudo-collection for Dashboard objects. + +=head1 SYNOPSIS + + use RT::Dashboards + +=head1 DESCRIPTION + + Dashboards is an object consisting of a number of Dashboard objects. + It works more or less like a DBIx::SearchBuilder collection, although it + is not. + +=head1 METHODS + + +=cut + +package RT::Dashboards; + +use RT::Dashboard; + +use strict; +use base 'RT::SharedSettings'; + +sub RecordClass { + return 'RT::Dashboard'; +} + +=head2 LimitToPrivacy + +Takes one argument: a privacy string, of the format "<class>-<id>", as produced +by RT::Dashboard::Privacy(). The Dashboards object will load the dashboards +belonging to that user or group. Repeated calls to the same object should DTRT. + +=cut + +sub LimitToPrivacy { + my $self = shift; + my $privacy = shift; + + my $object = $self->_GetObject($privacy); + + if ($object) { + $self->{'objects'} = []; + my @dashboard_atts = $object->Attributes->Named('Dashboard'); + foreach my $att (@dashboard_atts) { + my $dashboard = RT::Dashboard->new($self->CurrentUser); + $dashboard->Load($privacy, $att->Id); + push(@{$self->{'objects'}}, $dashboard); + } + } else { + $RT::Logger->error("Could not load object $privacy"); + } +} + +sub ColumnMapClassName { + return 'RT__Dashboard'; +} + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/Generated.pm b/rt/lib/RT/Generated.pm new file mode 100644 index 000000000..b02a413d2 --- /dev/null +++ b/rt/lib/RT/Generated.pm @@ -0,0 +1,81 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT; +use warnings; +use strict; + +our $VERSION = '4.0.5'; + + + +$BasePath = '/opt/rt3'; +$EtcPath = '/opt/rt3/etc'; +$BinPath = '/opt/rt3/bin'; +$SbinPath = '/opt/rt3/sbin'; +$VarPath = '/opt/rt3/var'; +$LexiconPath = '/opt/rt3/share/po'; +$PluginPath = '/opt/rt3/plugins'; +$LocalPath = '/opt/rt3/local'; +$LocalEtcPath = '/opt/rt3/local/etc'; +$LocalLibPath = '/opt/rt3/local/lib'; +$LocalLexiconPath = '/opt/rt3/local/po'; +$LocalPluginPath = '/opt/rt3/local/plugins'; +# $MasonComponentRoot is where your rt instance keeps its mason html files +$MasonComponentRoot = '/var/www/freeside/rt'; +# $MasonLocalComponentRoot is where your rt instance keeps its site-local +# mason html files. +$MasonLocalComponentRoot = '/opt/rt3/local/html'; +# $MasonDataDir Where mason keeps its datafiles +$MasonDataDir = '/usr/local/etc/freeside/masondata'; +# RT needs to put session data (for preserving state between connections +# via the web interface) +$MasonSessionDir = '/opt/rt3/var/session_data'; + + +1; diff --git a/rt/lib/RT/Generated.pm.in b/rt/lib/RT/Generated.pm.in new file mode 100644 index 000000000..ac15bdea4 --- /dev/null +++ b/rt/lib/RT/Generated.pm.in @@ -0,0 +1,81 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT; +use warnings; +use strict; + +our $VERSION = '@RT_VERSION_MAJOR@.@RT_VERSION_MINOR@.@RT_VERSION_PATCH@'; + +@DATABASE_ENV_PREF@ + +$BasePath = '@RT_PATH@'; +$EtcPath = '@RT_ETC_PATH@'; +$BinPath = '@RT_BIN_PATH@'; +$SbinPath = '@RT_SBIN_PATH@'; +$VarPath = '@RT_VAR_PATH@'; +$LexiconPath = '@RT_LEXICON_PATH@'; +$PluginPath = '@RT_PLUGIN_PATH@'; +$LocalPath = '@RT_LOCAL_PATH@'; +$LocalEtcPath = '@LOCAL_ETC_PATH@'; +$LocalLibPath = '@LOCAL_LIB_PATH@'; +$LocalLexiconPath = '@LOCAL_LEXICON_PATH@'; +$LocalPluginPath = '@LOCAL_PLUGIN_PATH@'; +# $MasonComponentRoot is where your rt instance keeps its mason html files +$MasonComponentRoot = '@MASON_HTML_PATH@'; +# $MasonLocalComponentRoot is where your rt instance keeps its site-local +# mason html files. +$MasonLocalComponentRoot = '@MASON_LOCAL_HTML_PATH@'; +# $MasonDataDir Where mason keeps its datafiles +$MasonDataDir = '@MASON_DATA_PATH@'; +# RT needs to put session data (for preserving state between connections +# via the web interface) +$MasonSessionDir = '@MASON_SESSION_PATH@'; + + +1; diff --git a/rt/lib/RT/Lifecycle.pm b/rt/lib/RT/Lifecycle.pm new file mode 100644 index 000000000..edb179569 --- /dev/null +++ b/rt/lib/RT/Lifecycle.pm @@ -0,0 +1,670 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + + +package RT::Lifecycle; + +our %LIFECYCLES; +our %LIFECYCLES_CACHE; +__PACKAGE__->RegisterRights; + +# cache structure: +# { +# '' => { # all valid statuses +# '' => [...], +# initial => [...], +# active => [...], +# inactive => [...], +# }, +# lifecycle_x => { +# '' => [...], # all valid in lifecycle +# initial => [...], +# active => [...], +# inactive => [...], +# transitions => { +# status_x => [status_next1, status_next2,...], +# }, +# rights => { +# 'status_y -> status_y' => 'right', +# .... +# } +# actions => [ +# { from => 'a', to => 'b', label => '...', update => '...' }, +# .... +# ] +# } +# } + +=head1 NAME + +RT::Lifecycle - class to access and manipulate lifecycles + +=head1 DESCRIPTION + +A lifecycle is a list of statuses that a ticket can have. There are three +groups of statuses: initial, active and inactive. A lifecycle also defines +possible transitions between statuses. For example, in the 'default' lifecycle, +you may only change status from 'stalled' to 'open'. + +It is also possible to define user-interface labels and the action a user +should perform during a transition. For example, the "open -> stalled" +transition would have a 'Stall' label and the action would be Comment. The +action only defines what form is showed to the user, but actually performing +the action is not required. The user can leave the comment box empty yet still +Stall a ticket. Finally, the user can also just use the Basics or Jumbo form to +change the status with the usual dropdown. + +=head1 METHODS + +=head2 new + +Simple constructor, takes no arguments. + +=cut + +sub new { + my $proto = shift; + my $self = bless {}, ref($proto) || $proto; + + $self->FillCache unless keys %LIFECYCLES_CACHE; + + return $self; +} + +=head2 Load + +Takes a name of the lifecycle and loads it. If name is empty or undefined then +loads the global lifecycle with statuses from all named lifecycles. + +Can be called as class method, returns a new object, for example: + + my $lifecycle = RT::Lifecycle->Load('default'); + +=cut + +sub Load { + my $self = shift; + my $name = shift || ''; + return $self->new->Load( $name, @_ ) + unless ref $self; + + return unless exists $LIFECYCLES_CACHE{ $name }; + + $self->{'name'} = $name; + $self->{'data'} = $LIFECYCLES_CACHE{ $name }; + + return $self; +} + +=head2 List + +Returns sorted list of the lifecycles' names. + +=cut + +sub List { + my $self = shift; + + $self->FillCache unless keys %LIFECYCLES_CACHE; + + return sort grep length && $_ ne '__maps__', keys %LIFECYCLES_CACHE; +} + +=head2 Name + +Returns name of the laoded lifecycle. + +=cut + +sub Name { return $_[0]->{'name'} } + +=head2 Queues + +Returns L<RT::Queues> collection with queues that use this lifecycle. + +=cut + +sub Queues { + my $self = shift; + require RT::Queues; + my $queues = RT::Queues->new( RT->SystemUser ); + $queues->Limit( FIELD => 'Lifecycle', VALUE => $self->Name ); + return $queues; +} + +=head2 Getting statuses and validating. + +Methods to get statuses in different sets or validating them. + +=head3 Valid + +Returns an array of all valid statuses for the current lifecycle. +Statuses are not sorted alphabetically, instead initial goes first, +then active and then inactive. + +Takes optional list of status types, from 'initial', 'active' or +'inactive'. For example: + + $lifecycle->Valid('initial', 'active'); + +=cut + +sub Valid { + my $self = shift; + my @types = @_; + unless ( @types ) { + return @{ $self->{'data'}{''} || [] }; + } + + my @res; + push @res, @{ $self->{'data'}{ $_ } || [] } foreach @types; + return @res; +} + +=head3 IsValid + +Takes a status and returns true if value is a valid status for the current +lifecycle. Otherwise, returns false. + +Takes optional list of status types after the status, so it's possible check +validity in particular sets, for example: + + # returns true if status is valid and from initial or active set + $lifecycle->IsValid('some_status', 'initial', 'active'); + +See also </valid>. + +=cut + +sub IsValid { + my $self = shift; + my $value = shift or return 0; + return 1 if grep lc($_) eq lc($value), $self->Valid( @_ ); + return 0; +} + +=head3 StatusType + +Takes a status and returns its type, one of 'initial', 'active' or +'inactive'. + +=cut + +sub StatusType { + my $self = shift; + my $status = shift; + foreach my $type ( qw(initial active inactive) ) { + return $type if $self->IsValid( $status, $type ); + } + return ''; +} + +=head3 Initial + +Returns an array of all initial statuses for the current lifecycle. + +=cut + +sub Initial { + my $self = shift; + return $self->Valid('initial'); +} + +=head3 IsInitial + +Takes a status and returns true if value is a valid initial status. +Otherwise, returns false. + +=cut + +sub IsInitial { + my $self = shift; + my $value = shift or return 0; + return 1 if grep lc($_) eq lc($value), $self->Valid('initial'); + return 0; +} + + +=head3 Active + +Returns an array of all active statuses for this lifecycle. + +=cut + +sub Active { + my $self = shift; + return $self->Valid('active'); +} + +=head3 IsActive + +Takes a value and returns true if value is a valid active status. +Otherwise, returns false. + +=cut + +sub IsActive { + my $self = shift; + my $value = shift or return 0; + return 1 if grep lc($_) eq lc($value), $self->Valid('active'); + return 0; +} + +=head3 inactive + +Returns an array of all inactive statuses for this lifecycle. + +=cut + +sub Inactive { + my $self = shift; + return $self->Valid('inactive'); +} + +=head3 is_inactive + +Takes a value and returns true if value is a valid inactive status. +Otherwise, returns false. + +=cut + +sub IsInactive { + my $self = shift; + my $value = shift or return 0; + return 1 if grep lc($_) eq lc($value), $self->Valid('inactive'); + return 0; +} + + +=head2 Default statuses + +In some cases when status is not provided a default values should +be used. + +=head3 DefaultStatus + +Takes a situation name and returns value. Name should be +spelled following spelling in the RT config file. + +=cut + +sub DefaultStatus { + my $self = shift; + my $situation = shift; + return $self->{data}{defaults}{ $situation }; +} + +=head3 DefaultOnCreate + +Returns the status that should be used by default +when ticket is created. + +=cut + +sub DefaultOnCreate { + my $self = shift; + return $self->DefaultStatus('on_create'); +} + + +=head3 DefaultOnMerge + +Returns the status that should be used when tickets +are merged. + +=cut + +sub DefaultOnMerge { + my $self = shift; + return $self->DefaultStatus('on_merge'); +} + +=head2 Transitions, rights, labels and actions. + +=head3 Transitions + +Takes status and returns list of statuses it can be changed to. + +Is status is empty or undefined then returns list of statuses for +a new ticket. + +If argument is ommitted then returns a hash with all possible +transitions in the following format: + + status_x => [ next_status, next_status, ... ], + status_y => [ next_status, next_status, ... ], + +=cut + +sub Transitions { + my $self = shift; + return %{ $self->{'data'}{'transitions'} || {} } + unless @_; + + my $status = shift; + return @{ $self->{'data'}{'transitions'}{ $status || '' } || [] }; +} + +=head1 IsTransition + +Takes two statuses (from -> to) and returns true if it's valid +transition and false otherwise. + +=cut + +sub IsTransition { + my $self = shift; + my $from = shift; + my $to = shift or return 0; + return 1 if grep lc($_) eq lc($to), $self->Transitions($from); + return 0; +} + +=head3 CheckRight + +Takes two statuses (from -> to) and returns the right that should +be checked on the ticket. + +=cut + +sub CheckRight { + my $self = shift; + my $from = shift; + my $to = shift; + if ( my $rights = $self->{'data'}{'rights'} ) { + my $check = + $rights->{ $from .' -> '. $to } + || $rights->{ '* -> '. $to } + || $rights->{ $from .' -> *' } + || $rights->{ '* -> *' }; + return $check if $check; + } + return $to eq 'deleted' ? 'DeleteTicket' : 'ModifyTicket'; +} + +=head3 RegisterRights + +Registers all defined rights in the system, so they can be addigned +to users. No need to call it, as it's called when module is loaded. + +=cut + +sub RegisterRights { + my $self = shift; + + my %rights = $self->RightsDescription; + + require RT::ACE; + + require RT::Queue; + my $RIGHTS = $RT::Queue::RIGHTS; + + while ( my ($right, $description) = each %rights ) { + next if exists $RIGHTS->{ $right }; + + $RIGHTS->{ $right } = $description; + RT::Queue->AddRightCategories( $right => 'Status' ); + $RT::ACE::LOWERCASERIGHTNAMES{ lc $right } = $right; + } +} + +=head3 RightsDescription + +Returns hash with description of rights that are defined for +particular transitions. + +=cut + +sub RightsDescription { + my $self = shift; + + $self->FillCache unless keys %LIFECYCLES_CACHE; + + my %tmp; + foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { + next unless exists $lifecycle->{'rights'}; + while ( my ($transition, $right) = each %{ $lifecycle->{'rights'} } ) { + push @{ $tmp{ $right } ||=[] }, $transition; + } + } + + my %res; + while ( my ($right, $transitions) = each %tmp ) { + my (@from, @to); + foreach ( @$transitions ) { + ($from[@from], $to[@to]) = split / -> /, $_; + } + my $description = 'Change status' + . ( (grep $_ eq '*', @from)? '' : ' from '. join ', ', @from ) + . ( (grep $_ eq '*', @to )? '' : ' to '. join ', ', @to ); + + $res{ $right } = $description; + } + return %res; +} + +=head3 Actions + +Takes a status and returns list of defined actions for the status. Each +element in the list is a hash reference with the following key/value +pairs: + +=over 4 + +=item from - either the status or * + +=item to - next status + +=item label - label of the action + +=item update - 'Respond', 'Comment' or '' (empty string) + +=back + +=cut + +sub Actions { + my $self = shift; + my $from = shift || return (); + + $self->FillCache unless keys %LIFECYCLES_CACHE; + + my @res = grep $_->{'from'} eq $from || ( $_->{'from'} eq '*' && $_->{'to'} ne $from ), + @{ $self->{'data'}{'actions'} }; + + # skip '* -> x' if there is '$from -> x' + foreach my $e ( grep $_->{'from'} eq '*', @res ) { + $e = undef if grep $_->{'from'} ne '*' && $_->{'to'} eq $e->{'to'}, @res; + } + return grep defined, @res; +} + +=head2 Moving tickets between lifecycles + +=head3 MoveMap + +Takes lifecycle as a name string or an object and returns a hash reference with +move map from this cycle to provided. + +=cut + +sub MoveMap { + my $from = shift; # self + my $to = shift; + $to = RT::Lifecycle->Load( $to ) unless ref $to; + return $LIFECYCLES{'__maps__'}{ $from->Name .' -> '. $to->Name } || {}; +} + +=head3 HasMoveMap + +Takes a lifecycle as a name string or an object and returns true if move map +defined for move from this cycle to provided. + +=cut + +sub HasMoveMap { + my $self = shift; + my $map = $self->MoveMap( @_ ); + return 0 unless $map && keys %$map; + return 0 unless grep defined && length, values %$map; + return 1; +} + +=head3 NoMoveMaps + +Takes no arguments and returns hash with pairs that has no +move maps. + +=cut + +sub NoMoveMaps { + my $self = shift; + my @list = $self->List; + my @res; + foreach my $from ( @list ) { + foreach my $to ( @list ) { + next if $from eq $to; + push @res, $from, $to + unless RT::Lifecycle->Load( $from )->HasMoveMap( $to ); + } + } + return @res; +} + +=head2 Localization + +=head3 ForLocalization + +A class method that takes no arguments and returns list of strings +that require translation. + +=cut + +sub ForLocalization { + my $self = shift; + $self->FillCache unless keys %LIFECYCLES_CACHE; + + my @res = (); + + push @res, @{ $LIFECYCLES_CACHE{''}{''} || [] }; + foreach my $lifecycle ( values %LIFECYCLES ) { + push @res, + grep defined && length, + map $_->{'label'}, + grep ref($_), + @{ $lifecycle->{'actions'} || [] }; + } + + push @res, $self->RightsDescription; + + my %seen; + return grep !$seen{lc $_}++, @res; +} + +sub loc { return RT->SystemUser->loc( @_ ) } + +sub FillCache { + my $self = shift; + + my $map = RT->Config->Get('Lifecycles') or return; + + %LIFECYCLES_CACHE = %LIFECYCLES = %$map; + $_ = { %$_ } foreach values %LIFECYCLES_CACHE; + + my %all = ( + '' => [], + initial => [], + active => [], + inactive => [], + ); + foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { + my @res; + foreach my $type ( qw(initial active inactive) ) { + push @{ $all{ $type } }, @{ $lifecycle->{ $type } || [] }; + push @res, @{ $lifecycle->{ $type } || [] }; + } + + my %seen; + @res = grep !$seen{ lc $_ }++, @res; + $lifecycle->{''} = \@res; + + unless ( $lifecycle->{'transitions'}{''} ) { + $lifecycle->{'transitions'}{''} = [ grep $_ ne 'deleted', @res ]; + } + } + foreach my $type ( qw(initial active inactive), '' ) { + my %seen; + @{ $all{ $type } } = grep !$seen{ lc $_ }++, @{ $all{ $type } }; + push @{ $all{''} }, @{ $all{ $type } } if $type; + } + $LIFECYCLES_CACHE{''} = \%all; + + foreach my $lifecycle ( values %LIFECYCLES_CACHE ) { + my @res; + if ( ref $lifecycle->{'actions'} eq 'HASH' ) { + foreach my $k ( sort keys %{ $lifecycle->{'actions'} } ) { + push @res, $k, $lifecycle->{'actions'}{ $k }; + } + } elsif ( ref $lifecycle->{'actions'} eq 'ARRAY' ) { + @res = @{ $lifecycle->{'actions'} }; + } + + my @tmp = splice @res; + while ( my ($transition, $info) = splice @tmp, 0, 2 ) { + my ($from, $to) = split /\s*->\s*/, $transition, 2; + push @res, { %$info, from => $from, to => $to }; + } + $lifecycle->{'actions'} = \@res; + } + return; +} + +1; diff --git a/rt/lib/RT/ObjectClass.pm b/rt/lib/RT/ObjectClass.pm new file mode 100644 index 000000000..e1c66da0f --- /dev/null +++ b/rt/lib/RT/ObjectClass.pm @@ -0,0 +1,222 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::ObjectClass; +use base 'RT::Record'; + +sub Table {'ObjectClasses'} + + + +sub Create { + my $self = shift; + my %args = ( + Class => '0', + ObjectType => '', + ObjectId => '0', + + @_); + + unless ( $self->CurrentUser->HasRight( Right => 'AdminClass', + Object => $RT::System ) ) { + return ( 0, $self->loc('Permission Denied') ); + } + + $self->SUPER::Create( + Class => $args{'Class'}, + ObjectType => $args{'ObjectType'}, + ObjectId => $args{'ObjectId'}, + ); + +} + + +=head2 id + +Returns the current value of id. +(In the database, id is stored as int(11).) + + +=cut + + +=head2 Class + +Returns the current value of Class. +(In the database, Class is stored as int(11).) + + + +=head2 SetClass VALUE + + +Set Class to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Class will be stored as a int(11).) + + +=cut + + +=head2 ClassObj + +Returns the Class Object which has the id returned by Class + + +=cut + +sub ClassObj { + my $self = shift; + my $Class = RT::Class->new($self->CurrentUser); + $Class->Load($self->Class()); + return($Class); +} + +=head2 ObjectType + +Returns the current value of ObjectType. +(In the database, ObjectType is stored as varchar(255).) + + + +=head2 SetObjectType VALUE + + +Set ObjectType to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ObjectType will be stored as a varchar(255).) + + +=cut + + +=head2 ObjectId + +Returns the current value of ObjectId. +(In the database, ObjectId is stored as int(11).) + + + +=head2 SetObjectId VALUE + + +Set ObjectId to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ObjectId will be stored as a int(11).) + + +=cut + + +=head2 Creator + +Returns the current value of Creator. +(In the database, Creator is stored as int(11).) + + +=cut + + +=head2 Created + +Returns the current value of Created. +(In the database, Created is stored as datetime.) + + +=cut + + +=head2 LastUpdatedBy + +Returns the current value of LastUpdatedBy. +(In the database, LastUpdatedBy is stored as int(11).) + + +=cut + + +=head2 LastUpdated + +Returns the current value of LastUpdated. +(In the database, LastUpdated is stored as datetime.) + + +=cut + + + +sub _CoreAccessible { + { + + id => + {read => 1, type => 'int(11)', default => ''}, + Class => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + ObjectType => + {read => 1, write => 1, type => 'varchar(255)', default => ''}, + ObjectId => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + Creator => + {read => 1, auto => 1, type => 'int(11)', default => '0'}, + Created => + {read => 1, auto => 1, type => 'datetime', default => ''}, + LastUpdatedBy => + {read => 1, auto => 1, type => 'int(11)', default => '0'}, + LastUpdated => + {read => 1, auto => 1, type => 'datetime', default => ''}, + + } +}; + + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/ObjectClasses.pm b/rt/lib/RT/ObjectClasses.pm new file mode 100644 index 000000000..ac95adef5 --- /dev/null +++ b/rt/lib/RT/ObjectClasses.pm @@ -0,0 +1,87 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +use warnings; + +package RT::ObjectClasses; +use base 'RT::SearchBuilder'; + +sub Table {'ObjectClasses'} + + +=head2 LimitToClass + +Takes a Class id and limits this collection to ObjectClasses +that reference it. + +=cut + +sub LimitToClass { + my $self = shift; + my $id = shift; + + return $self->Limit( FIELD => 'Class', VALUE => $id ); + +} + +=head2 NewItem + +Returns an empty new RT::ObjectClass item + +=cut + +sub NewItem { + my $self = shift; + return(RT::ObjectClass->new($self->CurrentUser)); +} + + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/ObjectTopic.pm b/rt/lib/RT/ObjectTopic.pm new file mode 100644 index 000000000..ae5abb35c --- /dev/null +++ b/rt/lib/RT/ObjectTopic.pm @@ -0,0 +1,214 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +# Autogenerated by DBIx::SearchBuilder factory (by <jesse@bestpractical.com>) +# WARNING: THIS FILE IS AUTOGENERATED. ALL CHANGES TO THIS FILE WILL BE LOST. +# +# !! DO NOT EDIT THIS FILE !! +# + + +=head1 NAME + +RT::ObjectTopic + + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +=head1 METHODS + +=cut + +no warnings 'redefine'; +package RT::ObjectTopic; +use RT::Record; +use RT::Topic; + + +use base qw( RT::Record ); + +sub _Init { + my $self = shift; + + $self->Table('ObjectTopics'); + $self->SUPER::_Init(@_); +} + + + + + +=head2 Create PARAMHASH + +Create takes a hash of values and creates a row in the database: + + int(11) 'Topic'. + varchar(64) 'ObjectType'. + int(11) 'ObjectId'. + +=cut + + + + +sub Create { + my $self = shift; + my %args = ( + Topic => '0', + ObjectType => '', + ObjectId => '0', + + @_); + $self->SUPER::Create( + Topic => $args{'Topic'}, + ObjectType => $args{'ObjectType'}, + ObjectId => $args{'ObjectId'}, +); + +} + + + +=head2 id + +Returns the current value of id. +(In the database, id is stored as int(11).) + + +=cut + + +=head2 Topic + +Returns the current value of Topic. +(In the database, Topic is stored as int(11).) + + + +=head2 SetTopic VALUE + + +Set Topic to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Topic will be stored as a int(11).) + + +=cut + + +=head2 TopicObj + +Returns the Topic Object which has the id returned by Topic + + +=cut + +sub TopicObj { + my $self = shift; + my $Topic = RT::Topic->new($self->CurrentUser); + $Topic->Load($self->Topic()); + return($Topic); +} + +=head2 ObjectType + +Returns the current value of ObjectType. +(In the database, ObjectType is stored as varchar(64).) + + + +=head2 SetObjectType VALUE + + +Set ObjectType to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ObjectType will be stored as a varchar(64).) + + +=cut + + +=head2 ObjectId + +Returns the current value of ObjectId. +(In the database, ObjectId is stored as int(11).) + + + +=head2 SetObjectId VALUE + + +Set ObjectId to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ObjectId will be stored as a int(11).) + + +=cut + + + +sub _CoreAccessible { + { + + id => + {read => 1, type => 'int(11)', default => ''}, + Topic => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + ObjectType => + {read => 1, write => 1, type => 'varchar(64)', default => ''}, + ObjectId => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + + } +}; + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/ObjectTopics.pm b/rt/lib/RT/ObjectTopics.pm new file mode 100644 index 000000000..1ffb146b5 --- /dev/null +++ b/rt/lib/RT/ObjectTopics.pm @@ -0,0 +1,115 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use warnings; +use strict; + +package RT::ObjectTopics; + +use base 'RT::SearchBuilder'; + + +sub Table {'ObjectTopics'} + + +# {{{ LimitToTopic + +=head2 LimitToTopic FIELD + +Returns values for the topic with Id FIELD + +=cut + +sub LimitToTopic { + my $self = shift; + my $cf = shift; + return ($self->Limit( FIELD => 'Topic', + VALUE => $cf, + OPERATOR => '=')); + +} + +# }}} + + +# {{{ LimitToObject + +=head2 LimitToObject OBJ + +Returns associations for the given OBJ only + +=cut + +sub LimitToObject { + my $self = shift; + my $object = shift; + + $self->Limit( FIELD => 'ObjectType', + VALUE => ref($object)); + $self->Limit( FIELD => 'ObjectId', + VALUE => $object->Id); + +} + +# }}} + +=head2 NewItem + +Returns an empty new RT::ObjectTopic item + +=cut + +sub NewItem { + my $self = shift; + return(RT::ObjectTopic->new($self->CurrentUser)); +} + + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/SharedSettings.pm b/rt/lib/RT/SharedSettings.pm new file mode 100644 index 000000000..c2c9abeb8 --- /dev/null +++ b/rt/lib/RT/SharedSettings.pm @@ -0,0 +1,155 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 NAME + + RT::SharedSettings - a pseudo-collection for SharedSetting objects. + +=head1 SYNOPSIS + + use RT::SharedSettings + +=head1 DESCRIPTION + + SharedSettings is an object consisting of a number of SharedSetting objects. + It works more or less like a DBIx::SearchBuilder collection, although it + is not. + +=head1 METHODS + + +=cut + +package RT::SharedSettings; + +use RT::SharedSetting; + +use strict; +use base 'RT::Base'; + +sub new { + my $proto = shift; + my $class = ref($proto) || $proto; + my $self = {}; + bless ($self, $class); + $self->CurrentUser(@_); + $self->{'idx'} = 0; + $self->{'objects'} = []; + return $self; +} + +### Accessor methods + +=head2 Next + +Returns the next object in the collection. + +=cut + +sub Next { + my $self = shift; + my $search = $self->{'objects'}->[$self->{'idx'}]; + if ($search) { + $self->{'idx'}++; + } else { + # We have run out of objects; reset the counter. + $self->{'idx'} = 0; + } + return $search; +} + +=head2 Count + +Returns the number of search objects found. + +=cut + +sub Count { + my $self = shift; + return scalar @{$self->{'objects'}}; +} + +=head2 CountAll + +Returns the number of search objects found + +=cut + +sub CountAll { + my $self = shift; + return $self->Count; +} + +=head2 GotoPage + +Act more like a normal L<DBIx::SearchBuilder> collection. +Moves the internal index around + +=cut + +sub GotoPage { + my $self = shift; + $self->{idx} = shift; +} + +### Internal methods + +# _GetObject: helper routine to load the correct object whose parameters +# have been passed. + +sub _GetObject { + my $self = shift; + my $privacy = shift; + + return $self->RecordClass->new($self->CurrentUser)->_GetObject($privacy); +} + +RT::Base->_ImportOverlays(); + +1; + diff --git a/rt/lib/RT/Squish.pm b/rt/lib/RT/Squish.pm new file mode 100644 index 000000000..eb31a63a1 --- /dev/null +++ b/rt/lib/RT/Squish.pm @@ -0,0 +1,122 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 SYNOPSIS + +=head1 DESCRIPTION + +base class of RT::Squish::JS and RT::Squish::CSS + +=head1 METHODS + +=cut + +use strict; +use warnings; + +package RT::Squish; +use base 'Class::Accessor::Fast'; +__PACKAGE__->mk_accessors(qw/Content Key ModifiedTime ModifiedTimeString/); + +use Digest::MD5 'md5_hex'; +use HTTP::Date; + +=head2 new (ARGS) + +ARGS is a hash of named parameters. Valid parameters are: + + Name - name for this object + +=cut + +sub new { + my $class = shift; + my %args = @_; + my $self = \%args; + bless $self, $class; + + my $content = $self->Squish; + $self->Content($content); + $self->Key( md5_hex $content ); + $self->ModifiedTime( time() ); + $self->ModifiedTimeString( HTTP::Date::time2str( $self->ModifiedTime ) ); + return $self; +} + +=head2 Squish + +virtual method which does nothing, +you need to implement this method in subclasses. + +=cut + +sub Squish { + $RT::Logger->warn( "you need to implement this method in subclasses" ); + return 1; +} + +=head2 Content + +squished content + +=head2 Key + +md5 of the squished content + +=head2 ModifiedTime + +created time of squished content, i.e. seconds since 00:00:00 UTC, January 1, 1970 + +=head2 ModifiedTimeString + +created time of squished content, with HTTP::Date format + +=cut + +1; + diff --git a/rt/lib/RT/Squish/CSS.pm b/rt/lib/RT/Squish/CSS.pm new file mode 100644 index 000000000..991451495 --- /dev/null +++ b/rt/lib/RT/Squish/CSS.pm @@ -0,0 +1,105 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 SYNOPSIS + + use RT::Squish::CSS; + my $squish = RT::Squish::CSS->new( Style => 'aileron'); + +=head1 DESCRIPTION + +This module lets you create squished content of css files. + +=head1 METHODS + +=cut + +use strict; +use warnings; + +package RT::Squish::CSS; +use base 'RT::Squish', 'CSS::Squish'; +__PACKAGE__->mk_accessors(qw/Style/); + +=head2 Squish + +use CSS::Squish to squish css + +=cut + +sub Squish { + my $self = shift; + my $style = $self->Style; + return $self->concatenate( "$style/main.css", RT->Config->Get('CSSFiles') ); +} + +=head2 file_handle + +subclass CSS::Squish::file_handle for RT + +=cut + +sub file_handle { + my $self = shift; + my $file = shift; + + my $path = "/NoAuth/css/$file"; + my $content; + if ( $HTML::Mason::Commands::m->comp_exists($path) ) { + $content = $HTML::Mason::Commands::m->scomp("$path"); + } else { + RT->Logger->error("Unable to open $path for CSS Squishing"); + return undef; + } + + open( my $fh, '<', \$content ) or die $!; + return $fh; +} + +1; + diff --git a/rt/lib/RT/Squish/JS.pm b/rt/lib/RT/Squish/JS.pm new file mode 100644 index 000000000..6309d016f --- /dev/null +++ b/rt/lib/RT/Squish/JS.pm @@ -0,0 +1,127 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +=head1 SYNOPSIS + + use RT::Squish::JS; + my $squish = RT::Squish::JS->new(); + +=head1 DESCRIPTION + +This module lets you create squished content of js files. + +=head1 METHODS + +=cut + +use strict; +use warnings; + +package RT::Squish::JS; +use base 'RT::Squish'; + +=head2 Squish + +not only concatenate files, but also minify them + +=cut + +sub Squish { + my $self = shift; + my $content; + + for my $file ( RT->Config->Get('JSFiles') ) { + my $path = "/NoAuth/js/$file"; + if ( $HTML::Mason::Commands::m->comp_exists($path) ) { + $content .= $HTML::Mason::Commands::m->scomp($path); + } else { + RT->Logger->error("Unable to open $path for JS Squishing"); + next; + } + } + + return $self->Filter($content); +} + +sub Filter { + my $self = shift; + my $content = shift; + + my $minified; + my $jsmin = RT->Config->Get('JSMinPath'); + if ( $jsmin && -x $jsmin ) { + my $input = $content; + my ( $output, $error ); + + # If we're running under fastcgi, STDOUT and STDERR are tied + # filehandles, which cause IPC::Run3 to flip out. Construct + # temporary, not-tied replacements for it to see instead. + my $stdout = IO::Handle->new; + $stdout->fdopen( 1, 'w' ); + local *STDOUT = $stdout; + my $stderr = IO::Handle->new; + $stderr->fdopen( 2, 'w' ); + local *STDERR = $stderr; + + local $SIG{'CHLD'} = 'DEFAULT'; + require IPC::Run3; + IPC::Run3::run3( [$jsmin], \$input, \$output, \$error ); + if ( $? >> 8 ) { + $RT::Logger->warning("failed to jsmin: $error "); + } + else { + $content = $output; + $minified = 1; + } + } + + return $content; +} + +1; + diff --git a/rt/lib/RT/Test/Apache.pm b/rt/lib/RT/Test/Apache.pm new file mode 100644 index 000000000..b2733eadb --- /dev/null +++ b/rt/lib/RT/Test/Apache.pm @@ -0,0 +1,270 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Test::Apache; +use strict; +use warnings; + +my %MODULES = ( + '2.2' => { + "mod_perl" => [qw(authz_host env alias perl)], + "fastcgi" => [qw(authz_host env alias mime fastcgi)], + }, +); + +my $apache_module_prefix = $ENV{RT_TEST_APACHE_MODULES}; +my $apxs = + $ENV{RT_TEST_APXS} + || RT::Test->find_executable('apxs') + || RT::Test->find_executable('apxs2'); + +if ($apxs and not $apache_module_prefix) { + $apache_module_prefix = `$apxs -q LIBEXECDIR`; + chomp $apache_module_prefix; +} + +$apache_module_prefix ||= 'modules'; + +sub basic_auth { + my $self = shift; + my $passwd = File::Spec->rel2abs( File::Spec->catfile( + 't', 'data', 'configs', 'passwords' ) ); + + return <<"EOT"; + AuthType Basic + AuthName "restricted area" + AuthUserFile $passwd + Require user root +EOT +} + +sub start_server { + my ($self, %config) = @_; + my %tmp = %{$config{tmp}}; + my %info = $self->apache_server_info( %config ); + + RT::Test::diag(do { + open( my $fh, '<', $tmp{'config'}{'RT'} ) or die $!; + local $/; + <$fh> + }); + + my $tmpl = File::Spec->rel2abs( File::Spec->catfile( + 't', 'data', 'configs', + 'apache'. $info{'version'} .'+'. $config{variant} .'.conf' + ) ); + my %opt = ( + listen => $config{port}, + server_root => $info{'HTTPD_ROOT'} || $ENV{'HTTPD_ROOT'} + || Test::More::BAIL_OUT("Couldn't figure out server root"), + document_root => $RT::MasonComponentRoot, + tmp_dir => "$tmp{'directory'}", + rt_bin_path => $RT::BinPath, + rt_sbin_path => $RT::SbinPath, + rt_site_config => $ENV{'RT_SITE_CONFIG'}, + load_modules => $info{load_modules}, + basic_auth => $config{basic_auth} ? $self->basic_auth : "", + ); + foreach (qw(log pid lock)) { + $opt{$_ .'_file'} = File::Spec->catfile( + "$tmp{'directory'}", "apache.$_" + ); + } + + $tmp{'config'}{'apache'} = File::Spec->catfile( + "$tmp{'directory'}", "apache.conf" + ); + $self->process_in_file( + in => $tmpl, + out => $tmp{'config'}{'apache'}, + options => \%opt, + ); + + $self->fork_exec($info{'executable'}, '-f', $tmp{'config'}{'apache'}); + my $pid = do { + my $tries = 15; + while ( !-s $opt{'pid_file'} ) { + $tries--; + last unless $tries; + sleep 1; + } + my $pid_fh; + unless (-e $opt{'pid_file'} and open($pid_fh, '<', $opt{'pid_file'})) { + Test::More::BAIL_OUT("Couldn't start apache server, no pid file (unknown error)") + unless -e $opt{log_file}; + + open my $log, "<", $opt{log_file}; + my $error = do {local $/; <$log>}; + close $log; + $RT::Logger->error($error) if $error; + Test::More::BAIL_OUT("Couldn't start apache server!"); + } + + my $pid = <$pid_fh>; + chomp $pid; + $pid; + }; + + Test::More::ok($pid, "Started apache server #$pid"); + return $pid; +} + +sub apache_server_info { + my $self = shift; + my %res = @_; + + my $bin = $res{'executable'} = $ENV{'RT_TEST_APACHE'} + || $self->find_apache_server + || Test::More::BAIL_OUT("Couldn't find apache server, use RT_TEST_APACHE"); + + Test::More::BAIL_OUT( + "Couldn't find apache modules directory (set APXS= or RT_TEST_APACHE_MODULES=)" + ) unless -d $apache_module_prefix; + + + RT::Test::diag("Using '$bin' apache executable for testing"); + + my $info = `$bin -V`; + ($res{'version'}) = ($info =~ m{Server\s+version:\s+Apache/(\d+\.\d+)\.}); + Test::More::BAIL_OUT( + "Couldn't figure out version of the server" + ) unless $res{'version'}; + + my %opts = ($info =~ m/^\s*-D\s+([A-Z_]+?)(?:="(.*)")$/mg); + %res = (%res, %opts); + + $res{'modules'} = [ + map {s/^\s+//; s/\s+$//; $_} + grep $_ !~ /Compiled in modules/i, + split /\r*\n/, `$bin -l` + ]; + + Test::More::BAIL_OUT( + "Unsupported apache version $res{version}" + ) unless exists $MODULES{$res{version}}; + + Test::More::BAIL_OUT( + "Unsupported apache variant $res{variant}" + ) unless exists $MODULES{$res{version}}{$res{variant}}; + + my @mlist = @{$MODULES{$res{version}}{$res{variant}}}; + push @mlist, "authn_file", "auth_basic", "authz_user" if $res{basic_auth}; + + $res{'load_modules'} = ''; + foreach my $mod ( @mlist ) { + next if grep $_ =~ /^(mod_|)$mod\.c$/, @{ $res{'modules'} }; + + my $so_file = $apache_module_prefix."/mod_".$mod.".so"; + Test::More::BAIL_OUT( "Couldn't load $mod module (expected in $so_file)" ) + unless -f $so_file; + $res{'load_modules'} .= + "LoadModule ${mod}_module $so_file\n"; + } + return %res; +} + +sub find_apache_server { + my $self = shift; + return $_ foreach grep defined, + map RT::Test->find_executable($_), + qw(httpd apache apache2 apache1); + return undef; +} + +sub apache_mpm_type { + my $self = shift; + my $apache = $self->find_apache_server; + my $out = `$apache -l`; + if ( $out =~ /^\s*(worker|prefork|event|itk)\.c\s*$/m ) { + return $1; + } +} + +sub fork_exec { + my $self = shift; + + RT::Test::__disconnect_rt(); + my $pid = fork; + unless ( defined $pid ) { + die "cannot fork: $!"; + } elsif ( !$pid ) { + exec @_; + die "can't exec `". join(' ', @_) ."` program: $!"; + } else { + RT::Test::__reconnect_rt(); + return $pid; + } +} + +sub process_in_file { + my $self = shift; + my %args = ( in => undef, options => undef, @_ ); + + my $text = RT::Test->file_content( $args{'in'} ); + while ( my ($opt) = ($text =~ /\%\%(.+?)\%\%/) ) { + my $value = $args{'options'}{ lc $opt }; + die "no value for $opt" unless defined $value; + + $text =~ s/\%\%\Q$opt\E\%\%/$value/g; + } + + my ($out_fh, $out_conf); + unless ( $args{'out'} ) { + ($out_fh, $out_conf) = tempfile(); + } else { + $out_conf = $args{'out'}; + open( $out_fh, '>', $out_conf ) + or die "couldn't open '$out_conf': $!"; + } + print $out_fh $text; + seek $out_fh, 0, 0; + + return ($out_fh, $out_conf); +} + +1; diff --git a/rt/lib/RT/Test/GnuPG.pm b/rt/lib/RT/Test/GnuPG.pm new file mode 100644 index 000000000..6cebb775b --- /dev/null +++ b/rt/lib/RT/Test/GnuPG.pm @@ -0,0 +1,360 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Test::GnuPG; +use strict; +use Test::More; +use base qw(RT::Test); +use File::Temp qw(tempdir); + +our @EXPORT = + qw(create_a_ticket update_ticket cleanup_headers set_queue_crypt_options + check_text_emails send_email_and_check_transaction + create_and_test_outgoing_emails + ); + +sub import { + my $class = shift; + my %args = @_; + my $t = $class->builder; + + $t->plan( skip_all => 'GnuPG required.' ) + unless eval { require GnuPG::Interface; 1 }; + $t->plan( skip_all => 'gpg executable is required.' ) + unless RT::Test->find_executable('gpg'); + + require RT::Crypt::GnuPG; + $class->SUPER::import(%args); + + RT::Test::diag "GnuPG --homedir " . RT->Config->Get('GnuPGOptions')->{'homedir'}; + + $class->set_rights( + Principal => 'Everyone', + Right => ['CreateTicket', 'ShowTicket', 'SeeQueue', 'ReplyToTicket', 'ModifyTicket'], + ); + + $class->export_to_level(1); +} + +sub bootstrap_more_config { + my $self = shift; + my $handle = shift; + my $args = shift; + + $self->SUPER::bootstrap_more_config($handle, $args, @_); + + my %gnupg_options = ( + 'no-permission-warning' => undef, + $args->{gnupg_options} ? %{ $args->{gnupg_options} } : (), + ); + $gnupg_options{homedir} ||= scalar tempdir( CLEANUP => 1 ); + + use Data::Dumper; + local $Data::Dumper::Terse = 1; # "{...}" instead of "$VAR1 = {...};" + my $dumped_gnupg_options = Dumper(\%gnupg_options); + + print $handle qq{ +Set(\%GnuPG, ( + Enable => 1, + OutgoingMessagesFormat => 'RFC', +)); +Set(\%GnuPGOptions => \%{ $dumped_gnupg_options }); +Set(\@MailPlugins => qw(Auth::MailFrom Auth::GnuPG)); +}; + +} + +sub create_a_ticket { + my $queue = shift; + my $mail = shift; + my $m = shift; + my %args = (@_); + + RT::Test->clean_caught_mails; + + $m->goto_create_ticket( $queue ); + $m->form_name('TicketCreate'); + $m->field( Subject => 'test' ); + $m->field( Requestors => 'rt-test@example.com' ); + $m->field( Content => 'Some content' ); + + foreach ( qw(Sign Encrypt) ) { + if ( $args{ $_ } ) { + $m->tick( $_ => 1 ); + } else { + $m->untick( $_ => 1 ); + } + } + + $m->submit; + is $m->status, 200, "request successful"; + + $m->content_lacks("unable to sign outgoing email messages"); + + + my @mail = RT::Test->fetch_caught_mails; + check_text_emails(\%args, @mail ); + categorize_emails($mail, \%args, @mail ); +} + +sub update_ticket { + my $tid = shift; + my $mail = shift; + my $m = shift; + my %args = (@_); + + RT::Test->clean_caught_mails; + + $m->get( $m->rt_base_url . "/Ticket/Update.html?Action=Respond&id=$tid" ); + $m->form_number(3); + $m->field( UpdateContent => 'Some content' ); + + foreach ( qw(Sign Encrypt) ) { + if ( $args{ $_ } ) { + $m->tick( $_ => 1 ); + } else { + $m->untick( $_ => 1 ); + } + } + + $m->click('SubmitTicket'); + is $m->status, 200, "request successful"; + $m->content_contains("Message recorded", 'Message recorded') or diag $m->content; + + + my @mail = RT::Test->fetch_caught_mails; + check_text_emails(\%args, @mail ); + categorize_emails($mail, \%args, @mail ); +} + +sub categorize_emails { + my $mail = shift; + my $args = shift; + my @mail = @_; + + if ( $args->{'Sign'} && $args->{'Encrypt'} ) { + push @{ $mail->{'signed_encrypted'} }, @mail; + } + elsif ( $args->{'Sign'} ) { + push @{ $mail->{'signed'} }, @mail; + } + elsif ( $args->{'Encrypt'} ) { + push @{ $mail->{'encrypted'} }, @mail; + } + else { + push @{ $mail->{'plain'} }, @mail; + } +} + +sub check_text_emails { + my %args = %{ shift @_ }; + my @mail = @_; + + ok scalar @mail, "got some mail"; + for my $mail (@mail) { + for my $type ('email', 'attachment') { + next if $type eq 'attachment' && !$args{'Attachment'}; + + my $content = $type eq 'email' + ? "Some content" + : "Attachment content"; + + if ( $args{'Encrypt'} ) { + unlike $mail, qr/$content/, "outgoing $type was encrypted"; + } else { + like $mail, qr/$content/, "outgoing $type was not encrypted"; + } + + next unless $type eq 'email'; + + if ( $args{'Sign'} && $args{'Encrypt'} ) { + like $mail, qr/BEGIN PGP MESSAGE/, 'outgoing email was signed'; + } elsif ( $args{'Sign'} ) { + like $mail, qr/SIGNATURE/, 'outgoing email was signed'; + } else { + unlike $mail, qr/SIGNATURE/, 'outgoing email was not signed'; + } + } + } +} + +sub cleanup_headers { + my $mail = shift; + # strip id from subject to create new ticket + $mail =~ s/^(Subject:)\s*\[.*?\s+#\d+\]\s*/$1 /m; + # strip several headers + foreach my $field ( qw(Message-ID X-RT-Original-Encoding RT-Originator RT-Ticket X-RT-Loop-Prevention) ) { + $mail =~ s/^$field:.*?\n(?! |\t)//gmsi; + } + return $mail; +} + +sub set_queue_crypt_options { + my $queue = shift; + my %args = @_; + $queue->SetEncrypt($args{'Encrypt'}); + $queue->SetSign($args{'Sign'}); +} + +sub send_email_and_check_transaction { + my $mail = shift; + my $type = shift; + + my ( $status, $id ) = RT::Test->send_via_mailgate($mail); + is( $status >> 8, 0, "The mail gateway exited normally" ); + ok( $id, "got id of a newly created ticket - $id" ); + + my $tick = RT::Ticket->new( RT->SystemUser ); + $tick->Load($id); + ok( $tick->id, "loaded ticket #$id" ); + + my $txn = $tick->Transactions->First; + my ( $msg, @attachments ) = @{ $txn->Attachments->ItemsArrayRef }; + + if ( $attachments[0] ) { + like $attachments[0]->Content, qr/Some content/, + "RT's mail includes copy of ticket text"; + } + else { + like $msg->Content, qr/Some content/, + "RT's mail includes copy of ticket text"; + } + + if ( $type eq 'plain' ) { + ok !$msg->GetHeader('X-RT-Privacy'), "RT's outgoing mail has no crypto"; + is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Not encrypted', + "RT's outgoing mail looks not encrypted"; + ok !$msg->GetHeader('X-RT-Incoming-Signature'), + "RT's outgoing mail looks not signed"; + } + elsif ( $type eq 'signed' ) { + is $msg->GetHeader('X-RT-Privacy'), 'PGP', + "RT's outgoing mail has crypto"; + is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Not encrypted', + "RT's outgoing mail looks not encrypted"; + like $msg->GetHeader('X-RT-Incoming-Signature'), + qr/<rt-recipient\@example.com>/, + "RT's outgoing mail looks signed"; + } + elsif ( $type eq 'encrypted' ) { + is $msg->GetHeader('X-RT-Privacy'), 'PGP', + "RT's outgoing mail has crypto"; + is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Success', + "RT's outgoing mail looks encrypted"; + ok !$msg->GetHeader('X-RT-Incoming-Signature'), + "RT's outgoing mail looks not signed"; + + } + elsif ( $type eq 'signed_encrypted' ) { + is $msg->GetHeader('X-RT-Privacy'), 'PGP', + "RT's outgoing mail has crypto"; + is $msg->GetHeader('X-RT-Incoming-Encryption'), 'Success', + "RT's outgoing mail looks encrypted"; + like $msg->GetHeader('X-RT-Incoming-Signature'), + qr/<rt-recipient\@example.com>/, + "RT's outgoing mail looks signed"; + } + else { + die "unknown type: $type"; + } +} + +sub create_and_test_outgoing_emails { + my $queue = shift; + my $m = shift; + my @variants = + ( {}, { Sign => 1 }, { Encrypt => 1 }, { Sign => 1, Encrypt => 1 }, ); + + # collect emails + my %mail; + + # create a ticket for each combination + foreach my $ticket_set (@variants) { + create_a_ticket( $queue, \%mail, $m, %$ticket_set ); + } + + my $tid; + { + my $ticket = RT::Ticket->new( RT->SystemUser ); + ($tid) = $ticket->Create( + Subject => 'test', + Queue => $queue->id, + Requestor => 'rt-test@example.com', + ); + ok $tid, 'ticket created'; + } + + # again for each combination add a reply message + foreach my $ticket_set (@variants) { + update_ticket( $tid, \%mail, $m, %$ticket_set ); + } + +# ------------------------------------------------------------------------------ +# now delete all keys from the keyring and put back secret/pub pair for rt-test@ +# and only public key for rt-recipient@ so we can verify signatures and decrypt +# like we are on another side recieve emails +# ------------------------------------------------------------------------------ + + unlink $_ + foreach glob( RT->Config->Get('GnuPGOptions')->{'homedir'} . "/*" ); + RT::Test->import_gnupg_key( 'rt-recipient@example.com', 'public' ); + RT::Test->import_gnupg_key('rt-test@example.com'); + + $queue = RT::Test->load_or_create_queue( + Name => 'Regression', + CorrespondAddress => 'rt-test@example.com', + CommentAddress => 'rt-test@example.com', + ); + ok $queue && $queue->id, 'changed props of the queue'; + + for my $type ( keys %mail ) { + for my $mail ( map cleanup_headers($_), @{ $mail{$type} } ) { + send_email_and_check_transaction( $mail, $type ); + } + } +} diff --git a/rt/lib/RT/Tickets_SQL.pm b/rt/lib/RT/Tickets_SQL.pm new file mode 100644 index 000000000..ec1bb4997 --- /dev/null +++ b/rt/lib/RT/Tickets_SQL.pm @@ -0,0 +1,423 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::Tickets; + +use strict; +use warnings; + + +use RT::SQL; + +# Import configuration data from the lexcial scope of __PACKAGE__ (or +# at least where those two Subroutines are defined.) + +our (%FIELD_METADATA, %dispatch, %can_bundle); + +# Lower Case version of FIELDS, for case insensitivity +my %lcfields = map { ( lc($_) => $_ ) } (keys %FIELD_METADATA); + +sub _InitSQL { + my $self = shift; + + # Private Member Variables (which should get cleaned) + $self->{'_sql_transalias'} = undef; + $self->{'_sql_trattachalias'} = undef; + $self->{'_sql_cf_alias'} = undef; + $self->{'_sql_object_cfv_alias'} = undef; + $self->{'_sql_watcher_join_users_alias'} = undef; + $self->{'_sql_query'} = ''; + $self->{'_sql_looking_at'} = {}; +} + +sub _SQLLimit { + my $self = shift; + my %args = (@_); + if ($args{'FIELD'} eq 'EffectiveId' && + (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) { + $self->{'looking_at_effective_id'} = 1; + } + + if ($args{'FIELD'} eq 'Type' && + (!$args{'ALIAS'} || $args{'ALIAS'} eq 'main' ) ) { + $self->{'looking_at_type'} = 1; + } + + # All SQL stuff goes into one SB subclause so we can deal with all + # the aggregation + $self->SUPER::Limit(%args, + SUBCLAUSE => 'ticketsql'); +} + +sub _SQLJoin { + # All SQL stuff goes into one SB subclause so we can deal with all + # the aggregation + my $this = shift; + + $this->SUPER::Join(@_, + SUBCLAUSE => 'ticketsql'); +} + +# Helpers +sub _OpenParen { + $_[0]->SUPER::_OpenParen( 'ticketsql' ); +} +sub _CloseParen { + $_[0]->SUPER::_CloseParen( 'ticketsql' ); +} + +=head1 SQL Functions + +=cut + +=head2 Robert's Simple SQL Parser + +Documentation In Progress + +The Parser/Tokenizer is a relatively simple state machine that scans through a SQL WHERE clause type string extracting a token at a time (where a token is: + + VALUE -> quoted string or number + AGGREGator -> AND or OR + KEYWORD -> quoted string or single word + OPerator -> =,!=,LIKE,etc.. + PARENthesis -> open or close. + +And that stream of tokens is passed through the "machine" in order to build up a structure that looks like: + + KEY OP VALUE + AND KEY OP VALUE + OR KEY OP VALUE + +That also deals with parenthesis for nesting. (The parentheses are +just handed off the SearchBuilder) + +=cut + +sub _close_bundle { + my ($self, @bundle) = @_; + return unless @bundle; + + if ( @bundle == 1 ) { + $bundle[0]->{'dispatch'}->( + $self, + $bundle[0]->{'key'}, + $bundle[0]->{'op'}, + $bundle[0]->{'val'}, + SUBCLAUSE => '', + ENTRYAGGREGATOR => $bundle[0]->{ea}, + SUBKEY => $bundle[0]->{subkey}, + ); + } + else { + my @args; + foreach my $chunk (@bundle) { + push @args, [ + $chunk->{key}, + $chunk->{op}, + $chunk->{val}, + SUBCLAUSE => '', + ENTRYAGGREGATOR => $chunk->{ea}, + SUBKEY => $chunk->{subkey}, + ]; + } + $bundle[0]->{dispatch}->( $self, \@args ); + } +} + +sub _parser { + my ($self,$string) = @_; + my @bundle; + my $ea = ''; + + my %callback; + $callback{'OpenParen'} = sub { + $self->_close_bundle(@bundle); @bundle = (); + $self->_OpenParen + }; + $callback{'CloseParen'} = sub { + $self->_close_bundle(@bundle); @bundle = (); + $self->_CloseParen; + }; + $callback{'EntryAggregator'} = sub { $ea = $_[0] || '' }; + $callback{'Condition'} = sub { + my ($key, $op, $value) = @_; + + # key has dot then it's compound variant and we have subkey + my $subkey = ''; + ($key, $subkey) = ($1, $2) if $key =~ /^([^\.]+)\.(.+)$/; + + # normalize key and get class (type) + my $class; + if (exists $lcfields{lc $key}) { + $key = $lcfields{lc $key}; + $class = $FIELD_METADATA{$key}->[0]; + } + die "Unknown field '$key' in '$string'" unless $class; + + # replace __CurrentUser__ with id + $value = $self->CurrentUser->id if $value eq '__CurrentUser__'; + + + unless( $dispatch{ $class } ) { + die "No dispatch method for class '$class'" + } + my $sub = $dispatch{ $class }; + + if ( $can_bundle{ $class } + && ( !@bundle + || ( $bundle[-1]->{dispatch} == $sub + && $bundle[-1]->{key} eq $key + && $bundle[-1]->{subkey} eq $subkey + ) + ) + ) + { + push @bundle, { + dispatch => $sub, + key => $key, + op => $op, + val => $value, + ea => $ea, + subkey => $subkey, + }; + } + else { + $self->_close_bundle(@bundle); @bundle = (); + $sub->( $self, $key, $op, $value, + SUBCLAUSE => '', # don't need anymore + ENTRYAGGREGATOR => $ea, + SUBKEY => $subkey, + ); + } + $self->{_sql_looking_at}{lc $key} = 1; + $ea = ''; + }; + RT::SQL::Parse($string, \%callback); + $self->_close_bundle(@bundle); @bundle = (); +} + +=head2 ClausesToSQL + +=cut + +sub ClausesToSQL { + my $self = shift; + my $clauses = shift; + my @sql; + + for my $f (keys %{$clauses}) { + my $sql; + my $first = 1; + + # Build SQL from the data hash + for my $data ( @{ $clauses->{$f} } ) { + $sql .= $data->[0] unless $first; $first=0; # ENTRYAGGREGATOR + $sql .= " '". $data->[2] . "' "; # FIELD + $sql .= $data->[3] . " "; # OPERATOR + $sql .= "'". $data->[4] . "' "; # VALUE + } + + push @sql, " ( " . $sql . " ) "; + } + + return join("AND",@sql); +} + +=head2 FromSQL + +Convert a RT-SQL string into a set of SearchBuilder restrictions. + +Returns (1, 'Status message') on success and (0, 'Error Message') on +failure. + + + + +=cut + +sub FromSQL { + my ($self,$query) = @_; + + { + # preserve first_row and show_rows across the CleanSlate + local ($self->{'first_row'}, $self->{'show_rows'}); + $self->CleanSlate; + } + $self->_InitSQL(); + + return (1, $self->loc("No Query")) unless $query; + + $self->{_sql_query} = $query; + eval { $self->_parser( $query ); }; + if ( $@ ) { + $RT::Logger->error( $@ ); + return (0, $@); + } + + # We only want to look at EffectiveId's (mostly) for these searches. + unless ( exists $self->{_sql_looking_at}{'effectiveid'} ) { + #TODO, we shouldn't be hard #coding the tablename to main. + $self->SUPER::Limit( FIELD => 'EffectiveId', + VALUE => 'main.id', + ENTRYAGGREGATOR => 'AND', + QUOTEVALUE => 0, + ); + } + # FIXME: Need to bring this logic back in + + # if ($self->_isLimited && (! $self->{'looking_at_effective_id'})) { + # $self->SUPER::Limit( FIELD => 'EffectiveId', + # OPERATOR => '=', + # QUOTEVALUE => 0, + # VALUE => 'main.id'); #TODO, we shouldn't be hard coding the tablename to main. + # } + # --- This is hardcoded above. This comment block can probably go. + # Or, we need to reimplement the looking_at_effective_id toggle. + + # Unless we've explicitly asked to look at a specific Type, we need + # to limit to it. + unless ( $self->{looking_at_type} ) { + $self->SUPER::Limit( FIELD => 'Type', VALUE => 'ticket' ); + } + + # We don't want deleted tickets unless 'allow_deleted_search' is set + unless( $self->{'allow_deleted_search'} ) { + $self->SUPER::Limit( FIELD => 'Status', + OPERATOR => '!=', + VALUE => 'deleted', + ); + } + + # set SB's dirty flag + $self->{'must_redo_search'} = 1; + $self->{'RecalcTicketLimits'} = 0; + + return (1, $self->loc("Valid Query")); +} + +=head2 Query + +Returns the query that this object was initialized with + +=cut + +sub Query { + return ($_[0]->{_sql_query}); +} + +{ +my %inv = ( + '=' => '!=', '!=' => '=', '<>' => '=', + '>' => '<=', '<' => '>=', '>=' => '<', '<=' => '>', + 'is' => 'IS NOT', 'is not' => 'IS', + 'like' => 'NOT LIKE', 'not like' => 'LIKE', + 'matches' => 'NOT MATCHES', 'not matches' => 'MATCHES', + 'startswith' => 'NOT STARTSWITH', 'not startswith' => 'STARTSWITH', + 'endswith' => 'NOT ENDSWITH', 'not endswith' => 'ENDSWITH', +); + +my %range = map { $_ => 1 } qw(> >= < <=); + +sub ClassifySQLOperation { + my $self = shift; + my $op = shift; + + my $is_negative = 0; + if ( $op eq '!=' || $op =~ /\bNOT\b/i ) { + $is_negative = 1; + } + + my $is_null = 0; + if ( 'is not' eq lc($op) || 'is' eq lc($op) ) { + $is_null = 1; + } + + return ($is_negative, $is_null, $inv{lc $op}, $range{lc $op}); +} } + +1; + +=pod + +=head2 Exceptions + +Most of the RT code does not use Exceptions (die/eval) but it is used +in the TicketSQL code for simplicity and historical reasons. Lest you +be worried that the dies will trigger user visible errors, all are +trapped via evals. + +99% of the dies fall in subroutines called via FromSQL and then parse. +(This includes all of the _FooLimit routines in Tickets_Overlay.pm.) +The other 1% or so are via _ProcessRestrictions. + +All dies are trapped by eval {}s, and will be logged at the 'error' +log level. The general failure mode is to not display any tickets. + +=head2 General Flow + +Legacy Layer: + + Legacy LimitFoo routines build up a RestrictionsHash + + _ProcessRestrictions converts the Restrictions to Clauses + ([key,op,val,rest]). + + Clauses are converted to RT-SQL (TicketSQL) + +New RT-SQL Layer: + + FromSQL calls the parser + + The parser calls the _FooLimit routines to do DBIx::SearchBuilder + limits. + +And then the normal SearchBuilder/Ticket routines are used for +display/navigation. + +=cut + diff --git a/rt/lib/RT/Topic.pm b/rt/lib/RT/Topic.pm new file mode 100644 index 000000000..3499a4d2f --- /dev/null +++ b/rt/lib/RT/Topic.pm @@ -0,0 +1,376 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use warnings; +use strict; + +package RT::Topic; +use base 'RT::Record'; + +sub Table {'Topics'} + +# {{{ Create + +=head2 Create PARAMHASH + +Create takes a hash of values and creates a row in the database: + + int(11) 'Parent'. + varchar(255) 'Name'. + varchar(255) 'Description'. + varchar(64) 'ObjectType'. + int(11) 'ObjectId'. + +=cut + +sub Create { + my $self = shift; + my %args = ( + Parent => '', + Name => '', + Description => '', + ObjectType => '', + ObjectId => '0', + @_); + + my $obj = $RT::System; + if ($args{ObjectId}) { + $obj = $args{ObjectType}->new($self->CurrentUser); + $obj->Load($args{ObjectId}); + $obj = $RT::System unless $obj->id; + } + + return ( 0, $self->loc("Permission denied")) + unless ( $self->CurrentUser->HasRight( + Right => "AdminTopics", + Object => $obj, + EquivObjects => [ $RT::System, $obj ], + ) ); + + $self->SUPER::Create(@_); +} + +# }}} + + +# {{{ Delete + +=head2 Delete + +Deletes this topic, reparenting all sub-topics to this one's parent. + +=cut + +sub Delete { + my $self = shift; + + unless ( $self->CurrentUserHasRight('AdminTopics') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + my $kids = RT::Topics->new($self->CurrentUser); + $kids->LimitToKids($self->Id); + while (my $topic = $kids->Next) { + $topic->setParent($self->Parent); + } + + $self->SUPER::Delete(@_); + return (0, "Topic deleted"); +} + +# }}} + + +# {{{ DeleteAll + +=head2 DeleteAll + +Deletes this topic, and all of its descendants. + +=cut + +sub DeleteAll { + my $self = shift; + + unless ( $self->CurrentUserHasRight('AdminTopics') ) { + return ( 0, $self->loc("Permission Denied") ); + } + + $self->SUPER::Delete(@_); + my $kids = RT::Topics->new($self->CurrentUser); + $kids->LimitToKids($self->Id); + while (my $topic = $kids->Next) { + $topic->DeleteAll; + } + + return (0, "Topic tree deleted"); +} + +# }}} + + +# {{{ ParentObj + +=head2 ParentObj + +Returns the parent Topic of this one. + +=cut + +sub ParentObj { + my $self = shift; + my $id = $self->Parent; + my $obj = RT::Topic->new($self->CurrentUser); + $obj->Load($id); + return $obj; +} + +# }}} + +# {{{ Children + +=head2 Children + +Returns a Topics object containing this topic's children, +sorted by Topic.Name. + +=cut + +sub Children { + my $self = shift; + unless ($self->{'Children'}) { + $self->{'Children'} = RT::Topics->new($self->CurrentUser); + $self->{'Children'}->Limit('FIELD' => 'Parent', + 'VALUE' => $self->Id); + $self->{'Children'}->OrderBy('FIELD' => 'Name'); + } + return $self->{'Children'}; +} + +# {{{ _Set + +=head2 _Set + +Intercept attempts to modify the Topic so we can apply ACLs + +=cut + +sub _Set { + my $self = shift; + + unless ( $self->CurrentUserHasRight('AdminTopics') ) { + return ( 0, $self->loc("Permission Denied") ); + } + $self->SUPER::_Set(@_); +} + +# }}} + + +# {{{ CurrentUserHasRight + +=head2 CurrentUserHasRight + +Returns true if the current user has the right for this topic, for the +whole system or for whatever object this topic is associated with + +=cut + +sub CurrentUserHasRight { + my $self = shift; + my $right = shift; + + my $equiv = [ $RT::System ]; + if ($self->ObjectId) { + my $obj = $self->ObjectType->new($self->CurrentUser); + $obj->Load($self->ObjectId); + push @{$equiv}, $obj; + } + if ($self->Id) { + return ( $self->CurrentUser->HasRight( + Right => $right, + Object => $self, + EquivObjects => $equiv, + ) ); + } else { + # If we don't have an ID, we don't even know what object we're + # attached to -- so the only thing we can fall back on is the + # system object. + return ( $self->CurrentUser->HasRight( + Right => $right, + Object => $RT::System, + ) ); + } + + +} + +# }}} + + +=head2 id + +Returns the current value of id. +(In the database, id is stored as int(11).) + + +=cut + + +=head2 Parent + +Returns the current value of Parent. +(In the database, Parent is stored as int(11).) + + + +=head2 SetParent VALUE + + +Set Parent to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Parent will be stored as a int(11).) + + +=cut + + +=head2 Name + +Returns the current value of Name. +(In the database, Name is stored as varchar(255).) + + + +=head2 SetName VALUE + + +Set Name to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Name will be stored as a varchar(255).) + + +=cut + + +=head2 Description + +Returns the current value of Description. +(In the database, Description is stored as varchar(255).) + + + +=head2 SetDescription VALUE + + +Set Description to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, Description will be stored as a varchar(255).) + + +=cut + + +=head2 ObjectType + +Returns the current value of ObjectType. +(In the database, ObjectType is stored as varchar(64).) + + + +=head2 SetObjectType VALUE + + +Set ObjectType to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ObjectType will be stored as a varchar(64).) + + +=cut + + +=head2 ObjectId + +Returns the current value of ObjectId. +(In the database, ObjectId is stored as int(11).) + + + +=head2 SetObjectId VALUE + + +Set ObjectId to VALUE. +Returns (1, 'Status message') on success and (0, 'Error Message') on failure. +(In the database, ObjectId will be stored as a int(11).) + + +=cut + + + +sub _CoreAccessible { + { + + id => + {read => 1, type => 'int(11)', default => ''}, + Parent => + {read => 1, write => 1, type => 'int(11)', default => ''}, + Name => + {read => 1, write => 1, type => 'varchar(255)', default => ''}, + Description => + {read => 1, write => 1, type => 'varchar(255)', default => ''}, + ObjectType => + {read => 1, write => 1, type => 'varchar(64)', default => ''}, + ObjectId => + {read => 1, write => 1, type => 'int(11)', default => '0'}, + + } +}; + +RT::Base->_ImportOverlays(); +1; diff --git a/rt/lib/RT/Topics.pm b/rt/lib/RT/Topics.pm new file mode 100644 index 000000000..fe7c3d819 --- /dev/null +++ b/rt/lib/RT/Topics.pm @@ -0,0 +1,119 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +use strict; +no warnings qw(redefine); + +package RT::Topics; +use base 'RT::SearchBuilder'; + +sub Table {'Topics'} + + +# {{{ LimitToObject + +=head2 LimitToObject OBJ + +Find all Topics hung off of the given Object + +=cut + +sub LimitToObject { + my $self = shift; + my $object = shift; + + my $subclause = "limittoobject"; + + $self->_OpenParen($subclause); + $self->Limit(FIELD => 'ObjectId', + VALUE => $object->Id, + SUBCLAUSE => $subclause); + $self->Limit(FIELD => 'ObjectType', + VALUE => ref($object), + SUBCLAUSE => $subclause, + ENTRYAGGREGATOR => 'AND'); + $self->_CloseParen($subclause); +} + +# }}} + + +# {{{ LimitToKids + +=head2 LimitToKids TOPIC + +Find all Topics which are immediate children of Id TOPIC. Note this +does not do the recursive query of their kids, etc. + +=cut + +sub LimitToKids { + my $self = shift; + my $topic = shift; + + $self->Limit(FIELD => 'Parent', + VALUE => $topic); +} + +# }}} + +=head2 NewItem + +Returns an empty new RT::Topic item + +=cut + +sub NewItem { + my $self = shift; + return(RT::Topic->new($self->CurrentUser)); +} + + +RT::Base->_ImportOverlays(); + +1; diff --git a/rt/lib/RT/URI/a.pm b/rt/lib/RT/URI/a.pm new file mode 100644 index 000000000..b88af26fc --- /dev/null +++ b/rt/lib/RT/URI/a.pm @@ -0,0 +1,92 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::URI::a; + +use strict; +use warnings; + +use RT::Article; +use base qw/RT::URI::fsck_com_article/; + +my $scheme = "a"; + +=head2 ParseURI URI + +When handed an a: URI, figures out if it is an article. + +=cut + +sub ParseURI { + my $self = shift; + my $uri = shift; + + # "a:<articlenum>" + # Pass this off to fsck_com_article, which is equipped to deal with + # articles after stripping off the a: prefix. + + if ($uri =~ /^$scheme:(\d+)/) { + my $value = $1; + return $self->SUPER::ParseURI($value); + } else { + $self->{'uri'} = $uri; + return undef; + } +} + +=head2 Scheme + +Return the URI scheme + +=cut + +sub Scheme { + return $scheme; +} + +1; diff --git a/rt/lib/RT/URI/fsck_com_article.pm b/rt/lib/RT/URI/fsck_com_article.pm new file mode 100644 index 000000000..503434a1c --- /dev/null +++ b/rt/lib/RT/URI/fsck_com_article.pm @@ -0,0 +1,216 @@ +# BEGIN BPS TAGGED BLOCK {{{ +# +# COPYRIGHT: +# +# This software is Copyright (c) 1996-2012 Best Practical Solutions, LLC +# <sales@bestpractical.com> +# +# (Except where explicitly superseded by other copyright notices) +# +# +# LICENSE: +# +# This work is made available to you under the terms of Version 2 of +# the GNU General Public License. A copy of that license should have +# been provided with this software, but in any event can be snarfed +# from www.gnu.org. +# +# This work is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301 or visit their web page on the internet at +# http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. +# +# +# CONTRIBUTION SUBMISSION POLICY: +# +# (The following paragraph is not intended to limit the rights granted +# to you to modify and distribute this software under the terms of +# the GNU General Public License and is only of importance to you if +# you choose to contribute your changes and enhancements to the +# community by submitting them to Best Practical Solutions, LLC.) +# +# By intentionally submitting any modifications, corrections or +# derivatives to this work, or any other work intended for use with +# Request Tracker, to Best Practical Solutions, LLC, you confirm that +# you are the copyright holder for those contributions and you grant +# Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, +# royalty-free, perpetual, license to use, copy, create derivative +# works based on those contributions, and sublicense and distribute +# those contributions and any derivatives thereof. +# +# END BPS TAGGED BLOCK }}} + +package RT::URI::fsck_com_article; + +use strict; +use warnings; +no warnings 'redefine'; + +use RT::Article; +use base qw/RT::URI::base/; + +=head2 LocalURIPrefix + +Returns the prefix for a local article URI + +=cut + +sub LocalURIPrefix { + my $self = shift; + my $prefix = $self->Scheme. "://". RT->Config->Get('Organization') + . "/article/"; + return ($prefix); +} + +=head2 URIForObject RT::article + +Returns the RT URI for a local RT::article object + +=cut + +sub URIForObject { + + my $self = shift; + + my $obj = shift; + return ($self->LocalURIPrefix. $obj->Id); +} + + +=head2 ParseObject $ArticleObj + +When handed an L<RT::Article> object, figure out its URI + +=cut + +=head2 ParseURI URI + +When handed an fsck.com-article URI, figures out things like whether its a local article +and what its ID is + +=cut + +sub ParseURI { + my $self = shift; + my $uri = shift; + + my $article; + + if ($uri =~ /^(\d+)$/) { + $article = RT::Article->new($self->CurrentUser); + $article->Load($uri); + $self->{'uri'} = $article->URI; + } + else { + $self->{'uri'} = $uri; + } + + #If it's a local URI, load the article object and return its URI + if ( $self->IsLocal) { + + my $local_uri_prefix = $self->LocalURIPrefix; + if ($self->{'uri'} =~ /^$local_uri_prefix(\d+)$/) { + my $id = $1; + + + $article = RT::Article->new( $self->CurrentUser ); + $article->Load($id); + + #If we couldn't find a article, return undef. + unless ( defined $article->Id ) { + return undef; + } + } else { + return undef; + } + } + + $self->{'object'} = $article; + return ($article->Id); +} + +=head2 IsLocal + +Returns true if this URI is for a local article. +Returns undef otherwise. + +=cut + +sub IsLocal { + my $self = shift; + my $local_uri_prefix = $self->LocalURIPrefix; + if ($self->{'uri'} =~ /^$local_uri_prefix/) { + return 1; + } + else { + return undef; + } +} + + + +=head2 Object + +Returns the object for this URI, if it's local. Otherwise returns undef. + +=cut + +sub Object { + my $self = shift; + return ($self->{'object'}); + +} + +=head2 Scheme + +Return the URI scheme for RT articles + +=cut + +sub Scheme { + my $self = shift; + return "fsck.com-article"; +} + +=head2 HREF + +If this is a local article, return an HTTP url to it. +Otherwise, return its URI + +=cut + +sub HREF { + my $self = shift; + if ($self->IsLocal && $self->Object) { + return ( RT->Config->Get('WebURL') . "/Articles/Article/Display.html?id=".$self->Object->Id); + } + else { + return ($self->URI); + } +} + +=head2 AsString + +Return "Article 23" + +=cut + +sub AsString { + my $self = shift; + if ($self->IsLocal && $self->Object) { + return $self->loc('Article [_1]', $self->Object->id); + + } else { + return $self->SUPER::AsString(@_); + } + +} + + +1; |