Tuesday, November 24, 2009

Uploading Files to a webserver from CKEditor under mod_perl

Just got a request to elaborate on this topic which I had mentioned in a recent tweet. So here's some more details. Note that there is no file-type checking here. In my case the uploads are only being allowed from logged-in administrators, so I can assume they are not uploading malicious files.

1) Of course, remember to use enctype="multipart/form-data" in the HTML form. Obvious but I've forgotten it more than once over the years...

2) To tell CKEditor what URL to send the uploaded file to, I pass the target URL to the CKEditor command which replaces the form <textarea> with a rich text editor:

CKEDITOR.replace( 'form_textarea', { filebrowserUploadUrl : '/upload_file' } );

In my case I did this inside a jQuery "ready" handler:


$(document).ready(
function() {
CKEDITOR.replace( 'form_textarea', { filebrowserUploadUrl : '/upload_file' } );
}
);


3) Map the URL to a mod_perl handler

I'm using object oriented perl handlers, so the relevant portion of my httpd.conf file looks something like:


<Location ~ "^/upload_file$">
SetHandler perl-script
PerlHandler NacreData::Controllers::FormHandler->upload_file
</Location>


This will look different if you're using a procedural handler or mod_request or some such.

4) Require/Use a perl module to do the uploading

The one I got to work is Apache::Upload::Slurp

I installed this with CPAN and used a PerlModule directive in httpd.conf to make sure it was loaded at runtime.

5) Write the mod_perl handler

Here's a cleaned up version of what mine looks like:


sub upload_file( $$ ) {
my ( $self, $r ) = @_;

use Apache::Constants qw( :common );
my $apr = Apache::Request->instance( $r );
$r->content_type('text/html');
$r->send_http_header;

require Apache::Upload::Slurp;
my $Uploader = new Apache::Upload::Slurp;
my $uploads = $Uploader->uploads();
my $upload = $uploads->[0];
my $dir = '/upload_files'; # directory where the uploaded files will go
return unless $upload;
return unless $upload->{data};

# make this into a nice filename, no special chars, no spaces, etc.
my $filename = $upload->{filename};
$filename =~ m{([^\\/]+$)};
$filename = $1;
$filename =~ s/[^.\w]//g;
$filename =~ s/__+/_/g;
$filename =~ s/^_//;
$filename =~ s/_$//;
$filename ||= 'file';

# make sure we don't overwrite another file by the same name
while( -f $dir.'/'.$filename || -d $dir.'/'.$filename ) {
my $ext = '';
if( $filename =~ m/(\.[^.]+$)/ ) {
$ext = $1;
$filename =~ s/\.[^.]+$//;
}
my $d = 0;
if( $filename =~ m/(\d+)$/ ) {
$d = $1;
$filename =~ s/\d+$//;
}
$d++;
$filename = $filename . $d . $ext;
}

my $fh;
$filename = $dir.'/'.$filename;
open ( $fh, '>', $filename ) || die "can't open $filename";
print {$fh} $upload->{data};
close $fh;

# now we return the information about the uploaded file to CKEditor.

# reference passed in from CKEditor. We use this to call back, letting
# the editor know we've don the upload.
my $funcNum = $apr->param('CKEditorFuncNum');

$filename =~ s|^/htdocs||; # path under document root to uploaded file
# you probably need ot change this line -- in my case, since I'm
# running under chroot, the webroot is just "/" and docroot /htdocs

my $hostname = $r->server->server_hostname;
$hostname =~ m{(\w+\.\w{2,4})$};
my $url = 'http://' . $hostname . $filename;

print "<script>parent.CKEDITOR.tools.callFunction( $funcNum, '$url' );</script>";
}

2 comments:

Anonymous said...

Thank you so much! This really helped me out in my project. I wished the new ckeditor docs were a little better but oh well. :)

Unknown said...

Thanks for posting this. You saved me days banging my head against the wall. Maybe one day there will be better documentation for CKEditor and mod_perl... but probably not.