X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=rt%2Flib%2FRT%2FArticle.pm;fp=rt%2Flib%2FRT%2FArticle.pm;h=7310241ee3bf08605110930d6b8db23f65226044;hb=6587f6ba7d047ddc1686c080090afe7d53365bd4;hp=0000000000000000000000000000000000000000;hpb=47153aae5c2fc00316654e7277fccd45f72ff611;p=freeside.git 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 +# +# +# (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;