/** Copyright (c) 2006 UPT Ltd. Версия $Id: xsight.cConnection.js,v 1.9 2007/03/23 09:56:56 andy Exp $ */

/**
 * Класс для динамической загрузки данных через AJAX
 * Потомок от cObject
 * @class
 * @extends cObject
 */
cConnection = newClass(cObject, {

  /**
   * Массив ActiveX MSFT для XMLHttpRequest
   * @private
   * @type array
   */
	_msxml_progid:[
 		'MSXML2.XMLHTTP.7.0',
  		'MSXML2.XMLHTTP.6.0',
		'MSXML2.XMLHTTP.5.0',
		'MSXML2.XMLHTTP.4.0',
		'MSXML2.XMLHTTP.3.0',
		'MSXML2.XMLHTTP',
		'Microsoft.XMLHTTP'
	],

	/**
	* Переменная, в которую заносится имя loader'а
	* @private
	* @type string
	*/
	loader:null,

    /**
    * Переменная, которая храних указатель на ф-цию обработки состояния
    * onreadystatechange объекта
    * @public
    * @type Object
    */
    onreadystatechange: null,

    /**
    * Состояние ответа
    * @public
    * @type integer
    */
    readyState:         0,

    /**
    * Текст с ответом (для отладочной информации)
    * @public
    * @type string
    */
    responseText:       null,

    /**
    * XML с ответом
    * @public
    * @type Object
    */
    responseXML:        null,

    /**
    * Статус ответа
    * @public
    * @type integer
    */
    status:             200,

    /**
    * Текст статуса ответа
    * @public
    * @type string
    */
    statusText:         "OK",

    /**
    * Имя переменной сессии
    * @public
    * @type string
    */
    session_name:       "PHPSESSID",

    /**
    * Javascript ответ от сервера
    * @public
    * @type object
    */
    responseJS:         null,

    /**
    * Флаг кеширования. Устанавливается, если нужно кеширование результатов.
    * @public
    * @type boolean
    */
    caching:            false,

    /**
    * Элемент SPAN для тега SCRIPT (для совместимости с IE)
    * @private
    * @type Object
    */
    _span:              null,

    /**
    * Внутренний идентификатор соединения
    * @private
    * @type integer
    */
    _id:                null,

    /**
    * Объект XmlHttpRequest
    * @private
    * @type object
    */
    _xmlReq:            null,

    /**
    * Массив аргументов соединения
    * @private
    * @type array
    */
    _openArg:           null,

    /**
    * Массив заголовков соединения
    * @private
    * @type array
    */
    _reqHeaders:        null,

    /**
    * Максимальная длина url при get-запросе
    * @private
    * @type integer
    */
    _maxUrlLen:         2000,


	/**
	* Конструктор класса
	* @constructor
	*/
	constructor:function()
	{
		// вызов родительского конструктора
		this.constructor.prototype.constructor.call(this);
	},

	/**
	* Пустая функция (заглушка)
	* @public
	*/
	fDummy: function() {},

	/**
	* Метод обрыва соединения
	* @public
	*/
	fAbort: function()
	{
	    if (this._xmlReq)
	    {
	    	this._xmlReq.abort();
	    	this._xmlReq = null;
	    }

	    this.fCleanupScript();
	    this.fChangeReadyState(4, true);
	},

	/**
	* Метод подготовки соединения
	* @public
	* @param {string} pMethod метод отправки (GET или POST)
	* @param {string} pUrl адрес серверной части для ответа
	* @param {boolean} pAsyncFlag использовать ли асинхронное соединение
	* @param {string} pUserName имя пользователя для авторизации
	* @param {string} pPassword пароль для авторизации
	*/
	fOpen: function(pMethod, pUrl, pAsyncFlag, pUsername, pPassword)
	{
		var sid = this.fGetSid();
		if (sid) pUrl += (pUrl.indexOf('?')>=0? '&' : '?') + this.session_name + "=" + this.fEscape(sid);

	    this._openArg =
	    {
	        'method': (pMethod||'').toUpperCase(),
	        'url': pUrl,
	        'asyncFlag': pAsyncFlag,
	        'username': pUsername != null? pUsername : '',
	        'password': pPassword != null? pPassword : ''
	    };

	    this._id = null;
	    this._xmlReq = null;
	    this._reqHeaders = [];
	    this.fChangeReadyState(1, true);
	    return true;
	},

	/**
	* Метод отправки данных на сервер
	* @public
	* @param {Object} объект со парами свойство=>значение
	*/
	fSend: function(pContent)
	{
		this.fChangeReadyState(1, true);

	    var id = (new Date().getTime()) + "" + cConnection.count++;
	    var url = this._openArg.url;

	    var queryText = [];
	    var queryElem = [];
	    if (!this.fHash2query(pContent, null, queryText, queryElem)) return;

	    var loader = (this.loader||'').toLowerCase();
        var method = this._openArg.method;
	    var xmlReq = null;

        if (queryElem.length && !loader)
        {
             // Always use form loader if we have at least one form element.
             loader = 'form';
        } else
        {
           // Try to obtain XML request object.
            xmlReq = this.fObtainXmlReq(id, url)
        }

        // Full URL if parameters are passed via GET.
        var fullGetUrl = url + (url.indexOf('?')>=0? '&' : '?') + queryText.join('&');

        // Solve hashcode BEFORE appending ID and check if cache is already present.
        this.hash = null;
        if (this.caching && !queryElem.length)
        {
             this.hash = fullGetUrl;
             if (cConnection.cache[this.hash])
             {
                 var c = cConnection.cache[this.hash];
                 this._fDataReady(c[0], c[1]);
                 return false;
             }
        }

        // Detect loader and method. (Yes, lots of code and conditions!)
        var canSetHeaders = xmlReq && (window.ActiveXObject || xmlReq.setRequestHeader);
        if (!loader) {
            // Auto-detect loader.
            if (xmlReq) {
                // Can use XMLHttpRequest.
                loader = 'xml';
                switch (method) {
                    case "POST":
                        if (!canSetHeaders) {
                            // Use POST method. Pass query in request body.
                            // Opera 8.01 does not support setRequestHeader, so no POST method.
                            loader = 'form';
                        }
                        break;
                    case "GET":
                        // Length of the query is checked later.
                        break;
                    default:
                        // Method is not set: auto-detect method.
                        if (canSetHeaders) {
                            method = 'POST';
                        } else {
                            if (fullGetUrl.length > this._maxUrlLen) {
                                method = 'POST';
                                loader = 'form';
                            } else {
                                method = 'GET';
                            }
                        }
                }
            } else {
                // Cannot use XMLHttpRequest.
                loader = 'script';
                switch (method) {
                    case "POST":
                        loader = 'form';
                        break;
                    case "GET":
                        // Length of the query is checked later.
                        break;
                    default:
                        if (fullGetUrl.length > this._maxUrlLen) {
                            method = 'POST';
                            loader = 'form';
                        } else {
                            method = 'GET';
                        }
                }
            }
        } else if (!method) {
            // Loader is pre-defined, but method is not set.
            switch (loader) {
                case 'form':
                    method = 'POST';
                    break;
                case 'script':
                    method = 'GET';
                    break;
                default:
                    if (canSetHeaders) {
                        method = 'POST';
                    } else {
                        method = 'GET';
                    }
            }
        }

        // Correct GET URL.
        var requestBody = null;
        if (method == 'GET') {
            url = fullGetUrl;
            if (url.length > this._maxUrlLen) return this.fError('Cannot use so long query (URL is ' + url.length + ' byte(s) length) with GET request.');
        } else if (method == 'POST') {
            requestBody = queryText.join('&');
        } else {
            return this.fError('Unknown method: ' + method + '. Only GET and POST are supported.');
        }

        // Append loading ID to URL: a=aaa&b=bbb&<id>
        url = url + (url.indexOf('?')>=0? '&' : '?') + 'JsHttpRequest=' + id + '-' + loader;

        //alert(loader);

        // Save loading script.
        cConnection.pending[id] = this;

        // Send the request.
        switch (loader)
        {
            case 'xml':
                // Use XMLHttpRequest.
                if (!xmlReq) return this.fError('Cannot use XMLHttpRequest or ActiveX loader: not supported');
                if (method == "POST" && !canSetHeaders) return this.fError('Cannot use XMLHttpRequest loader or ActiveX loader, POST method: headers setting is not supported');
                if (queryElem.length) return this.fError('Cannot use XMLHttpRequest loader: direct form elements using and uploading are not implemented');
                this._xmlReq = xmlReq;
                var a = this._openArg;
                this._xmlReq.open(method, url, a.asyncFlag, a.username, a.password);
                if (canSetHeaders) {
                    // Pass pending headers.
                    for (var i=0; i<this._reqHeaders.length; i++)
                        this._xmlReq.setRequestHeader(this._reqHeaders[i][0], this._reqHeaders[i][1]);
                    // Set non-default Content-type. We cannot use
                    // "application/x-www-form-urlencoded" here, because
                    // in PHP variable HTTP_RAW_POST_DATA is accessible only when
                    // enctype is not default (e.g., "application/octet-stream"
                    // is a good start). We parse POST data manually in backend
                    // library code.
                    this._xmlReq.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
                    //this._xmlReq.setRequestHeader('Content-Type', 'text/html; charset=utf8');
                }
                // Send the request.
                return this._xmlReq.send(requestBody);

            case 'script':
                // Create <script> element and run it.
                if (method != 'GET') return this.fError('Cannot use SCRIPT loader: it supports only GET method');
                if (queryElem.length) return this.fEerror('Cannot use SCRIPT loader: direct form elements using and uploading are not implemented');
                this.fObtainScript(id, url);
                return true;

            case 'form':
                // Create & submit FORM.
                if (!this.fObtainForm(id, url, method, queryText, queryElem)) return null;
                return true;

            default:
                return this.fError('Unknown loader: ' + loader);
        }
	},

	/**
	* Возвращает все заголовки ответа
	* @public
	* @return {string} значение заголовков
	*/
	fGetAllResponseHeaders: function()
	{
	    if (this._xmlReq) return this._xmlReq.getAllResponseHeaders();
	    return '';
	},

	/**
	* Возвращает заголовок ответа по имени
	* @public
	* @param {string} pLabel имя заголовка
	* @return {string} значение заголовка
	*/
	fGetResponseHeader: function(pLabel)
	{
	    if (this._xmlReq) return this._xmlReq.getResponseHeader(pLabel);
	    return '';
	},

	/**
	* Устанавливает заголовок запроса
	* @public
	* @param {string} pLabel имя заголовка
	* @param {string} pValue значение
	*/
	fSetRequestHeader: function(pLabel, pValue)
	{
	    // Собираем заголовки.
	    this._reqHeaders[this._reqHeaders.length] = [pLabel, pValue];
	},

	/**
	* Внутренний обработчик DataReady
	* @private
	* @param {string} pText ответный текст
	* @param {string} pJs ответный javascript
	*/
	_fDataReady: function(pText, pJs)
	{
		with (this)
		{
		    if (pText !== null || pJs !== null)
		    {
		    	status = 4;
		        responseText = responseXML = pText;
		        responseJS = pJs;
		    }
		    else
		    {
		        status = 500;
		        responseText = responseXML = responseJS = null;
		    }

		    fChangeReadyState(2);
		    fChangeReadyState(3);
		    fChangeReadyState(4);
		    fCleanupScript();
		}
	},

	/**
	* Метод вывода ошибки
	* @public
	* @param {string} pMessage сообщение
	*/
	fError: function(pMessage)
	{
		throw (window.Error? new Error(pMessage) : pMessage);
	},

	/**
	* Создает новый экземпляр XMLHttpRequest
	* @private
	* @param {integer} pId идентификатор
	* @param {string} pUrl url источника
	* @return {Object} экземпляр объекта
	*/
	fObtainXmlReq: function(pId, pUrl)
	{
        // Try to use built-in loaders.
        var req = null;
        if (window.XMLHttpRequest)
        {
            try { req = new XMLHttpRequest() } catch(e) {}
        }
        else if (window.ActiveXObject)
        {
	    	for (var i=0; i<this._msxml_progid.length; i++)
	    	{
		        try
		        {
		        	req = new ActiveXObject(this._msxml_progid[i]);
		        	if (req) break;
		        }
		        catch(e)
		        {}
	    	}
        }

        //alert(req);

        if (req)
        {
            var th = this;
            req.onreadystatechange = function()
            {
                if (req.readyState == 4)
                {
                    // Avoid memory leak by removing closure.
                    req.onreadystatechange = th.fDummy;
                    th.status = null;
                    try
                    {
                        // In case of abort() call, req.status is unavailable and generates exception.
                        // But req.readyState equals to 4 in this case. Stupid behaviour. :-(
                        th.status = req.status;
                        th.responseText = req.responseText;
                    } catch (e) {}
                    if (!th.status) return;
                    var funcRequestBody = null;
                    try
                    {
                        // Prepare generator function & catch syntax errors on this stage.
                        eval('funcRequestBody = function() {\n' + th.responseText + '\n}');
                    } catch (e)
                    {
                        return th.fError("JavaScript code generated by backend is invalid!\n" + th.responseText)
                    }
                    // Call associated dataReady() outside try-catch block
                    // to pass excaptions in onreadystatechange in usual manner.
                    funcRequestBody();
                }
            };
            this._id = pId;
        }

        return req;
	},

	/**
	* Создает новый тег SCRIPT и начинает загрузку
	* @private
	* @param {integer} pId идентификатор транзакции
	* @param {string} pHref src скрипта
	*/
	fObtainScript: function(pId, pHref)
	{
		with (document)
		{
			var span = createElement('SPAN');
            span.style.display = 'none';
            body.insertBefore(span, body.lastChild);
            span.innerHTML = 'Text for stupid IE.<s'+'cript></' + 'script>';
            setTimeout(function()
            {
                var s = span.getElementsByTagName('script')[0];
                s.language = 'JavaScript';
                if (s.setAttribute) s.setAttribute('src', pHref); else s.src = pHref;
            }, 10);
            this._id = pId;
            this._span = span;
		}
	},

	/**
	* Метод инициализации отправки данных через скрытую форму + iframe
	* @private
	* @param {integer} pId
	* @param {string} pUrl
	* @param {string} pMethod
	* @param {string} pQueryText
	* @param {mixed} pQueryElem
	*/
	fObtainForm: function(pId, pUrl, pMethod, pQueryText, pQueryElem)
	{
        // In case of GET method - split real query string.
        if (pMethod == 'GET')
        {
            pQueryText = pUrl.split('?', 2)[1].split('&');
            pUrl = pUrl.split('?', 2)[0];
        }

        // Create invisible IFRAME with temporary form (form is used on empty queryElem).
        var div = document.createElement('DIV');
        div.id = 'jshr_d_' + pId;
        div.style.position = 'absolute';
        div.style.visibility = 'hidden';
        div.innerHTML =
            '<form enctype="multipart/form-data"></form>' + // stupid IE, MUST use innerHTML assignment :-(
            '<iframe src="javascript:\'\'" name="jshr_i_' + pId + '" style="width:0px; height:0px; overflow:hidden; border:none"></iframe>';
        var form = div.getElementsByTagName('FORM')[0];
        var iframe = div.getElementsByTagName('IFRAME')[0];

        // Check if all form elements belong to same form.
        if (pQueryElem.length)
        {
            // If we have at least one form element, we use its form as POST container.
            form = pQueryElem[0][1].form;
            var foundFile = false;
            for (var i = 0; i < pQueryElem.length; i++)
            {
                var e = pQueryElem[i][1];
                if (!e.form)
                {
                    return this.fError('Element "' + e.name + '" do not belongs to any form!');
                }
                if (e.form != form)
                {
                    return this.fError('Element "' + e.name + '" belongs to different form. All elements must belong to the same form!');
                }
                foundFile = foundFile || (e.tagName.toLowerCase() == 'input' && (e.type||'').toLowerCase() == 'file');
            }

            var et = "multipart/form-data";
            if (form.enctype != et && foundFile)
            {
                return this.fError('Attribute "enctype" of elements\' form must be "' + et + '" (for IE), "' + form.enctype + '" given.');
            }
        }

        // Temporary disable ALL form elements in 'form' (including custom!).
        for (var i = 0; i < form.elements.length; i++)
        {
            var e = form.elements[i];
            if (e.name != null)
            {
                e.jshrSaveName = e.name;
                e.name = '';
            }
        }

        // Insert hidden fields to the form.
        var tmpE = [];
        for (var i=0; i<pQueryText.length; i++)
        {
            var pair = pQueryText[i].split('=', 2);
            var e = document.createElement('INPUT');
            e.type = 'hidden';
            e.name = unescape(pair[0]);
            e.value = pair[1] != null? unescape(pair[1]) : '';
            form.appendChild(e);
            tmpE[tmpE.length] = e;
        }

        // Enable custom form elements back & change their names.
        for (var i = 0; i < pQueryElem.length; i++) pQueryElem[i][1].name = pQueryElem[i][0];

        // Insert generated form inside the document.
        // Be careful: don't forget to close FORM container in document body!
        document.body.insertBefore(div, document.body.lastChild);
        this._span = div;

        // Temporary modify form attributes, submit form, restore attributes back.
        var sv = {};
        sv.enctype  = form.enctype;  form.enctype = "multipart/form-data";
        sv.action   = form.action;   form.action = pUrl;
        sv.method   = form.method;   form.method = pMethod;
        sv.target   = form.target;   form.target = iframe.name;
        sv.onsubmit = form.onsubmit; form.onsubmit = null;
        form.submit();
        for (var i in sv) form[i] = sv[i];

        // Remove generated temporary hidden elements from form.
        for (var i = 0; i < tmpE.length; i++) tmpE[i].parentNode.removeChild(tmpE[i]);

        // Enable all disabled elements back.
        for (var i = 0; i < form.elements.length; i++)
        {
            var e = form.elements[i];
            if (e.jshrSaveName != null)
            {
                e.name = e.jshrSaveName;
                e.jshrSaveName = null;
            }
        }
	},

	/**
	* Убирает последний используемый элемент SCRIPT (очищает память)
	* @private
	*/
	fCleanupScript: function()
	{
	    var span = this._span;
	    if (span)
	    {
	        this._span = null;
	        setTimeout(function()
	        {
	            // без setTimeout - крешается IE 5.0!
	            span.parentNode.removeChild(span);
	        }, 50);
	    }

	    if (this._id)
	    {
	    	cConnection.pending[this._id] = false;
	    }
	    return false;
	},

	/**
	* Преобразование хеша в QUERY_STRING
	* @private
	* @param {object} pContent хеш
	* @param {string} pPrefix префикс
	* @return {string} строку вида param=value&param=value...
	*/
	fHash2query: function(pContent, pPrefix, pQueryText, pQueryElem)
	{
	    if (pPrefix == null) pPrefix = "";
        if (pContent instanceof Object)
        {
            for (var k in pContent)
            {
                var v = pContent[k];
                if (v instanceof Function) continue;
                var curPrefix = pPrefix? pPrefix+'['+this.fEscape(k)+']' : this.fEscape(k);
                if (this.fIsFormElement(v))
                {
                    var tn = v.tagName.toLowerCase();
                    if (tn == 'form')
                    {
                        // This is FORM itself. Add all its elements.
                        for (var i=0; i<v.elements.length; i++)
                        {
                            var e = v.elements[i];
                            if (e.name) pQueryElem[pQueryElem.length] = [e.name, e];
                        }
                    } else if (tn == 'input' || tn == 'textarea' || tn == 'select')
                    {
                        // This is a single form elemenent.
                        pQueryElem[pQueryElem.length] = [curPrefix, v];
                    } else
                    {
                        return this.fError('Invalid FORM element detected: name=' + (e.name||'') + ', tag=' + e.tagName);
                    }
                }
                else if (v instanceof Object)
                {
                    this.fHash2query(v, curPrefix, pQueryText, pQueryElem);
                }
                else
                {
                    // We MUST skip NULL values, because there is no method
                    // to pass NULL's via GET or POST request in PHP.
                    if (v === null) continue;
                    pQueryText[pQueryText.length] = curPrefix + "=" + this.fEscape('' + v);
                }
            }
        }
        else
        {
            pQueryText[pQueryText.length] = pContent;
        }
        return true;
	},

    // Return true if e is any form element of FORM itself.
    fIsFormElement: function(e)
    {
        // Fast & dirty method.
        return e && e.ownerDocument && e.parentNode && e.parentNode.appendChild && e.tagName;
    },

	/**
	* Возвращает идентификатор сессии, в PHP-совместимом формате
	* @private
	* @return {string} идентификатор сессии
	*/
	fGetSid: function()
	{
	    var m = document.location.search.match(new RegExp('[&?]'+this.session_name+'=([^&?]*)'));
	    var sid = null;
	    if (m)
	    {
	        sid = m[1];
	    } else
	    {
	        var m = document.cookie.match(new RegExp(s='(;|^)\\s*'+this.session_name+'=([^;]*)'));
	        if (m) sid = m[2];
	    }
	    return sid;
	},

    // Change current readyState and call trigger method.
    fChangeReadyState: function(s, reset)
    {
    	with (this)
    	{
	        if (reset)
	        {
	            status = statusText = responseJS = null;
	            responseText = '';
	        }
	        readyState = s;
	        if (onreadystatechange) onreadystatechange();
	    }
   },


	/**
	* Метод для escape строки
	* Квотирует также знак '+'
	* @param {string} pString входная строка
	* @return результат работы
	*/
	fEscape: function(pString)
	{
		return encodeURIComponent(unescape(pString).replace(new RegExp('\\+','g'), '%2B'));
	    //return escape(pString).replace(new RegExp('\\+','g'), '%2B');
	}

});

/**
 * Глобальный счетчик объектов
 *
 * @type integer
 * @public
 */
cConnection.count = 0;

/**
 * Глобальный массив объектов, ожидающих обработки
 *
 * @type Array
 * @public
 */
cConnection.pending = [];

/**
 * Глобальный массив кешируемых объектов
 *
 * @type Array
 * @public
 */
cConnection.cache = [];

/**
* Глобальная функция для обработки результатов динамического запроса
* Вызывает метод _fDataReady заданного объекта с идентификатором pId
* @public
* @param {integer} pId идентификатор запроса
* @param {string} pText текст
* @param {string} pJs javascript
*/
cConnection.fDataReady = function(pId, pText, pJs)
{
	var undef;
	var th = cConnection.pending[pId];
	delete cConnection.pending[pId];
	if (th)
	{
	    delete th._xmlReq;
	    if (th.caching && th.hash)
	    	cConnection.cache[th.hash] = [pText, pJs];
	    th._fDataReady(pText, pJs);
	}
	else if (th !== false)
	{
	    alert("ScriptLoader: unknown pending id: " + pId);
	}
}

/**
*
*/
cConnection.fQuery = function(pUrl, pContent, pOnReady, pNoCache)
{
	var req = new cConnection();
	req.caching = !nocache;
	req.onreadystatechange = function()
	{
            if (req.readyState == 4)
            {
				onready(req.responseJS, req.responseText);
            }
        }
        req.fOpen(null, pUrl, true);
        req.fSend(pContent);
}
