RT#18830: Upload file to message template
authorJonathan Prykop <jonathan@freeside.biz>
Tue, 18 Aug 2015 04:01:31 +0000 (23:01 -0500)
committerJonathan Prykop <jonathan@freeside.biz>
Tue, 18 Aug 2015 04:01:31 +0000 (23:01 -0500)
12 files changed:
FS/FS/Schema.pm
FS/FS/template_image.pm [new file with mode: 0644]
httemplate/browse/msg_template.html
httemplate/browse/template_image.html [new file with mode: 0644]
httemplate/edit/msg_template.html
httemplate/elements/form-file_upload.html
httemplate/elements/images/ui-icons_ef8c08_256x240.png [new file with mode: 0644]
httemplate/elements/template_image-dialog.html [new file with mode: 0644]
httemplate/misc/email-customers.html
httemplate/misc/process/template_image-delete.cgi [new file with mode: 0644]
httemplate/misc/process/template_image-upload.cgi [new file with mode: 0644]
httemplate/misc/xmlhttp-template_image.cgi [new file with mode: 0644]

index 184c6c9..55dc99e 100644 (file)
@@ -204,6 +204,7 @@ sub dbdef_dist {
            && ( ! /^queue(_arg|_depend|_stat)?$/ || ! $opt->{'queue-no_history'} )
            && ! $tables_hashref_torrus->{$_}
            && ! /^cacti_page$/
+           && ! /^template_image$/
          }
       $dbdef->tables
   ) {
@@ -6346,6 +6347,19 @@ sub tables_hashref {
                         ],
     },
 
+    'template_image' => {
+      'columns' => [
+        'imgnum',     'serial',     '',      '', '', '',
+        'name',      'varchar',     '', $char_d, '', '',
+        'agentnum',      'int', 'NULL',      '', '', '',
+        'mime_type', 'varchar',     '', $char_d, '', '',
+        'base64',       'text',     '',      '', '', '',
+      ],
+      'primary_key'  => 'imgnum',
+      'unique'       => [ ],
+      'index'        => [ ['name'], ['agentnum'] ],
+    },
+
     'cust_msg' => {
       'columns' => [
         'custmsgnum', 'serial',     '',     '', '', '',
diff --git a/FS/FS/template_image.pm b/FS/FS/template_image.pm
new file mode 100644 (file)
index 0000000..e7f4bab
--- /dev/null
@@ -0,0 +1,222 @@
+package FS::template_image;
+use base qw( FS::Agent_Mixin FS::Record );
+
+use strict;
+use FS::Record qw( qsearchs );
+use File::Slurp qw( slurp );
+use MIME::Base64 qw( encode_base64 );
+
+my %ext_to_type = (
+  'jpeg' => 'image/jpeg',
+  'jpg'  => 'image/jpeg',
+  'png'  => 'image/png',
+  'gif'  => 'image/gif',
+);
+
+=head1 NAME
+
+FS::template_image - Object methods for template_image records
+
+=head1 SYNOPSIS
+
+  use FS::template_image;
+
+  $record = new FS::template_image {
+              'name'      => 'logo',
+              'agentnum'  => $agentnum,
+              'base64'    => encode_base64($rawdata),
+              'mime_type' => 'image/jpg',
+  };
+
+  $error = $record->insert;
+
+  $error = $new_record->replace($old_record);
+
+  $error = $record->delete;
+
+  $error = $record->check;
+
+=head1 DESCRIPTION
+
+An FS::template_image object represents an uploaded image for insertion into templates.
+FS::template_image inherits from FS::Record.  The following fields are currently supported:
+
+=over 4
+
+=item imgnum - primary key
+
+=item name - unique name, for selecting/editing images
+
+=item agentnum - image agent
+
+=item mime-type - image mime-type
+
+=item base64 - base64-encoded raw contents of image file
+
+=back
+
+=head1 METHODS
+
+=over 4
+
+=item new HASHREF
+
+Creates a new object.  To add the object to the database, see L<"insert">.
+
+Note that this stores the hash reference, not a distinct copy of the hash it
+points to.  You can ask the object for a copy with the I<hash> method.
+
+=cut
+
+# the new method can be inherited from FS::Record, if a table method is defined
+
+sub table { 'template_image'; }
+
+=item insert
+
+Adds this record to the database.  If there is an error, returns the error,
+otherwise returns false.
+
+=cut
+
+# the insert method can be inherited from FS::Record
+
+=item delete
+
+Delete this record from the database.
+
+=cut
+
+# the delete method can be inherited from FS::Record
+
+=item replace OLD_RECORD
+
+Replaces the OLD_RECORD with this one in the database.  If there is an error,
+returns the error, otherwise returns false.
+
+=cut
+
+# the replace method can be inherited from FS::Record
+
+=item check
+
+Checks all fields to make sure this is a valid example.  If there is
+an error, returns the error, otherwise returns false.  Called by the insert
+and replace methods.
+
+=cut
+
+# the check method should currently be supplied - FS::Record contains some
+# data checking routines
+
+sub check {
+  my $self = shift;
+
+  my $error = 
+    $self->ut_numbern('imgnum','agentnum')
+    || $self->ut_text('name','mime-type')
+    || $self->ut_anything('base64')
+  ;
+  return $error if $error;
+
+  $self->SUPER::check;
+}
+
+=item src
+
+Returns a data url for this image, incorporating mime_type & base64
+
+=cut
+
+sub src {
+  my $self = shift;
+  'data:'
+  . $self->mime_type
+  . ';base64,'
+  . $self->base64;
+}
+
+=item html
+
+Returns html for a basic img tag for this image (no attributes)
+
+=cut
+
+sub html {
+  my $self = shift;
+  '<IMG SRC="'
+  . $self->src
+  . '">';
+}
+
+=item process_image_delete
+
+Process for deleting an image.  Run as a job using L<FS::queue>.
+
+=cut
+
+sub process_image_delete {
+  my $job = shift;
+  my $param = shift;
+  my $template_image = qsearchs('template_image',{ 'imgnum' => $param->{'imgnum'} })
+    or die "Could not load template_image";
+  my $error = $template_image->delete;
+  die $error if $error;
+  '';
+}
+
+=item process_image_upload
+
+Process for uploading an image.  Run as a job using L<FS::queue>.
+
+=cut
+
+sub process_image_upload {
+  my $job = shift;
+  my $param = shift;
+
+  my $files = $param->{'uploaded_files'}
+    or die "No files provided.\n";
+
+  my (%files) = map { /^(\w+):([\.\w]+)$/ ? ($1,$2):() } split /,/, $files;
+
+  my $dir = '%%%FREESIDE_CACHE%%%/cache.'. $FS::UID::datasrc. '/';
+  my $file = $dir. $files{'file'};
+
+  my $type;
+  if ( $file =~ /\.(\w+)$/i ) {
+    my $ext = lc($1);
+    die "Unrecognized file extension $ext"
+      unless $ext_to_type{$ext};
+    $type = $ext_to_type{$ext};
+  } else {
+    die "Cannot upload image file without extension"
+  }
+
+  my $template_image = new FS::template_image {
+    'name'   => $param->{'name'},
+    'mime_type' => $type,
+    'agentnum'  => $param->{'agentnum'},
+    'base64'    => encode_base64( slurp($file, binmode => ':raw'), '' ),
+  };
+  my $error = $template_image->insert();
+  die $error if $error;
+  unlink $file;
+  '';
+
+}
+
+=back
+
+=head1 BUGS
+
+Will be described here once found.
+
+=head1 SEE ALSO
+
+L<FS::Record>
+
+=cut
+
+1;
+
index ef0b2da..1646bc1 100644 (file)
@@ -28,6 +28,7 @@ my @menubar = ();
 if ( $curuser->access_right(['Edit templates', 'Edit global templates']) ) {
   push @menubar, 'Add a new template' => $p.'edit/msg_template.html';
 }
+push @menubar, 'View template images' => $p.'browse/template_image.html';
 
 my $link = [ "${p}edit/msg_template.html?msgnum=", 'msgnum' ];
 
@@ -52,7 +53,7 @@ my $disable_link = sub {
     action      => $p.'misc/disable-msg_template.cgi?msgnum=' .
                      $template->msgnum .
                      ($template->disabled ? ';enable=1' : ''),
-    actionlabel => 'Disable lemplate',
+    actionlabel => 'Disable template',
   );
 };
 
diff --git a/httemplate/browse/template_image.html b/httemplate/browse/template_image.html
new file mode 100644 (file)
index 0000000..eb4325f
--- /dev/null
@@ -0,0 +1,68 @@
+<% include('/elements/init_overlib.html') %>
+
+<% include( 'elements/browse.html',
+              'title'         => 'Template images',
+              'name_singular' => 'image',
+              'menubar'       => \@menubar,
+              'query'         => { 'table' => 'template_image', },
+              'count_query'   => 'SELECT COUNT(*) FROM template_image',
+              'agent_virt'         => 1,
+              'agent_null_right'   => ['View global templates','Edit global templates'],
+              'agent_pos'          => 1,
+              'header'      => [ 'Name', '', '' ],
+              'fields'      => [ 'name', $tag, $delete_text ],
+              'links'       => [ '', '', '' ],
+              'cell_style'    => [ '', '', '' ],
+          )
+%>
+
+<% include('/elements/template_image-dialog.html',
+     'url' => $p.'browse/template_image.html'
+   ) %>
+
+<%init>
+use FS::template_image;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right([ 'View templates', 'View global templates',
+                                  'Edit templates', 'Edit global templates', ]);
+
+my $canedit = $curuser->access_right(['Edit templates', 'Edit global templates']);
+
+my @menubar = ();
+if ($canedit) {
+  push @menubar, 'Upload a new image' => 'javascript:insertImageDialog(\'upload\')';
+}
+push @menubar, ( 'View message templates' => $p.'browse/msg_template.html' );
+
+my $tag = sub { qq!<A HREF="javascript:insertImageDialog(! . $_[0]->imgnum . qq!)">view</A>! };
+
+my $delete_text = $canedit ? sub {
+  my $image = shift;
+  my $imgnum = $image->imgnum;
+  unless ($image->agentnum) {
+    unless ($FS::CurrentUser::CurrentUser->access_right('Edit global templates')) {
+      return '';
+    }
+  }
+  my $out = <<EOF;
+<FORM name="delete_template_image_$imgnum">
+<INPUT TYPE="hidden" name="imgnum" value="$imgnum">
+</FORM>
+EOF
+  $out .= include('/elements/progress-init.html',
+            "delete_template_image_$imgnum",
+            [ 'imgnum' ],
+            $p.'misc/process/template_image-delete.cgi',
+            $p.'browse/template_image.html',
+            "imgnum$imgnum",
+          );
+  my $onclick = 'if ( confirm(\'';
+  $onclick .= emt('Are you sure you want to delete template image ') . $imgnum;
+  $onclick .= '\') ) { imgnum' . $imgnum . 'process() }';
+  return $out . '<A HREF="javascript:void(0)" ONCLICK="' . $onclick . '">delete</A>';
+} : '';
+
+</%init>
index 7f38241..df72c5b 100644 (file)
@@ -27,6 +27,7 @@
      'no_submit'        => $no_submit,
 &>
 <%init>
+use FS::template_image;
 
 my $curuser = $FS::CurrentUser::CurrentUser;
 
@@ -345,10 +346,16 @@ function areyousure(url, message) {
 <TD valign="top"><FORM name="dummy">
 Substitutions: '
 . $widget->html .
-'<BR>Click links to insert.
-<BR>Enclose substitutions and other Perl expressions in braces:
+'<P>Click above links to insert substitution code.</P>
+<P>
+Enclose substitutions and other Perl expressions in braces:
 <BR>{ $name } = ExampleCo (Smith, John)
 <BR>{ time2str("%D", time) } = '.time2str("%D", time).'
+</P>';
+$sidebar .= include('/elements/template_image-dialog.html',
+              'callback' => 'insertHtml'
+            );
+$sidebar .= '<P><A HREF="javascript:insertImageDialog()">Insert Uploaded Image</A></P>
 </FONT></TD>
 ';
 
index 45b6c97..3542a5a 100644 (file)
@@ -69,6 +69,7 @@ Example:
 <div style="display:none:" id="uploadError"></div>
 
 <FORM NAME     = "<% $opt{name} %>"
+      ID       = "<% $opt{id} %>"
       ACTION   = "<% $fsurl %>misc/file-upload.html"
       METHOD   = "POST"
       ENCTYPE  = "multipart/form-data"
diff --git a/httemplate/elements/images/ui-icons_ef8c08_256x240.png b/httemplate/elements/images/ui-icons_ef8c08_256x240.png
new file mode 100644 (file)
index 0000000..85e63e9
Binary files /dev/null and b/httemplate/elements/images/ui-icons_ef8c08_256x240.png differ
diff --git a/httemplate/elements/template_image-dialog.html b/httemplate/elements/template_image-dialog.html
new file mode 100644 (file)
index 0000000..5691d52
--- /dev/null
@@ -0,0 +1,279 @@
+<%doc>
+
+Creates a jquery dialog box that opens when javascript function insertImageDialog
+is called, allows user to select an image and specify attributes for it, then passes 
+img tag with base64 encoded data url to a callback javascript function.
+
+Accepts the following options:
+
+callback - pass the html for the selected img to this javascript function;
+if omitted, will only include fields for viewing/uploading image
+
+url - to redirect to after upload, otherwise just refreshes dialog window
+
+</%doc>
+
+<% include('/elements/xmlhttp.html',
+        'url'  => $p.'misc/xmlhttp-template_image.cgi',
+        'subs' => [ 'get_template_image' ],
+   ) %>
+
+<DIV ID="insert_image_dialog" title="Template Images">
+
+<TABLE BORDER="0" STYLE="width: 100%"><TR><TD>
+
+<FORM ID="insert_image_form">
+
+<% &ntable("#cccccc", 2) %>
+
+  <TR>
+    <TH>Image</TH>
+    <TD>
+      <SELECT ID="insert_image_imgnum" ONCHANGE="insertImageDialog($('#insert_image_imgnum').val())">
+        <OPTION VALUE="">(select an image)</OPTION>
+      </SELECT>
+    </TD>
+  </TR>
+% if ($opt{'callback'}) {
+  <TR>
+    <TH>Width</TH>
+    <TD><INPUT TYPE="text" SIZE="5" ID="insert_image_width" ONCHANGE="previewInsertImage()"></TD>
+  </TR>
+  <TR>
+    <TH>Height</TH>
+    <TD><INPUT TYPE="text" SIZE="5" ID="insert_image_height" ONCHANGE="previewInsertImage()"></TD>
+  </TR>
+  <TR>
+    <TH>Align</TH>
+    <TD>
+      <SELECT ID="insert_image_float" ONCHANGE="previewInsertImage()">
+        <OPTION VALUE="none">inline</OPTION>
+        <OPTION VALUE="left">left</OPTION>
+        <OPTION VALUE="right">right</OPTION>
+      </SELECT>
+    </TD>
+  </TR>
+  <TR>
+    <TH>Alt Text</TH>
+    <TD><INPUT TYPE="text" SIZE="20" ID="insert_image_alt" ONCHANGE="previewInsertImage()"></TD>
+  </TR>
+  <TR>
+    <TD COLSPAN="2" ALIGN="center" STYLE="padding-top:6px">
+      <INPUT TYPE="button" ID="insert_image_button" VALUE="Insert Image" ONCLICK="insertImage()">
+    </TD>
+  </TR>
+% } # if $opt{'callback'}
+
+</TABLE>
+
+</FORM>
+
+% if ($canedit) {
+
+<P><B><% emt('Upload New Image') %></B></P>
+
+<% include('/elements/form-file_upload.html',
+     'name'      => 'TemplateImageUploadForm',
+     'id'        => 'TemplateImageUploadForm',
+     'action'    => $p.'misc/process/template_image-upload.cgi',
+     'num_files' => 1,
+     'fields'    => [ 'name', 'agentnum' ],
+     'url'       => $opt{'url'} || 'javascript:refreshImageList(1)',
+   )
+ %>
+
+ <% &ntable("#cccccc", 2) %>
+
+  <% include( '/elements/tr-input-text.html',
+                'field' => 'name',
+                'label' => 'Name',
+                'required' => 1,
+                'id' => 'upload_form_name',
+            )
+  %>
+
+  <% include( '/elements/tr-select-agent.html',
+                 'label'       => "<B>Agent</B>",
+                 'empty_label' => 'Select agent',
+                 'agent_virt'  => 1,
+                 'agent_null_right' => 'Edit global templates',
+             )
+  %>
+
+  <% include( '/elements/tr-file-upload.html',
+                'field' => 'file',
+                'label' => 'File',
+            )
+  %>
+
+  <TR>
+    <TD COLSPAN="2" ALIGN="center" STYLE="padding-top:6px">
+      <INPUT TYPE    = "submit"
+             NAME    = "submitButton"
+             ID      = "submitButton"
+             VALUE   = "Upload image"
+      >
+    </TD>
+  </TR>
+
+</TABLE>
+
+</FORM>
+
+% } #if canedit
+
+</TD><TD width="100%">
+
+<DIV ID="insert_image_preview_box">
+  <P><B><% emt('Image Preview') %></B></P>
+  <SPAN ID="insert_image_loading"><B>(<% emt('Loading image...') %>)</B></SPAN>
+  <IMG SRC="" ID="insert_image_preview">
+</DIV>
+
+</TD></TR></TABLE>
+</DIV>
+
+<SCRIPT>
+
+// initialize & close dialog window, initialize imgobj cache && image list
+$( '#insert_image_dialog' ).dialog({
+  width: 800,
+  height: 550,
+  resizable: true,
+  autoOpen: false,
+});
+var imgobj = new Object;
+refreshImageList(0);
+
+// this is the main func to invoke from links outside this file.
+// opens dialog if needed
+// updates dialog with passed imgnum
+// caches image info through an xmlhttp request if needed
+// pass 'upload' as imgnum for upload-only view
+function insertImageDialog (imgnum) {
+  if (imgnum == 'upload') {
+    $('#insert_image_form').hide();
+    $('#insert_image_preview_box').hide();
+    imgnum = undefined;
+  } else {
+    $('#insert_image_form').show();
+    $('#insert_image_preview_box').show();
+  }
+  if (imgnum && !imgobj[imgnum]) {
+    clearInsertImageDialog();
+    $('#insert_image_loading').show();
+    $('#insert_image_imgnum').val(imgnum);
+    get_template_image('imgnum',imgnum,
+      function (result) {
+        var images = JSON.parse(result) || [];
+        for (i = 0; i < images.length; i++) {
+          imgobj[images[i].imgnum] = images[i];
+        }
+        updateInsertImageDialog();
+      }
+    );
+  } else if (imgnum) {
+    $('#insert_image_imgnum').val(imgnum);
+    updateInsertImageDialog();
+  } else {
+    clearInsertImageDialog();
+  }
+  if (!$( '#insert_image_dialog' ).dialog( 'isOpen' )) {
+    $( '#insert_image_dialog' ).dialog( 'open' );
+  }
+}
+
+// sets dialog values to a default "Loading..." state, including imgnum
+function clearInsertImageDialog () {
+  $('#insert_image_imgnum').val('');
+  $('#insert_image_preview').attr('src','');
+  $('#insert_image_loading').hide();
+}
+
+// updates preview src from cache based on imgnum from form
+// then calls previewInsertImage
+function updateInsertImageDialog () {
+  var imgnum = $('#insert_image_imgnum').val();
+  $('#insert_image_loading').hide();
+  $('#insert_image_preview').attr('src',imgobj[imgnum].src);
+  previewInsertImage();
+}
+
+// updates preview width/height/alt/float based on current form values
+function previewInsertImage () {
+  $('#insert_image_preview').css('width',$('#insert_image_width').val());
+  $('#insert_image_preview').css('height',$('#insert_image_height').val());
+  $('#insert_image_preview').css('float',$('#insert_image_float').val());
+  $('#insert_image_preview').attr('alt',$('#insert_image_alt').val());
+}
+
+// constructs html based on the form contents,
+// passes it to callback & closes dialog
+function insertImage() {
+  var imgnum = $('#insert_image_imgnum').val();
+  if (!(imgnum && imgobj[imgnum])) {
+    return '';
+  }
+  var width = $('#insert_image_width').val() || '';
+  var height = $('#insert_image_height').val() || '';
+  var alt = $('#insert_image_alt').val() || '';
+  var float = $('#insert_image_float').val();
+  var imgtag = '<IMG SRC="' + imgobj[imgnum].src + '"';
+  if (width) {
+    imgtag += ' WIDTH="' + width + '"';
+  }
+  if (height) {
+    imgtag += ' HEIGHT="' + height + '"';
+  }
+  if (alt) {
+    imgtag += ' ALT="' + alt + '"';
+  }
+  if (float) {
+    imgtag += ' STYLE="float: ' + float + '"';
+  }
+  imgtag += '>';
+  <% $opt{'callback'} %>(imgtag);
+  $( '#insert_image_dialog' ).dialog( 'close' );
+}
+
+// uses xmlhttp request to initialize image list & refresh it after uploads
+function refreshImageList (fromupload) {
+  get_template_image('no_src','1',
+    function (result) {
+      if (fromupload) {
+        $("#TemplateImageUploadForm")[0].reset();
+      }
+      var images = JSON.parse(result) || [];
+      var latest;
+      for (i = 0; i < images.length; i++) {
+        if ( $("#insert_image_imgnum option[value='" + images[i].imgnum + "']").length == 0 ) {
+          $("#insert_image_imgnum").append('<OPTION VALUE="'+images[i].imgnum+'">'+images[i].name+'</OPTION>');
+          latest = images[i].imgnum;
+        }
+      }
+      if (fromupload) {
+        location.hash = "insert_image_dialog";
+        if (latest) {
+          // small risk of a race condition with other newly-uploaded images,
+          // but does no real damage (our image still shows up in the list)
+          insertImageDialog(latest);
+        }
+      }
+    }
+  );
+}
+
+</SCRIPT>
+
+<%init>
+my %opt = @_;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right([ 'View templates', 'View global templates',
+                                  'Edit templates', 'Edit global templates', ]);
+
+my $canedit = $curuser->access_right([ 'Edit templates', 'Edit global templates' ]);
+</%init>
+
index 47e6a5b..8ac44af 100644 (file)
@@ -36,7 +36,7 @@ should be used to set msgnum or from/subject/html_body cgi params
 % }
 
 
-<FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="GET">
+<FORM NAME="OneTrueForm" ACTION="<% $form_action %>" METHOD="POST">
 <INPUT TYPE="hidden" NAME="table" VALUE="<% $table %>">
 %# Mixing search params with from address, subject, etc. required special-case
 %# handling of those, risked name conflicts, and caused massive problems with 
diff --git a/httemplate/misc/process/template_image-delete.cgi b/httemplate/misc/process/template_image-delete.cgi
new file mode 100644 (file)
index 0000000..58c3f2c
--- /dev/null
@@ -0,0 +1,28 @@
+<% $server->process %>
+
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+# make sure user can generally edit
+die "access denied"
+  unless $curuser->access_right([ 'Edit templates', 'Edit global templates' ]);
+
+# make sure user can edit this particular image
+my %arg = $cgi->param('arg');
+my $imgnum = $arg{'imgnum'};
+die "bad imgnum" unless $imgnum =~ /^\d+$/;
+die "access denied" unless qsearchs({
+               'table'     => 'template_image',
+               'select'    => 'imgnum',
+               'hashref'   => { 'imgnum' => $imgnum },
+               'extra_sql' => ' AND ' . 
+                              $curuser->agentnums_sql(
+                                'null_right' => ['Edit global templates']
+                              ),
+             });
+
+my $server =
+  new FS::UI::Web::JSRPC 'FS::template_image::process_image_delete', $cgi;
+
+</%init>
diff --git a/httemplate/misc/process/template_image-upload.cgi b/httemplate/misc/process/template_image-upload.cgi
new file mode 100644 (file)
index 0000000..c3c9059
--- /dev/null
@@ -0,0 +1,26 @@
+<% $server->process %>
+
+<%init>
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right([ 'Edit templates', 'Edit global templates' ]);
+
+my %arg = $cgi->param('arg');
+my $agentnum = $arg{'agentnum'};
+
+if (!$agentnum) {
+  die "access denied"
+    unless $curuser->access_right([ 'Edit global templates' ]);
+} else {
+  die "bad agentnum"
+    unless $agentnum =~ /^\d+$/;
+  die "access denied"
+    unless $curuser->agentnum($agentnum);
+}
+
+my $server =
+  new FS::UI::Web::JSRPC 'FS::template_image::process_image_upload', $cgi;
+
+</%init>
diff --git a/httemplate/misc/xmlhttp-template_image.cgi b/httemplate/misc/xmlhttp-template_image.cgi
new file mode 100644 (file)
index 0000000..a8c50ed
--- /dev/null
@@ -0,0 +1,48 @@
+<%doc>
+Returns JSON encoded array of objects with details about FS::template_image
+objects.  Attributes in each returned object are imgnum, name, and src.
+
+Accepts the following options:
+
+imgnum - only return object for this imgnum
+
+no_src - do not include the src field
+
+</%doc>
+<% encode_json(\@result) %>\
+<%init>
+use FS::template_image;
+
+my $curuser = $FS::CurrentUser::CurrentUser;
+
+die "access denied"
+  unless $curuser->access_right([ 'View templates', 'View global templates',
+                                  'Edit templates', 'Edit global templates', ]);
+
+my %arg = $cgi->param('arg');
+
+my $search = {
+  'table' => 'template_image',
+  'hashref' => {},
+};
+
+my $imgnum = $arg{'imgnum'} || '';
+die "Bad imgnum" unless $imgnum =~ /^\d*$/;
+$search->{'hashref'}->{'imgnum'} = $imgnum if $imgnum;
+
+$search->{'select'} = 'imgnum, name' if $arg{'no_src'};
+
+$search->{'extra_sql'} = ($imgnum ? ' AND ' : ' WHERE ')
+                       . $curuser->agentnums_sql(
+                           'null_right' => ['View global templates','Edit global templates']
+                         );
+
+my @images = qsearch($search); #needs agent virtualization
+
+my @result = map { +{
+  'imgnum' => $_->imgnum,
+  'name'   => $_->name,
+  'src'    => $arg{'no_src'} ? '' : $_->src,
+} } @images;
+
+</%init>