<?php
/**
 * Horde_ActiveSync_Request_Provision::
 *
 * Portions of this class were ported from the Z-Push project:
 *   File      :   wbxml.php
 *   Project   :   Z-Push
 *   Descr     :   WBXML mapping file
 *
 *   Created   :   01.10.2007
 *
 *   � Zarafa Deutschland GmbH, www.zarafaserver.de
 *   This file is distributed under GPL-2.0.
 *   Consult COPYING file for details
 *
 * @license   http://www.horde.org/licenses/gpl GPLv2
 *
 * @copyright 2009-2020 Horde LLC (http://www.horde.org)
 * @author    Michael J Rubinsky <mrubinsk@horde.org>
 * @package   ActiveSync
 */
/**
 * Hanlde Provision requests.
 *
 * @license   http://www.horde.org/licenses/gpl GPLv2
 *
 * @copyright 2009-2020 Horde LLC (http://www.horde.org)
 * @author    Michael J Rubinsky <mrubinsk@horde.org>
 * @package   ActiveSync
 * @internal
 */
class Horde_ActiveSync_Request_Provision extends Horde_ActiveSync_Request_Base
{
    /* Status Constants */
    const STATUS_SUCCESS           = 1;
    const STATUS_PROTERROR         = 2; // Global status
    const STATUS_NOTDEFINED        = 2; // Policy status

    const STATUS_SERVERERROR       = 3; // Global
    const STATUS_POLICYUNKNOWN     = 3; // Policy

    const STATUS_DEVEXTMANAGED     = 4; // Global
    const STATUS_POLICYCORRUPT     = 4; // Policy

    const STATUS_POLKEYMISM        = 5;

    /* Client -> Server Status */
    const STATUS_CLIENT_SUCCESS    = 1;
    const STATUS_CLIENT_PARTIAL    = 2; // Only pin was enabled.
    const STATUS_CLIENT_FAILED     = 3; // No policies applied at all.
    const STATUS_CLIENT_THIRDPARTY = 4; // Client provisioned by 3rd party?

    /**
     * Handle the Provision request. This is a 3-phase process. Phase 1 is
     * actually the enforcement, when the server rejects a request and forces
     * the client to perform this PROVISION request...so we are handling phase
     * 2 (download policies) and 3 (acknowledge policies) here.
     *
     * @return boolean
     * @throws Horde_ActiveSync_Exception
     */
    protected function _handle()
    {
        // Be optimistic
        $status = self::STATUS_SUCCESS;
        $policyStatus = self::STATUS_SUCCESS;

        if ($error = $this->_activeSync->checkGlobalError()) {
            $this->_globalError($error);
            return true;
        }

        // Start by assuming we are in stage 2
        $phase2 = true;
        if (!$this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_PROVISION)) {
            return $this->_globalError(self::STATUS_PROTERROR);
        }

        // Handle remote wipe status response for Android devices.
        if ($this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_REMOTEWIPE)) {
            if (!$this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_STATUS)) {
                return $this->_globalError(self::STATUS_PROTERROR);
            }
            $status = $this->_decoder->getElementContent();
            if (!$this->_decoder->getElementEndTag() ||
                !$this->_decoder->getElementEndTag()) {
                return $this->_globalError(self::STATUS_PROTERROR);
            }
            if ($status == self::STATUS_CLIENT_SUCCESS) {
                $this->_state->setDeviceRWStatus($this->_devId, Horde_ActiveSync::RWSTATUS_WIPED);
            }
            $policytype = Horde_ActiveSync::POLICYTYPE_XML;
        } else {
            if ($deviceinfo = $this->_handleSettings()) {
                $deviceinfo['version'] = $this->_device->version;
                $this->_device->setDeviceProperties($deviceinfo);
                $this->_device->save();
            }

            if (!$this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_POLICIES) ||
                !$this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_POLICY)) {

                return $this->_globalError(self::STATUS_PROTERROR);
            }

            // iOS (at least 5.0.1) incorrectly sends a STATUS tag before the
            // REMOTEWIPE response.
            if (!$this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_POLICYTYPE)) {
                if ($this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_STATUS)) {
                    $this->_decoder->getElementContent();
                    $this->_decoder->getElementEndTag(); // status
                }
            } else {
                $policytype = $this->_decoder->getElementContent();
                if ($this->_device->version < Horde_ActiveSync::VERSION_TWELVE && $policytype != Horde_ActiveSync::POLICYTYPE_XML) {
                    $this->_logger->err('EAS version < 12.0 but policy type is not POLICYTYPE_XML');
                    $policyStatus = self::STATUS_POLICYUNKNOWN;
                }
                if ($this->_device->version >= Horde_ActiveSync::VERSION_TWELVE && $policytype != Horde_ActiveSync::POLICYTYPE_WBXML) {
                    $this->_logger->err('EAS version >= 12.0 but policy type is not POLICYTYPE_WBXML');
                    $policyStatus = self::STATUS_POLICYUNKNOWN;
                }
                if (!$this->_decoder->getElementEndTag()) {//policytype
                    return $this->_globalError(self::STATUS_PROTERROR);
                }
            }

            // POLICYKEY is only sent by client in phase 3
            if ($this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_POLICYKEY)) {
                $policykey = $this->_decoder->getElementContent();
                $this->_logger->meta(sprintf('PHASE 3 policykey sent from client: %s', $policykey));
                if (!$this->_decoder->getElementEndTag() ||
                    !$this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_STATUS)) {

                    return $this->_globalError(self::STATUS_PROTERROR);
                }
                if ($this->_decoder->getElementContent() != self::STATUS_SUCCESS) {
                    $this->_logger->err('Policy not accepted by device: ' . $this->_device->id);

                    if ($this->_provisioning == Horde_ActiveSync::PROVISIONING_LOOSE) {
                        // Loose provisioning, don't error out, just don't reqiure provision.
                        $this->_sendNoProvisionNeededResponse($status);
                        return true;
                    }

                    $policyStatus = self::STATUS_POLICYCORRUPT;
                }

                if (!$this->_decoder->getElementEndTag()) {
                    return $this->_globalError(self::STATUS_PROTERROR);
                }
                $phase2 = false;
            }

            if (!$this->_decoder->getElementEndTag() ||
                !$this->_decoder->getElementEndTag()) {

                return $this->_globalError(self::STATUS_PROTERROR);
            }

            // Handle remote wipe status for other devices
            if ($this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_REMOTEWIPE)) {
                if (!$this->_decoder->getElementStartTag(Horde_ActiveSync::PROVISION_STATUS)) {
                    return $this->_globalError(self::STATUS_PROTERROR);
                }
                $status = $this->_decoder->getElementContent();
                if (!$this->_decoder->getElementEndTag() ||
                    !$this->_decoder->getElementEndTag()) {
                    return $this->_globalError(self::STATUS_PROTERROR);
                }
                if ($status == self::STATUS_CLIENT_SUCCESS) {
                    $this->_state->setDeviceRWStatus($this->_device->id, Horde_ActiveSync::RWSTATUS_WIPED);
                }
            }
        }

        if (!$this->_decoder->getElementEndTag()) { //provision
            return $this->_globalError(self::STATUS_PROTERROR);
        }

        // Check to be sure that we *need* to PROVISION
        if ($this->_provisioning === false) {
            $this->_sendNoProvisionNeededResponse($status);
            return true;
        }

        // Start handling request and sending output
        $this->_encoder->StartWBXML();

        // End of Phase 3 - We create the "final" policy key, store it, then
        // send it to the client.
        if (!$phase2) {
            // Verify intermediate key
            $this->_logger->meta(sprintf(
                'Verifying Phase 3 policykey: From Device: %s, Stored: %s',
                $policykey, $this->_device->policykey)
            );
            if ($this->_state->getPolicyKey($this->_device->id) != $policykey) {
                $policyStatus = self::STATUS_POLKEYMISM;
            } else {
                // Set the final key
                $policykey = $this->_state->generatePolicyKey();
                $this->_state->setPolicyKey($this->_device->id, $policykey);
                $this->_state->setDeviceRWStatus($this->_device->id, Horde_ActiveSync::RWSTATUS_OK);
            }
            $this->_cleanUpAfterPairing();
        } elseif (empty($policykey)) {
            // This is phase2 - we need to set the intermediate key
            $policykey = $this->_state->generatePolicyKey();
            $this->_logger->meta(sprintf(
                'Generating PHASE2 policy key: %s', $policykey));
            $this->_state->setPolicyKey($this->_device->id, $policykey);
        }

        // If we are phase2 we need to check this here, before the status is
        // sent. Prevents devices not supporting the required policies from
        // being able to connect.
        if ($phase2 && $status == self::STATUS_SUCCESS &&
            $policyStatus == self::STATUS_SUCCESS &&
            $this->_provisioning == Horde_ActiveSync::PROVISIONING_FORCE) {

            $policyHandler = new Horde_ActiveSync_Policies(
                $this->_encoder,
                $this->_device->version,
                $this->_driver->getCurrentPolicy($deviceinfo)
            );

            if (!$policyHandler->validatePolicyVersion()) {
                $this->_handleVersionMismatch();
                return true;
            }
        }

        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_PROVISION);
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_STATUS);
        $this->_encoder->content($status);
        $this->_encoder->endTag();
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_POLICIES);
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_POLICY);
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_POLICYTYPE);
        $this->_encoder->content($policytype);
        $this->_encoder->endTag();
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_STATUS);
        $this->_encoder->content($policyStatus);
        $this->_encoder->endTag();
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_POLICYKEY);
        $this->_encoder->content($policykey);
        $this->_encoder->endTag();

        // Send security policies.
        if ($phase2 && $status == self::STATUS_SUCCESS && $policyStatus == self::STATUS_SUCCESS) {
            $this->_encoder->startTag(Horde_ActiveSync::PROVISION_DATA);
            if ($policytype == Horde_ActiveSync::POLICYTYPE_XML) {
                $policyHandler->toXml();
            } else {
                $policyHandler->toWbxml();
            }
            $this->_encoder->endTag(); //data
        }

        $this->_encoder->endTag();     //policy
        $this->_encoder->endTag();     //policies

        // Remote wipe if requested.
        $rwstatus = $this->_state->getDeviceRWStatus($this->_device->id);
        if ($rwstatus == Horde_ActiveSync::RWSTATUS_PENDING ||
            $rwstatus == Horde_ActiveSync::RWSTATUS_WIPED) {

            $this->_encoder->startTag(Horde_ActiveSync::PROVISION_REMOTEWIPE, false, true);
            $this->_state->setDeviceRWStatus($this->_device->id, Horde_ActiveSync::RWSTATUS_WIPED);
        }
        $this->_encoder->endTag();     //provision

        return true;
    }

    /**
     * Send a WBXML response to the output stream indicating that no
     * provision requests are necessary.
     *
     * @param integer $status  The status code to send along with the response.
     */
    protected function _sendNoProvisionNeededResponse($status)
    {
        $this->_encoder->startWBXML();
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_PROVISION);
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_STATUS);
        $this->_encoder->content($status);
        $this->_encoder->endTag();
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_POLICIES);
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_POLICY);
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_STATUS);
        $this->_encoder->content(self::STATUS_NOTDEFINED);
        $this->_encoder->endTag();
        $this->_encoder->endTag();
        $this->_encoder->endTag();
        $this->_encoder->endTag();
    }

    /**
     * Handle global provision request errors, and send the output to the
     * output stream.
     *
     * @param integer $status  The status code to send.
     */
    protected function _globalError($status)
    {
        $this->_encoder->StartWBXML();
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_PROVISION);
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_STATUS);
        $this->_encoder->content($status);
        $this->_encoder->endTag();
        $this->_encoder->endTag();

        return false;
    }

    /**
     * Handle the EAS 14.1 SETTINGS_DEVICEINFORMATION parsing.
     *
     * @return boolean|array  An array of received device information or false
     *                        on any protocol error.
     */
    protected function _handleSettings()
    {
        // EAS 14.1 REQUIRES SETTINGS_DEVICEINFORMATION in the PROVISION command.
        if (!$this->_decoder->getElementStartTag(Horde_ActiveSync_Request_Settings::SETTINGS_DEVICEINFORMATION)) {
            return false;
        }
        if (!$this->_decoder->getElementStartTag(Horde_ActiveSync_Request_Settings::SETTINGS_SET)) {
            return false;
        }
        $di = array();
        $settings = Horde_ActiveSync::messageFactory('DeviceInformation');
        $settings->decodeStream($this->_decoder);
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_MODEL] = $settings->model;
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_IMEI] = $settings->imei;
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_FRIENDLYNAME] = $settings->friendlyname;
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_OS] = $settings->os;
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_OSLANGUAGE] = $settings->oslanguage;
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_PHONENUMBER] = $settings->phonenumber;
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_USERAGENT] = $settings->useragent;
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_MOBILEOPERATOR] = $settings->mobileoperator;
        $di[Horde_ActiveSync_Request_Settings::SETTINGS_ENABLEOUTBOUNDSMS] = $settings->enableoutboundsms;

        $this->_decoder->getElementEndTag();
        $this->_decoder->getElementEndTag();

        return $di;
    }

    /**
     * Output status that indicates device does not support the required
     * policies.
     *
     */
    protected function _handleVersionMismatch()
    {
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_PROVISION);
        $this->_encoder->startTag(Horde_ActiveSync::PROVISION_STATUS);
        $this->_encoder->content(Horde_ActiveSync_Status::DEVICE_NOT_FULLY_PROVISIONABLE);
        $this->_encoder->endTag();
        $this->_encoder->endTag();
    }

}
