Dev to 4.0.11
[usit-rt.git] / lib / RT / Article.pm
CommitLineData
84fb5b46
MKG
1# BEGIN BPS TAGGED BLOCK {{{
2#
3# COPYRIGHT:
4#
403d7b0b 5# This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC
84fb5b46
MKG
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
dab09ea8 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 );
c36a7e1d
MKG
402 unless ( $uri_obj->FromURI( $args{'Target'}||$args{'Base'} )) {
403 my $msg = $self->loc( "Couldn't resolve '[_1]' into a Link.", $args{'Target'} || $args{'Base'} );
84fb5b46
MKG
404 $RT::Logger->warning( $msg );
405 return( 0, $msg );
406 }
407
408
409 $self->_AddLink(%args);
410}
411
412sub URI {
413 my $self = shift;
414
415 unless ( $self->CurrentUserHasRight('ShowArticle') ) {
416 return $self->loc("Permission Denied");
417 }
418
419 my $uri = RT::URI::fsck_com_article->new( $self->CurrentUser );
420 return ( $uri->URIForObject($self) );
421}
422
423# }}}
424
425# {{{ sub URIObj
426
427=head2 URIObj
428
429Returns this article's URI
430
431
432=cut
433
434sub URIObj {
435 my $self = shift;
436 my $uri = RT::URI->new( $self->CurrentUser );
437 if ( $self->CurrentUserHasRight('ShowArticle') ) {
438 $uri->FromObject($self);
439 }
440
441 return ($uri);
442}
443
444# }}}
445# }}}
446
447# {{{ Topics
448
449# {{{ Topics
450sub Topics {
451 my $self = shift;
452
453 my $topics = RT::ObjectTopics->new( $self->CurrentUser );
454 if ( $self->CurrentUserHasRight('ShowArticle') ) {
455 $topics->LimitToObject($self);
456 }
457 return $topics;
458}
459
460# }}}
461
462# {{{ AddTopic
463sub AddTopic {
464 my $self = shift;
465 my %args = (@_);
466
467 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
468 return ( 0, $self->loc("Permission Denied") );
469 }
470
471 my $t = RT::ObjectTopic->new( $self->CurrentUser );
472 my ($tid) = $t->Create(
473 Topic => $args{'Topic'},
474 ObjectType => ref($self),
475 ObjectId => $self->Id
476 );
477 if ($tid) {
478 return ( $tid, $self->loc("Topic membership added") );
479 }
480 else {
481 return ( 0, $self->loc("Unable to add topic membership") );
482 }
483}
484
485# }}}
486
487sub DeleteTopic {
488 my $self = shift;
489 my %args = (@_);
490
491 unless ( $self->CurrentUserHasRight('ModifyArticleTopics') ) {
492 return ( 0, $self->loc("Permission Denied") );
493 }
494
495 my $t = RT::ObjectTopic->new( $self->CurrentUser );
496 $t->LoadByCols(
497 Topic => $args{'Topic'},
498 ObjectId => $self->Id,
499 ObjectType => ref($self)
500 );
501 if ( $t->Id ) {
502 my $del = $t->Delete;
503 unless ($del) {
504 return (
505 undef,
506 $self->loc(
507 "Unable to delete topic membership in [_1]",
508 $t->TopicObj->Name
509 )
510 );
511 }
512 else {
513 return ( 1, $self->loc("Topic membership removed") );
514 }
515 }
516 else {
517 return (
518 undef,
519 $self->loc(
520 "Couldn't load topic membership while trying to delete it")
521 );
522 }
523}
524
525=head2 CurrentUserHasRight
526
527Returns true if the current user has the right for this article, for the whole system or for this article's class
528
529=cut
530
531sub CurrentUserHasRight {
532 my $self = shift;
533 my $right = shift;
534
535 return (
536 $self->CurrentUser->HasRight(
537 Right => $right,
538 Object => $self,
539 EquivObjects => [ $RT::System, $RT::System, $self->ClassObj ]
540 )
541 );
542
543}
544
545=head2 CurrentUserCanSee
546
547Returns true if the current user can see the article, using ShowArticle
548
549=cut
550
551sub CurrentUserCanSee {
552 my $self = shift;
553 return $self->CurrentUserHasRight('ShowArticle');
554}
555
556# }}}
557
558# {{{ _Set
559
560=head2 _Set { Field => undef, Value => undef
561
562Internal helper method to record a transaction as we update some core field of the article
563
564
565=cut
566
567sub _Set {
568 my $self = shift;
569 my %args = (
570 Field => undef,
571 Value => undef,
572 @_
573 );
574
575 unless ( $self->CurrentUserHasRight('ModifyArticle') ) {
576 return ( 0, $self->loc("Permission Denied") );
577 }
578
579 $self->_NewTransaction(
580 Type => 'Set',
581 Field => $args{'Field'},
582 NewValue => $args{'Value'},
583 OldValue => $self->__Value( $args{'Field'} )
584 );
585
586 return ( $self->SUPER::_Set(%args) );
587
588}
589
590=head2 _Value PARAM
591
592Return "PARAM" for this object. if the current user doesn't have rights, returns undef
593
594=cut
595
596sub _Value {
597 my $self = shift;
598 my $arg = shift;
599 unless ( ( $arg eq 'Class' )
600 || ( $self->CurrentUserHasRight('ShowArticle') ) )
601 {
602 return (undef);
603 }
604 return $self->SUPER::_Value($arg);
605}
606
607# }}}
608
609sub CustomFieldLookupType {
610 "RT::Class-RT::Article";
611}
612
84fb5b46
MKG
613=head2 LoadByInclude Field Value
614
615Takes the name of a form field from "Include Article"
616and the value submitted by the browser and attempts to load an Article.
617
618This handles Articles included by searching, by the Name and via
619the hotlist.
620
621If you optionaly pass an id as the Queue argument, this will check that
622the Article's Class is applied to that Queue.
623
624=cut
625
626sub LoadByInclude {
627 my $self = shift;
628 my %args = @_;
629 my $Field = $args{Field};
630 my $Value = $args{Value};
631 my $Queue = $args{Queue};
632
633 return unless $Field;
634
635 my ($ok, $msg);
636 if ( $Field eq 'Articles-Include-Article' && $Value ) {
637 ($ok, $msg) = $self->Load( $Value );
638 } elsif ( $Field =~ /^Articles-Include-Article-(\d+)$/ ) {
639 ($ok, $msg) = $self->Load( $1 );
640 } elsif ( $Field =~ /^Articles-Include-Article-Named/ && $Value ) {
641 if ( $Value =~ /\D/ ) {
642 ($ok, $msg) = $self->LoadByCols( Name => $Value );
643 } else {
644 ($ok, $msg) = $self->LoadByCols( id => $Value );
645 }
646 }
647
648 unless ($ok) { # load failed, don't check Class
649 return ($ok, $msg);
650 }
651
652 unless ($Queue) { # we haven't requested extra sanity checking
653 return ($ok, $msg);
654 }
655
656 # ensure that this article is available for the Queue we're
657 # operating under.
658 my $class = $self->ClassObj;
659 unless ($class->IsApplied(0) || $class->IsApplied($Queue)) {
660 $self->LoadById(0);
661 return (0, $self->loc("The Class of the Article identified by [_1] is not applied to the current Queue",$Value));
662 }
663
664 return ($ok, $msg);
665
666}
667
668
669=head2 id
670
671Returns the current value of id.
672(In the database, id is stored as int(11).)
673
674
675=cut
676
677
678=head2 Name
679
680Returns the current value of Name.
681(In the database, Name is stored as varchar(255).)
682
683
684
685=head2 SetName VALUE
686
687
688Set Name to VALUE.
689Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
690(In the database, Name will be stored as a varchar(255).)
691
692
693=cut
694
695
696=head2 Summary
697
698Returns the current value of Summary.
699(In the database, Summary is stored as varchar(255).)
700
701
702
703=head2 SetSummary VALUE
704
705
706Set Summary to VALUE.
707Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
708(In the database, Summary will be stored as a varchar(255).)
709
710
711=cut
712
713
714=head2 SortOrder
715
716Returns the current value of SortOrder.
717(In the database, SortOrder is stored as int(11).)
718
719
720
721=head2 SetSortOrder VALUE
722
723
724Set SortOrder to VALUE.
725Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
726(In the database, SortOrder will be stored as a int(11).)
727
728
729=cut
730
731
732=head2 Class
733
734Returns the current value of Class.
735(In the database, Class is stored as int(11).)
736
737
738
739=head2 SetClass VALUE
740
741
742Set Class to VALUE.
743Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
744(In the database, Class will be stored as a int(11).)
745
746
747=cut
748
749
750=head2 ClassObj
751
752Returns the Class Object which has the id returned by Class
753
754
755=cut
756
757sub ClassObj {
758 my $self = shift;
759 my $Class = RT::Class->new($self->CurrentUser);
760 $Class->Load($self->Class());
761 return($Class);
762}
763
764=head2 Parent
765
766Returns the current value of Parent.
767(In the database, Parent is stored as int(11).)
768
769
770
771=head2 SetParent VALUE
772
773
774Set Parent to VALUE.
775Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
776(In the database, Parent will be stored as a int(11).)
777
778
779=cut
780
781
782=head2 URI
783
784Returns the current value of URI.
785(In the database, URI is stored as varchar(255).)
786
787
788
789=head2 SetURI VALUE
790
791
792Set URI to VALUE.
793Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
794(In the database, URI will be stored as a varchar(255).)
795
796
797=cut
798
799
800=head2 Creator
801
802Returns the current value of Creator.
803(In the database, Creator is stored as int(11).)
804
805
806=cut
807
808
809=head2 Created
810
811Returns the current value of Created.
812(In the database, Created is stored as datetime.)
813
814
815=cut
816
817
818=head2 LastUpdatedBy
819
820Returns the current value of LastUpdatedBy.
821(In the database, LastUpdatedBy is stored as int(11).)
822
823
824=cut
825
826
827=head2 LastUpdated
828
829Returns the current value of LastUpdated.
830(In the database, LastUpdated is stored as datetime.)
831
832
833=cut
834
835
836
837sub _CoreAccessible {
838 {
839
840 id =>
841 {read => 1, type => 'int(11)', default => ''},
842 Name =>
843 {read => 1, write => 1, type => 'varchar(255)', default => ''},
844 Summary =>
845 {read => 1, write => 1, type => 'varchar(255)', default => ''},
846 SortOrder =>
847 {read => 1, write => 1, type => 'int(11)', default => '0'},
848 Class =>
849 {read => 1, write => 1, type => 'int(11)', default => '0'},
850 Parent =>
851 {read => 1, write => 1, type => 'int(11)', default => '0'},
852 URI =>
853 {read => 1, write => 1, type => 'varchar(255)', default => ''},
854 Creator =>
855 {read => 1, auto => 1, type => 'int(11)', default => '0'},
856 Created =>
857 {read => 1, auto => 1, type => 'datetime', default => ''},
858 LastUpdatedBy =>
859 {read => 1, auto => 1, type => 'int(11)', default => '0'},
860 LastUpdated =>
861 {read => 1, auto => 1, type => 'datetime', default => ''},
862
863 }
864};
865
866RT::Base->_ImportOverlays();
867
8681;
869
870
8711;