1 # BEGIN BPS TAGGED BLOCK {{{
5 # This software is Copyright (c) 1996-2014 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 );
105 unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
106 return ( 0, $self->loc('Permission Denied') );
109 if (exists $args{Value}) {
110 if ($args{Field} eq 'Queue') {
112 # moving to another queue
113 my $queue = RT::Queue->new( $self->CurrentUser );
114 $queue->Load($args{Value});
115 unless ($queue->Id and $queue->CurrentUserHasRight('ModifyTemplate')) {
116 return ( 0, $self->loc('Permission Denied') );
120 unless ($self->CurrentUser->HasRight( Object => RT->System, Right => 'ModifyTemplate' )) {
121 return ( 0, $self->loc('Permission Denied') );
127 return $self->SUPER::_Set( @_ );
132 Takes the name of a table column. Returns its value as a string,
133 if the user passes an ACL check, otherwise returns undef.
140 unless ( $self->CurrentUserCanRead() ) {
143 return $self->__Value( @_ );
147 =head2 Load <identifier>
149 Load a template, either by number or by name.
151 Note that loading templates by name using this method B<is
152 ambiguous>. Several queues may have template with the same name
153 and as well global template with the same name may exist.
154 Use L</LoadGlobalTemplate> and/or L<LoadQueueTemplate> to get
161 my $identifier = shift;
162 return undef unless $identifier;
164 if ( $identifier =~ /\D/ ) {
165 return $self->LoadByCol( 'Name', $identifier );
167 return $self->LoadById( $identifier );
170 =head2 LoadGlobalTemplate NAME
172 Load the global template with the name NAME
176 sub LoadGlobalTemplate {
180 return ( $self->LoadQueueTemplate( Queue => 0, Name => $name ) );
183 =head2 LoadQueueTemplate (Queue => QUEUEID, Name => NAME)
185 Loads the Queue template named NAME for Queue QUEUE.
187 Note that this method doesn't load a global template with the same name
188 if template in the queue doesn't exist. THe following code can be used:
190 $template->LoadQueueTemplate( Queue => $queue_id, Name => $template_name );
191 unless ( $template->id ) {
192 $template->LoadGlobalTemplate( $template_name );
193 unless ( $template->id ) {
198 # ok, template either queue's or global
203 sub LoadQueueTemplate {
211 return ( $self->LoadByCols( Name => $args{'Name'}, Queue => $args{'Queue'} ) );
217 Takes a paramhash of Content, Queue, Name and Description.
218 Name should be a unique string identifying this Template.
219 Description and Content should be the template's title and content.
220 Queue should be 0 for a global template and the queue # for a queue-specific
223 Returns the Template's id # if the create was successful. Returns undef for
224 unknown database failure.
233 Description => '[no description]',
239 if ( $args{Type} eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System) ) {
240 return ( undef, $self->loc('Permission Denied') );
243 unless ( $args{'Queue'} ) {
244 unless ( $self->CurrentUser->HasRight(Right =>'ModifyTemplate', Object => $RT::System) ) {
245 return ( undef, $self->loc('Permission Denied') );
250 my $QueueObj = RT::Queue->new( $self->CurrentUser );
251 $QueueObj->Load( $args{'Queue'} ) || return ( undef, $self->loc('Invalid queue') );
253 unless ( $QueueObj->CurrentUserHasRight('ModifyTemplate') ) {
254 return ( undef, $self->loc('Permission Denied') );
256 $args{'Queue'} = $QueueObj->Id;
259 my ( $result, $msg ) = $self->SUPER::Create(
260 Content => $args{'Content'},
261 Queue => $args{'Queue'},
262 Description => $args{'Description'},
263 Name => $args{'Name'},
264 Type => $args{'Type'},
268 return ( $result, $msg );
277 Delete this template.
284 unless ( $self->CurrentUserHasQueueRight('ModifyTemplate') ) {
285 return ( 0, $self->loc('Permission Denied') );
288 return ( $self->SUPER::Delete(@_) );
293 Returns true value if content of the template is empty, otherwise
300 my $content = $self->Content;
301 return 0 if defined $content && length $content;
307 Returns L<MIME::Entity> object parsed using L</Parse> method. Returns
308 undef if last call to L</Parse> failed or never be called.
310 Note that content of the template is characters, but the contents of all
311 L<MIME::Entity> objects (including the one returned by this function,
318 return ( $self->{'MIMEObj'} );
323 This routine performs L<Text::Template> parsing on the template and then
324 imports the results into a L<MIME::Entity> so we can really use it. Use
325 L</MIMEObj> method to get the L<MIME::Entity> object.
327 Takes a hash containing Argument, TicketObj, and TransactionObj and other
328 arguments that will be available in the template's code. TicketObj and
329 TransactionObj are not mandatory, but highly recommended.
331 It returns a tuple of (val, message). If val is false, the message contains
341 if (not $self->IsEmpty and $self->Content =~ m{^Content-Type:\s+text/html\b}im) {
342 local $RT::Transaction::PreferredContentType = 'text/html';
343 ($rv, $msg) = $self->_Parse(@_);
346 ($rv, $msg) = $self->_Parse(@_);
349 return ($rv, $msg) unless $rv;
351 my $mime_type = $self->MIMEObj->mime_type;
352 if (defined $mime_type and $mime_type eq 'text/html') {
353 $self->_DowngradeFromHTML(@_);
362 # clear prev MIME object
363 $self->{'MIMEObj'} = undef;
365 #We're passing in whatever we were passed. it's destined for _ParseContent
366 my ($content, $msg) = $self->_ParseContent(@_);
367 return ( 0, $msg ) unless defined $content && length $content;
369 if ( $content =~ /^\S/s && $content !~ /^\S+:/ ) {
371 "Template #". $self->id ." has leading line that doesn't"
372 ." look like header field, if you don't want to override"
373 ." any headers and don't want to see this error message"
374 ." then leave first line of the template empty"
376 $content = "\n".$content;
379 my $parser = MIME::Parser->new();
380 $parser->output_to_core(1);
381 $parser->tmp_to_core(1);
382 $parser->use_inner_files(1);
384 ### Should we forgive normally-fatal errors?
385 $parser->ignore_errors(1);
386 # Always provide bytes, not characters, to MIME objects
387 $content = Encode::encode( 'UTF-8', $content );
388 $self->{'MIMEObj'} = eval { $parser->parse_data( \$content ) };
389 if ( my $error = $@ || $parser->last_error ) {
390 $RT::Logger->error( "$error" );
391 return ( 0, $error );
395 $self->{'MIMEObj'}->head->unfold;
396 $self->{'MIMEObj'}->head->modify(1);
398 return ( 1, $self->loc("Template parsed") );
402 # Perform Template substitutions on the template
409 TransactionObj => undef,
413 unless ( $self->CurrentUserCanRead() ) {
414 return (undef, $self->loc("Permission Denied"));
417 if ( $self->IsEmpty ) {
418 return ( undef, $self->loc("Template is empty") );
421 my $content = $self->SUPER::_Value('Content');
422 # We need to untaint the content of the template, since we'll be working
424 $content =~ s/^(.*)$/$1/;
426 $args{'Ticket'} = delete $args{'TicketObj'} if $args{'TicketObj'};
427 $args{'Transaction'} = delete $args{'TransactionObj'} if $args{'TransactionObj'};
428 $args{'Requestor'} = eval { $args{'Ticket'}->Requestors->UserMembersObj->First->Name }
430 $args{'rtname'} = RT->Config->Get('rtname');
431 if ( $args{'Ticket'} ) {
432 my $t = $args{'Ticket'}; # avoid memory leak
433 $args{'loc'} = sub { $t->loc(@_) };
435 $args{'loc'} = sub { $self->loc(@_) };
438 if ($self->Type eq 'Perl') {
439 return $self->_ParseContentPerl(
441 TemplateArgs => \%args,
445 return $self->_ParseContentSimple(
447 TemplateArgs => \%args,
452 # uses Text::Template for Perl templates
453 sub _ParseContentPerl {
461 foreach my $key ( keys %{ $args{TemplateArgs} } ) {
462 my $val = $args{TemplateArgs}{ $key };
463 next unless ref $val;
464 next if ref($val) =~ /^(ARRAY|HASH|SCALAR|CODE)$/;
465 $args{TemplateArgs}{ $key } = \$val;
468 my $template = Text::Template->new(
470 SOURCE => $args{Content},
472 my ($ok) = $template->compile;
474 $RT::Logger->error("Template parsing error in @{[$self->Name]} (#@{[$self->id]}): $Text::Template::ERROR");
475 return ( undef, $self->loc('Template parsing error: [_1]', $Text::Template::ERROR) );
479 my $retval = $template->fill_in(
480 HASH => $args{TemplateArgs},
483 $RT::Logger->error("Template parsing error: $args{error}")
484 unless $args{error} =~ /^Died at /; # ignore intentional die()
489 return ( undef, $self->loc('Template parsing error') ) if $is_broken;
494 sub _ParseContentSimple {
502 $self->_MassageSimpleTemplateArgs(%args);
504 my $template = Text::Template->new(
506 SOURCE => $args{Content},
508 my ($ok) = $template->compile;
509 return ( undef, $self->loc('Template parsing error: [_1]', $Text::Template::ERROR) ) if !$ok;
511 # copied from Text::Template::fill_in and refactored to be simple variable
514 foreach my $fi_item (@{$template->{SOURCE}}) {
515 my ($fi_type, $fi_text, $fi_lineno) = @$fi_item;
516 if ($fi_type eq 'TEXT') {
518 } elsif ($fi_type eq 'PROG') {
520 my $original_fi_text = $fi_text;
522 # strip surrounding whitespace for simpler regexes
523 $fi_text =~ s/^\s+//;
524 $fi_text =~ s/\s+$//;
526 # if the codeblock is a simple $Variable lookup, use the value from
527 # the TemplateArgs hash...
528 if (my ($var) = $fi_text =~ /^\$(\w+)$/) {
529 if (exists $args{TemplateArgs}{$var}) {
530 $fi_res = $args{TemplateArgs}{$var};
534 # if there was no substitution then just reinsert the codeblock
535 if (!defined $fi_res) {
536 $fi_res = "{$original_fi_text}";
539 # If the value of the filled-in text really was undef,
540 # change it to an explicit empty string to avoid undefined
541 # value warnings later.
542 $fi_res = '' unless defined $fi_res;
551 sub _MassageSimpleTemplateArgs {
558 my $template_args = $args{TemplateArgs};
560 if (my $ticket = $template_args->{Ticket}) {
561 for my $column (qw/Id Subject Type InitialPriority FinalPriority Priority TimeEstimated TimeWorked Status TimeLeft Told Starts Started Due Resolved RequestorAddresses AdminCcAddresses CcAddresses/) {
562 $template_args->{"Ticket".$column} = $ticket->$column;
565 $template_args->{"TicketQueueId"} = $ticket->Queue;
566 $template_args->{"TicketQueueName"} = $ticket->QueueObj->Name;
568 $template_args->{"TicketOwnerId"} = $ticket->Owner;
569 $template_args->{"TicketOwnerName"} = $ticket->OwnerObj->Name;
570 $template_args->{"TicketOwnerEmailAddress"} = $ticket->OwnerObj->EmailAddress;
572 my $cfs = $ticket->CustomFields;
573 while (my $cf = $cfs->Next) {
574 $template_args->{"TicketCF" . $cf->Name} = $ticket->CustomFieldValuesAsString($cf->Name);
578 if (my $txn = $template_args->{Transaction}) {
579 for my $column (qw/Id TimeTaken Type Field OldValue NewValue Data Content Subject Description BriefDescription/) {
580 $template_args->{"Transaction".$column} = $txn->$column;
583 my $cfs = $txn->CustomFields;
584 while (my $cf = $cfs->Next) {
585 $template_args->{"TransactionCF" . $cf->Name} = $txn->CustomFieldValuesAsString($cf->Name);
590 sub _DowngradeFromHTML {
592 my $orig_entity = $self->MIMEObj;
594 my $new_entity = $orig_entity->dup; # this will fail badly if we go away from InCore parsing
595 $new_entity->head->mime_attr( "Content-Type" => 'text/plain' );
596 $new_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
598 $orig_entity->head->mime_attr( "Content-Type" => 'text/html' );
599 $orig_entity->head->mime_attr( "Content-Type.charset" => 'utf-8' );
600 $orig_entity->make_multipart('alternative', Force => 1);
602 require HTML::FormatText;
603 require HTML::TreeBuilder;
604 # MIME objects are always bytes, not characters
605 my $tree = HTML::TreeBuilder->new_from_content(
606 Encode::decode( 'UTF-8', $new_entity->bodyhandle->as_string)
608 my $text = HTML::FormatText->new(
612 $text = Encode::encode( "UTF-8", $text );
614 $new_entity->bodyhandle(MIME::Body::InCore->new( \$text ));
617 $orig_entity->add_part($new_entity, 0); # plain comes before html
618 $self->{MIMEObj} = $orig_entity;
623 =head2 CurrentUserHasQueueRight
625 Helper function to call the template's queue's CurrentUserHasQueueRight with the passed in args.
629 sub CurrentUserHasQueueRight {
631 return ( $self->QueueObj->CurrentUserHasRight(@_) );
636 If setting Type to Perl, require the ExecuteCode right.
644 if ($NewType eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
645 return ( undef, $self->loc('Permission Denied') );
648 return $self->_Set( Field => 'Type', Value => $NewType );
653 If changing content and the type is Perl, require the ExecuteCode right.
659 my $NewContent = shift;
661 if ($self->Type eq 'Perl' && !$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
662 return ( undef, $self->loc('Permission Denied') );
665 return $self->_Set( Field => 'Content', Value => $NewContent );
668 sub _UpdateAttributes {
675 my $type = $args{NewValues}{Type} || $self->Type;
677 # forbid updating content when the (possibly new) value of Type is Perl
678 if ($type eq 'Perl' && exists $args{NewValues}{Content}) {
679 if (!$self->CurrentUser->HasRight(Right => 'ExecuteCode', Object => $RT::System)) {
680 return $self->loc('Permission Denied');
684 return $self->SUPER::_UpdateAttributes(%args);
689 If the template's Type is Perl, then compile check all the codeblocks to see if
690 they are syntactically valid. We eval them in a codeblock to avoid actually
693 Returns an (ok, message) pair.
700 return (1, $self->loc("Template does not include Perl code"))
701 unless $self->Type eq 'Perl';
703 my $content = $self->Content;
704 $content = '' if !defined($content);
706 my $template = Text::Template->new(
710 my ($ok) = $template->compile;
711 return ( undef, $self->loc('Template parsing error: [_1]', $Text::Template::ERROR) ) if !$ok;
713 # copied from Text::Template::fill_in and refactored to be compile checks
714 foreach my $fi_item (@{$template->{SOURCE}}) {
715 my ($fi_type, $fi_text, $fi_lineno) = @$fi_item;
716 next unless $fi_type eq 'PROG';
720 eval "sub { $fi_text }";
726 # provide a (hopefully) useful line number for the error, but clean up
727 # all the other extraneous garbage
728 $error =~ s/\(eval \d+\) line (\d+).*/"template line " . ($1+$fi_lineno-1)/es;
730 return (0, $self->loc("Couldn't compile template codeblock '[_1]': [_2]", $fi_text, $error));
733 return (1, $self->loc("Template compiles"));
736 =head2 CurrentUserCanRead
740 sub CurrentUserCanRead {
743 return 1 if $self->CurrentUserHasQueueRight('ShowTemplate');
745 return $self->CurrentUser->HasRight( Right =>'ShowGlobalTemplates', Object => $RT::System )
746 if !$self->QueueObj->Id;
754 use base 'RT::Record';
756 sub Table {'Templates'}
765 Returns the current value of id.
766 (In the database, id is stored as int(11).)
774 Returns the current value of Queue.
775 (In the database, Queue is stored as int(11).)
779 =head2 SetQueue VALUE
783 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
784 (In the database, Queue will be stored as a int(11).)
792 Returns the Queue Object which has the id returned by Queue
799 my $Queue = RT::Queue->new($self->CurrentUser);
800 $Queue->Load($self->__Value('Queue'));
806 Returns the current value of Name.
807 (In the database, Name is stored as varchar(200).)
815 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
816 (In the database, Name will be stored as a varchar(200).)
824 Returns the current value of Description.
825 (In the database, Description is stored as varchar(255).)
829 =head2 SetDescription VALUE
832 Set Description to VALUE.
833 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
834 (In the database, Description will be stored as a varchar(255).)
842 Returns the current value of Type.
843 (In the database, Type is stored as varchar(16).)
851 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
852 (In the database, Type will be stored as a varchar(16).)
860 Returns the current value of Language.
861 (In the database, Language is stored as varchar(16).)
865 =head2 SetLanguage VALUE
868 Set Language to VALUE.
869 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
870 (In the database, Language will be stored as a varchar(16).)
878 Returns the current value of TranslationOf.
879 (In the database, TranslationOf is stored as int(11).)
883 =head2 SetTranslationOf VALUE
886 Set TranslationOf to VALUE.
887 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
888 (In the database, TranslationOf will be stored as a int(11).)
896 Returns the current value of Content.
897 (In the database, Content is stored as text.)
901 =head2 SetContent VALUE
904 Set Content to VALUE.
905 Returns (1, 'Status message') on success and (0, 'Error Message') on failure.
906 (In the database, Content will be stored as a text.)
914 Returns the current value of LastUpdated.
915 (In the database, LastUpdated is stored as datetime.)
923 Returns the current value of LastUpdatedBy.
924 (In the database, LastUpdatedBy is stored as int(11).)
932 Returns the current value of Creator.
933 (In the database, Creator is stored as int(11).)
941 Returns the current value of Created.
942 (In the database, Created is stored as datetime.)
949 sub _CoreAccessible {
953 {read => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => ''},
955 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
957 {read => 1, write => 1, sql_type => 12, length => 200, is_blob => 0, is_numeric => 0, type => 'varchar(200)', default => ''},
959 {read => 1, write => 1, sql_type => 12, length => 255, is_blob => 0, is_numeric => 0, type => 'varchar(255)', default => ''},
961 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
963 {read => 1, write => 1, sql_type => 12, length => 16, is_blob => 0, is_numeric => 0, type => 'varchar(16)', default => ''},
965 {read => 1, write => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
967 {read => 1, write => 1, sql_type => -4, length => 0, is_blob => 1, is_numeric => 0, type => 'text', default => ''},
969 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
971 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
973 {read => 1, auto => 1, sql_type => 4, length => 11, is_blob => 0, is_numeric => 1, type => 'int(11)', default => '0'},
975 {read => 1, auto => 1, sql_type => 11, length => 0, is_blob => 0, is_numeric => 0, type => 'datetime', default => ''},
980 RT::Base->_ImportOverlays();