Page 1 of 1

Upload documents plugin

Posted: Wed Feb 01, 2006 3:24 pm
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

Re: Upload documents plugin

Posted: Wed Feb 01, 2006 3:47 pm
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

Posted: Wed Feb 01, 2006 5:07 pm
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.

Posted: Wed Feb 01, 2006 5:20 pm
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

Posted: Wed Feb 01, 2006 6:39 pm
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]

Posted: Thu Feb 02, 2006 10:33 am
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;
        }
    }
}

Posted: Thu Feb 02, 2006 11:41 am
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!

Posted: Thu Feb 02, 2006 11:43 am
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