Upload documents plugin

Creating and modifying plugins.
Post Reply
alexmloveless

Upload documents plugin

Post by alexmloveless »

I'm trying to build a plugin that allows users to upload a document or file that will be associated with their entry. There's a standard file upload HTML form on the page and the plugin will simply place the file in /uploads and write to a simple table that associates the file with the entry.

I'm having a problem though. I've added the upload plugin with some basic backend logic, but when I upload the file it isn't available to the plugin.

print_r($_FILES); returns an empty array. I've tried accessing the array outside of the class definition and it's available, with all the relevant details - I guess that's because this stuff gets run when the class is loaded, not when it actually runs.

I heard a rumour that PHP has some sort of event based restriction that automatically removes the file if it's not accessed early on within the scripts instantiation, although I've not managed to find any documentation on this.

Is anyone aware of this, or can suggest how I get around this?

PHP5, MySQL5, Apache2, Windows XP
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Re: Upload documents plugin

Post by garvinhicking »

It seems that your problem is just that the <form> element you are using does not have the "enctype="multipart/form-data"" attribute set.

Up until now, this wasn't ever required. In your case you should be able to hack the include/functions_entries_Admin.inc.php file and insert that attribute into the form.

I've just made this patch and committed it to our trunk:

Code: Select all

Index: functions_entries_admin.inc.php
===================================================================
--- functions_entries_admin.inc.php     (revision 880)
+++ functions_entries_admin.inc.php     (working copy)
@@ -113,7 +113,7 @@
 ?>
         <div class="serendipityAdminMsgError"><?php echo $errMsg; ?></div>
 <?php } ?>
-        <form action="<?php echo $targetURL; ?>" method="post" <?php echo ($serendipity['XHTML11'] ? 'id' : 'name'); ?>="serendipityEntry" style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px">
+        <form <?php echo $entry['entry_form']; ?> action="<?php echo $targetURL; ?>" method="post" <?php echo ($serendipity['XHTML11'] ? 'id' : 'name'); ?>="serendipityEntry" style="margin-top: 0px; margin-bottom: 0px; padding-top: 0px; padding-bottom: 0px">
         <?php echo $hidden; ?>

         <table class="serendipityEntryEdit" border="0" width="100%">
With this, you can use the 'backend_entryform' hook in your plugin and set

Code: Select all

$eventData['entry_form'] = ' enctype="multipart/form-data"';
Then your admin form will contain this statement, and file uploads should work.

HTH,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
alexmloveless

Post by alexmloveless »

I hacked the functions_entries_admin.inc.php on my dev stack to include that. Still doesn't work, in fact, I tried passing the value of the filename in as part of the serendipity array $serendipity[properties][userfile] (or something like that) and this works when the multipart string is included but is broken when it's not. Strange.

When using the multipart form thing, I hacked the index.php to print_r($_FILES) and it was correctly populated, but when I do the same from within the plugin class, it is empty. I even used the file_exists function to check the temp_file path, and it exists in the first case, but it fails from within the plugin class.

I'll paste the code in when I get access to my dev instance tonight.
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Post by garvinhicking »

Yes, please do post that code.

I would'nt know where $_FILES got reset anywhere, anyhow, so I'd really like to check it out.

Regards,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
alexmloveless

Post by alexmloveless »

OK, try this in a dummy plugin. Remember to add the multipart thingy to the

include/functions_entries_Admin.inc.php file.

You should notice that the variables are available outside of the class, aren't available within. I think it's got something to do with when/how the classes are loaded.

I'm hoping you'll tell me that it all works fine and I'm being thick, because this has been killing me for 2 days now.

Thanks for your help so far!

Code: Select all

<?php


// Probe for a language include with constants. Still include defines later on, if some constants were missing
$probelang = dirname(__FILE__) . '/' . $serendipity['charset'] . 'lang_' . $serendipity['lang'] . '.inc.php';
if (file_exists($probelang)) {
    include $probelang;
}

include dirname(__FILE__) . '/lang_en.inc.php';

print_r($_FILES);
$serendipity['thisisatest1'] = '****SOMETHING****';
$serendipity['thisisatest2'] = $_FILES['userfile']['name'];


class serendipity_event_documentupload extends serendipity_event
{
    var $title = PLUGIN_EVENT_DOCUMENTUPLOAD_TITLE;
    var $cache = array();

    function introspect(&$propbag)
    {
        global $serendipity;

        $propbag->add('name',          PLUGIN_EVENT_DOCUMENTUPLOAD_TITLE);
        $propbag->add('description',   PLUGIN_EVENT_DOCUMENTUPLOAD_DESC);
        $propbag->add('stackable',     false);
        $propbag->add('author',        'Alex Loveless');
        $propbag->add('version',       '0.0');
        $propbag->add('requirements',  array(
            'serendipity' => '0.8',
            'smarty'      => '2.6.7',
            'php'         => '4.1.0'
        ));
        $propbag->add('event_hooks',    array(
            'backend_display' => true,
            'backend_save' => true

        ));
        $propbag->add('groups', array('BACKEND_EDITOR'));
    }

    function generate_content(&$title) {
        $title = PLUGIN_EVENT_DOCUMENTUPLOAD_TITLE;
    }

    function event_hook($event, &$bag, &$eventData, $addData = null) {
        global $serendipity;

        $hooks = &$bag->get('event_hooks');
        if (isset($hooks[$event])) {
            switch($event) {


		case 'backend_save':

			print_r($_FILES);
			echo "control $serendipity['thisisatest1']<br/>\n";
			echo "filename $serendipity['thisisatest1']<br/>\n";

                case 'backend_display':

?>
                    <fieldset style="margin: 5px">
                        <legend><?php echo PLUGIN_EVENT_DOCUMENTUPLOAD_TITLE; ?></legend>
                            <?php echo PLUGIN_EVENT_DOCUMENTUPLOAD_DESC; ?>
                            <br />
                            <input type=file name=userfile>
                    </fieldset>
<?php
                    return true;
                    break;

                default:
                    return false;
                    break;
            }
        } else {
            return false;
        }
    }
}

[/code]
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Post by garvinhicking »

Hi Alex!

Okay, the problem was quite easy to spot. I didn't think of it earlier, though.

This is a tad lengthy description, hope you stand by. :)

Serendipity saves an entry to the serendipity_admin.php script page. That request will receive all POST Data. To make some posting operations independent from the current page, an iframe is spawned to do the actual entry saving. This can send trackbacks and so on, with the admin page already being displayed, which is a quite good thing for the user-experience.

Now, for the iframe to post data, it needs all the POST data, of course. Serendpity stores those in a Session variable. It stores both POST and GET request superglobals. HOWEVER, it does not store the $_FILES superglobal. The reason is that if we saved that in a session, it would not help a thing, because uploaded temporary files are only existing during the first script call. The iframe call is a seperate script call, where the temporary file is already destroyed by the PHP process.

The solution is, to handle the $_FILES data on the same page as the POST request. You can use the "serendipity_entry_iframe" event hook for that, store your entry there, save its filename in a POST variable (which gets transported to the iframe). And in the iframe you can do whatever else you like to do with your files.

This is the plugin how I've modified it:

Code: Select all

<?php


// Probe for a language include with constants. Still include defines later on, if some constants were missing
$probelang = dirname(__FILE__) . '/' . $serendipity['charset'] . 'lang_' . $serendipity['lang'] . '.inc.php';
if (file_exists($probelang)) {
    include $probelang;
}

@include dirname(__FILE__) . '/lang_en.inc.php';

class serendipity_event_documentupload extends serendipity_event {
    var $title = PLUGIN_EVENT_DOCUMENTUPLOAD_TITLE;
    var $cache = array();

    function introspect(&$propbag)     {
        global $serendipity;

        $propbag->add('name',          PLUGIN_EVENT_DOCUMENTUPLOAD_TITLE);
        $propbag->add('description',   PLUGIN_EVENT_DOCUMENTUPLOAD_DESC);
        $propbag->add('stackable',     false);
        $propbag->add('author',        'Alex Loveless');
        $propbag->add('version',       '0.0');
        $propbag->add('requirements',  array(
            'serendipity' => '0.8',
            'smarty'      => '2.6.7',
            'php'         => '4.1.0'
        ));
        $propbag->add('event_hooks',    array(
            'backend_display'           => true,
            'backend_save'              => true,
            'backend_entryform'         => true,
            'backend_entry_iframe'      => true

        ));
        $propbag->add('groups', array('BACKEND_EDITOR'));
    }

    function generate_content(&$title) {
        $title = PLUGIN_EVENT_DOCUMENTUPLOAD_TITLE;
    }

    function event_hook($event, &$bag, &$eventData, $addData = null) {
        global $serendipity;

        $hooks = &$bag->get('event_hooks');
        if (isset($hooks[$event])) {
            switch($event) {
                case 'backend_entryform':
                    $eventData['entry_form'] = ' enctype="multipart/form-data" ';
                    break;

                case 'backend_entry_iframe':
                    // $_FILE uploads must be handled here. You will need to move the uploaded
                    // file to a user-specified directory, because the temporary file will be deleted
                    // before the iframe takes on operation.
                    print_r($_FILES);
                    if (is_uploaded_file($_FILES['userfile']['tmp_name'])) {
                        echo "Received uploaded file. Storing.<br />\n";
                        // Store a new filename in our array. We md5() it because of security issues. The file
                        // can be renamed later on, but you'll need to check on "bad" file extesions like .php, .js and so on.
                        $serendipity['POST']['userfile_target'] = $serendipity['uploadPath'] . '/tmp/' . md5($_FILES['userfile']['name']);
                        @mkdir(dirname($serendipity['POST']['userfile_target']));
                        move_uploaded_file($_FILES['userfile']['tmp_name'], $serendipity['POST']['userfile_target']);
                    } else {
                        echo "Did not receive valid upload.<br />\n";
                    }
                    break;

                case 'backend_save':
                    // The iframe saves POST and GET request. Thus, you can now operate on
                    // $serendipity['POST']['userfile_target'] any way you like.
                    print_r($serendipity['POST']);
                    break;

                case 'backend_display':
?>
                    <fieldset style="margin: 5px">
                        <legend><?php echo PLUGIN_EVENT_DOCUMENTUPLOAD_TITLE; ?></legend>
                            <?php echo PLUGIN_EVENT_DOCUMENTUPLOAD_DESC; ?>
                            <br />
                            <input type="file" name="userfile" />
                    </fieldset>
<?php
                    return true;
                    break;

                default:
                    return false;
                    break;
            }
        } else {
            return false;
        }
    }
}
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
alexmloveless

Post by alexmloveless »

You are my hero! I've been ripping my hair out over this one for several days.

I'll put together the upload plugin and hopefully submit it for general consumption soon.

Thanks!
garvinhicking
Core Developer
Posts: 30022
Joined: Tue Sep 16, 2003 9:45 pm
Location: Cologne, Germany
Contact:

Post by garvinhicking »

Cool, I'm very curious about this. It sees useful to me!

I'm awfully sorry for your problem. I do agree it's a "WTF" in the code, and not documented. Sadly, technically, there's no real other solution for it.

The work-around should work fine though. Have fun!

Regards,
Garvin
# Garvin Hicking (s9y Developer)
# Did I help you? Consider making me happy: http://wishes.garv.in/
# or use my PayPal account "paypal {at} supergarv (dot) de"
# My "other" hobby: http://flickr.garv.in/
Post Reply