<?php
/***********************************************
* File      :   index.php
* Descr     :   Controller for Push - Pushed from the Atmail project and modified for Atmail and Zend
*
* Created   :   01.10.2010
*
* Original by Zarafa Deutschland GmbH released under GPLv2.
* Modifications by Atmail published under GPLv2
* Consult LICENSE file for details
************************************************/


ob_start();

include_once('atmaildefs.php');
include_once("config.php");
include_once("proto.php");
include_once("request.php");
include_once("compat.php");
include_once("version.php");
include_once("headers.php");


global $protocolversion;
global $policykey;
global $useragent;
global $user;

require_once( dirname(__FILE__) . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . 'bootloader.php');

license::validate();
license::p2();

// User logging
require_once('log.php');
require_once('domains.php');


// Attempt to set maximum execution time
ini_set('max_execution_time', SCRIPT_TIMEOUT);
set_time_limit(SCRIPT_TIMEOUT);

//debugLog("---------------------------------------");
//debugLog("Start");

$input = fopen("php://input", "r");
$output = fopen("php://output", "w+");

// The script must always be called with authorisation info
if(!isset($_SERVER['PHP_AUTH_PW']))
{
    header("WWW-Authenticate: Basic realm=\"Atmail PUSH\"");
    header("HTTP/1.1 401 Unauthorized");
    //print("Access denied. Please send authorisation information");
    //debugLog("Access denied: no password sent.");
    return;
}


function splitAccountDomainInput($str)
{
    // split username & domain if received as one
    $pos = strrpos($str, '\\');
    if($pos === false)
    {
        $user = $str;
        $domain = '';
    }
    else
    {
        $domain = substr($str,0,$pos);
        $user = substr($str,$pos+1);
    }

    if($domain == '' && ( $pos = strpos($user, '@') ) !== false)
    {
        $domain = substr($user,$pos+1);
    }

    return array($user, $domain);
}


$cmd = $user = $devid = $devtype = "";

// detect compressed headers (>= 12.1
if(count($_GET) == 1)
{
    //debugLog("Compressed headers detected.");
    $protocolversion = false;
    foreach($_GET as $k => $v)
    {
        $_GET[0] = ($v == '' ? $k : $v);
        // single post/get var
        // lets try decompress
        $in= base64_decode($_GET[0]);

        for($a=0;$a<strlen($in); $a++)
        {
            $compressed[$a] = ord($in[$a]);
        }


        // Protocol version (1 byte): An integer that specifies the version of the ActiveSync protocol that is being used. This value SHOULD<1> be 141. This value MAY<2> be 140 or 121.
        if($compressed[0] == 121 || $compressed[0] == 140 || $compressed[0] == 141)
        {
            $protocolversion = $compressed[0] / 10;
            $_SERVER["HTTP_MS-ASProtocolVersion"] = $protocolversion;

            // ok, passed sanity check
            // lets start decompressing
            //    Command code (1 byte): An integer that specifies the command (see table of command codes in section 2.2.1.1.1.1.2).
            $codeMapping = array(
                0 => "Sync",
                1 => "SendMail",
                2 => "SmartForward",
                3 => "SmartReply",
                4 => "GetAttachment",
                9 => "FolderSync",
                10 => "FolderCreate",
                11 => "FolderDelete",
                12 => "FolderUpdate",
                13 => "MoveItems",
                14 => "GetItemEstimate",
                15 => "MeetingResponse",
                16 => "Search",
                17 => "Settings",
                18 => "Ping",
                19 => "ItemOperations",
                20 => "Provision",
                21 => "ResolveRecipients",
                22 => "ValidateCert");

            $_GET["Cmd"] = $codeMapping[$compressed[1]];



            // Locale (2 bytes): An integer that specifies the locale of the language that is used for the response. Locale integer values are specified in [MS-LCID].
            // ignore

            // Device ID length (1 byte): An integer that specifies the length of the Device ID field. A value of 0 indicates that the Device ID field is absent.
            $devidLength = $compressed[4];
            $_GET["DeviceId"] = '';
            $current = 5;
            for($a=5;$a<$devidLength + $current; $a++)
            {
                $_GET["DeviceId"] .= sprintf("%02x", $compressed[$a]);
            }


            //    Policy key length (1 byte): An integer that specifies the length of the policy key. The only valid values are 0 or 4. A value of 0 indicates that the policy key field is absent.
            //    Policy key (4 bytes, optional): An unsigned integer that indicates the state of policy settings on the client device, as specified in [MS-ASPROV] section 2.2.2.41. If the value of the Policy key length field is 0, this field is absent.
            $policykeyLength = $compressed[$a];
            $policykey = '';
            $current = $a;
            for(;$a<=$policykeyLength + $current; $a++)
            {
                $policykey .= chr($compressed[$a]);
            }

            //    Device type length (1 byte): An integer that specifies the length of the Device type field.
            //    Device type (variable): A string that specifies the type of client device. For details, see section 2.2.1.1.1.2.4. The length of this field is specified by the Device type length field.
            $devtypeLength = $compressed[$a];

            $_GET["DeviceType"] = '';
            $current = $a;

            for(;$a<=$devtypeLength + $current; $a++)
            {
                $_GET["DeviceType"] .= chr($compressed[$a]);
            }

            $a++;
            $current = $a;
            for(;$a<count($compressed); $a++)
            {
                $tag = $compressed[$a];
                $a++;
                $length = $compressed[$a];
                $a++;
                $tagMapping =
                    array(
                        0 => "AttachmentName",
                        1 => "CollectionId",
                        3 => "ItemId",
                        4 => "LongId",
                        6 => "Occurrence",
                        7 => "Options",
                        8 => "User"
                    );
                $_GET[$tagMapping[$tag]] = '';
                for(;$a<$length; $a++)
                {
                    $optionsMapping =
                        array(
                            1 => "SaveInSent",
                            2 => "AcceptMultiPart"
                        );
                    if($tagMapping[$tag] == "Options")
                    {
                        $_GET[$tagMapping[$tag]] = $optionsMapping[$compressed[$a]];
                    }
                    else
                    {
                        $_GET[$tagMapping[$tag]] .= chr($compressed[$a]);
                    }
                }
            }
            list($junk, $_GET['User']) = explode("\\", $_SERVER[PHP_AUTH_USER], 2);
//    Command parameters (variable): An array of Encoded Parameter structures as specified in section 2.2.1.1.1.1.1. This field is only present if there are command-specific parameters associated with the command specified by the Command code field. See section 2.2.1.1.1.1.3 for a list of command-specific parameters.
        }
    }
}


// massage username / domain
list($auth_user, $auth_domain) = splitAccountDomainInput($_SERVER['PHP_AUTH_USER']);
$auth_pw = $_SERVER['PHP_AUTH_PW'];
//debugLog("PHP_AUTH_USER: " . $_SERVER['PHP_AUTH_USER']);
//debugLog("auth_user: $auth_user");
//debugLog("auth_domain: $auth_domain");

// Parse the standard GET parameters
if(isset($_GET["Cmd"]))
    $cmd = $_GET["Cmd"];
if(isset($_GET["User"]))
    $user = $_GET["User"];
if(isset($_GET["DeviceId"]))
    $devid = $_GET["DeviceId"];
if(isset($_GET["DeviceType"]))
    $devtype = $_GET["DeviceType"];


list($user_user, $user_domain) = splitAccountDomainInput($user);

$user = ($user_domain != "" && strpos($user_user, '@') === false ? ($user_user . "@" . $user_domain) : $user);
$auth_user = ($auth_domain != "" && strpos($auth_user, '@') === false ? ($auth_user . "@" . $auth_domain) : $auth_user);

//debugLog($auth_user . " " . $user . " : " . $devtype . " " . $devid . "\n");

// The GET parameters are required
if($_SERVER["REQUEST_METHOD"] == "POST") {
    if(!isset($user) || !isset($devid) || !isset($devtype)) {
        header("HTTP/1.1 400 Bad Request");
        header("WWW-Authenticate: Basic realm=\"Atmail PUSH\"");
        //print("Your device requested the Atmail URL without the required GET parameters.");
        //debugLog("Access denied: Your device requested the Atmail URL without the required GET parameters.");
        //debugLog("end");
        //debugLog("--------");
        return;
    }
}

// check username provided
$userInfo = array();
$authSuccess = false;
if( strlen($auth_user) > 0 )
{
    try
    {
        $userInfo = users::get($auth_user);
        $authSuccess = users::auth( $auth_user, $auth_pw );
    }
    catch( Exception $e)
    {
        $authSuccess = false;
    }
}


if( !$authSuccess )
{

    header("HTTP/1.1 401 Unauthorized");
    header("WWW-Authenticate: Basic realm=\"Atmail PUSH\"");
    //debugLog("Access denied. Username or password incorrect.");
    //debugLog("Access denied: Main logon failed.");
    //debugLog("end");
    //debugLog("--------");
    return;

}


$groupDefaultInfo = groups::get('default');
$groupUsersInfo = groups::get($userInfo['Ugroup']);

if( $groupDefaultInfo['PushSupport'] != '1' || $groupUsersInfo['PushSupport'] != '1' )
{

    header("HTTP/1.1 403 Forbidden");
    //debugLog("Access denied. ActiveSync not enabled for this user (" . $auth_user . ").");
    //debugLog("Access denied. ActiveSync not enabled for this user (" . $auth_user . ").");
    //debugLog("end");
    //debugLog("--------");
    //die("Access denied. ActiveSync not enabled for this user (" . $auth_user . ").");
    return;
}



// Get the request headers so we can see the versions
$requestheaders = apache_request_headers();
if (isset($requestheaders["Ms-Asprotocolversion"])) $requestheaders["MS-ASProtocolVersion"] = $requestheaders["Ms-Asprotocolversion"];
if(isset($requestheaders["MS-ASProtocolVersion"]))
{
    $protocolversion = $requestheaders["MS-ASProtocolVersion"];
    //debugLog("Client supports version " . $protocolversion);
}
else if($protocolversion === false)
{
    $protocolversion = "1.0";
}

if (isset($requestheaders["X-Ms-Policykey"])) $requestheaders["X-MS-PolicyKey"] = $requestheaders["X-Ms-Policykey"];
if (isset($requestheaders["X-MS-PolicyKey"])) {
    $policykey = $requestheaders["X-MS-PolicyKey"];

} else {
    $policykey = 0;
}

//get user agent
if (isset($requestheaders["User-Agent"])) {
    $useragent = $requestheaders["User-Agent"];
} else {
    $useragent = "unknown";
}

// Load our backend driver
$backend_dir = opendir(BASE_PATH . "/backend");
while($entry = readdir($backend_dir)) {
    if(substr($entry,0,1) == "." || substr($entry,-3) != "php")
        continue;

    if( $entry == "maildir.php" )
        continue;

    if (!function_exists("mapi_logon") && ($entry == "ics.php"))
        continue;

    include_once(BASE_PATH . "/backend/" . $entry);
}

// Initialize our backend
//$backend = new $BACKEND_PROVIDER();
$backend = new $BACKEND_PROVIDER($BACKEND_CONFIG);

if( $authSuccess )
{

    //check if user licensed to use Push
    $dbAdapter = Zend_Registry::get('dbAdapter');
    $dbTables = new dbTables();
    $q = ' SELECT COUNT(DISTINCT(`Account`)) FROM ' . $dbTables->SerialConf;

    if( $dbAdapter->fetchOne($q) >= Zend_Registry::get('w4m70Yt2hYUduM2r0lrrIuY0zgKG4A3vvftl4NbbCWfW'))
    {

        //if user count >= licensed then check if already in, then allow to proceed, else fail no remaining sets.
        $q = "SELECT COUNT(`Account`) FROM SerialConf WHERE `Account` = " . $dbAdapter->quote($auth_user) . " LIMIT 1";
        $PushUserExists = $dbAdapter->fetchOne($q);

        if( $PushUserExists == '0' )
        {

            header("HTTP/1.1 403 Forbidden");
            //print("Access denied. All Push seats in license used. Purchase more Push License seats.");
            //debugLog("Access denied. All Push seats in license used. Purchase more Push License seats.");
            //debugLog("end");
            //debugLog("--------");
            return;

        }

    }

}

if($backend->Logon($auth_user, $auth_domain, $auth_pw) == false) {
    header("HTTP/1.1 401 Unauthorized");
    header("WWW-Authenticate: Basic realm=\"Atmail PUSH\"");
    //print("Access denied. Username or password incorrect.");
    //debugLog("Access denied: backend logon failed.");
    //debugLog("end");
    //debugLog("--------");
    return;
}

// $user is usually the same as the PHP_AUTH_USER. This allows you to sync the 'john' account if you
// have sufficient privileges as user 'joe'.
if($backend->Setup($user, $devid, $protocolversion) == false) {
    header("HTTP/1.1 401 Unauthorized");
    header("WWW-Authenticate: Basic realm=\"Atmail PUSH\"");
    //print("Access denied or user '$user' unknown.");
    //debugLog("Access denied: backend setup failed.");
    //debugLog("end");
    //debugLog("--------");
    return;
}

// check policy header
if (PROVISIONING === true && $_SERVER["REQUEST_METHOD"] != 'OPTIONS' && $cmd != 'Ping' && $cmd != 'Provision' &&
    $backend->CheckPolicy($policykey, $devid) != SYNC_PROVISION_STATUS_SUCCESS &&
    (LOOSE_PROVISIONING === false ||
        (LOOSE_PROVISIONING === true && isset($requestheaders["X-MS-PolicyKey"])))) {

    header("HTTP/1.1 449 Retry after sending a PROVISION command");
    header(activesyncHeaders::getServerHeader());
    header(activesyncHeaders::getProtocolVersionHeader());
    header(activesyncHeaders::getProtocolCommandsHeader());
    header("Cache-Control: private");
    //debugLog("POST cmd $cmd denied: Retry after sending a PROVISION command");
    //debugLog("end");
    //debugLog("--------");
    return;
}


// Log the hit
$logEntry = new logEntry($auth_user);
$logEntry->insert("Log_Login", array('LogType' => '7', 'LogIP' => $_SERVER["REMOTE_ADDR"], 'Account' => $auth_user ) );
if($devtype != '' || $devid != '')
{
    try
    {
        $logEntry->insert("Log_Device", array('DeviceType' => $devtype, 'DeviceID' => $devid, 'LogIP' => $_SERVER["REMOTE_ADDR"], 'Account' => $auth_user ), array('Account', 'DeviceID') );
    }
    catch(Exception $e)
    {
        // ignore, table might not exist during upgrade
    }
}

// Do the actual request
switch($_SERVER["REQUEST_METHOD"]) {
    case 'OPTIONS':
        header(activesyncHeaders::getServerHeader());
        header(activesyncHeaders::getProtocolVersionHeader());
        header(activesyncHeaders::getProtocolCommandsHeader());
        //debugLog("Options request");
        break;
    case 'POST':
        header(activesyncHeaders::getServerHeader());
        //debugLog("POST cmd: $cmd" );
        // Do the actual request
        if(!HandleRequest($backend, $cmd, $devid, $protocolversion)) {
            // Request failed. Try to output some kind of error information. We can only do this if
            // output had not started yet. If it has started already, we can't show the user the error, and
            // the device will give its own (useless) error message.
            if(!headers_sent()) {
               /* header("Content-type: text/html");
                print("<BODY>\n");
                print("<h3>Error</h3><p>\n");
                print("There was a problem processing the <i>$cmd</i> command from your PDA.\n");
                print("</pre>\n");
                print("</BODY>\n");*/

                header("HTTP/1.1 400 Bad Request");
                header("WWW-Authenticate: Basic realm=\"Atmail PUSH\"");
            }
        }
        break;
    case 'GET':
        header("HTTP/1.1 400 Bad Request");
        header("WWW-Authenticate: Basic realm=\"Atmail PUSH\"");
        break;
}


$len = ob_get_length();
$data = ob_get_contents();

ob_end_clean();

// Unfortunately, even though Atmail can stream the data to the client
// with a chunked encoding, using chunked encoding also breaks the progress bar
// on the PDA. So we de-chunk here and just output a content-length header and
// send it as a 'normal' packet. If the output packet exceeds 1MB (see ob_start)
// then it will be sent as a chunked packet anyway because PHP will have to flush
// the buffer.

header("Content-Length: $len");
print $data;

// destruct backend after all data is on the stream
$backend->Logoff();


//debugLog("\n\n=================== data response =====================\n" . "Content-Length: $len \n" . $data . "\n=================== data end =================\n\n");


//debugLog("end");
//debugLog("-------------------------------");
