<?php
class Rentsoft
{
    public static $AG_DOMAIN = "ag.rentsoft.ru";
    public static $NO_HTTPS = false;

    /**
     * Called at Agent's side.
     * 
     * Returns HTML code to create an IFRAME element. This code should be
     * enbedded to Agent's dashboard (user's personal area).
     * Through this IFRAME abonent accesses subscription management interface.
     *
     * @param string $rsUri             GET-parameter of the page opened in the browser.
     * @param string $agRef             Deprecated. Always pass NULL.
     * @param string $agName            Agent's system name (AG_NAME).
     * @param string $agUuid            ID of the current logged abonent at Agent's side.
     * @param string $agApi             Deprecated. Always pass NULL here.
     * @param string $agSecret          Secret key for digital signature.
     * @param string $devDomainSuffix   Deprecated, not used.
     * @param int $width                IFRAME width (100% by default)
     * @return string                   HTML code to create IFRAME tag.
     */
    public static function getIframe($rsUri, $agRef, $agName, $agUuid, $agApi, $agSecret, $devDomainSuffix = '', $width = null)
    {
        // $agRef, $agApi, $devDomainSuffix are not used, but they are
        // still in the interface to keep the function backward-compatible.

        // Build and sign response params.
        $rsSignedParams = 
            "ag_uuid=" . urlencode($agUuid) . "&" . // agent's user ID
            "ag_timestamp=" . time() . "&" .        // timestamp of this request creation WITH MILLISECONDS
            "ag_rnd=" . mt_rand();                  // a random number to get unique URL
        $rsSignedParams .= "&ag_sign=" . md5($agSecret . $rsSignedParams);

        // Build response URI.
        $rsResponseUri = "/2.01/iframe/" . ltrim($rsUri, '/')
            . (false !== strpos($rsUri, '?')? '&' : '?') 
            . $rsSignedParams;

        // Build IFRAME hostname.
        $rsHostname = $agName . "." . self::$AG_DOMAIN;
        
        // Build IFRAME full URL.
        // Note that width must be set by JS together with height due to IE bug.
        $proto = self::$NO_HTTPS? "http" : "https";
        return self::getStretchingIframe('rentsoft_ag', "{$proto}://{$rsHostname}{$rsResponseUri}", $width);
    }

    /**
     * Called at Agent's side.
     *
     * Returns HTML code to generate IFRAME with auto-adjustable height.
     *
     * @param  string $id          ID attribute prefix for IFRAME and its container (e.g. 'rentsoft_showcase')
     * @param  string $src         Full URL of the page to be opened inside IFRAME (src)
     * @param  string $width       IFRAME width (100% by default)
     * @param  string $height      Initial IFRAME height (will be auto-adjusted automatically).
     * @return string              HTML code which generates IFRAME tag.
     */
    public static function getStretchingIframe($id, $src, $width, $height = "500px")
    {
        if (!$width) $width = '100%';
        if (!$id) {
            // Try to make IFRAME ID variation as small as possible.
            static $numIframe = null, $uniqIframe = null;
            if (!$numIframe) $numIframe = 0;
            if (!$uniqIframe) $uniqIframe = uniqid('');
            $id = 'rs_' . $uniqIframe . "_" . ($numIframe++);
        }
        $src = addcslashes($src, "\\'\"\n\r/"); // note '/' at the end to avoid XSS!!!
        $divId = 'rs_' . uniqid('') . '_div';
        $cookieTransportGet = self::getCookieTransportGetJsCode();
        return rtrim("
            <div id='{$divId}'></div>
            {$cookieTransportGet}
            <script type='text/javascript'>
            (function () {
                var div = document.getElementById('{$divId}');
                var id = '{$id}_iframe';
                var src = '{$src}';
                // Creation via innerHTML is the ONLY way for IE to pass window.name inside.
                div.innerHTML = '<iframe name=\"' + id + '\" frameBorder=no scrolling=no allowTransparency=true></iframe>';
                var ifr = div.firstChild;
                ifr.id = id;
                ifr.style.padding = ifr.style.margin = '0';
                ifr.style.overflow = 'hidden';
                ifr.style.height = '{$height}'; // initial height
                ifr.style.width = '{$width}';
                ifr.src = src + (src.indexOf('?') >= 0? '&' : '?') + 'ag_ref=' + escape(location.href.replace(/#.*/, ''));
                var prevH = null;
                setInterval(function() {
                    try {
                        var h = null;
                        if (!h) {
                            // Try to extract height from the cookie transport.
                            h = cookieTransportGet('h_' + ifr.name);
                        }
                        if (!h) {
                            // Try to extract height from the hash.
                            h = location.hash.match(/^#h([0-9]+)/)? RegExp.$1 : null;
                        }
                        if (!h) {
                            // Try to use window.name transport.
                            try { delete(frames['h' + prevH]) } catch (e) {} // delete frame if it has changed its window.name.
                            if (frames['h' + prevH]) {
                                // Cross-domain magic: if the marker still exists, it is real.
                                h = prevH;
                            } else for (var i = 0; i < 10000; i += 30) if (frames['h' + i]) {
                                // Previous marker disappeared: found a new one.
                                h = i;
                                break;
                            }
                        }
                        if (h && h != prevH) {
                            ifr.style.height = parseInt(h) + 'px';
                            ifr.style.width = '{$width}';
                            prevH = h;
                        }
                    } catch (e) {
                        if (window.console) console.log(e);
                    }
                }, 100);
            })();
            </script>
        ");
    }

    /**
     * Called at Agent's side.
     *
     * Implements a mutex which restricts parallel queries (or charges) to the same
     * user account. Mutex lock files are created at session_save_path, so deletion of
     * old files are performed by PHP automatically.
     *
     * @param string $id
     * @return resource $mutex
     */
    public static function aquireMutex($id)
    {
        if (!ctype_alnum($id)) $id = md5($id);
        // We create the lock file in session directory, because it is 
        // auto-cleaned by PHP session engine on expiration.
        $fname = session_save_path() . "/sess_rs_{$id}.lck";
        $f = @fopen($fname, "a+");
        if (!$f) {
            die(
                "Cannot create a lock file at $fname: make sure you have a writable " .
                "directory in session.save_path of php.ini " .
                "(current value: \"" . session_save_path() . "\" is invalid)."
            );
        }
        flock($f, LOCK_EX);
        return $f;
    }

    /**
     * Called at Agent's side.
     *
     * Releases the mutex. This method is essential for PHP 5.3+ - earlier versions
     * were releasing mutexes automatically when you free the resource variable.
     *
     * @param resource $mutex
     */
    public static function releaseMutex($mutex)
    {
        flock($mutex, LOCK_UN);
        fclose($mutex);
    }

    /**
     * Called at RentSoft side, do not use at Agent's side.
     * This source code is here only as a sample.
     *
     * Returns javascript-code which implements auto-adjustable IFRAME height
     * at RentSoft sift.
     *
     * @param  bool $useCookieTransport  If true, use CookieTransport for height adjusting.
     * @return string                    HTML code which adjust IFRAME height at RentSoft sidt.
     */
    public static function getStretchingIframeRSSide($useCookieTransport = false)
    {
        $useCookieTransport = intval($useCookieTransport);
        return trim("
            <script type='text/javascript'>
            function cookieTransportSet(k, v) {
                var ns = arguments.callee;
                if (ns.url && !ns.ifr) {
                    var ifr = document.createElement('iframe');
                    ifr.style.position = 'absolute';
                    ifr.style.left = '-5000px';
                    ifr.style.top = '-5000px';
                    document.body.appendChild(ifr);
                    ns.ifr = ifr.contentWindow || ifr.contentDocument.window; // for IE
                }
                if (ns.ifr) {
                    ns.ifr.location.replace(ns.url + '#' + escape(k) + '=' + escape(v) + '&ncrnd=' + new Date().getTime());
                }
            }
            function detectIE() {
                var ua = window.navigator.userAgent;
                var msie = ua.indexOf('MSIE ');
                var trident = ua.indexOf('Trident/');
                if (msie > 0) {
                    return parseInt(ua.substring(msie + 5, ua.indexOf('.', msie)), 10);
                }
                if (trident > 0) {
                    var rv = ua.indexOf('rv:');
                    return parseInt(ua.substring(rv + 3, ua.indexOf('.', rv)), 10);
                }
                return false;
            }
            (function() {
                if (location.href.match(/[?&]in_frame=1/)) return; // only for frameset cabinet
                var ie = !!detectIE();
                var ff27 = parseInt((navigator.userAgent.match(new RegExp('firefox/([0-9]+)', 'i')) || [0, 0])[1]) >= 27;
                var dt = 100;
                var prevH = null;
                var timeout = null;
                var agRef = null;
                // Read ag_ref from query string?
                if (!agRef) {
                    agRef = location.search.match(/[?&]ag_ref=([^&?]*)/)? unescape(RegExp.$1) : null;
                }
                // Use ag_ref to initialize cookie transport.
                if ($useCookieTransport && agRef) {
                    cookieTransportSet.url = agRef.match(/^(\w+:\/\/[^\/]+)/)? RegExp.$1 + '/cookietransport.html?14': null;
                }
                // Read agRef from cookie?
                if (!agRef) {
                    agRef = document.cookie.match(/(?:^|;\s*)ag_ref=([^;]*)/)? unescape(RegExp.$1) : null;
                }
                // We don't know agRef. Do nothing (for IE we MUST have agRef in any case).
                if (!agRef) {
                    return;
                }
                // We know agRef. Store it in cookie.
                document.cookie = 'ag_ref=' + escape(agRef) + '; path=/';
                // Proceed with height monitoring.
                var callback = function(first) {
                    setTimeout(callback, dt / 2);
                    // Find bottom point of the page.
                    var bp = document.getElementById('bottom_point');
                    if (!bp) return;
                    // If nothing is changed, do nothing.
                    var hOrig = Math.round(bp.offsetTop);
                    if (prevH === hOrig) return;
                    // Use setTimeout to reduce continuous smooth resize
                    // (e.g. jQuery effects) into one parent window signal.
                    var h = Math.ceil(hOrig / 30) * 30;
                    var innerCallback = function() {
                        if (cookieTransportSet.url) {
                            // Pass height over the cookie transport.
                            cookieTransportSet('h_' + window.name, hOrig);
                        } else if (!ie && !ff27) {
                            // For non-IE use window.name transport.
                            window.name = 'h' + h;
                        } else {
                            // For IE use hash transport (works only for 1-level nested iframes).
                            parent.location.replace(agRef + '#h' + h);
                        }
                    };
                    if (timeout) clearTimeout(timeout);
                    if (first) innerCallback(); else timeout = setTimeout(innerCallback, dt);
                    // Save the height which was currently sent.
                    prevH = hOrig;
                };
                callback(1); // it is IMPORTANT to call it synchronously
            })();
            </script>
        ");
    }

    public static function getCookieTransportGetJsCode()
    {
        return "
            <script type='text/javascript'>
            function cookieTransportGet(k) {
                var ns = arguments.callee;
                var key = 'cookietransport_' + k;
                if (document.cookie.match(new RegExp('(?:^|;\\\\s*)' + key + '=([^;]+)'))) {
                    var v = unescape(RegExp.$1);
                    document.cookie = key + '=; path=/; expires=Thu, 01-Jan-70 00:00:01 GMT';
                    return v;
                }
                return null;
            }
            </script>
        ";
    }

    /**
     * Called at RentSoft side, do not use at Agent's side.
     * This source code is here only as a sample.
     *
     * Parses REQUEST_URI and HTTP_HOST, checks IFRAME digital signature etc.
     *
     * @param string $httpHost
     * @param string $requestUri
     * @param string $rsSecretCallback  Callback to get a secret key by Agent's name.
     * @param callback $nonceCallback   Callback to control double-requesting
     * @return array                    Parsed arguments
     * @throws Exception
     */
    public static function readArgs($httpHost, $requestUri, $rsSecretCallback, $nonceCallback)
    {
        $agName = preg_replace('/\..*$/s', '', $httpHost);
        $rsSecret = call_user_func($rsSecretCallback, $agName);

        $expireDelta = 2 * 24 * 3600; // must be > 2 days, because we know nothing about time zone here
        $args = array();
        parse_str(parse_url($requestUri, PHP_URL_QUERY), $args);
        $prefix = preg_replace('/&ag_sign=.*$/s', '', $requestUri);

        if (md5($rsSecret . $prefix) !== @$args['ag_sign']) {
            throw new Exception("Invalid digital signature!");
        }
        $agTimestamp = @$args['ag_timestamp'];
        $dt = time() - $agTimestamp;
        // Check if the request is knowingly expired.
        if ($dt > $expireDelta) {
            throw new Exception("The request is expired (age is $dt seconds which is more than $expireDelta seconds)");
        }
        // Check nonce (typically all used URIs are stored in DB).
        if (!call_user_func($nonceCallback, $prefix)) {
            throw new Exception("This request was already used: $prefix");
        }
        return array(
            'agName' => $agName,
            'agUuid' => @$args['ag_uuid'],
            'agWsdl' => @$args['ag_wsdl'],
            'agRef'  => @$args['ag_ref'],
            'agTimestamp' => $agTimestamp,
            'agSign' => @$args['ag_sign'],
        );
    }
}
