1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2012 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
78 use Scalar::Util 'blessed';
85 Description => 'read/write',
86 Type => 'read/write', #Type is one of Perl or Simple
87 Content => 'read/write',
88 Queue => 'read/write',
89 Creator => 'read/auto',
90 Created => 'read/auto',
91 LastUpdatedBy => 'read/auto',
92 LastUpdated => 'read/auto'
94 return $self->SUPER::_Accessible( @_, %Cols );
100 unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
101 return ( 0, $self->loc('Permission Denied') );
103 return $self->SUPER::_Set( @_ );
108 Takes the name of a table column. Returns its value as a string,
109 if the user passes an ACL check, otherwise returns undef.
116 unless ( $self->CurrentUserCanRead() ) {
119 return $self->__Value( @_ );
123 =head2 Load <identifier>
125 Load a template, either by number or by name.
127 Note that loading templates by name using this method B<is
128 ambiguous>. Several queues may have template with the same name
129 and as well global template with the same name may exist.
130 Use L</LoadGlobalTemplate> and/or L<LoadQueueTemplate> to get
137 my $identifier = shift;
138 return undef unless $identifier;
140 if ( $identifier =~ /\D/ ) {
141 return $self->LoadByCol( 'Name', $identifier );
143 return $self->LoadById( $identifier );
146 =head2 LoadGlobalTemplate NAME
148 Load the global template with the name NAME
152 sub LoadGlobalTemplate {
156 return ( $self->LoadQueueTemplate( Queue => 0, Name => $name ) );
159 =head2 LoadQueueTemplate (Queue => QUEUEID, Name => NAME)
161 Loads the Queue template named NAME for Queue QUEUE.
163 Note that this method doesn't load a global template with the same name
164 if template in the queue doesn't exist. THe following code can be used:
166 $template->LoadQueueTemplate( Queue => $queue_id, Name => $template_name );
167 unless ( $template->id ) {
168 $template->LoadGlobalTemplate( $template_name );
169 unless ( $template->id ) {
174 # ok, template either queue's or global
179 sub LoadQueueTemplate {
187 return ( $self->LoadByCols( Name => $args{'Name'}, Queue => $args{'Queue'} ) );
193 Takes a paramhash of Content, Queue, Name and Description.
194 Name should be a unique string identifying this Template.
195 Description and Content should be the template's title and content.
196 Queue should be 0 for a global template and the queue # for a queue-specific
199 Returns the Template's id # if the create was successful. Returns undef for
200 unknown database failure.
209 Description => '[no description]',
215 if ( $args{Type} eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System) ) {
216 return ( undef, $self->loc('Permission Denied') );
219 unless ( $args{'Queue'} ) {
220 unless ( $self->CurrentUser->HasRight(Right =>'ModifyTemplate', Object => $RT::System) ) {
221 return ( undef, $self->loc('Permission Denied') );
226 my $QueueObj = RT::Queue->new( $self->CurrentUser );
227 $QueueObj->Load( $args{'Queue'} ) || return ( undef, $self->loc('Invalid queue') );
229 unless ( $QueueObj->CurrentUserHasRight('ModifyTemplate') ) {
230 return ( undef, $self->loc('Permission Denied') );
232 $args{'Queue'} = $QueueObj->Id;
235 my $result = $self->SUPER::Create(
236 Content => $args{'Content'},
237 Queue => $args{'Queue'},
238 Description => $args{'Description'},
239 Name => $args{'Name'},
240 Type => $args{'Type'},
249 Delete this template.
256 unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
257 return ( 0, $self->loc('Permission Denied') );
260 return ( $self->SUPER::Delete(@_) );
265 Returns true value if content of the template is empty, otherwise
272 my $content = $self->Content;
273 return 0 if defined $content && length $content;
279 Returns L<MIME::Entity> object parsed using L</Parse> method. Returns
280 undef if last call to L</Parse> failed or never be called.
282 Note that content of the template is UTF-8, but L<MIME::Parser> is not
283 good at handling it and all data of the entity should be treated as
284 octets and converted to perl strings using Encode::decode_utf8 or
291 return ( $self->{'MIMEObj'} );
296 This routine performs L<Text::Template> parsing on the template and then
297 imports the results into a L<MIME::Entity> so we can really use it. Use
298 L</MIMEObj> method to get the L<MIME::Entity> object.
300 Takes a hash containing Argument, TicketObj, and TransactionObj and other
301 arguments that will be available in the template's code. TicketObj and
302 TransactionObj are not mandatory, but highly recommended.
304 It returns a tuple of (val, message). If val is false, the message contains
314 if ($self->Content =~ m{^Content-Type:\s+text/html\b}im) {
315 local $RT::Transaction::PreferredContentType = 'text/html';
316 ($rv, $msg) = $self->_Parse(@_);
319 ($rv, $msg) = $self->_Parse(@_);
322 return ($rv, $msg) unless $rv;
324 my $mime_type = $self->MIMEObj->mime_type;
325 if (defined $mime_type and $mime_type eq 'text/html') {
326 $self->_DowngradeFromHTML(@_);
335 # clear prev MIME object
336 $self->{'MIMEObj'} = undef;
338 #We're passing in whatever we were passed. it's destined for _ParseContent
339 my ($content, $msg) = $self->_ParseContent(@_);
340 return ( 0, $msg ) unless defined $content && length $content;
342 if ( $content =~ /^\S/s && $content !~ /^\S+:/ ) {
344 "Template #". $self->id ." has leading line that doesn't"
345 ." look like header field, if you don't want to override"
346 ." any headers and don't want to see this error message"
347 ." then leave first line of the template empty"
349 $content = "\n".$content;
352 my $parser = MIME::Parser->new();
353 $parser->output_to_core(1);
354 $parser->tmp_to_core(1);
355 $parser->use_inner_files(1);
357 ### Should we forgive normally-fatal errors?
358 $parser->ignore_errors(1);
359 # MIME::Parser doesn't play well with perl strings
360 utf8::encode($content);
361 $self->{'MIMEObj'} = eval { $parser->parse_data( \$content ) };
362 if ( my $error = $@ || $parser->last_error ) {
363 $RT::Logger->error( "$error" );
364 return ( 0, $error );
368 $self->{'MIMEObj'}->head->unfold;
370 return ( 1, $self->loc("Template parsed") );
374 # Perform Template substitutions on the template
381 TransactionObj => undef,
385 unless ( $self->CurrentUserCanRead() ) {
386 return (undef, $self->loc("Permission Denied"));
389 if ( $self->IsEmpty ) {
390 return ( undef, $self->loc("Template is empty") );
393 my $content = $self->SUPER::_Value('Content');
394 # We need to untaint the content of the template, since we'll be working
396 $content =~ s/^(.*)$/$1/;
398 $args{'Ticket'} = delete $args{'TicketObj'} if $args{'TicketObj'};
399 $args{'Transaction'} = delete $args{'TransactionObj'} if $args{'TransactionObj'};
400 $args{'Requestor'} = eval { $args{'Ticket'}->Requestors->UserMembersObj->First->Name }
402 $args{'rtname'} = RT->Config->Get('rtname');
403 if ( $args{'Ticket'} ) {
404 my $t = $args{'Ticket'}; # avoid memory leak
405 $args{'loc'} = sub { $t->loc(@_) };
407 $args{'loc'} = sub { $self->loc(@_) };
410 if ($self->Type eq 'Perl') {
411 return $self->_ParseContentPerl(
413 TemplateArgs => \%args,
417 return $self->_ParseContentSimple(
419 TemplateArgs => \%args,
424 # uses Text::Template for Perl templates
425 sub _ParseContentPerl {
433 foreach my $key ( keys %{ $args{TemplateArgs} } ) {
434 my $val = $args{TemplateArgs}{ $key };
435 next unless ref $val;
436 next if ref $val =~ /^(ARRAY|HASH|SCALAR|CODE)$/;
437 $args{TemplateArgs}{ $key } = \$val;
440 my $template = Text::Template->new(
442 SOURCE => $args{Content},
445 my $retval = $template->fill_in(
446 HASH => $args{TemplateArgs},
449 $RT::Logger->error("Template parsing error: $args{error}")
450 unless $args{error} =~ /^Died at /; # ignore intentional die()
455 return ( undef, $self->loc('Template parsing error') ) if $is_broken;
460 sub _ParseContentSimple {
468 $self->_MassageSimpleTemplateArgs(%args);
470 my $template = Text::Template->new(
472 SOURCE => $args{Content},
474 my ($ok) = $template->compile;
475 return ( undef, $self->loc('Template parsing error: [_1]', $Text::Template::ERROR) ) if !$ok;
477 # copied from Text::Template::fill_in and refactored to be simple variable
480 foreach my $fi_item (@{$template->{SOURCE}}) {
481 my ($fi_type, $fi_text, $fi_lineno) = @$fi_item;
482 if ($fi_type eq 'TEXT') {
484 } elsif ($fi_type eq 'PROG') {
486 my $original_fi_text = $fi_text;
488 # strip surrounding whitespace for simpler regexes
489 $fi_text =~ s/^\s+//;
490 $fi_text =~ s/\s+$//;
492 # if the codeblock is a simple $Variable lookup, use the value from
493 # the TemplateArgs hash...
494 if (my ($var) = $fi_text =~ /^\$(\w+)$/) {
495 if (exists $args{TemplateArgs}{$var}) {
496 $fi_res = $args{TemplateArgs}{$var};
500 # if there was no substitution then just reinsert the codeblock
501 if (!defined $fi_res) {
502 $fi_res = "{$original_fi_text}";
505 # If the value of the filled-in text really was undef,
506 # change it to an explicit empty string to avoid undefined
507 # value warnings later.
508 $fi_res = '' unless defined $fi_res;
517 sub _MassageSimpleTemplateArgs {
524 my $template_args = $args{TemplateArgs};
526 if (my $ticket = $template_args->{Ticket}) {
527 for my $column (qw/Id Subject Type InitialPriority FinalPriority Priority TimeEstimated TimeWorked Status TimeLeft Told Starts Started Due Resolved RequestorAddresses AdminCcAddresses CcAddresses/) {
528 $template_args->{"Ticket".$column} = $ticket->$column;
531 $template_args->{"TicketQueueId"} = $ticket->Queue;
532 $template_args->{"TicketQueueName"} = $ticket->QueueObj->Name;
534 $template_args->{"TicketOwnerId"} = $ticket->Owner;
535 $template_args->{"TicketOwnerName"} = $ticket->OwnerObj->Name;
536 $template_args->{"TicketOwnerEmailAddress"} = $ticket->OwnerObj->EmailAddress;
538 my $cfs = $ticket->CustomFields;
539 while (my $cf = $cfs->Next) {
540 $template_args->{"TicketCF" . $cf->Name} = $ticket->CustomFieldValuesAsString($cf->Name);
544 if (my $txn = $template_args->{Transaction}) {
545 for my $column (qw/Id TimeTaken Type Field OldValue NewValue Data Content Subject Description BriefDescription/) {
546 $template_args->{"Transaction".$column} = $txn->$column;
549 my $cfs = $txn->CustomFields;
550 while (my $cf = $cfs->Next) {
551 $template_args->{"TransactionCF" . $cf->Name} = $txn->CustomFieldValuesAsString($cf->Name);
556 sub _DowngradeFromHTML {
558 my $orig_entity = $self->MIMEObj;
560 my $new_entity = $orig_entity->dup; # this will fail badly if we go away from InCore parsing
561 $new_entity->head->mime_attr( "Content-Type" => 'text/plain' );
562 $new_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
564 $orig_entity->head->mime_attr( "Content-Type" => 'text/html' );
565 $orig_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
566 $orig_entity->make_multipart('alternative', Force => 1);
568 require HTML::FormatText;
569 require HTML::TreeBuilder;
571 # need to decode_utf8, see the doc of MIMEObj method
572 my $tree = HTML::TreeBuilder->new_from_content(
573 Encode::decode_utf8($new_entity->bodyhandle->as_string)
575 $new_entity->bodyhandle(MIME::Body::InCore->new(
576 \(scalar HTML::FormatText->new(
583 $orig_entity->add_part($new_entity, 0); # plain comes before html
584 $self->{MIMEObj} = $orig_entity;
589 =head2 CurrentUserHasQueueRight
591 Helper function to call the template's queue's CurrentUserHasQueueRight with the passed in args.
595 sub CurrentUserHasQueueRight {
597 return ( $self->QueueObj->CurrentUserHasRight(@_) );
602 If setting Type to Perl, require the ExecuteCode right.
610 if ($NewType eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
611 return ( undef, $self->loc('Permission Denied') );
614 return $self->_Set( Field => 'Type', Value => $NewType );
619 If changing content and the type is Perl, require the ExecuteCode right.
625 my $NewContent = shift;
627 if ($self->Type eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
628 return ( undef, $self->loc('Permission Denied') );
631 return $self->_Set( Field => 'Content', Value => $NewContent );
634 sub _UpdateAttributes {
641 my $type = $args{NewValues}{Type} || $self->Type;
643 # forbid updating content when the (possibly new) value of Type is Perl
644 if ($type eq 'Perl' && exists $args{NewValues}{Content}) {
645 if (!$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
646 return $self->loc('Permission Denied');
650 return $self->SUPER::_UpdateAttributes(%args);
655 If the template's Type is Perl, then compile check all the codeblocks to see if
656 they are syntactically valid. We eval them in a codeblock to avoid actually
659 Returns an (ok, message) pair.
666 return (1, $self->loc("Template does not include Perl code"))
667 unless $self->Type eq 'Perl';
669 my $content = $self->Content;
670 $content = '' if !defined($content);
672 my $template = Text::Template->new(
676 my ($ok) = $template->compile;
677 return ( undef, $self->loc('Template parsing error: [_1]', $Text::Template::ERROR) ) if !$ok;
679 # copied from Text::Template::fill_in and refactored to be compile checks
680 foreach my $fi_item (@{$template->{SOURCE}}) {
681 my ($fi_type, $fi_text, $fi_lineno) = @$fi_item;
682 next unless $fi_type eq 'PROG';
686 eval "sub { $fi_text }";
692 # provide a (hopefully) useful line number for the error, but clean up
693 # all the other extraneous garbage
694 $error =~ s/\(eval \d+\) line (\d+).*/"template line " . ($1+$fi_lineno-1)/es;
696 return (0, $self->loc("Couldn't compile template codeblock '[_1]': [_2]", $fi_text, $error));
699 return (1, $self->loc("Template compiles"));
702 =head2 CurrentUserCanRead
706 sub CurrentUserCanRead {
709 return 1 if $self->CurrentUserHasQueueRight('ShowTemplate');
711 return $self->CurrentUser->HasRight( Right =>'ShowGlobalTemplates', Object => $RT::System )
712 if !$self->QueueObj->Id;
720 use base 'RT::Record';
722 sub Table {'Templates'}
731 Returns the current value of id.
732 (In the database, id is stored as int(11).)
740 Returns the current value of Queue.
741 (In the database, Queue is stored as int(11).)
745 =head2 SetQueue VALUE
749 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
750 (In the database, Queue will be stored as a int(11).)
758 Returns the Queue Object which has the id returned by Queue
765 my $Queue = RT::Queue->new($self->CurrentUser);
766 $Queue->Load($self->__Value('Queue'));
772 Returns the current value of Name.
773 (In the database, Name is stored as varchar(200).)
781 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
782 (In the database, Name will be stored as a varchar(200).)
790 Returns the current value of Description.
791 (In the database, Description is stored as varchar(255).)
795 =head2 SetDescription VALUE
798 Set Description to VALUE.
799 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
800 (In the database, Description will be stored as a varchar(255).)
808 Returns the current value of Type.
809 (In the database, Type is stored as varchar(16).)
817 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
818 (In the database, Type will be stored as a varchar(16).)
826 Returns the current value of Language.
827 (In the database, Language is stored as varchar(16).)
831 =head2 SetLanguage VALUE
834 Set Language to VALUE.
835 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
836 (In the database, Language will be stored as a varchar(16).)
844 Returns the current value of TranslationOf.
845 (In the database, TranslationOf is stored as int(11).)
849 =head2 SetTranslationOf VALUE
852 Set TranslationOf to VALUE.
853 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
854 (In the database, TranslationOf will be stored as a int(11).)
862 Returns the current value of Content.
863 (In the database, Content is stored as text.)
867 =head2 SetContent VALUE
870 Set Content to VALUE.
871 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
872 (In the database, Content will be stored as a text.)
880 Returns the current value of LastUpdated.
881 (In the database, LastUpdated is stored as datetime.)
889 Returns the current value of LastUpdatedBy.
890 (In the database, LastUpdatedBy is stored as int(11).)
898 Returns the current value of Creator.
899 (In the database, Creator is stored as int(11).)
907 Returns the current value of Created.
908 (In the database, Created is stored as datetime.)
915 sub _CoreAccessible {
919 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
921 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
923 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
925 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
927 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
929 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
931 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
933 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''},
935 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
937 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
939 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
941 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
946 RT::Base->_ImportOverlays();