Files
server/usr/share/psa-pear/pear/php/Horde/ActiveSync.php
2026-01-07 20:52:11 +01:00

1276 lines
46 KiB
PHP

<?php
/**
* @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
*/
/**
* The Horde ActiveSync server. Entry point for performing all ActiveSync
* operations.
*
* @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
*
* @property-read Horde_ActiveSync_Wbxml_Encoder $encoder The Wbxml encoder.
* @property-read Horde_ActiveSync_Wbxml_Decoder $decoder The Wbxml decoder.
* @property-read Horde_ActiveSync_State_Base $state The state object.
* @property-read Horde_Controller_Reqeust_Http $request The HTTP request object.
* @property-read Horde_ActiveSync_Driver_Base $driver The backend driver object.
* @property-read boolean|string $provisioning Provisioning support: True, False, or 'loose'
* @property-read boolean $multipart Indicate this is a multipart request.
* @property-read string $certPath Local path to the certificate bundle.
* @property-read Horde_ActiveSync_Device $device The current device object.
* @property-read Horde_ActiveSync_Log_Logger $logger The logger object.
*/
class Horde_ActiveSync
{
/* Conflict resolution */
const CONFLICT_OVERWRITE_SERVER = 0;
const CONFLICT_OVERWRITE_PIM = 1;
/* TRUNCATION Constants */
const TRUNCATION_ALL = 0;
const TRUNCATION_1 = 1;
const TRUNCATION_2 = 2;
const TRUNCATION_3 = 3;
const TRUNCATION_4 = 4;
const TRUNCATION_5 = 5;
const TRUNCATION_6 = 6;
const TRUNCATION_7 = 7;
const TRUNCATION_8 = 8;
const TRUNCATION_9 = 9;
const TRUNCATION_NONE = 9; // @deprecated
/* FOLDERHIERARCHY */
const FOLDERHIERARCHY_FOLDERS = 'FolderHierarchy:Folders';
const FOLDERHIERARCHY_FOLDER = 'FolderHierarchy:Folder';
const FOLDERHIERARCHY_DISPLAYNAME = 'FolderHierarchy:DisplayName';
const FOLDERHIERARCHY_SERVERENTRYID = 'FolderHierarchy:ServerEntryId';
const FOLDERHIERARCHY_PARENTID = 'FolderHierarchy:ParentId';
const FOLDERHIERARCHY_TYPE = 'FolderHierarchy:Type';
const FOLDERHIERARCHY_RESPONSE = 'FolderHierarchy:Response';
const FOLDERHIERARCHY_STATUS = 'FolderHierarchy:Status';
const FOLDERHIERARCHY_CONTENTCLASS = 'FolderHierarchy:ContentClass';
const FOLDERHIERARCHY_CHANGES = 'FolderHierarchy:Changes';
const FOLDERHIERARCHY_SYNCKEY = 'FolderHierarchy:SyncKey';
const FOLDERHIERARCHY_FOLDERSYNC = 'FolderHierarchy:FolderSync';
const FOLDERHIERARCHY_COUNT = 'FolderHierarchy:Count';
const FOLDERHIERARCHY_VERSION = 'FolderHierarchy:Version';
/* SYNC */
const SYNC_SYNCHRONIZE = 'Synchronize';
const SYNC_REPLIES = 'Replies';
const SYNC_ADD = 'Add';
const SYNC_MODIFY = 'Modify';
const SYNC_REMOVE = 'Remove';
const SYNC_FETCH = 'Fetch';
const SYNC_SYNCKEY = 'SyncKey';
const SYNC_CLIENTENTRYID = 'ClientEntryId';
const SYNC_SERVERENTRYID = 'ServerEntryId';
const SYNC_STATUS = 'Status';
const SYNC_FOLDER = 'Folder';
const SYNC_FOLDERTYPE = 'FolderType';
const SYNC_VERSION = 'Version';
const SYNC_FOLDERID = 'FolderId';
const SYNC_GETCHANGES = 'GetChanges';
const SYNC_MOREAVAILABLE = 'MoreAvailable';
const SYNC_WINDOWSIZE = 'WindowSize';
const SYNC_COMMANDS = 'Commands';
const SYNC_OPTIONS = 'Options';
const SYNC_FILTERTYPE = 'FilterType';
const SYNC_TRUNCATION = 'Truncation';
const SYNC_RTFTRUNCATION = 'RtfTruncation';
const SYNC_CONFLICT = 'Conflict';
const SYNC_FOLDERS = 'Folders';
const SYNC_DATA = 'Data';
const SYNC_DELETESASMOVES = 'DeletesAsMoves';
const SYNC_NOTIFYGUID = 'NotifyGUID';
const SYNC_SUPPORTED = 'Supported';
const SYNC_SOFTDELETE = 'SoftDelete';
const SYNC_MIMESUPPORT = 'MIMESupport';
const SYNC_MIMETRUNCATION = 'MIMETruncation';
const SYNC_NEWMESSAGE = 'NewMessage';
const SYNC_PARTIAL = 'Partial';
const SYNC_WAIT = 'Wait';
const SYNC_LIMIT = 'Limit';
// 14
const SYNC_HEARTBEATINTERVAL = 'HeartbeatInterval';
const SYNC_CONVERSATIONMODE = 'ConversationMode';
const SYNC_MAXITEMS = 'MaxItems';
/* Document library */
const SYNC_DOCUMENTLIBRARY_LINKID = 'DocumentLibrary:LinkId';
const SYNC_DOCUMENTLIBRARY_DISPLAYNAME = 'DocumentLibrary:DisplayName';
const SYNC_DOCUMENTLIBRARY_ISFOLDER = 'DocumentLibrary:IsFolder';
const SYNC_DOCUMENTLIBRARY_CREATIONDATE = 'DocumentLibrary:CreationDate';
const SYNC_DOCUMENTLIBRARY_LASTMODIFIEDDATE = 'DocumentLibrary:LastModifiedDate';
const SYNC_DOCUMENTLIBRARY_ISHIDDEN = 'DocumentLibrary:IsHidden';
const SYNC_DOCUMENTLIBRARY_CONTENTLENGTH = 'DocumentLibrary:ContentLength';
const SYNC_DOCUMENTLIBRARY_CONTENTTYPE = 'DocumentLibrary:ContentType';
/* AIRSYNCBASE */
const AIRSYNCBASE_BODYPREFERENCE = 'AirSyncBase:BodyPreference';
const AIRSYNCBASE_TYPE = 'AirSyncBase:Type';
const AIRSYNCBASE_TRUNCATIONSIZE = 'AirSyncBase:TruncationSize';
const AIRSYNCBASE_ALLORNONE = 'AirSyncBase:AllOrNone';
const AIRSYNCBASE_BODY = 'AirSyncBase:Body';
const AIRSYNCBASE_DATA = 'AirSyncBase:Data';
const AIRSYNCBASE_ESTIMATEDDATASIZE = 'AirSyncBase:EstimatedDataSize';
const AIRSYNCBASE_TRUNCATED = 'AirSyncBase:Truncated';
const AIRSYNCBASE_ATTACHMENTS = 'AirSyncBase:Attachments';
const AIRSYNCBASE_ATTACHMENT = 'AirSyncBase:Attachment';
const AIRSYNCBASE_DISPLAYNAME = 'AirSyncBase:DisplayName';
const AIRSYNCBASE_FILEREFERENCE = 'AirSyncBase:FileReference';
const AIRSYNCBASE_METHOD = 'AirSyncBase:Method';
const AIRSYNCBASE_CONTENTID = 'AirSyncBase:ContentId';
const AIRSYNCBASE_CONTENTLOCATION = 'AirSyncBase:ContentLocation';
const AIRSYNCBASE_ISINLINE = 'AirSyncBase:IsInline';
const AIRSYNCBASE_NATIVEBODYTYPE = 'AirSyncBase:NativeBodyType';
const AIRSYNCBASE_CONTENTTYPE = 'AirSyncBase:ContentType';
const AIRSYNCBASE_LOCATION = 'AirSyncBase:Location';
// 14.0
const AIRSYNCBASE_PREVIEW = 'AirSyncBase:Preview';
// 14.1
const AIRSYNCBASE_BODYPARTPREFERENCE = 'AirSyncBase:BodyPartPreference';
const AIRSYNCBASE_BODYPART = 'AirSyncBase:BodyPart';
const AIRSYNCBASE_STATUS = 'AirSyncBase:Status';
// 16.0
const AIRSYNCBASE_ADD = 'AirSyncBase:Add';
const AIRSYNCBASE_DELETE = 'AirSyncBase:Delete';
const AIRSYNCBASE_CLIENTID = 'AirSyncBase:ClientId';
const AIRSYNCBASE_CONTENT = 'AirSyncBase:Content';
const AIRSYNCBASE_ANNOTATION = 'AirSyncBase:Annotation';
const AIRSYNCBASE_STREET = 'AirSyncBase:Street';
const AIRSYNCBASE_CITY = 'AirSyncBase:City';
const AIRSYNCBASE_STATE = 'AirSyncBase:State';
const AIRSYNCBASE_COUNTRY = 'AirSyncBase:Country';
const AIRSYNCBASE_POSTALCODE = 'AirSyncBase:PostalCode';
const AIRSYNCBASE_LATITUDE = 'AirSyncBase:Latitude';
const AIRSYNCBASE_LONGITUDE = 'AirSyncBase:Longitude';
const AIRSYNCBASE_ACCURACY = 'AirSyncBase:Accuracy';
const AIRSYNCBASE_ALTITUDE = 'AirSyncBase:Altitude';
const AIRSYNCBASE_ALTITUDEACCURACY = 'AirSyncBase:AltitudeAccuracy';
const AIRSYNCBASE_LOCATIONURI = 'AirSyncBase:LocationUri';
const AIRSYNCBASE_INSTANCEID = 'AirSyncBase:InstanceId';
/* Body type prefs */
const BODYPREF_TYPE_PLAIN = 1;
const BODYPREF_TYPE_HTML = 2;
const BODYPREF_TYPE_RTF = 3;
const BODYPREF_TYPE_MIME = 4;
/* PROVISION */
const PROVISION_PROVISION = 'Provision:Provision';
const PROVISION_POLICIES = 'Provision:Policies';
const PROVISION_POLICY = 'Provision:Policy';
const PROVISION_POLICYTYPE = 'Provision:PolicyType';
const PROVISION_POLICYKEY = 'Provision:PolicyKey';
const PROVISION_DATA = 'Provision:Data';
const PROVISION_STATUS = 'Provision:Status';
const PROVISION_REMOTEWIPE = 'Provision:RemoteWipe';
const PROVISION_EASPROVISIONDOC = 'Provision:EASProvisionDoc';
/* Policy types */
const POLICYTYPE_XML = 'MS-WAP-Provisioning-XML';
const POLICYTYPE_WBXML = 'MS-EAS-Provisioning-WBXML';
/* Flags */
// @TODO: H6 Change this to CHANGE_TYPE_NEW
const FLAG_NEWMESSAGE = 'NewMessage';
/* Folder types */
const FOLDER_TYPE_OTHER = 1;
const FOLDER_TYPE_INBOX = 2;
const FOLDER_TYPE_DRAFTS = 3;
const FOLDER_TYPE_WASTEBASKET = 4;
const FOLDER_TYPE_SENTMAIL = 5;
const FOLDER_TYPE_OUTBOX = 6;
const FOLDER_TYPE_TASK = 7;
const FOLDER_TYPE_APPOINTMENT = 8;
const FOLDER_TYPE_CONTACT = 9;
const FOLDER_TYPE_NOTE = 10;
const FOLDER_TYPE_JOURNAL = 11;
const FOLDER_TYPE_USER_MAIL = 12;
const FOLDER_TYPE_USER_APPOINTMENT = 13;
const FOLDER_TYPE_USER_CONTACT = 14;
const FOLDER_TYPE_USER_TASK = 15;
const FOLDER_TYPE_USER_JOURNAL = 16;
const FOLDER_TYPE_USER_NOTE = 17;
const FOLDER_TYPE_UNKNOWN = 18;
const FOLDER_TYPE_RECIPIENT_CACHE = 19;
// @TODO, remove const definition in H6, not used anymore.
const FOLDER_TYPE_DUMMY = 999999;
/* Origin of changes **/
const CHANGE_ORIGIN_PIM = 0;
const CHANGE_ORIGIN_SERVER = 1;
const CHANGE_ORIGIN_NA = 3;
/* Remote wipe **/
const RWSTATUS_NA = 0;
const RWSTATUS_OK = 1;
const RWSTATUS_PENDING = 2;
const RWSTATUS_WIPED = 3;
/* GAL **/
const GAL_DISPLAYNAME = 'GAL:DisplayName';
const GAL_PHONE = 'GAL:Phone';
const GAL_OFFICE = 'GAL:Office';
const GAL_TITLE = 'GAL:Title';
const GAL_COMPANY = 'GAL:Company';
const GAL_ALIAS = 'GAL:Alias';
const GAL_FIRSTNAME = 'GAL:FirstName';
const GAL_LASTNAME = 'GAL:LastName';
const GAL_HOMEPHONE = 'GAL:HomePhone';
const GAL_MOBILEPHONE = 'GAL:MobilePhone';
const GAL_EMAILADDRESS = 'GAL:EmailAddress';
// 14.1
const GAL_PICTURE = 'GAL:Picture';
const GAL_STATUS = 'GAL:Status';
const GAL_DATA = 'GAL:Data';
/* Request Type */
const REQUEST_TYPE_SYNC = 'sync';
const REQUEST_TYPE_FOLDERSYNC = 'foldersync';
/* Change Type */
const CHANGE_TYPE_CHANGE = 'change';
const CHANGE_TYPE_DELETE = 'delete';
const CHANGE_TYPE_FLAGS = 'flags';
const CHANGE_TYPE_MOVE = 'move';
const CHANGE_TYPE_FOLDERSYNC = 'foldersync';
const CHANGE_TYPE_SOFTDELETE = 'softdelete';
// @since 2.36.0
const CHANGE_TYPE_DRAFT = 'draft';
/* Internal flags to indicate change is a change in reply/forward state */
const CHANGE_REPLY_STATE = '@--reply--@';
const CHANGE_REPLYALL_STATE = '@--replyall--@';
const CHANGE_FORWARD_STATE = '@--forward--@';
/* RM */
const RM_SUPPORT = 'RightsManagement:RightsManagementSupport';
const RM_TEMPLATEID = 'RightsManagement:TemplateId';
/* Collection Classes */
const CLASS_EMAIL = 'Email';
const CLASS_CONTACTS = 'Contacts';
const CLASS_CALENDAR = 'Calendar';
const CLASS_TASKS = 'Tasks';
const CLASS_NOTES = 'Notes';
const CLASS_SMS = 'SMS';
/* Filtertype constants */
const FILTERTYPE_ALL = 0;
const FILTERTYPE_1DAY = 1;
const FILTERTYPE_3DAYS = 2;
const FILTERTYPE_1WEEK = 3;
const FILTERTYPE_2WEEKS = 4;
const FILTERTYPE_1MONTH = 5;
const FILTERTYPE_3MONTHS = 6;
const FILTERTYPE_6MONTHS = 7;
const FILTERTYPE_INCOMPLETETASKS = 8;
// @todo normalize to string values.
const PROVISIONING_FORCE = true;
const PROVISIONING_LOOSE = 'loose';
const PROVISIONING_NONE = false;
const FOLDER_ROOT = 0;
const VERSION_TWOFIVE = '2.5';
const VERSION_TWELVE = '12.0';
const VERSION_TWELVEONE = '12.1';
const VERSION_FOURTEEN = '14.0';
const VERSION_FOURTEENONE = '14.1';
const VERSION_SIXTEEN = '16.0';
const MIME_SUPPORT_NONE = 0;
const MIME_SUPPORT_SMIME = 1;
const MIME_SUPPORT_ALL = 2;
const IMAP_FLAG_REPLY = 'reply';
const IMAP_FLAG_FORWARD = 'forward';
/* Result Type */
const RESOLVE_RESULT_GAL = 1;
const RESOLVE_RESULT_ADDRESSBOOK = 2;
/* Auth failure reasons */
const AUTH_REASON_USER_DENIED = 'user';
const AUTH_REASON_DEVICE_DENIED = 'device';
/* Internal flag indicates all possible fields are ghosted */
const ALL_GHOSTED = 'allghosted';
const LIBRARY_VERSION = '2.41.5';
/**
* Logger
*
* @var Horde_ActiveSync_Interface_LoggerFactory
*/
protected $_loggerFactory;
/**
* The logger for this class.
*
* @var Horde_Log_Logger
*/
protected static $_logger;
/**
* Provisioning support
*
* @var string
*/
protected $_provisioning;
/**
* Highest version to support.
*
* @var float
*/
protected $_maxVersion = self::VERSION_SIXTEEN;
/**
* The actual version we are supporting.
*
* @var float
*/
protected static $_version;
/**
* Multipart support?
*
* @var boolean
*/
protected $_multipart = false;
/**
* Support gzip compression of certain data parts?
*
* @var boolean
*/
protected $_compression = false;
/**
* Local cache of Get variables/decoded base64 uri
*
* @var array
*/
protected $_get = array();
/**
* Path to root certificate bundle
*
* @var string
*/
protected $_certPath;
/**
*
* @var Horde_ActiveSync_Device
*/
protected static $_device;
/**
* Wbxml encoder
*
* @var Horde_ActiveSync_Wbxml_Encoder
*/
protected $_encoder;
/**
* Wbxml decoder
*
* @var Horde_ActiveSync_Wbxml_Decoder
*/
protected $_decoder;
/**
* The singleton collections handler.
*
* @var Horde_ActiveSync_Collections
*/
protected $_collectionsObj;
/**
* Global error flag.
*
* @var boolean
*/
protected $_globalError = false;
/**
* Process id (used in logging).
*
* @var integer
*/
protected $_procid;
/**
* Flag to indicate we need to update the device version.
*
* @var boolean
*/
protected $_needMsRp = false;
/**
* Supported EAS versions.
*
* @var array
*/
protected static $_supportedVersions = array(
self::VERSION_TWOFIVE,
self::VERSION_TWELVE,
self::VERSION_TWELVEONE,
self::VERSION_FOURTEEN,
self::VERSION_FOURTEENONE,
self::VERSION_SIXTEEN
);
/**
* Factory method for creating Horde_ActiveSync_Message objects.
*
* @param string $message The message type.
* @since 2.4.0
*
* @return Horde_ActiveSync_Message_Base The concrete message object.
* @todo For H6, move to Horde_ActiveSync_Message_Base::factory()
*/
public static function messageFactory($message)
{
$class = 'Horde_ActiveSync_Message_' . $message;
if (!class_exists($class)) {
throw new InvalidArgumentException(sprintf('Class %s does not exist.', $class));
}
return new $class(array(
'logger' => self::$_logger,
'protocolversion' => self::$_version,
'device' => self::$_device));
}
/**
* Const'r
*
* @param Horde_ActiveSync_Driver_Base $driver The backend driver.
* @param Horde_ActiveSync_Wbxml_Decoder $decoder The Wbxml decoder.
* @param Horde_ActiveSync_Wbxml_Endcoder $encoder The Wbxml encoder.
* @param Horde_ActiveSync_State_Base $state The state driver.
* @param Horde_Controller_Request_Http $request The HTTP request object.
*
* @return Horde_ActiveSync The ActiveSync server object.
*/
public function __construct(
Horde_ActiveSync_Driver_Base $driver,
Horde_ActiveSync_Wbxml_Decoder $decoder,
Horde_ActiveSync_Wbxml_Encoder $encoder,
Horde_ActiveSync_State_Base $state,
Horde_Controller_Request_Http $request)
{
// The http request
$this->_request = $request;
// Backend driver
$this->_driver = $driver;
$this->_driver->setProtocolVersion($this->getProtocolVersion());
// Device state manager
$this->_state = $state;
// Wbxml handlers
$this->_encoder = $encoder;
$this->_decoder = $decoder;
$this->_procid = getmypid();
}
/**
* Return a collections singleton.
*
* @return Horde_ActiveSync_Collections
* @since 2.4.0
*/
public function getCollectionsObject()
{
if (empty($this->_collectionsObj)) {
$this->_collectionsObj = new Horde_ActiveSync_Collections($this->getSyncCache(), $this);
}
return $this->_collectionsObj;
}
/**
* Return a new, fully configured SyncCache.
*
* @return Horde_ActiveSync_SyncCache
* @since 2.4.0
*/
public function getSyncCache()
{
return new Horde_ActiveSync_SyncCache(
$this->_state,
self::$_device->id,
self::$_device->user,
self::$_logger
);
}
/**
* Return an Importer object.
*
* @return Horde_ActiveSync_Connector_Importer
* @since 2.4.0
*/
public function getImporter()
{
$importer = new Horde_ActiveSync_Connector_Importer($this);
$importer->setLogger(self::$_logger);
return $importer;
}
/**
* Authenticate to the backend.
*
* @param Horde_ActiveSync_Credentials $credentials The credentials object.
*
* @return boolean True on successful authentication to the backend.
* @throws Horde_ActiveSync_Exception
*/
public function authenticate(Horde_ActiveSync_Credentials $credentials)
{
if (!$credentials->username) {
// No provided username or Authorization header.
self::$_logger->notice('Client did not provide authentication data.');
return false;
}
$user = $this->_driver->getUsernameFromEmail($credentials->username);
$pos = strrpos($user, '\\');
if ($pos !== false) {
$domain = substr($user, 0, $pos);
$user = substr($user, $pos + 1);
} else {
$domain = null;
}
// Authenticate
if ($result = $this->_driver->authenticate($user, $credentials->password, $domain)) {
if ($result === self::AUTH_REASON_USER_DENIED) {
$this->_globalError = Horde_ActiveSync_Status::SYNC_NOT_ALLOWED;
} elseif ($result === self::AUTH_REASON_DEVICE_DENIED) {
$this->_globalError = Horde_ActiveSync_Status::DEVICE_BLOCKED_FOR_USER;
} elseif ($result !== true) {
$this->_globalError = Horde_ActiveSync_Status::DENIED;
}
} else {
return false;
}
if (!$this->_driver->setup($user)) {
return false;
}
return true;
}
/**
* Allow to force the highest version to support.
*
* @param float $version The highest version
*/
public function setSupportedVersion($version)
{
$this->_maxVersion = $version;
}
/**
* Set the local path to the root certificate bundle.
*
* @param string $path The local path to the bundle.
*/
public function setRootCertificatePath($path)
{
$this->_certPath = $path;
}
/**
* Getter
*
* @param string $property The property to return.
*
* @return mixed The value of the requested property.
*/
public function __get($property)
{
switch ($property) {
case 'encoder':
case 'decoder':
case 'state':
case 'request':
case 'driver':
case 'provisioning':
case 'multipart':
case 'certPath':
$property = '_' . $property;
return $this->$property;
case 'logger':
return self::$_logger;
case 'device':
return self::$_device;
default:
throw new InvalidArgumentException(sprintf(
'The property %s does not exist',
$property)
);
}
}
/**
* Setter for the logger factory.
*
* @param Horde_ActiveSync_Interface_LoggerFactory $logger The logger factory.
*/
public function setLogger(Horde_ActiveSync_Interface_LoggerFactory $logger)
{
$this->_loggerFactory = $logger;
}
/**
* Instantiate the logger from the factory and inject into all needed
* objects.
*
* @param array $options [description]
*/
protected function _setLogger(array $options)
{
if (!empty($this->_loggerFactory)) {
// @TODO. Remove wrapper.
self::$_logger = self::_wrapLogger($this->_loggerFactory->create($options));
$this->_encoder->setLogger(self::$_logger);
$this->_decoder->setLogger(self::$_logger);
$this->_driver->setLogger(self::$_logger);
$this->_state->setLogger(self::$_logger);
}
}
public static function _wrapLogger(Horde_Log_Logger $logger)
{
if (!($logger instanceof Horde_ActiveSync_Log_Logger)) {
return new Horde_ActiveSync_Log_Logger_Deprecated(null, $logger);
}
return $logger;
}
/**
* Setter for provisioning support
*
*/
public function setProvisioning($provision)
{
$this->_provisioning = $provision;
}
/**
* Send the headers indicating that provisioning is required.
*/
public function provisioningRequired()
{
$this->provisionHeader();
$this->activeSyncHeader();
$this->versionHeader();
$this->commandsHeader();
header('Cache-Control: private');
}
/**
* The heart of the server. Dispatch a request to the appropriate request
* handler.
*
* @param string $cmd The command we are requesting.
* @param string $devId The device id making the request. @deprecated
*
* @return string|boolean false if failed, true if succeeded and response
* content is wbxml, otherwise the
* content-type string to send in the response.
* @throws Horde_ActiveSync_Exception
* @throws Horde_ActiveSync_Exception_InvalidRequest
* @throws Horde_ActiveSync_PermissionDenied
*/
public function handleRequest($cmd, $devId)
{
$get = $this->getGetVars();
if (empty($cmd)) {
$cmd = $get['Cmd'];
}
if (empty($devId)) {
$devId = !empty($get['DeviceId']) ? Horde_String::upper($get['DeviceId']) : null;
} else {
$devId = Horde_String::upper($devId);
}
$this->_setLogger($get);
// @TODO: Remove is_callable check for H6.
// Callback to give the backend the option to limit EAS version based
// on user/device/etc...
if (is_callable(array($this->_driver, 'versionCallback'))) {
$this->_driver->versionCallback($this);
}
// Autodiscovery handles authentication on it's own.
if ($cmd == 'Autodiscover') {
$request = new Horde_ActiveSync_Request_Autodiscover($this, new Horde_ActiveSync_Device($this->_state));
if (!empty(self::$_logger)) {
$request->setLogger(self::$_logger);
}
$result = $request->handle($this->_request);
$this->_driver->clearAuthentication();
return $result;
}
if (!$this->authenticate(new Horde_ActiveSync_Credentials($this))) {
$this->activeSyncHeader();
$this->versionHeader();
$this->commandsHeader();
throw new Horde_Exception_AuthenticationFailure();
}
self::$_logger->info(sprintf(
'%s%s request received for user %s',
str_repeat('-', 10),
Horde_String::upper($cmd),
$this->_driver->getUser())
);
// These are all handled in the same class.
if ($cmd == 'FolderDelete' || $cmd == 'FolderUpdate') {
$cmd = 'FolderCreate';
}
// Device id is REQUIRED
if (empty($devId)) {
if ($cmd == 'Options') {
$this->_handleOptionsRequest();
$this->_driver->clearAuthentication();
return true;
}
$this->_driver->clearAuthentication();
throw new Horde_ActiveSync_Exception_InvalidRequest('Device failed to send device id.');
}
// EAS Version
$version = $this->getProtocolVersion();
// Device. Even though versions of EAS > 12.1 are supposed to send
// EAS status codes back to indicate various errors in allowing a client
// to connect, we just throw an exception (thus causing a HTTP error
// code to be sent as in versions 12.1 and below). Until we refactor for
// Horde 6, we don't know the response type to wrap the status code in
// until we load the request handler, which requires we start to parse
// the WBXML stream and device information etc... This saves resources
// as well as keeps things cleaner until we refactor.
$device_result = $this->_handleDevice($devId);
// Don't bother with everything else if all we want are Options
if ($cmd == 'Options') {
$this->_handleOptionsRequest();
$this->_driver->clearAuthentication();
return true;
}
// Set provisioning support now that we are authenticated.
$this->setProvisioning($this->_driver->getProvisioning(self::$_device));
// Read the initial Wbxml header
$this->_decoder->readWbxmlHeader();
// Support Multipart response for ITEMOPERATIONS requests?
$headers = $this->_request->getHeaders();
if ((!empty($headers['ms-asacceptmultipart']) && $headers['ms-asacceptmultipart'] == 'T') ||
!empty($get['AcceptMultiPart'])) {
$this->_multipart = true;
self::$_logger->info('Requesting multipart data.');
}
// Load the request handler to handle the request
// We must send the EAS header here, since some requests may start
// output and be large enough to flush the buffer (e.g., GetAttachment)
// See Bug: 12486
$this->activeSyncHeader();
if ($cmd != 'GetAttachment') {
$this->contentTypeHeader();
}
// Should we announce a new version is available to the client?
if (!empty($this->_needMsRp)) {
self::$_logger->info('Announcing X-MS-RP to client.');
header("X-MS-RP: ". $this->getSupportedVersions());
}
// @TODO: Look at getting rid of having to set the version in the driver
// and get it from the device object for H6.
$this->_driver->setDevice(self::$_device);
$class = 'Horde_ActiveSync_Request_' . basename($cmd);
if (class_exists($class)) {
$request = new $class($this);
$request->setLogger(self::$_logger);
$result = $request->handle();
self::$_logger->info(sprintf(
'Maximum memory usage for ActiveSync request: %d bytes.',
memory_get_peak_usage(true))
);
return $result;
}
$this->_driver->clearAuthentication();
throw new Horde_ActiveSync_Exception_InvalidRequest(basename($cmd) . ' not supported.');
}
/**
* Handle device checks. Takes into account permissions and restrictions
* via various callback methods.
*
* @param string $devId The client provided device id.
*
* @return boolean If EAS version is > 12.1 returns false on any type of
* failure in allowing the device to connect. Sets
* appropriate internal variables to indicate the type of
* error to return to the client. Failure on EAS version
* < 12.1 results in throwing exceptions. Otherwise, return
* true.
* @throws Horde_ActiveSync_Exception, Horde_Exception_AuthenticationFailure
*/
protected function _handleDevice($devId)
{
$get = $this->getGetVars();
$version = $this->getProtocolVersion();
// Does device exist AND does the user have an account on the device?
if (!$this->_state->deviceExists($devId, $this->_driver->getUser())) {
// Device might exist, but with a new (additional) user account
if ($this->_state->deviceExists($devId)) {
self::$_device = $this->_state->loadDeviceInfo($devId);
} else {
self::$_device = new Horde_ActiveSync_Device($this->_state);
}
self::$_device->policykey = 0;
self::$_device->userAgent = $this->_request->getHeader('User-Agent');
self::$_device->deviceType = !empty($get['DeviceType']) ? $get['DeviceType'] : '';
self::$_device->rwstatus = self::RWSTATUS_NA;
self::$_device->user = $this->_driver->getUser();
self::$_device->id = $devId;
self::$_device->needsVersionUpdate($this->getSupportedVersions());
self::$_device->version = $version;
// @TODO: Remove is_callable check for H6.
// Combine this with the modifyDevice callback? Allow $device
// to be modified here?
if (is_callable(array($this->_driver, 'createDeviceCallback'))) {
$callback_ret = $this->_driver->createDeviceCallback(self::$_device);
if ($callback_ret !== true) {
$msg = sprintf(
'The device %s was disallowed for user %s per policy settings.',
self::$_device->id,
self::$_device->user);
self::$_logger->err($msg);
// Always throw exception in place of status code since we
// won't have a version number before the device is created.
throw new Horde_Exception_AuthenticationFailure($msg, $callback_ret);
} else {
// Give the driver a chance to modify device properties.
if (is_callable(array($this->_driver, 'modifyDeviceCallback'))) {
self::$_device = $this->_driver->modifyDeviceCallback(self::$_device);
}
}
}
} else {
self::$_device = $this->_state->loadDeviceInfo($devId, $this->_driver->getUser());
// If the device state was removed from storage, we may lose the
// device properties, so try to repopulate what we can. userAgent
// is ALWAYS available, so if it's missing, the state is gone.
if (empty(self::$_device->userAgent)) {
self::$_device->userAgent = $this->_request->getHeader('User-Agent');
self::$_device->deviceType = !empty($get['DeviceType']) ? $get['DeviceType'] : '';
self::$_device->user = $this->_driver->getUser();
}
if (empty(self::$_device->version)) {
self::$_device->version = $version;
}
if (self::$_device->version < $this->_maxVersion &&
self::$_device->needsVersionUpdate($this->getSupportedVersions())) {
$this->_needMsRp = true;
}
// Give the driver a chance to modify device properties.
if (is_callable(array($this->_driver, 'modifyDeviceCallback'))) {
self::$_device = $this->_driver->modifyDeviceCallback(self::$_device);
}
}
// Save the device now that we know it is at least allowed to connect,
// or it has connected successfully at least once in the past.
self::$_device->save();
if (is_callable(array($this->_driver, 'deviceCallback'))) {
$callback_ret = $this->_driver->deviceCallback(self::$_device);
if ($callback_ret !== true) {
$msg = sprintf(
'The device %s was disallowed for user %s per policy settings.',
self::$_device->id,
self::$_device->user);
self::$_logger->err($msg);
if ($version > self::VERSION_TWELVEONE) {
// Use a status code here, since the device has already
// connected.
$this->_globalError = $callback_ret;
return false;
} else {
throw new Horde_Exception_AuthenticationFailure($msg, $callback_ret);
}
}
}
// Lastly, check if the device has been set to blocked.
if (self::$_device->blocked) {
$msg = sprintf(
'The device %s was blocked.',
self::$_device->id);
self::$_logger->err($msg);
if ($version > self::VERSION_TWELVEONE) {
$this->_globalError = Horde_ActiveSync_Status::DEVICE_BLOCKED_FOR_USER;
return false;
} else {
throw new Horde_ActiveSync_Exception($msg);
}
}
return true;
}
/**
* Send the MS_Server-ActiveSync header.
*
* @return array Returns an array of the headers that were sent.
* @since 2.39.0
*/
public function activeSyncHeader()
{
$headers = array(
'Allow: OPTIONS,POST',
sprintf('Server: Horde_ActiveSync Library v%s', self::LIBRARY_VERSION),
'Public: OPTIONS,POST'
);
switch ($this->_maxVersion) {
case self::VERSION_TWOFIVE:
$headers[] = 'MS-Server-ActiveSync: 6.5.7638.1';
break;
case self::VERSION_TWELVE:
$headers[] = 'MS-Server-ActiveSync: 12.0';
break;
case self::VERSION_TWELVEONE:
$headers[] = 'MS-Server-ActiveSync: 12.1';
break;
case self::VERSION_FOURTEEN:
$headers[] = 'MS-Server-ActiveSync: 14.0';
break;
case self::VERSION_FOURTEENONE:
$headers[] = 'MS-Server-ActiveSync: 14.1';
break;
case self::VERSION_SIXTEEN:
$headers[] = 'MS-Server-ActiveSync: 16.0';
}
foreach ($headers as $hdr) {
header($hdr);
}
return $headers;
}
/**
* Send the protocol versions header.
*
* @return string The header that was sent. @since 2.39.0
*/
public function versionHeader()
{
$hdr = sprintf('MS-ASProtocolVersions: %s', $this->getSupportedVersions());
header($hdr);
return $hdr;
}
/**
* Return supported versions in a comma delimited string suitable for
* sending as the MS-ASProtocolVersions header.
*
* @return string
*/
public function getSupportedVersions()
{
return implode(',', array_slice(self::$_supportedVersions, 0, (array_search($this->_maxVersion, self::$_supportedVersions) + 1)));
}
/**
* Send protocol commands header.
*
* @return string The header that was sent. @since 2.39.0
*/
public function commandsHeader()
{
$hdr = sprintf('MS-ASProtocolCommands: %s', $this->getSupportedCommands());
header($hdr);
return $hdr;
}
/**
* Return the supported commands in a comma delimited string suitable for
* sending as the MS-ASProtocolCommands header.
*
* @return string
*/
public function getSupportedCommands()
{
switch ($this->_maxVersion) {
case self::VERSION_TWOFIVE:
return 'Sync,SendMail,SmartForward,SmartReply,GetAttachment,GetHierarchy,CreateCollection,DeleteCollection,MoveCollection,FolderSync,FolderCreate,FolderDelete,FolderUpdate,MoveItems,GetItemEstimate,MeetingResponse,ResolveRecipients,ValidateCert,Provision,Search,Ping';
case self::VERSION_TWELVE:
case self::VERSION_TWELVEONE:
case self::VERSION_FOURTEEN:
case self::VERSION_FOURTEENONE:
case self::VERSION_SIXTEEN:
return 'Sync,SendMail,SmartForward,SmartReply,GetAttachment,GetHierarchy,CreateCollection,DeleteCollection,MoveCollection,FolderSync,FolderCreate,FolderDelete,FolderUpdate,MoveItems,GetItemEstimate,MeetingResponse,Search,Settings,Ping,ItemOperations,Provision,ResolveRecipients,ValidateCert';
}
}
/**
* Send provision header
*/
public function provisionHeader()
{
header('HTTP/1.1 449 Retry after sending a PROVISION command');
}
/**
* Obtain the policy key header from the request.
*
* @return integer The policy key or '0' if not set.
*/
public function getPolicyKey()
{
// Policy key can come from header or encoded request parameters.
$this->_policykey = $this->_request->getHeader('X-MS-PolicyKey');
if (empty($this->_policykey)) {
$get = $this->getGetVars();
if (!empty($get['PolicyKey'])) {
$this->_policykey = $get['PolicyKey'];
} else {
$this->_policykey = 0;
}
}
return $this->_policykey;
}
/**
* Obtain the ActiveSync protocol version requested by the client headers.
*
* @return string The EAS version requested by the client.
*/
public function getProtocolVersion()
{
if (!isset(self::$_version)) {
self::$_version = $this->_request->getHeader('MS-ASProtocolVersion');
if (empty(self::$_version)) {
$get = $this->getGetVars();
self::$_version = empty($get['ProtVer']) ? '1.0' : $get['ProtVer'];
}
}
return self::$_version;
}
/**
* Return the GET variables passed from the device, decoding from
* base64 if needed.
*
* @return array A hash of get variables => values.
*/
public function getGetVars()
{
if (!empty($this->_get)) {
return $this->_get;
}
$results = array();
$get = $this->_request->getGetVars();
// Do we need to decode the request parameters?
if (!isset($get['Cmd']) && !isset($get['DeviceId']) && !isset($get['DeviceType'])) {
$serverVars = $this->_request->getServerVars();
if (isset($serverVars['QUERY_STRING']) && strlen($serverVars['QUERY_STRING']) >= 10) {
$results = Horde_ActiveSync_Utils::decodeBase64($serverVars['QUERY_STRING']);
// Normalize values.
switch ($results['DeviceType']) {
case 'PPC':
$results['DeviceType'] = 'PocketPC';
break;
case 'SP':
$results['DeviceType'] = 'SmartPhone';
break;
case 'WP':
case 'WP8':
$results['DeviceType'] = 'WindowsPhone';
break;
case 'android':
case 'android40':
$results['DeviceType'] = 'android';
}
$this->_get = $results;
}
} else {
$this->_get = $get;
}
return $this->_get;
}
/**
* Return any global errors that occured during initial connection.
*
* @since 2.4.0
* @return mixed A Horde_ActiveSync_Status:: constant of boolean false if
* no errors.
*/
public function checkGlobalError()
{
return $this->_globalError;
}
/**
* Send the content type header.
*
*/
public function contentTypeHeader($content_type = null)
{
if (!empty($content_type)) {
header('Content-Type: ' . $content_type);
return;
}
if ($this->_multipart) {
header('Content-Type: application/vnd.ms-sync.multipart');
} else {
header('Content-Type: application/vnd.ms-sync.wbxml');
}
}
/**
* Send the OPTIONS request response headers.
*/
protected function _handleOptionsRequest()
{
$as_headers = implode("\r\n", $this->activeSyncHeader());
$version_header = $this->versionHeader();
$cmd_header = $this->commandsHeader();
self::$_logger->meta(sprintf(
"Returning OPTIONS response:\r\n%s\r\n%s\r\n%s",
$as_headers, $version_header, $cmd_header)
);
}
/**
* Return the number of bytes corresponding to the requested trunction
* constant. This applies to MIMETRUNCATION only.
*
* @param integer $truncation The constant.
*
* @return integer|boolean Either the size, in bytes, to truncate or
* falso if no truncation.
* @since 2.20.0
*/
public static function getMIMETruncSize($truncation)
{
switch($truncation) {
case Horde_ActiveSync::TRUNCATION_ALL:
return 0;
case Horde_ActiveSync::TRUNCATION_1:
return 4096;
case Horde_ActiveSync::TRUNCATION_2:
return 5120;
case Horde_ActiveSync::TRUNCATION_3:
return 7168;
case Horde_ActiveSync::TRUNCATION_4:
return 10240;
case Horde_ActiveSync::TRUNCATION_5:
return 20480;
case Horde_ActiveSync::TRUNCATION_6:
return 51200;
case Horde_ActiveSync::TRUNCATION_7:
return 102400;
case Horde_ActiveSync::TRUNCATION_8:
return false;
default:
return 1024; // Default to 1Kb
}
}
/**
* Return the number of bytes corresponding to the requested trunction
* constant.
*
* @param integer $truncation The constant.
*
* @return integer|boolean Either the size, in bytes, to truncate or
* falso if no truncation.
*
*/
public static function getTruncSize($truncation)
{
switch($truncation) {
case Horde_ActiveSync::TRUNCATION_ALL:
return 0;
case Horde_ActiveSync::TRUNCATION_1:
return 512;
case Horde_ActiveSync::TRUNCATION_2:
return 1024;
case Horde_ActiveSync::TRUNCATION_3:
return 2048;
case Horde_ActiveSync::TRUNCATION_4:
return 5120;
case Horde_ActiveSync::TRUNCATION_5:
return 10240;
case Horde_ActiveSync::TRUNCATION_6:
return 20480;
case Horde_ActiveSync::TRUNCATION_7:
return 51200;
case Horde_ActiveSync::TRUNCATION_8:
return 102400;
case Horde_ActiveSync::TRUNCATION_9:
case Horde_ActiveSync::TRUNCATION_NONE: // @deprecated
return false;
default:
return 1024; // Default to 1Kb
}
}
}