summaryrefslogtreecommitdiff
path: root/rt/lib/RT
diff options
context:
space:
mode:
Diffstat (limited to 'rt/lib/RT')
-rw-r--r--rt/lib/RT/Action/SetStatus.pm152
-rw-r--r--rt/lib/RT/Article.pm870
-rw-r--r--rt/lib/RT/Articles.pm925
-rw-r--r--rt/lib/RT/Class.pm620
-rw-r--r--rt/lib/RT/Classes.pm104
-rw-r--r--rt/lib/RT/Dashboard/Mailer.pm577
-rw-r--r--rt/lib/RT/Dashboards.pm112
-rw-r--r--rt/lib/RT/Generated.pm81
-rw-r--r--rt/lib/RT/Generated.pm.in81
-rw-r--r--rt/lib/RT/Lifecycle.pm670
-rw-r--r--rt/lib/RT/ObjectClass.pm222
-rw-r--r--rt/lib/RT/ObjectClasses.pm87
-rw-r--r--rt/lib/RT/ObjectTopic.pm214
-rw-r--r--rt/lib/RT/ObjectTopics.pm115
-rw-r--r--rt/lib/RT/SharedSettings.pm155
-rw-r--r--rt/lib/RT/Squish.pm122
-rw-r--r--rt/lib/RT/Squish/CSS.pm105
-rw-r--r--rt/lib/RT/Squish/JS.pm127
-rw-r--r--rt/lib/RT/Test/Apache.pm270
-rw-r--r--rt/lib/RT/Test/GnuPG.pm360
-rw-r--r--rt/lib/RT/Tickets_SQL.pm423
-rw-r--r--rt/lib/RT/Topic.pm376
-rw-r--r--rt/lib/RT/Topics.pm119
-rw-r--r--rt/lib/RT/URI/a.pm92
-rw-r--r--rt/lib/RT/URI/fsck_com_article.pm216
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;