Patch for AJAX file uploads

thread: 7 messages  |  last: a year ago  |  started: monday, june 4, 2007, 7:43 pm pdt


#1  |  Matthew Lieder (Apple Valley, MN) United States of America
Monday, June 4, 2007, 7:43 PM PDT

I finally couldn't avoid the need for AJAX file upload functionality, so I spent a few hours today modifying Qcodo to allow that. I was actually able to achieve success without even using any outside libraries or having to modify QFileControl! I've tested it successfully in both IE7 and Fx2, though it should work in other browsers too. Here's what you'll need to patch (to 0.3.21, though I don't think the affected areas changed much in 0.3.24):

assets/js/_core/post.js (add/replace the following methods):

    qcodo.postAjax = function(strForm, strControl, strEvent, strParameter, strWaitIconControlId, blnUploadFile) {
        // Figure out if Queue is Empty
        var blnQueueEmpty = false;
        if (qcodo.ajaxQueue.length == 0)
            blnQueueEmpty = true;

        // Enqueue the AJAX Request
        qcodo.ajaxQueue.push(new Array(strForm, strControl, strEvent, strParameter, strWaitIconControlId, blnUploadFile));

        // If the Queue was originally empty, call the Dequeue
        if (blnQueueEmpty)
            qcodo.dequeueAjaxQueue();
    };

...

    qcodo.dequeueAjaxQueue = function() {
        if (qcodo.ajaxQueue.length > 0) {
            strForm = this.ajaxQueue[0][0];
            strControl = this.ajaxQueue[0][1];
            strEvent = this.ajaxQueue[0][2];
            strParameter = this.ajaxQueue[0][3];
            strWaitIconControlId = this.ajaxQueue[0][4];
            blnUploadFile = this.ajaxQueue[0][5];

            // Display WaitIcon (if applicable)
            if (strWaitIconControlId) {
                this.objAjaxWaitIcon = this.getWrapper(strWaitIconControlId);
                if (this.objAjaxWaitIcon)
                    this.objAjaxWaitIcon.style.display = 'inline';
            };

            var objForm = document.getElementById(strForm);
            objForm.Qform__FormControl.value = strControl;
            objForm.Qform__FormEvent.value = strEvent;
            objForm.Qform__FormParameter.value = strParameter;
            objForm.Qform__FormCallType.value = "Ajax";
            objForm.Qform__FormUpdates.value = qcodo.formUpdates();
            
            if(blnUploadFile) {
                objForm.Qform__FormCheckableControls.value = this.formCheckableControls(strForm, "Server");
                
                var id = objForm.id + "_iframe";
                
                var d = document.createElement('DIV');   
                d.innerHTML = '<iframe style="display:none" src="about:blank" id="'+id+'" name="'+id+'" onload="qcodo.onIframeLoad(this);"></iframe>';   
                document.body.appendChild(d);

                objForm.target = id;
                objForm.submit();
                
                return;
            } else
                objForm.Qform__FormCheckableControls.value = this.formCheckableControls(strForm, "Ajax");

            var strPostData = "";
            for (var i = 0; i < objForm.elements.length; i++) {
                switch (objForm.elements[i].type) {
                    case "checkbox":
                    case "radio":
                        if (objForm.elements[i].checked) {
                            var strTestName = objForm.elements[i].name + "_";
                            if (objForm.elements[i].id.substring(0, strTestName.length) == strTestName)
                                strPostData += "&" + objForm.elements[i].name + "=" + objForm.elements[i].id.substring(strTestName.length);
                            else
                                strPostData += "&" + objForm.elements[i].id + "=" + "1";
                        };
                        break;

                    case "select-multiple":
                        var blnOneSelected = false;
                        for (var intIndex = 0; intIndex < objForm.elements[i].options.length; intIndex++)
                            if (objForm.elements[i].options[intIndex].selected) {
                                strPostData += "&" + objForm.elements[i].name + "=";
                                strPostData += objForm.elements[i].options[intIndex].value;
                            };
                        break;

                    default:
                        strPostData += "&" + objForm.elements[i].id + "=";

                        // For Internationalization -- we must escape the element's value properly
                        var strPostValue = objForm.elements[i].value;
                        if (strPostValue) {
                            strPostValue = strPostValue.replace(/&/g, escape('&'));
                            strPostValue = strPostValue.replace(/\+/g, "+");
                        };
                        strPostData += strPostValue;
                        break;
                };
            };

            var strUri = objForm.action;

            var objRequest;
            if (window.XMLHttpRequest) {
                objRequest = new XMLHttpRequest();
            } else if (typeof ActiveXObject != "undefined") {
                objRequest = new ActiveXObject("Microsoft.XMLHTTP");
            };

            if (objRequest) {
                objRequest.open("POST", strUri, true);
                objRequest.setRequestHeader("Method", "POST " + strUri + " HTTP/1.1");
                objRequest.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");

                objRequest.onreadystatechange = function() {
                    if (objRequest.readyState == 4) {
                        qcodo.processResponse(objRequest.responseText, objRequest.responseXML);
                    };
                };

                objRequest.send(strPostData);
            };
        };
    };
    
    qcodo.onIframeLoad = function(frame) {
        try {
            var doc;
            if(frame.contentDocument)
                doc = frame.contentDocument;
            else if(frame.contentWindow)
                doc = frame.contentWindow.document;
            else
                doc = window.frames[frame.id].document;
            
            if(doc.location.href == "about:blank")
                return;
            
            var responseText, responseXML;
            if(doc && doc.firstChild)
                responseText = doc.firstChild.innerHTML;
            if(doc && doc.XMLDocument)
                responseXML = doc.XMLDocument;
            else
                responseXML = doc;
            
            qcodo.processResponse(responseText, responseXML);
        } catch(e) {
            alert("An error occurred during AJAX IFRAME Response parsing.");
        };
        
        setTimeout(function(){document.body.removeChild(frame.parentNode);}, 100);
    };
    
    qcodo.processResponse = function(responseText, objXmlDoc) {
        try {
//            qcodo.logMessage(objRequest.responseText, true);
//            alert('AJAX Response Received');

            if ((!objXmlDoc) || (!objXmlDoc.documentElement) || (objXmlDoc.getElementsByTagName('control').length == 0)) {
                alert("An error occurred during AJAX Response parsing.\r\n\r\nThe error response will appear in a new popup.");
                var objErrorWindow = window.open('about:blank', 'qcodo_error','menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes,width=1000,height=700,left=50,top=50');
                objErrorWindow.focus();
                objErrorWindow.document.write(responseText);
                return;
            } else {
                var intLength = 0;

                // Go through Controls
                var objXmlControls = objXmlDoc.getElementsByTagName('control');
                intLength = objXmlControls.length;

                for (var intIndex = 0; intIndex < intLength; intIndex++) {
                    var strControlId = objXmlControls[intIndex].attributes.getNamedItem('id').nodeValue;

                    var strControlHtml = "";
                    if (objXmlControls[intIndex].firstChild)
                        strControlHtml = objXmlControls[intIndex].firstChild.nodeValue;
                    if (qcodo.isBrowser(qcodo.FIREFOX))
                        strControlHtml = objXmlControls[intIndex].textContent;

                    // Perform Callback Responsibility
                    if (strControlId == "Qform__FormState") {
                        var objFormState = document.getElementById(strControlId);
                        objFormState.value = strControlHtml;                            
                    } else {
                        var objSpan = document.getElementById(strControlId + "_ctl");
                        if (objSpan)
                            objSpan.innerHTML = strControlHtml;
                    };
                };

                // Go through Commands
                var objXmlCommands = objXmlDoc.getElementsByTagName('command');
                intLength = objXmlCommands.length;

                for (var intIndex = 0; intIndex < intLength; intIndex++) {
                    if (objXmlCommands[intIndex] && objXmlCommands[intIndex].firstChild) {
                        var strCommand = "";
                        intChildLength = objXmlCommands[intIndex].childNodes.length;
                        for (var intChildIndex = 0; intChildIndex < intChildLength; intChildIndex++)
                            strCommand += objXmlCommands[intIndex].childNodes[intChildIndex].nodeValue;
                        eval(strCommand);
                    };
                };
            };
        } catch (objExc) {
            alert(objExc.message + "\r\non line number " + objExc.lineNumber + "\r\nin file " + objExc.fileName);
            alert("An error occurred during AJAX Response handling.\r\n\r\nThe error response will appear in a new popup.");
            var objErrorWindow = window.open('about:blank', 'qcodo_error','menubar=no,toolbar=no,location=no,status=no,scrollbars=yes,resizable=yes,width=1000,height=700,left=50,top=50');
            objErrorWindow.focus();
            objErrorWindow.document.write(responseText);
            return;
        };

        // Perform the Dequeue
        qcodo.ajaxQueue.reverse();
        qcodo.ajaxQueue.pop();
        qcodo.ajaxQueue.reverse();
        
        // Hid the WaitIcon (if applicable)
        if (qcodo.objAjaxWaitIcon)
            qcodo.objAjaxWaitIcon.style.display = 'none';
            
        // Clean up after any elements that got removed
        for (var strGroupingId in qcodo.dropZoneGrouping)
            for (var strControlId in qcodo.dropZoneGrouping[strGroupingId])
                if(!document.getElementById(strControlId))
                    qcodo.dropZoneGrouping[strGroupingId][strControlId] = false;

        // If there are still AjaxEvents in the queue, go ahead and process/dequeue them
        if (qcodo.ajaxQueue.length > 0)
            qcodo.dequeueAjaxQueue();
    };

includes/qcodo/_core/qform/_actions.inc.php (replace the following classes; ParamOverride is an unrelated modification of mine):

    class QAjaxAction extends QAction {
        protected $strMethodName;
        protected $objWaitIconControl;
        protected $strParamOverride;
        protected $blnUploadFile;

        public function __construct($strMethodName = null, $objWaitIconControl = 'default', $strParamOverride = null, $blnUploadFile = false) {
            $this->strMethodName = $strMethodName;
            $this->objWaitIconControl = $objWaitIconControl;
            $this->strParamOverride = $strParamOverride;
            $this->blnUploadFile = $blnUploadFile;
        }

        public function __get($strName) {
            switch ($strName) {
                case 'MethodName':
                    return $this->strMethodName;
                case 'WaitIconControl':
                    return $this->objWaitIconControl;
                default:
                    try {
                        return parent::__get($strName);
                    } catch (QCallerException $objExc) {
                        $objExc->IncrementOffset();
                        throw $objExc;
                    }
            }
        }

        public function RenderScript(QControl $objControl) {
            $strWaitIconControlId = null;
            if ((gettype($this->objWaitIconControl) == 'string') && ($this->objWaitIconControl == 'default')) {
                if ($objControl->Form->DefaultWaitIcon)
                    $strWaitIconControlId = $objControl->Form->DefaultWaitIcon->ControlId;
            } else if ($this->objWaitIconControl) {
                $strWaitIconControlId = $this->objWaitIconControl->ControlId;
            }

            return sprintf("qc.pA('%s', '%s', '%s', %s, '%s', %s);",
                $objControl->Form->FormId, $objControl->ControlId, get_class($this->objEvent), $this->strParamOverride !== null ? $this->strParamOverride : ("'" . addslashes($objControl->ActionParameter) . "'"), $strWaitIconControlId, $this->blnUploadFile ? 'true' : 'false');
        }
    }

    class QAjaxControlAction extends QAjaxAction {
        public function __construct(QControl $objControl, $strMethodName, $objWaitIconControl = 'default', $strParamOverride = null, $blnUploadFile = false) {
            parent::__construct($objControl->ControlId . ':' . $strMethodName, $objWaitIconControl, $strParamOverride, $blnUploadFile);
        }
    }

includes/qcodo/_core/qform/QRadioButton.class.php (replace the following method; I removed the apparently unnecessary QRequestMode::Ajax check which was causing problems with my hybrid post/ajax request):

        public function ParsePostData() {
            if ($this->objForm->IsCheckableControlRendered($this->strControlId)) {
                if ($this->strGroupName) {
                    if(array_key_exists($this->strGroupName, $_POST))
                        $this->blnChecked = $_POST[$this->strGroupName] == $this->strControlId;
                } else
                    $this->blnChecked = array_key_exists($this->strControlId, $_POST) && $_POST[$this->strControlId];
            }
        }

Once the patches are applied, all you need to do to AJAXify file uploading is add a true to the appropriate place in the QAjaxAction constructor of the button that you want to use to trigger the file upload. That's it!

I'd appreciate testers and feedback. If this works well for everyone, it'd be really nice to have put into the core.

#2  |  Mike Ho (San Diego, CA) United States of America Qcodo Administrator
Monday, June 4, 2007, 11:09 PM PDT

Matt... great job!  I'm definitely impressed and intrigued...

I don't have time to read through the code... but at a high level could you explain what you're doing?

Are you using standard base64 encoding of the file on the client and sending that into the post data?  Or...?

please enlighten us... thanks!

#3  |  Matthew Lieder (Apple Valley, MN) United States of America
Tuesday, June 5, 2007, 6:38 AM PDT

Basically the submission/request is the same as a Server action (form POST, but targetted to a hidden IFRAME), and the response processing is the same as an AJAX action. See the “if(blnUploadFile) {" block in qcodo.dequeueAjaxQueue and also qcodo.onIframeLoad to see how I'm doing it (those are the only major blocks of new code; the rest are little tweaks here and there).

#4  |  Mike Ho (San Diego, CA) United States of America Qcodo Administrator
Tuesday, June 5, 2007, 9:18 PM PDT

And so are you passing formstate through the iframe back to the server on the file post?

#5  |  Matthew Lieder (Apple Valley, MN) United States of America
Wednesday, June 6, 2007, 6:35 AM PDT

Yep. To Qcodo on the server, it's virtually identical to a normal AJAX request. The additional beauty of this is that because of the added boolean variable to QAjaxAction, this modified behavior only happens when the developer explicitly says they want it; thus the chance of regressions is virtually nill :).

#6  |  Matthew Lieder (Apple Valley, MN) United States of America
Thursday, June 7, 2007, 6:01 PM PDT

Some unfortunate news: it doesn't work with IE6 or lower. In a year or so's time that will hopefully be a non-issue, but in the meantime I'm trying to track down a fix without having to bring in any third-party libraries. The problem stems from the fact that IE6 won't call onload on an IFRAME under any circumstance whatsoever; when I get more time, hopefully I'll be able to figure out a fix using a timer or something.

#7  |  Igor Butuc (Bucharest) Romania
Friday, March 23, 2012, 1:07 AM PDT

First of all thanks Matthew for this patch. I was using it for a while and has found a bug but only today finally solved it.

The bug is when using ajax file uploads (for example a modified QFileAsset) together with a normal serveraction for form. Your patch modify form target to the id of the iframe that you dynamically create and the file upload goes just fine. The problem is when submit later the form with standard serveraction. The form has left to that target, but iframe is already deleted, so it submits in a new page, etc. So, my solution was very simple:

                var bkTarget = objForm.target;
                objForm.target = id;
                objForm.submit();
                objForm.target = bkTarget;

Hope it will help someone.



Copyright © 2005 - 2013, Quasidea Development, LLC
This open-source framework for PHP is released under the terms of The MIT License.