Upgrade to 4.0.8 with modification of ExternalAuth.
[usit-rt.git] / lib / RT / Article.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
5# This software is Copyright (c) 1996-2012 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
49use strict;
50use warnings;
51
52package RT::Article;
53
54use base 'RT::Record';
55
56use RT::Articles;
57use RT::ObjectTopics;
58use RT::Classes;
59use RT::Links;
60use RT::CustomFields;
61use RT::URI::fsck_com_article;
62use RT::Transactions;
63
64
65sub Table {'Articles'}
66
67# This object takes custom fields
68
69use RT::CustomField;
70RT::CustomField->_ForObjectType( CustomFieldLookupType() => 'Articles' )
71 ; #loc
72
73# {{{ Create
74
75=head2 Create PARAMHASH
76
77Create 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
93sub Create {
94 my $self = shift;
95 my %args = (
96 Name => '',
97 Summary => '',
98 Class => '0',
99 CustomFields => {},
100 Links => {},
101 Topics => [],
102 @_
103 );
104
86404187 105 my $class = RT::Class->new( $self->CurrentUser );
84fb5b46
MKG
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
224Takes a string name. Returns true if that name isn't in use by another article
225
226Empty names are permitted.
227
228
229=cut
230
231sub 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
256Delete all its transactions
257Delete all its custom field values
258Delete all its relationships
259Delete this article.
260
261=cut
262
263sub 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
333Returns an RT::Articles object which contains
334all articles which have this article as their parent. This
335routine will not recurse and will not find grandchildren, great-grandchildren, uncles, aunts, nephews or any other such thing.
336
337=cut
338
339sub 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
355Takes a paramhash of Type and one of Base or Target. Adds that link to this tick
356et.
357
358=cut
359
360sub 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
377sub 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
413sub 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
430Returns this article's URI
431
432
433=cut
434
435sub 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
451sub 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
464sub 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
488sub 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
528Returns 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
532sub 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
548Returns true if the current user can see the article, using ShowArticle
549
550=cut
551
552sub 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
563Internal helper method to record a transaction as we update some core field of the article
564
565
566=cut
567
568sub _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
593Return "PARAM" for this object. if the current user doesn't have rights, returns undef
594
595=cut
596
597sub _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
610sub CustomFieldLookupType {
611 "RT::Class-RT::Article";
612}
613
614# _LookupId is the id of the toplevel type object the customfield is joined to
615# in this case, that's an RT::Class.
616
617sub _LookupId {
618 my $self = shift;
619 return $self->ClassObj->id;
620
621}
622
623=head2 LoadByInclude Field Value
624
625Takes the name of a form field from "Include Article"
626and the value submitted by the browser and attempts to load an Article.
627
628This handles Articles included by searching, by the Name and via
629the hotlist.
630
631If you optionaly pass an id as the Queue argument, this will check that
632the Article's Class is applied to that Queue.
633
634=cut
635
636sub LoadByInclude {
637 my $self = shift;
638 my %args = @_;
639 my $Field = $args{Field};
640 my $Value = $args{Value};
641 my $Queue = $args{Queue};
642
643 return unless $Field;
644
645 my ($ok, $msg);
646 if ( $Field eq 'Articles-Include-Article' && $Value ) {
647 ($ok, $msg) = $self->Load( $Value );
648 } elsif ( $Field =~ /^Articles-Include-Article-(\d+)$/ ) {
649 ($ok, $msg) = $self->Load( $1 );
650 } elsif ( $Field =~ /^Articles-Include-Article-Named/ && $Value ) {
651 if ( $Value =~ /\D/ ) {
652 ($ok, $msg) = $self->LoadByCols( Name => $Value );
653 } else {
654 ($ok, $msg) = $self->LoadByCols( id => $Value );
655 }
656 }
657
658 unless ($ok) { # load failed, don't check Class
659 return ($ok, $msg);
660 }
661
662 unless ($Queue) { # we haven't requested extra sanity checking
663 return ($ok, $msg);
664 }
665
666 # ensure that this article is available for the Queue we're
667 # operating under.
668 my $class = $self->ClassObj;
669 unless ($class->IsApplied(0) || $class->IsApplied($Queue)) {
670 $self->LoadById(0);
671 return (0, $self->loc("The Class of the Article identified by [_1] is not applied to the current Queue",$Value));
672 }
673
674 return ($ok, $msg);
675
676}
677
678
679=head2 id
680
681Returns the current value of id.
682(In the database, id is stored as int(11).)
683
684
685=cut
686
687
688=head2 Name
689
690Returns the current value of Name.
691(In the database, Name is stored as varchar(255).)
692
693
694
695=head2 SetName VALUE
696
697
698Set Name to VALUE.
699Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
700(In the database, Name will be stored as a varchar(255).)
701
702
703=cut
704
705
706=head2 Summary
707
708Returns the current value of Summary.
709(In the database, Summary is stored as varchar(255).)
710
711
712
713=head2 SetSummary VALUE
714
715
716Set Summary to VALUE.
717Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
718(In the database, Summary will be stored as a varchar(255).)
719
720
721=cut
722
723
724=head2 SortOrder
725
726Returns the current value of SortOrder.
727(In the database, SortOrder is stored as int(11).)
728
729
730
731=head2 SetSortOrder VALUE
732
733
734Set SortOrder to VALUE.
735Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
736(In the database, SortOrder will be stored as a int(11).)
737
738
739=cut
740
741
742=head2 Class
743
744Returns the current value of Class.
745(In the database, Class is stored as int(11).)
746
747
748
749=head2 SetClass VALUE
750
751
752Set Class to VALUE.
753Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
754(In the database, Class will be stored as a int(11).)
755
756
757=cut
758
759
760=head2 ClassObj
761
762Returns the Class Object which has the id returned by Class
763
764
765=cut
766
767sub ClassObj {
768 my $self = shift;
769 my $Class = RT::Class->new($self->CurrentUser);
770 $Class->Load($self->Class());
771 return($Class);
772}
773
774=head2 Parent
775
776Returns the current value of Parent.
777(In the database, Parent is stored as int(11).)
778
779
780
781=head2 SetParent VALUE
782
783
784Set Parent to VALUE.
785Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
786(In the database, Parent will be stored as a int(11).)
787
788
789=cut
790
791
792=head2 URI
793
794Returns the current value of URI.
795(In the database, URI is stored as varchar(255).)
796
797
798
799=head2 SetURI VALUE
800
801
802Set URI to VALUE.
803Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
804(In the database, URI will be stored as a varchar(255).)
805
806
807=cut
808
809
810=head2 Creator
811
812Returns the current value of Creator.
813(In the database, Creator is stored as int(11).)
814
815
816=cut
817
818
819=head2 Created
820
821Returns the current value of Created.
822(In the database, Created is stored as datetime.)
823
824
825=cut
826
827
828=head2 LastUpdatedBy
829
830Returns the current value of LastUpdatedBy.
831(In the database, LastUpdatedBy is stored as int(11).)
832
833
834=cut
835
836
837=head2 LastUpdated
838
839Returns the current value of LastUpdated.
840(In the database, LastUpdated is stored as datetime.)
841
842
843=cut
844
845
846
847sub _CoreAccessible {
848 {
849
850 id =>
851 {read => 1, type => 'int(11)', default => ''},
852 Name =>
853 {read => 1, write => 1, type => 'varchar(255)', default => ''},
854 Summary =>
855 {read => 1, write => 1, type => 'varchar(255)', default => ''},
856 SortOrder =>
857 {read => 1, write => 1, type => 'int(11)', default => '0'},
858 Class =>
859 {read => 1, write => 1, type => 'int(11)', default => '0'},
860 Parent =>
861 {read => 1, write => 1, type => 'int(11)', default => '0'},
862 URI =>
863 {read => 1, write => 1, type => 'varchar(255)', default => ''},
864 Creator =>
865 {read => 1, auto => 1, type => 'int(11)', default => '0'},
866 Created =>
867 {read => 1, auto => 1, type => 'datetime', default => ''},
868 LastUpdatedBy =>
869 {read => 1, auto => 1, type => 'int(11)', default => '0'},
870 LastUpdated =>
871 {read => 1, auto => 1, type => 'datetime', default => ''},
872
873 }
874};
875
876RT::Base->_ImportOverlays();
877
8781;
879
880
8811;