589a6b814e8c122dd06fccf2372c42b2a812a191
[usit-rt.git] / lib / RT / Article.pm
1 # BEGIN BPS TAGGED BLOCK {{{
2 #
3 # COPYRIGHT:
4 #
5 # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
6 #                                          <sales@bestpractical.com>
7 #
8 # (Except where explicitly superseded by other copyright notices)
9 #
10 #
11 # LICENSE:
12 #
13 # This work is made available to you under the terms of Version 2 of
14 # the GNU General Public License. A copy of that license should have
15 # been provided with this software, but in any event can be snarfed
16 # from www.gnu.org.
17 #
18 # This work is distributed in the hope that it will be useful, but
19 # WITHOUT ANY WARRANTY; without even the implied warranty of
20 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
21 # General Public License for more details.
22 #
23 # You should have received a copy of the GNU General Public License
24 # along with this program; if not, write to the Free Software
25 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
26 # 02110-1301 or visit their web page on the internet at
27 # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html.
28 #
29 #
30 # CONTRIBUTION SUBMISSION POLICY:
31 #
32 # (The following paragraph is not intended to limit the rights granted
33 # to you to modify and distribute this software under the terms of
34 # the GNU General Public License and is only of importance to you if
35 # you choose to contribute your changes and enhancements to the
36 # community by submitting them to Best Practical Solutions, LLC.)
37 #
38 # By intentionally submitting any modifications, corrections or
39 # derivatives to this work, or any other work intended for use with
40 # Request Tracker, to Best Practical Solutions, LLC, you confirm that
41 # you are the copyright holder for those contributions and you grant
42 # Best Practical Solutions,  LLC a nonexclusive, worldwide, irrevocable,
43 # royalty-free, perpetual, license to use, copy, create derivative
44 # works based on those contributions, and sublicense and distribute
45 # those contributions and any derivatives thereof.
46 #
47 # END BPS TAGGED BLOCK }}}
48
49 use strict;
50 use warnings;
51
52 package RT::Article;
53
54 use base 'RT::Record';
55
56 use RT::Articles;
57 use RT::ObjectTopics;
58 use RT::Classes;
59 use RT::Links;
60 use RT::CustomFields;
61 use RT::URI::fsck_com_article;
62 use RT::Transactions;
63
64
65 sub Table {'Articles'}
66
67 # This object takes custom fields
68
69 use RT::CustomField;
70 RT::CustomField->_ForObjectType( CustomFieldLookupType() => 'Articles' )
71   ;    #loc
72
73 # {{{ Create
74
75 =head2 Create PARAMHASH
76
77 Create takes a hash of values and creates a row in the database:
78
79   varchar(200) 'Name'.
80   varchar(200) 'Summary'.
81   int(11) 'Content'.
82   Class ID  'Class'
83
84   A paramhash called  'CustomFields', which contains 
85   arrays of values for each custom field you want to fill in.
86   Arrays aRe ordered. 
87
88
89
90
91 =cut
92
93 sub Create {
94     my $self = shift;
95     my %args = (
96         Name         => '',
97         Summary      => '',
98         Class        => '0',
99         CustomFields => {},
100         Links        => {},
101         Topics       => [],
102         @_
103     );
104
105     my $class = RT::Class->new( $self->CurrentUser );
106     $class->Load( $args{'Class'} );
107     unless ( $class->Id ) {
108         return ( 0, $self->loc('Invalid Class') );
109     }
110
111     unless ( $class->CurrentUserHasRight('CreateArticle') ) {
112         return ( 0, $self->loc("Permission Denied") );
113     }
114
115     return ( undef, $self->loc('Name in use') )
116       unless $self->ValidateName( $args{'Name'} );
117
118     $RT::Handle->BeginTransaction();
119     my ( $id, $msg ) = $self->SUPER::Create(
120         Name    => $args{'Name'},
121         Class   => $class->Id,
122         Summary => $args{'Summary'},
123     );
124     unless ($id) {
125         $RT::Handle->Rollback();
126         return ( undef, $msg );
127     }
128
129     # {{{ Add custom fields
130
131     foreach my $key ( keys %args ) {
132         next unless ( $key =~ /CustomField-(.*)$/ );
133         my $cf   = $1;
134         my @vals = ref( $args{$key} ) eq 'ARRAY' ? @{ $args{$key} } : ( $args{$key} );
135         foreach my $value (@vals) {
136
137             my ( $cfid, $cfmsg ) = $self->_AddCustomFieldValue(
138                 (UNIVERSAL::isa( $value => 'HASH' )
139                     ? %$value
140                     : (Value => $value)
141                 ),
142                 Field             => $cf,
143                 RecordTransaction => 0
144             );
145
146             unless ($cfid) {
147                 $RT::Handle->Rollback();
148                 return ( undef, $cfmsg );
149             }
150         }
151
152     }
153
154     # }}}
155     # {{{ Add topics
156
157     foreach my $topic ( @{ $args{Topics} } ) {
158         my ( $cfid, $cfmsg ) = $self->AddTopic( Topic => $topic );
159
160         unless ($cfid) {
161             $RT::Handle->Rollback();
162             return ( undef, $cfmsg );
163         }
164     }
165
166     # }}}
167     # {{{ Add relationships
168
169     foreach my $type ( keys %args ) {
170         next unless ( $type =~ /^(RefersTo-new|new-RefersTo)$/ );
171         my @vals =
172           ref( $args{$type} ) eq 'ARRAY' ? @{ $args{$type} } : ( $args{$type} );
173         foreach my $val (@vals) {
174             my ( $base, $target );
175             if ( $type =~ /^new-(.*)$/ ) {
176                 $type   = $1;
177                 $base   = undef;
178                 $target = $val;
179             }
180             elsif ( $type =~ /^(.*)-new$/ ) {
181                 $type   = $1;
182                 $base   = $val;
183                 $target = undef;
184             }
185
186             my ( $linkid, $linkmsg ) = $self->AddLink(
187                 Type              => $type,
188                 Target            => $target,
189                 Base              => $base,
190                 RecordTransaction => 0
191             );
192
193             unless ($linkid) {
194                 $RT::Handle->Rollback();
195                 return ( undef, $linkmsg );
196             }
197         }
198
199     }
200
201     # }}}
202
203     # We override the URI lookup. the whole reason
204     # we have a URI column is so that joins on the links table
205     # aren't expensive and stupid
206     $self->__Set( Field => 'URI', Value => $self->URI );
207
208     my ( $txn_id, $txn_msg, $txn ) = $self->_NewTransaction( Type => 'Create' );
209     unless ($txn_id) {
210         $RT::Handle->Rollback();
211         return ( undef, $self->loc( 'Internal error: [_1]', $txn_msg ) );
212     }
213     $RT::Handle->Commit();
214
215     return ( $id, $self->loc('Article [_1] created',$self->id ));
216 }
217
218 # }}}
219
220 # {{{ ValidateName
221
222 =head2 ValidateName NAME
223
224 Takes a string name. Returns true if that name isn't in use by another article
225
226 Empty names are permitted.
227
228
229 =cut
230
231 sub ValidateName {
232     my $self = shift;
233     my $name = shift;
234
235     if ( !$name ) {
236         return (1);
237     }
238
239     my $temp = RT::Article->new($RT::SystemUser);
240     $temp->LoadByCols( Name => $name );
241     if ( $temp->id && 
242          (!$self->id || ($temp->id != $self->id ))) {
243         return (undef);
244     }
245
246     return (1);
247
248 }
249
250 # }}}
251
252 # {{{ Delete
253
254 =head2 Delete
255
256 Delete all its transactions
257 Delete all its custom field values
258 Delete all its relationships
259 Delete this article.
260
261 =cut
262
263 sub Delete {
264     my $self = shift;
265     unless ( $self->CurrentUserHasRight('DeleteArticle') ) {
266         return ( 0, $self->loc("Permission Denied") );
267     }
268
269     $RT::Handle->BeginTransaction();
270     my $linksto   = $self->_Links(  'Target' );
271     my $linksfrom = $self->_Links(  'Base' );
272     my $cfvalues = $self->CustomFieldValues;
273     my $txns     = $self->Transactions;
274     my $topics   = $self->Topics;
275
276     while ( my $item = $linksto->Next ) {
277         my ( $val, $msg ) = $item->Delete();
278         unless ($val) {
279             $RT::Logger->crit( ref($item) . ": $msg" );
280             $RT::Handle->Rollback();
281             return ( 0, $self->loc('Internal Error') );
282         }
283     }
284
285     while ( my $item = $linksfrom->Next ) {
286         my ( $val, $msg ) = $item->Delete();
287         unless ($val) {
288             $RT::Logger->crit( ref($item) . ": $msg" );
289             $RT::Handle->Rollback();
290             return ( 0, $self->loc('Internal Error') );
291         }
292     }
293
294     while ( my $item = $txns->Next ) {
295         my ( $val, $msg ) = $item->Delete();
296         unless ($val) {
297             $RT::Logger->crit( ref($item) . ": $msg" );
298             $RT::Handle->Rollback();
299             return ( 0, $self->loc('Internal Error') );
300         }
301     }
302
303     while ( my $item = $cfvalues->Next ) {
304         my ( $val, $msg ) = $item->Delete();
305         unless ($val) {
306             $RT::Logger->crit( ref($item) . ": $msg" );
307             $RT::Handle->Rollback();
308             return ( 0, $self->loc('Internal Error') );
309         }
310     }
311
312     while ( my $item = $topics->Next ) {
313         my ( $val, $msg ) = $item->Delete();
314         unless ($val) {
315             $RT::Logger->crit( ref($item) . ": $msg" );
316             $RT::Handle->Rollback();
317             return ( 0, $self->loc('Internal Error') );
318         }
319     }
320
321     $self->SUPER::Delete();
322     $RT::Handle->Commit();
323     return ( 1, $self->loc('Article Deleted') );
324
325 }
326
327 # }}}
328
329 # {{{ Children
330
331 =head2 Children
332
333 Returns an RT::Articles object which contains
334 all articles which have this article as their parent.  This 
335 routine will not recurse and will not find grandchildren, great-grandchildren, uncles, aunts, nephews or any other such thing.  
336
337 =cut
338
339 sub Children {
340     my $self = shift;
341     my $kids = RT::Articles->new( $self->CurrentUser );
342
343     unless ( $self->CurrentUserHasRight('ShowArticle') ) {
344         $kids->LimitToParent( $self->Id );
345     }
346     return ($kids);
347 }
348
349 # }}}
350
351 # {{{ sub AddLink
352
353 =head2 AddLink
354
355 Takes a paramhash of Type and one of Base or Target. Adds that link to this tick
356 et.
357
358 =cut
359
360 sub DeleteLink {
361     my $self = shift;
362     my %args = (
363         Target => '',
364         Base   => '',
365         Type   => '',
366         Silent => undef,
367         @_
368     );
369
370     unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
371         return ( 0, $self->loc("Permission Denied") );
372     }
373
374     $self->_DeleteLink(%args);
375 }
376
377 sub AddLink {
378     my $self = shift;
379     my %args = (
380         Target => '',
381         Base   => '',
382         Type   => '',
383         Silent => undef,
384         @_
385     );
386
387     unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
388         return ( 0, $self->loc("Permission Denied") );
389     }
390
391     # Disallow parsing of plain numbers in article links.  If they are
392     # allowed, they default to being tickets instead of articles, which
393     # is counterintuitive.
394     if (   $args{'Target'} && $args{'Target'} =~ /^\d+$/
395         || $args{'Base'} && $args{'Base'} =~ /^\d+$/ )
396     {
397         return ( 0, $self->loc("Cannot add link to plain number") );
398     }
399
400     # Check that we're actually getting a valid URI
401     my $uri_obj = RT::URI->new( $self->CurrentUser );
402     $uri_obj->FromURI( $args{'Target'}||$args{'Base'} );
403     unless ( $uri_obj->Resolver && $uri_obj->Scheme ) {
404         my $msg = $self->loc( "Couldn't resolve '[_1]' into a Link.", $args{'Target'} );
405         $RT::Logger->warning( $msg );
406         return( 0, $msg );
407     }
408
409
410     $self->_AddLink(%args);
411 }
412
413 sub URI {
414     my $self = shift;
415
416     unless ( $self->CurrentUserHasRight('ShowArticle') ) {
417         return $self->loc("Permission Denied");
418     }
419
420     my $uri = RT::URI::fsck_com_article->new( $self->CurrentUser );
421     return ( $uri->URIForObject($self) );
422 }
423
424 # }}}
425
426 # {{{ sub URIObj
427
428 =head2 URIObj
429
430 Returns this article's URI
431
432
433 =cut
434
435 sub URIObj {
436     my $self = shift;
437     my $uri  = RT::URI->new( $self->CurrentUser );
438     if ( $self->CurrentUserHasRight('ShowArticle') ) {
439         $uri->FromObject($self);
440     }
441
442     return ($uri);
443 }
444
445 # }}}
446 # }}}
447
448 # {{{ Topics
449
450 # {{{ Topics
451 sub Topics {
452     my $self = shift;
453
454     my $topics = RT::ObjectTopics->new( $self->CurrentUser );
455     if ( $self->CurrentUserHasRight('ShowArticle') ) {
456         $topics->LimitToObject($self);
457     }
458     return $topics;
459 }
460
461 # }}}
462
463 # {{{ AddTopic
464 sub AddTopic {
465     my $self = shift;
466     my %args = (@_);
467
468     unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
469         return ( 0, $self->loc("Permission Denied") );
470     }
471
472     my $t = RT::ObjectTopic->new( $self->CurrentUser );
473     my ($tid) = $t->Create(
474         Topic      => $args{'Topic'},
475         ObjectType => ref($self),
476         ObjectId   => $self->Id
477     );
478     if ($tid) {
479         return ( $tid, $self->loc("Topic membership added") );
480     }
481     else {
482         return ( 0, $self->loc("Unable to add topic membership") );
483     }
484 }
485
486 # }}}
487
488 sub DeleteTopic {
489     my $self = shift;
490     my %args = (@_);
491
492     unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
493         return ( 0, $self->loc("Permission Denied") );
494     }
495
496     my $t = RT::ObjectTopic->new( $self->CurrentUser );
497     $t->LoadByCols(
498         Topic      => $args{'Topic'},
499         ObjectId   => $self->Id,
500         ObjectType => ref($self)
501     );
502     if ( $t->Id ) {
503         my $del = $t->Delete;
504         unless ($del) {
505             return (
506                 undef,
507                 $self->loc(
508                     "Unable to delete topic membership in [_1]",
509                     $t->TopicObj->Name
510                 )
511             );
512         }
513         else {
514             return ( 1, $self->loc("Topic membership removed") );
515         }
516     }
517     else {
518         return (
519             undef,
520             $self->loc(
521                 "Couldn't load topic membership while trying to delete it")
522         );
523     }
524 }
525
526 =head2 CurrentUserHasRight
527
528 Returns true if the current user has the right for this article, for the whole system or for this article's class
529
530 =cut
531
532 sub CurrentUserHasRight {
533     my $self  = shift;
534     my $right = shift;
535
536     return (
537         $self->CurrentUser->HasRight(
538             Right        => $right,
539             Object       => $self,
540             EquivObjects => [ $RT::System, $RT::System, $self->ClassObj ]
541         )
542     );
543
544 }
545
546 =head2 CurrentUserCanSee
547
548 Returns true if the current user can see the article, using ShowArticle
549
550 =cut
551
552 sub CurrentUserCanSee {
553     my $self = shift;
554     return $self->CurrentUserHasRight('ShowArticle');
555 }
556
557 # }}}
558
559 # {{{ _Set
560
561 =head2 _Set { Field => undef, Value => undef
562
563 Internal helper method to record a transaction as we update some core field of the article
564
565
566 =cut
567
568 sub _Set {
569     my $self = shift;
570     my %args = (
571         Field => undef,
572         Value => undef,
573         @_
574     );
575
576     unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
577         return ( 0, $self->loc("Permission Denied") );
578     }
579
580     $self->_NewTransaction(
581         Type     => 'Set',
582         Field    => $args{'Field'},
583         NewValue => $args{'Value'},
584         OldValue => $self->__Value( $args{'Field'} )
585     );
586
587     return ( $self->SUPER::_Set(%args) );
588
589 }
590
591 =head2 _Value PARAM
592
593 Return "PARAM" for this object. if the current user doesn't have rights, returns undef
594
595 =cut
596
597 sub _Value {
598     my $self = shift;
599     my $arg  = shift;
600     unless ( ( $arg eq 'Class' )
601         || ( $self->CurrentUserHasRight('ShowArticle') ) )
602     {
603         return (undef);
604     }
605     return $self->SUPER::_Value($arg);
606 }
607
608 # }}}
609
610 sub CustomFieldLookupType {
611     "RT::Class-RT::Article";
612 }
613
614 =head2 LoadByInclude Field Value
615
616 Takes the name of a form field from "Include Article"
617 and the value submitted by the browser and attempts to load an Article.
618
619 This handles Articles included by searching, by the Name and via
620 the hotlist.
621
622 If you optionaly pass an id as the Queue argument, this will check that
623 the Article's Class is applied to that Queue.
624
625 =cut
626
627 sub LoadByInclude {
628     my $self = shift;
629     my %args = @_;
630     my $Field = $args{Field};
631     my $Value = $args{Value};
632     my $Queue = $args{Queue};
633
634     return unless $Field;
635
636     my ($ok, $msg);
637     if ( $Field eq 'Articles-Include-Article' && $Value ) {
638         ($ok, $msg) = $self->Load( $Value );
639     } elsif ( $Field =~ /^Articles-Include-Article-(\d+)$/ ) {
640         ($ok, $msg) = $self->Load( $1 );
641     } elsif ( $Field =~ /^Articles-Include-Article-Named/ && $Value ) {
642         if ( $Value =~ /\D/ ) {
643             ($ok, $msg) = $self->LoadByCols( Name => $Value );
644         } else {
645             ($ok, $msg) = $self->LoadByCols( id => $Value );
646         }
647     }
648
649     unless ($ok) { # load failed, don't check Class
650         return ($ok, $msg);
651     }
652
653     unless ($Queue) { # we haven't requested extra sanity checking
654         return ($ok, $msg);
655     }
656
657     # ensure that this article is available for the Queue we're
658     # operating under.
659     my $class = $self->ClassObj;
660     unless ($class->IsApplied(0) || $class->IsApplied($Queue)) {
661         $self->LoadById(0);
662         return (0, $self->loc("The Class of the Article identified by [_1] is not applied to the current Queue",$Value));
663     }
664
665     return ($ok, $msg);
666
667 }
668
669
670 =head2 id
671
672 Returns the current value of id. 
673 (In the database, id is stored as int(11).)
674
675
676 =cut
677
678
679 =head2 Name
680
681 Returns the current value of Name. 
682 (In the database, Name is stored as varchar(255).)
683
684
685
686 =head2 SetName VALUE
687
688
689 Set Name to VALUE. 
690 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
691 (In the database, Name will be stored as a varchar(255).)
692
693
694 =cut
695
696
697 =head2 Summary
698
699 Returns the current value of Summary. 
700 (In the database, Summary is stored as varchar(255).)
701
702
703
704 =head2 SetSummary VALUE
705
706
707 Set Summary to VALUE. 
708 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
709 (In the database, Summary will be stored as a varchar(255).)
710
711
712 =cut
713
714
715 =head2 SortOrder
716
717 Returns the current value of SortOrder. 
718 (In the database, SortOrder is stored as int(11).)
719
720
721
722 =head2 SetSortOrder VALUE
723
724
725 Set SortOrder to VALUE. 
726 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
727 (In the database, SortOrder will be stored as a int(11).)
728
729
730 =cut
731
732
733 =head2 Class
734
735 Returns the current value of Class. 
736 (In the database, Class is stored as int(11).)
737
738
739
740 =head2 SetClass VALUE
741
742
743 Set Class to VALUE. 
744 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
745 (In the database, Class will be stored as a int(11).)
746
747
748 =cut
749
750
751 =head2 ClassObj
752
753 Returns the Class Object which has the id returned by Class
754
755
756 =cut
757
758 sub ClassObj {
759         my $self = shift;
760         my $Class =  RT::Class->new($self->CurrentUser);
761         $Class->Load($self->Class());
762         return($Class);
763 }
764
765 =head2 Parent
766
767 Returns the current value of Parent. 
768 (In the database, Parent is stored as int(11).)
769
770
771
772 =head2 SetParent VALUE
773
774
775 Set Parent to VALUE. 
776 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
777 (In the database, Parent will be stored as a int(11).)
778
779
780 =cut
781
782
783 =head2 URI
784
785 Returns the current value of URI. 
786 (In the database, URI is stored as varchar(255).)
787
788
789
790 =head2 SetURI VALUE
791
792
793 Set URI to VALUE. 
794 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
795 (In the database, URI will be stored as a varchar(255).)
796
797
798 =cut
799
800
801 =head2 Creator
802
803 Returns the current value of Creator. 
804 (In the database, Creator is stored as int(11).)
805
806
807 =cut
808
809
810 =head2 Created
811
812 Returns the current value of Created. 
813 (In the database, Created is stored as datetime.)
814
815
816 =cut
817
818
819 =head2 LastUpdatedBy
820
821 Returns the current value of LastUpdatedBy. 
822 (In the database, LastUpdatedBy is stored as int(11).)
823
824
825 =cut
826
827
828 =head2 LastUpdated
829
830 Returns the current value of LastUpdated. 
831 (In the database, LastUpdated is stored as datetime.)
832
833
834 =cut
835
836
837
838 sub _CoreAccessible {
839     {
840      
841         id =>
842                 {read => 1, type => 'int(11)', default => ''},
843         Name => 
844                 {read => 1, write => 1, type => 'varchar(255)', default => ''},
845         Summary => 
846                 {read => 1, write => 1, type => 'varchar(255)', default => ''},
847         SortOrder => 
848                 {read => 1, write => 1, type => 'int(11)', default => '0'},
849         Class => 
850                 {read => 1, write => 1, type => 'int(11)', default => '0'},
851         Parent => 
852                 {read => 1, write => 1, type => 'int(11)', default => '0'},
853         URI => 
854                 {read => 1, write => 1, type => 'varchar(255)', default => ''},
855         Creator => 
856                 {read => 1, auto => 1, type => 'int(11)', default => '0'},
857         Created => 
858                 {read => 1, auto => 1, type => 'datetime', default => ''},
859         LastUpdatedBy => 
860                 {read => 1, auto => 1, type => 'int(11)', default => '0'},
861         LastUpdated => 
862                 {read => 1, auto => 1, type => 'datetime', default => ''},
863
864  }
865 };
866
867 RT::Base->_ImportOverlays();
868
869 1;
870
871
872 1;