1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2017 Best Practical Solutions, LLC
6 # <sales@bestpractical.com>
8 # (Except where explicitly superseded by other copyright notices)
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
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.
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.
30 # CONTRIBUTION SUBMISSION POLICY:
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.)
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.
47 # END BPS TAGGED BLOCK }}}
49 # Portions Copyright 2000 Tobias Brox <tobix@cpan.org>
53 RT::Template - RT's template object
73 use base 'RT::Record';
80 use Scalar::Util 'blessed';
87 Description => 'read/write',
88 Type => 'read/write', #Type is one of Perl or Simple
89 Content => 'read/write',
90 Queue => 'read/write',
91 Creator => 'read/auto',
92 Created => 'read/auto',
93 LastUpdatedBy => 'read/auto',
94 LastUpdated => 'read/auto'
96 return $self->SUPER::_Accessible( @_, %Cols );
107 unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
108 return ( 0, $self->loc('Permission Denied') );
111 if (exists $args{Value}) {
112 if ($args{Field} eq 'Queue') {
114 # moving to another queue
115 my $queue = RT::Queue->new( $self->CurrentUser );
116 $queue->Load($args{Value});
117 unless ($queue->Id and $queue->CurrentUserHasRight('ModifyTemplate')) {
118 return ( 0, $self->loc('Permission Denied') );
122 unless ($self->CurrentUser->HasRight( Object => RT->System, Right => 'ModifyTemplate' )) {
123 return ( 0, $self->loc('Permission Denied') );
129 return $self->SUPER::_Set( @_ );
134 Takes the name of a table column. Returns its value as a string,
135 if the user passes an ACL check, otherwise returns undef.
142 unless ( $self->CurrentUserCanRead() ) {
145 return $self->__Value( @_ );
149 =head2 Load <identifier>
151 Load a template, either by number or by name.
153 Note that loading templates by name using this method B<is
154 ambiguous>. Several queues may have template with the same name
155 and as well global template with the same name may exist.
156 Use L</LoadByName>, L</LoadGlobalTemplate> or L<LoadQueueTemplate> to get
163 my $identifier = shift;
164 return undef unless $identifier;
166 if ( $identifier =~ /\D/ ) {
167 return $self->LoadByCol( 'Name', $identifier );
169 return $self->LoadById( $identifier );
174 Takes Name and Queue arguments. Tries to load queue specific template
175 first, then global. If Queue argument is omitted then global template
176 is tried, not template with the name in any queue.
187 my $queue = $args{'Queue'};
188 if ( blessed $queue ) {
190 } elsif ( defined $queue and $queue =~ /\D/ ) {
191 my $tmp = RT::Queue->new( $self->CurrentUser );
196 return $self->LoadGlobalTemplate( $args{'Name'} ) unless $queue;
198 $self->LoadQueueTemplate( Queue => $queue, Name => $args{'Name'} );
199 return $self->id if $self->id;
200 return $self->LoadGlobalTemplate( $args{'Name'} );
203 =head2 LoadGlobalTemplate NAME
205 Load the global template with the name NAME
209 sub LoadGlobalTemplate {
213 return ( $self->LoadQueueTemplate( Queue => 0, Name => $name ) );
216 =head2 LoadQueueTemplate (Queue => QUEUEID, Name => NAME)
218 Loads the Queue template named NAME for Queue QUEUE.
220 Note that this method doesn't load a global template with the same name
221 if template in the queue doesn't exist. Use L</LoadByName>.
225 sub LoadQueueTemplate {
233 return ( $self->LoadByCols( Name => $args{'Name'}, Queue => $args{'Queue'} ) );
239 Takes a paramhash of Content, Queue, Name and Description.
240 Name should be a unique string identifying this Template.
241 Description and Content should be the template's title and content.
242 Queue should be 0 for a global template and the queue # for a queue-specific
245 Returns the Template's id # if the create was successful. Returns undef for
246 unknown database failure.
255 Description => '[no description]',
261 if ( $args{Type} eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System) ) {
262 return ( undef, $self->loc('Permission Denied') );
265 unless ( $args{'Queue'} ) {
266 unless ( $self->CurrentUser->HasRight(Right =>'ModifyTemplate', Object => $RT::System) ) {
267 return ( undef, $self->loc('Permission Denied') );
272 my $QueueObj = RT::Queue->new( $self->CurrentUser );
273 $QueueObj->Load( $args{'Queue'} ) || return ( undef, $self->loc('Invalid queue') );
275 unless ( $QueueObj->CurrentUserHasRight('ModifyTemplate') ) {
276 return ( undef, $self->loc('Permission Denied') );
278 $args{'Queue'} = $QueueObj->Id;
281 return ( undef, $self->loc('Name is required') )
285 my $tmp = $self->new( RT->SystemUser );
286 $tmp->LoadByCols( Name => $args{'Name'}, Queue => $args{'Queue'} );
287 return ( undef, $self->loc('A Template with that name already exists') )
291 my ( $result, $msg ) = $self->SUPER::Create(
292 Content => $args{'Content'},
293 Queue => $args{'Queue'},
294 Description => $args{'Description'},
295 Name => $args{'Name'},
296 Type => $args{'Type'},
300 return ( $result, $msg );
309 Delete this template.
316 unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
317 return ( 0, $self->loc('Permission Denied') );
320 if ( !$self->IsOverride && $self->UsedBy->Count ) {
321 return ( 0, $self->loc('Template is in use') );
324 return ( $self->SUPER::Delete(@_) );
329 Returns L<RT::Scrips> limitted to scrips that use this template. Takes
330 into account that template can be overriden in a queue.
337 my $scrips = RT::Scrips->new( $self->CurrentUser );
338 $scrips->LimitByTemplate( $self );
344 Returns true value if content of the template is empty, otherwise
351 my $content = $self->Content;
352 return 0 if defined $content && length $content;
358 Returns true if it's queue specific template and there is global
359 template with the same name.
365 return 0 unless $self->Queue;
367 my $template = RT::Template->new( $self->CurrentUser );
368 $template->LoadGlobalTemplate( $self->Name );
369 return $template->id;
375 Returns L<MIME::Entity> object parsed using L</Parse> method. Returns
376 undef if last call to L</Parse> failed or never be called.
378 Note that content of the template is characters, but the contents of all
379 L<MIME::Entity> objects (including the one returned by this function,
386 return ( $self->{'MIMEObj'} );
391 This routine performs L<Text::Template> parsing on the template and then
392 imports the results into a L<MIME::Entity> so we can really use it. Use
393 L</MIMEObj> method to get the L<MIME::Entity> object.
395 Takes a hash containing Argument, TicketObj, and TransactionObj and other
396 arguments that will be available in the template's code. TicketObj and
397 TransactionObj are not mandatory, but highly recommended.
399 It returns a tuple of (val, message). If val is false, the message contains
409 if (not $self->IsEmpty and $self->Content =~ m{^Content-Type:\s+text/html\b}im) {
410 local $RT::Transaction::PreferredContentType = 'text/html';
411 ($rv, $msg) = $self->_Parse(@_);
414 ($rv, $msg) = $self->_Parse(@_);
417 return ($rv, $msg) unless $rv;
419 my $mime_type = $self->MIMEObj->mime_type;
420 if (defined $mime_type and $mime_type eq 'text/html') {
421 $self->_DowngradeFromHTML(@_);
430 # clear prev MIME object
431 $self->{'MIMEObj'} = undef;
433 #We're passing in whatever we were passed. it's destined for _ParseContent
434 my ($content, $msg) = $self->_ParseContent(@_);
435 return ( 0, $msg ) unless defined $content && length $content;
437 if ( $content =~ /^\S/s && $content !~ /^\S+:/ ) {
439 "Template #". $self->id ." has leading line that doesn't"
440 ." look like header field, if you don't want to override"
441 ." any headers and don't want to see this error message"
442 ." then leave first line of the template empty"
444 $content = "\n".$content;
447 my $parser = MIME::Parser->new();
448 $parser->output_to_core(1);
449 $parser->tmp_to_core(1);
450 $parser->use_inner_files(1);
452 ### Should we forgive normally-fatal errors?
453 $parser->ignore_errors(1);
454 # Always provide bytes, not characters, to MIME objects
455 $content = Encode::encode( 'UTF-8', $content );
456 $self->{'MIMEObj'} = eval { $parser->parse_data( \$content ) };
457 if ( my $error = $@ || $parser->last_error ) {
458 $RT::Logger->error( "$error" );
459 return ( 0, $error );
463 $self->{'MIMEObj'}->head->unfold;
464 $self->{'MIMEObj'}->head->modify(1);
466 return ( 1, $self->loc("Template parsed") );
470 # Perform Template substitutions on the template
477 TransactionObj => undef,
481 unless ( $self->CurrentUserCanRead() ) {
482 return (undef, $self->loc("Permission Denied"));
485 if ( $self->IsEmpty ) {
486 return ( undef, $self->loc("Template is empty") );
489 my $content = $self->SUPER::_Value('Content');
491 $args{'Ticket'} = delete $args{'TicketObj'} if $args{'TicketObj'};
492 $args{'Transaction'} = delete $args{'TransactionObj'} if $args{'TransactionObj'};
493 $args{'Requestor'} = eval { $args{'Ticket'}->Requestors->UserMembersObj->First->Name }
495 $args{'rtname'} = RT->Config->Get('rtname');
496 if ( $args{'Ticket'} ) {
497 my $t = $args{'Ticket'}; # avoid memory leak
498 $args{'loc'} = sub { $t->loc(@_) };
500 $args{'loc'} = sub { $self->loc(@_) };
503 if ($self->Type eq 'Perl') {
504 return $self->_ParseContentPerl(
506 TemplateArgs => \%args,
510 return $self->_ParseContentSimple(
512 TemplateArgs => \%args,
517 # uses Text::Template for Perl templates
518 sub _ParseContentPerl {
526 foreach my $key ( keys %{ $args{TemplateArgs} } ) {
527 my $val = $args{TemplateArgs}{ $key };
528 next unless ref $val;
529 next if ref($val) =~ /^(ARRAY|HASH|SCALAR|CODE)$/;
530 $args{TemplateArgs}{ $key } = \$val;
533 my $template = Text::Template->new(
535 SOURCE => $args{Content},
537 my ($ok) = $template->compile;
539 $RT::Logger->error("Template parsing error in @{[$self->Name]} (#@{[$self->id]}): $Text::Template::ERROR");
540 return ( undef, $self->loc('Template parsing error: [_1]', $Text::Template::ERROR) );
544 my $retval = $template->fill_in(
545 HASH => $args{TemplateArgs},
548 $RT::Logger->error("Template parsing error: $args{error}")
549 unless $args{error} =~ /^Died at /; # ignore intentional die()
554 return ( undef, $self->loc('Template parsing error') ) if $is_broken;
559 sub _ParseContentSimple {
567 $self->_MassageSimpleTemplateArgs(%args);
569 my $template = Text::Template->new(
571 SOURCE => $args{Content},
573 my ($ok) = $template->compile;
574 return ( undef, $self->loc('Template parsing error: [_1]', $Text::Template::ERROR) ) if !$ok;
576 # copied from Text::Template::fill_in and refactored to be simple variable
579 foreach my $fi_item (@{$template->{SOURCE}}) {
580 my ($fi_type, $fi_text, $fi_lineno) = @$fi_item;
581 if ($fi_type eq 'TEXT') {
583 } elsif ($fi_type eq 'PROG') {
585 my $original_fi_text = $fi_text;
587 # strip surrounding whitespace for simpler regexes
588 $fi_text =~ s/^\s+//;
589 $fi_text =~ s/\s+$//;
591 # if the codeblock is a simple $Variable lookup, use the value from
592 # the TemplateArgs hash...
593 if (my ($var) = $fi_text =~ /^\$(\w+)$/) {
594 if (exists $args{TemplateArgs}{$var}) {
595 $fi_res = $args{TemplateArgs}{$var};
599 # if there was no substitution then just reinsert the codeblock
600 if (!defined $fi_res) {
601 $fi_res = "{$original_fi_text}";
604 # If the value of the filled-in text really was undef,
605 # change it to an explicit empty string to avoid undefined
606 # value warnings later.
607 $fi_res = '' unless defined $fi_res;
616 sub _MassageSimpleTemplateArgs {
623 my $template_args = $args{TemplateArgs};
625 if (my $ticket = $template_args->{Ticket}) {
626 for my $column (qw/Id Subject Type InitialPriority FinalPriority Priority TimeEstimated TimeWorked Status TimeLeft Told Starts Started Due Resolved RequestorAddresses AdminCcAddresses CcAddresses/) {
627 $template_args->{"Ticket".$column} = $ticket->$column;
630 $template_args->{"TicketQueueId"} = $ticket->Queue;
631 $template_args->{"TicketQueueName"} = $ticket->QueueObj->Name;
633 $template_args->{"TicketOwnerId"} = $ticket->Owner;
634 $template_args->{"TicketOwnerName"} = $ticket->OwnerObj->Name;
635 $template_args->{"TicketOwnerEmailAddress"} = $ticket->OwnerObj->EmailAddress;
637 my $cfs = $ticket->CustomFields;
638 while (my $cf = $cfs->Next) {
639 my $simple = $cf->Name;
641 $template_args->{"TicketCF" . $simple}
642 = $ticket->CustomFieldValuesAsString($cf->Name);
646 if (my $txn = $template_args->{Transaction}) {
647 for my $column (qw/Id TimeTaken Type Field OldValue NewValue Data Content Subject Description BriefDescription/) {
648 $template_args->{"Transaction".$column} = $txn->$column;
651 my $cfs = $txn->CustomFields;
652 while (my $cf = $cfs->Next) {
653 my $simple = $cf->Name;
655 $template_args->{"TransactionCF" . $simple}
656 = $txn->CustomFieldValuesAsString($cf->Name);
661 sub _DowngradeFromHTML {
663 my $orig_entity = $self->MIMEObj;
665 my $new_entity = $orig_entity->dup; # this will fail badly if we go away from InCore parsing
666 $new_entity->head->mime_attr( "Content-Type" => 'text/plain' );
667 $new_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
669 $orig_entity->head->mime_attr( "Content-Type" => 'text/html' );
670 $orig_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
672 my $body = $new_entity->bodyhandle->as_string;
673 $body = Encode::decode( "UTF-8", $body );
674 my $html = RT::Interface::Email::ConvertHTMLToText( $body );
675 $html = Encode::encode( "UTF-8", $html );
676 return unless defined $html;
678 $new_entity->bodyhandle(MIME::Body::InCore->new( \$html ));
680 $orig_entity->make_multipart('alternative', Force => 1);
681 $orig_entity->add_part($new_entity, 0); # plain comes before html
682 $self->{MIMEObj} = $orig_entity;
687 =head2 CurrentUserHasQueueRight
689 Helper function to call the template's queue's CurrentUserHasQueueRight with the passed in args.
693 sub CurrentUserHasQueueRight {
695 return ( $self->QueueObj->CurrentUserHasRight(@_) );
700 Changing queue is not implemented.
706 return ( undef, $self->loc('Changing queue is not implemented') );
711 Change name of the template.
719 return ( undef, $self->loc('Name is required') )
722 return $self->_Set( Field => 'Name', Value => $value )
723 if lc($self->Name) eq lc($value);
725 my $tmp = $self->new( RT->SystemUser );
726 $tmp->LoadByCols( Name => $value, Queue => $self->Queue );
727 return ( undef, $self->loc('A Template with that name already exists') )
730 return $self->_Set( Field => 'Name', Value => $value );
735 If setting Type to Perl, require the ExecuteCode right.
743 if ($NewType eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
744 return ( undef, $self->loc('Permission Denied') );
747 return $self->_Set( Field => 'Type', Value => $NewType );
752 If changing content and the type is Perl, require the ExecuteCode right.
758 my $NewContent = shift;
760 if ($self->Type eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
761 return ( undef, $self->loc('Permission Denied') );
764 return $self->_Set( Field => 'Content', Value => $NewContent );
767 sub _UpdateAttributes {
774 my $type = $args{NewValues}{Type} || $self->Type;
776 # forbid updating content when the (possibly new) value of Type is Perl
777 if ($type eq 'Perl' && exists $args{NewValues}{Content}) {
778 if (!$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
779 return $self->loc('Permission Denied');
783 return $self->SUPER::_UpdateAttributes(%args);
788 If the template's Type is Perl, then compile check all the codeblocks to see if
789 they are syntactically valid. We eval them in a codeblock to avoid actually
792 Returns an (ok, message) pair.
799 return (1, $self->loc("Template does not include Perl code"))
800 unless $self->Type eq 'Perl';
802 my $content = $self->Content;
803 $content = '' if !defined($content);
805 my $template = Text::Template->new(
809 my ($ok) = $template->compile;
810 return ( undef, $self->loc('Template parsing error: [_1]', $Text::Template::ERROR) ) if !$ok;
812 # copied from Text::Template::fill_in and refactored to be compile checks
813 foreach my $fi_item (@{$template->{SOURCE}}) {
814 my ($fi_type, $fi_text, $fi_lineno) = @$fi_item;
815 next unless $fi_type eq 'PROG';
819 eval "sub { $fi_text }";
825 # provide a (hopefully) useful line number for the error, but clean up
826 # all the other extraneous garbage
827 $error =~ s/\(eval \d+\) line (\d+).*/"template line " . ($1+$fi_lineno-1)/es;
829 return (0, $self->loc("Couldn't compile template codeblock '[_1]': [_2]", $fi_text, $error));
832 return (1, $self->loc("Template compiles"));
835 =head2 CurrentUserCanRead
839 sub CurrentUserCanRead {
842 if ($self->__Value('Queue')) {
843 my $queue = RT::Queue->new( RT->SystemUser );
844 $queue->Load( $self->__Value('Queue'));
845 return 1 if $self->CurrentUser->HasRight( Right => 'ShowTemplate', Object => $queue );
847 return 1 if $self->CurrentUser->HasRight( Right => 'ShowGlobalTemplates', Object => $RT::System );
848 return 1 if $self->CurrentUser->HasRight( Right => 'ShowTemplate', Object => $RT::System );
856 sub Table {'Templates'}
865 Returns the current value of id.
866 (In the database, id is stored as int(11).)
874 Returns the current value of Queue.
875 (In the database, Queue is stored as int(11).)
879 =head2 SetQueue VALUE
883 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
884 (In the database, Queue will be stored as a int(11).)
892 Returns the Queue Object which has the id returned by Queue
899 my $Queue = RT::Queue->new($self->CurrentUser);
900 $Queue->Load($self->__Value('Queue'));
906 Returns the current value of Name.
907 (In the database, Name is stored as varchar(200).)
915 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
916 (In the database, Name will be stored as a varchar(200).)
924 Returns the current value of Description.
925 (In the database, Description is stored as varchar(255).)
929 =head2 SetDescription VALUE
932 Set Description to VALUE.
933 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
934 (In the database, Description will be stored as a varchar(255).)
942 Returns the current value of Type.
943 (In the database, Type is stored as varchar(16).)
951 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
952 (In the database, Type will be stored as a varchar(16).)
960 Returns the current value of Content.
961 (In the database, Content is stored as text.)
965 =head2 SetContent VALUE
968 Set Content to VALUE.
969 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
970 (In the database, Content will be stored as a text.)
978 Returns the current value of LastUpdated.
979 (In the database, LastUpdated is stored as datetime.)
987 Returns the current value of LastUpdatedBy.
988 (In the database, LastUpdatedBy is stored as int(11).)
996 Returns the current value of Creator.
997 (In the database, Creator is stored as int(11).)
1005 Returns the current value of Created.
1006 (In the database, Created is stored as datetime.)
1013 sub _CoreAccessible {
1017 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
1019 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1021 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
1023 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
1025 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
1027 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''},
1029 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1031 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1033 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
1035 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
1040 sub FindDependencies {
1042 my ($walker, $deps) = @_;
1044 $self->SUPER::FindDependencies($walker, $deps);
1046 $deps->Add( out => $self->QueueObj ) if $self->QueueObj->Id;
1053 Dependencies => undef,
1056 my $deps = $args{'Dependencies'};
1060 push( @$list, $self->UsedBy );
1062 $deps->_PushDependencies(
1063 BaseObject => $self,
1064 Flags => RT::Shredder::Constants::DEPENDS_ON,
1065 TargetObjects => $list,
1066 Shredder => $args{'Shredder'},
1069 return $self->SUPER::__DependsOn( %args );
1074 my ($importer, $uid, $data) = @_;
1076 $class->SUPER::PreInflate( $importer, $uid, $data );
1078 my $obj = RT::Template->new( RT->SystemUser );
1079 if ($data->{Queue} == 0) {
1080 $obj->LoadGlobalTemplate( $data->{Name} );
1082 $obj->LoadQueueTemplate( Queue => $data->{Queue}, Name => $data->{Name} );
1086 $importer->Resolve( $uid => ref($obj) => $obj->Id );
1093 RT::Base->_ImportOverlays();