<?php

require_once(BASE_PATH . 'server/includes/core/class.encryptionstore.php');
require_once('zpushprops.php');

/**
 * PluginMDMModule Module
 */
class PluginMDMModule extends Module
{
	private $server = '';
	private $username = '';
	private $password = '';

	// content data
	const FOLDERUUID = 1;
	const FOLDERTYPE = 2;
	const FOLDERBACKENDID = 5;

	/**
	 * Constructor
	 * @param int $id unique id.
	 * @param array $data list of all actions.
	 */
	function __construct($id, $data)
	{
		parent::__construct($id, $data);

		$this->server = (PLUGIN_MDM_SERVER_SSL ? 'https://' : 'http://') . PLUGIN_MDM_SERVER;

		// Get the username and password from the Encryption store
		$encryptionStore = EncryptionStore::getInstance();
		$this->username = $encryptionStore->get('username');
		$this->password = $encryptionStore->get('password');

		$this->url = $this->server .'/Microsoft-Server-ActiveSync?Cmd=WebserviceDevice&DeviceId=webservice&DeviceType=webservice&User=' . $this->username;
	}

	/**
	 * Returns the version of the Z-Push server
	 * @return String
	 */
	function getServerVersion()
	{
		// Make a call to the service, so we can read the version
		// from the response headers
		try {
			$url = $this->server .'/Microsoft-Server-ActiveSync?Cmd=WebserviceInfo&DeviceId=webservice&DeviceType=webservice&User=' . $this->username;
			$client = $this->getSoapClient($url);
			return $client->About();
		} catch(Exception $e){}

		// If we can't find a version, we will simply return not available
		return dgettext('plugin_mdm', 'version not available');
	}

	/**
	 * Helper to setup a client.
	 */
	function getSoapClient($url='')
	{
		if ( empty($url) ){
			$url = $this->url;
		}

		return new SoapClient(null, array(
			'location' => $url,
			'uri' => $this->server,
			'trace' => 1,
			'login' => $this->username,
			'password' => $this->password
		));
	 }

	/**
	 * Function which calls the soap call to do a full resync
	 * @param int $deviceid of phone which has to be resynced
	 * @return json $response object contains the response of the soap request from Z-Push
	 */
	function resyncDevice($deviceid)
	{
		$client = $this->getSoapClient();
		return $client->ResyncDevice($deviceid);
	}

	/**
	 * Function which calls the wipeDevice soap call
	 * @param int $deviceid of phone which has to be wiped
	 * @param string $password user password
	 * @return json $response object contains the response of the soap request from Z-Push
	 */
	function wipeDevice($deviceid, $password)
	{
		if ($password == $this->password) {
			$client = $this->getSoapClient();
			return $client->WipeDevice($deviceid);
		} else {
			return false;
		}
	}

	/**
	 * function which calls the ListDeviceDetails soap call
	 * @return array $response array contains a list of devices connected to the users account
	 */
	function getDevices()
	{
		$client = $this->getSoapClient();
		return $client->ListDevicesDetails();
	}


	/**
	 * function which calls the wipeDevice soap call
	 * @param int $deviceid of phone which has to be wiped
	 * @return json $response object contains the response of the soap request from Z-Push
	 */
	function removeDevice($deviceid)
	{
		$client = $this->getSoapClient();
		return $client->RemoveDevice($deviceid);
	}


    /**
	 * Function witch is use get details of given device.
     * @param string $deviceid id of device.
     * @return array contains device props.
     */
    function getDeviceDetails($deviceid)
    {
        $client = $this->getSoapClient();
        $deviceRawData = $client->GetDeviceDetails($deviceid);
        $device = array();
        $device['props'] = $this->getDeviceProps($deviceRawData->data);
        $device["sharedfolders"] = array("item" => $this->getAdditionalFolderList($deviceid));
        return $device;
    }

	/**
	 * Executes all the actions in the $data variable.
	 * @return boolean true on success of false on fialure.
	 */
	function execute()
	{
		foreach($this->data as $actionType => $actionData)
		{
			if(isset($actionType)) {
				try {
					switch($actionType)
					{
						case 'wipe':
							$this->wipeDevice($actionData['deviceid'], $actionData['password']);
							$this->addActionData('wipe', array(
								'type' => 3,
								'wipe' => $this->wipeDevice($actionData['deviceid'], $actionData['password'])
							));
							$GLOBALS['bus']->addData($this->getResponseData());
							break;
						case 'resync':
							$this->addActionData('resync', array(
								'type' => 3,
								'resync' => $this->resyncDevice($actionData['deviceid'])
							));
							$GLOBALS['bus']->addData($this->getResponseData());
							break;
						case 'remove':
							$this->addActionData('remove', array(
								'type' => 3,
								'remove' => $this->removeDevice($actionData['deviceid'])
							));
							$GLOBALS['bus']->addData($this->getResponseData());
							break;
						case 'list':
							$items = array();
							$date['page'] = array();

							$rawData = $this->getDevices();
							foreach($rawData as $device){
								array_push($items, array('props' => $this->getDeviceProps($device->data)));
							}
							$data['page']['start'] = 0;
							$data['page']['rowcount'] = count($rawData);
							$data['page']['totalrowcount'] = $data['page']['rowcount'];
							$data = array_merge($data, array('item' => $items));

							$this->addActionData('list', $data);
							$GLOBALS['bus']->addData($this->getResponseData());
							break;

						case 'open':
                            $device = $this->getDeviceDetails($actionData["entryid"]);
                            $item = array("item" => $device);
							$this->addActionData('item', $item);
							$GLOBALS['bus']->addData($this->getResponseData());
							break;
                        case 'save':
                            $this ->saveDevice($actionData);
                            $device = $this->getDeviceDetails($actionData["entryid"]);
                            $item = array("item" => $device);
                            $this->addActionData('update', $item);
                            $GLOBALS['bus']->addData($this->getResponseData());
                            break;
						default:
							$this->handleUnknownActionType($actionType);
					}
				} catch (SoapFault $fault) {
					$display_message = dgettext('plugin_mdm', 'Something went wrong.');
					if ($fault->faultcode === 'HTTP') {
						if ($fault->getMessage() === "Unauthorized") {
							$display_message = dgettext('plugin_mdm', 'Unable to connect to Z-Push Server. Unauthorized.');
						}
						if ($fault->getMessage() === "Could not connect to host") {
							$display_message = dgettext('plugin_mdm', 'Unable to connect to Z-Push Server. Could not connect to host.');
						}
						if ($fault->getMessage() === "Not Found") {
							$display_message = dgettext('plugin_mdm', 'Unable to connect to Z-Push Server. Not found.');
						}
					} else if ($fault->faultcode === "ERROR") {
                        $errors = (explode(": ", $fault->getMessage()));
                        switch ($errors[0]) {
                            case "ASDevice->AddAdditionalFolder()":
                            case "ZPushAdmin::AdditionalFolderAdd()":
                                $display_message = dgettext('plugin_mdm', "Folder can not be added because there is already an additional folder with the same name");
                                break;

                            case "ASDevice->RemoveAdditionalFolder()":
                            case "ZPushAdmin::AdditionalFolderRemove()":
                                $display_message = dgettext('plugin_mdm', "Folder can not be removed because there is no folder known with given folder id");
                                break;
                            default:
                                $display_message = dgettext('plugin_mdm', "Device ID could not be found");
                        }
                    }
					$this->sendFeedback(false, array("type" => ERROR_GENERAL, "info" => array('display_message' => $display_message)));
				}
				catch (Exception $e) {
					$this->sendFeedback(true, array("type" => ERROR_GENERAL, "info" => array('display_message' => dgettext('plugin_mdm', 'Something went wrong'))));
				}
			}
		}
 	}


	/**
	 * Function which is use to get device properties.
	 *
	 * @param array $device array of device properties
	 * @return array
	 */
	function getDeviceProps($device)
	{
		$item = array();
		$propsList = ['devicetype', 'deviceos', 'devicefriendlyname', 'useragent', 'asversion', 'firstsynctime',
			'lastsynctime', 'lastupdatetime', 'wipestatus', 'policyname', 'koeversion', 'koebuild', 'koebuilddate'];

		$item['entryid'] = $device['deviceid'];
		$item['message_class'] = "IPM.MDM";
		foreach ($propsList as $prop) {
			if (isset($device[$prop])) {
				$item[$prop] = $device[$prop];
			}
		}

		$item = array_merge($item, $this->getSyncFoldersProps($device));
		return $item;
	}

	/**
	 * Function which is use to gather some statistics about synchronized folders.
	 * @param array $device array of device props
	 * @return array $syncFoldersProps has list of properties related to synchronized folders
	 */
	function getSyncFoldersProps($device)
	{
		$contentData = $device['contentdata'];
		$folders = array_keys($contentData);
		$synchedFolderTypes = array();
		$synchronizedFolders = 0;
		$hierarchyCache = isset($device['hierarchycache']) ? $device['hierarchycache'] : false;

		foreach ($folders as $folderid) {
			if (isset($contentData[$folderid][self::FOLDERUUID]) || isset($device['hierarchyuuid'])) {
				$type = $contentData[$folderid][self::FOLDERTYPE];
				$name = "unknown";
				if ($hierarchyCache) {
					if (array_key_exists($folderid, $hierarchyCache->cacheById)) {
						$folder = $hierarchyCache->cacheById[$folderid];
					} else {
						$folder = $hierarchyCache->cacheByIdOld[$folderid];
					}
					if ($folder) {
						$name = $folder->displayname;
					}
				}

				$folderType = $this->getSyncFolderType($type, $name);

				if (isset($contentData[$folderid][self::FOLDERUUID])) {
					if(isset($synchedFolderTypes[$folderType])){
                        $synchedFolderTypes[$folderType]++;
					} else {
                        $synchedFolderTypes[$folderType] = 1;
					}
				}
			}
		}
		$syncFoldersProps = array();
		foreach ($synchedFolderTypes as $key => $value) {
			$synchronizedFolders += $value;
			$syncFoldersProps[strtolower($key) . 'folder'] = $value;
		}
		$client = $this->getSoapClient();
		$items = $client->AdditionalFolderList($device['deviceid']);
		$syncFoldersProps['sharedfolders'] = count($items);
		$syncFoldersProps["shortfolderids"] = $device['hasfolderidmapping'] ? dgettext('plugin_mdm', "Yes") : dgettext('plugin_mdm', "No");
		$syncFoldersProps['synchronizedfolders'] = $synchronizedFolders + count($items);

		return $syncFoldersProps;
	}


	/**
	 * Function which is use to get general type like Mail,Calendar,Contacts,etc. from folder type.
	 * @param int $type foldertype for a folder already known to the mobile
	 * @param string $name folder name
	 * @return string general folder type
	 */
	function getSyncFolderType($type, $name)
	{
		switch ($type) {
			case SYNC_FOLDER_TYPE_APPOINTMENT:
			case SYNC_FOLDER_TYPE_USER_APPOINTMENT:
				if (KOE_GAB_NAME != "" && $name == KOE_GAB_NAME) {
					$folderType = "GAB";

				} else {
					$folderType = "Calendars";
				}
				break;
			case SYNC_FOLDER_TYPE_CONTACT:
			case SYNC_FOLDER_TYPE_USER_CONTACT:
				$folderType = "Contacts";
				break;
			case SYNC_FOLDER_TYPE_TASK:
			case SYNC_FOLDER_TYPE_USER_TASK:
				$folderType = "Tasks";
				break;
			case SYNC_FOLDER_TYPE_NOTE:
			case SYNC_FOLDER_TYPE_USER_NOTE:
				$folderType = "Notes";
				break;
			default:
				$folderType = "Emails";
				break;
		}
		return $folderType;
	}

	/**
	 * Function which is use to get list of additional folders which was shared with given device
	 * @param string $devid device id
	 * @return array has list of properties related to shared folders
	 */
	function getAdditionalFolderList($devid)
	{
		$stores = $GLOBALS["mapisession"]->getAllMessageStores();
		$client = $this->getSoapClient();
		$items = $client->AdditionalFolderList($devid);
		$data = array();
		foreach ($items as $item)
		{
			foreach ($stores as $store)
			{
				try {
					$entryid = mapi_msgstore_entryidfromsourcekey($store, hex2bin($item->folderid));
				} catch (MAPIException $me) {
					continue;
				}
			}
			if (isset($entryid)) {
				$item->entryid = bin2hex($entryid);
			}
			array_push($data, array("props" => $item));
		}
		return $data;
	}

    /**
	 * Function which is use to remove additional folder which was shared with given device.
     * @param string $entryId id of device.
     * @param string $folderid id of folder which will remove from device.
     */
	function additionalFolderRemove($entryId, $folderid)
    {
        $client = $this->getSoapClient();
        $client->AdditionalFolderRemove($entryId, $folderid);
    }

    /**
	 * Function which is use to add additional folder which will share with given device.
     * @param string $entryId id of device.
     * @param array $folder folder which will share with device.
     */
    function additionalFolderAdd($entryId, $folder)
    {
        $client = $this->getSoapClient();
        $containerClass = isset($folder[PR_CONTAINER_CLASS]) ? $folder[PR_CONTAINER_CLASS] : "IPF.Note";
        $folderId = bin2hex($folder[PR_SOURCE_KEY]);
        $userName = $folder["user"];
        $folderName = $userName === "SYSTEM" ? $folder[PR_DISPLAY_NAME] : $folder[PR_DISPLAY_NAME]." - ".$userName;
        $folderType = $this->getFolderTypeFromContainerClass($containerClass);
        $client->AdditionalFolderAdd($entryId, $userName, $folderId, $folderName, $folderType, FLD_FLAGS_REPLYASUSER);
    }

    /**
	 * Function which use to save the device.
	 * It will use to add or remove folders in the device.
     * @param array $data array of added and removed folders.
     */
	function saveDevice($data)
    {
        $entryid = $data["entryid"];
        if (isset($data['sharedfolders'])) {
            if (isset($data['sharedfolders']['remove'])) {
                $deletedFolders = $data['sharedfolders']['remove'];
                foreach ($deletedFolders as $folder) {
                    $this->additionalFolderRemove($entryid, $folder["folderid"]);
                }
            }
            if (isset($data['sharedfolders']['add'])) {
                $addFolders = $data['sharedfolders']['add'];
                $hierarchyFolders = $this->getHierarchyList();
                foreach ($addFolders as $folder) {
                    foreach ($hierarchyFolders as $hierarchyFolder) {
                        $folderEntryid = bin2hex($hierarchyFolder[PR_ENTRYID]);
                        if ($folderEntryid === $folder["entryid"]) {
                            $this->additionalFolderAdd($entryid, $hierarchyFolder);
                            continue 2;
                        }
                    }
                }
            }
        }
    }

    /**
	 * Gets the hierarchy list of all required stores.
	 * Function which is use to get the hierarchy list with PR_SOURCE_KEY.
     * @return array the array of all hierarchy folders.
     */
	function getHierarchyList()
    {
        $storeList = $GLOBALS["mapisession"]->getAllMessageStores();
        $properties = $GLOBALS["properties"]->getFolderListProperties();
        $properties["source_key"] = PR_SOURCE_KEY;
        $storeData = array();
        foreach ($storeList as $store) {
            $msgstore_props = mapi_getprops($store, array(PR_ENTRYID, PR_DISPLAY_NAME, PR_IPM_SUBTREE_ENTRYID, PR_IPM_OUTBOX_ENTRYID, PR_IPM_SENTMAIL_ENTRYID, PR_IPM_WASTEBASKET_ENTRYID, PR_MDB_PROVIDER, PR_IPM_PUBLIC_FOLDERS_ENTRYID, PR_IPM_FAVORITES_ENTRYID, PR_OBJECT_TYPE, PR_STORE_SUPPORT_MASK, PR_MAILBOX_OWNER_ENTRYID, PR_MAILBOX_OWNER_NAME, PR_USER_ENTRYID, PR_USER_NAME, PR_QUOTA_WARNING_THRESHOLD, PR_QUOTA_SEND_THRESHOLD, PR_QUOTA_RECEIVE_THRESHOLD, PR_MESSAGE_SIZE_EXTENDED, PR_MAPPING_SIGNATURE, PR_COMMON_VIEWS_ENTRYID, PR_FINDER_ENTRYID));
            $storeType = $msgstore_props[PR_MDB_PROVIDER];

            if ($storeType == ZARAFA_STORE_DELEGATE_GUID) {
                $storeUserName = $GLOBALS["mapisession"]->getUserNameOfStore($msgstore_props[PR_ENTRYID]);
            } else if( $storeType == ZARAFA_STORE_PUBLIC_GUID){
                $storeUserName = "SYSTEM";
			} else {
                $storeUserName = $msgstore_props[PR_USER_NAME];
            }
            if (isset($msgstore_props[PR_IPM_SUBTREE_ENTRYID])) {
                $subtreeFolderEntryID = $msgstore_props[PR_IPM_SUBTREE_ENTRYID];
                try {
                    $subtreeFolder = mapi_msgstore_openentry($store, $subtreeFolderEntryID);
                } catch (MAPIException $e) {
                    // We've handled the event
                    $e->setHandled();
                }
                try {

                     // remove hidden folders, folders with PR_ATTR_HIDDEN property set
                     // should not be shown to the client
                    $restriction = Array(RES_OR, Array(
                        Array(RES_PROPERTY,
                            Array(
                                RELOP => RELOP_EQ,
                                ULPROPTAG => PR_ATTR_HIDDEN,
                                VALUE => Array(PR_ATTR_HIDDEN => false)
                            )
                        ),
                        Array(RES_NOT,
                            Array(
                                Array(RES_EXIST,
                                    Array(
                                        ULPROPTAG => PR_ATTR_HIDDEN
                                    )
                                )
                            )
                        )
                    ));

                    $expand = Array(
                        Array(
                            'folder' => $subtreeFolder,
                            'props' => mapi_getprops($subtreeFolder, Array(PR_ENTRYID, PR_SUBFOLDERS))
                        )
                    );

                    // Start looping through the $expand array, during each loop we grab the first item in
                    // the array and obtain the hierarchy table for that particular folder. If one of those
                    // subfolders has subfolders of its own, it will be appended to $expand again to ensure
                    // it will be expanded later.
                    while (!empty($expand)) {
                        $item = array_shift($expand);

                        $hierarchyTable = mapi_folder_gethierarchytable($item['folder'], MAPI_DEFERRED_ERRORS);
                        mapi_table_restrict($hierarchyTable, $restriction, TBL_BATCH);

                        mapi_table_setcolumns($hierarchyTable, $properties);

                        // Load the hierarchy in small batches
                        $batchcount = 100;
                        do {
                            $rows = mapi_table_queryrows($hierarchyTable, null, 0, $batchcount);

                            foreach ($rows as $subfolder) {

                                // If the subfolders has subfolders of its own, append the folder
                                // to the $expand array, so it can be expanded in the next loop.
                                if ($subfolder[PR_SUBFOLDERS]) {
                                    $folderObject = mapi_msgstore_openentry($store, $subfolder[PR_ENTRYID]);
                                    array_push($expand, array('folder' => $folderObject, 'props' => $subfolder));
                                }
                                $subfolder["user"] = $storeUserName;
                                // Add the folder to the return list.
                                array_push($storeData, $subfolder);
                            }

                            // When the server returned a different number of rows then was requested,
                            // we have reached the end of the table and we should exit the loop.
                        } while (count($rows) === $batchcount);
                    }

                } catch (MAPIException $e) {

                    // We've handled the event
                    $e->setHandled();
                }
            }
        }
        return $storeData;
    }

    /**
	 * Function which is use get folder types from the container class
     * @param string $containerClass container class of folder
     * @return int folder type
     */
    function getFolderTypeFromContainerClass($containerClass)
	{
        switch ($containerClass) {
            case  "IPF.Note":
				return SYNC_FOLDER_TYPE_USER_MAIL;
            case "IPF.Appointment":
				return SYNC_FOLDER_TYPE_USER_APPOINTMENT;
            case "IPF.Contact":
                return SYNC_FOLDER_TYPE_USER_CONTACT;
            case "IPF.StickyNote":
               return SYNC_FOLDER_TYPE_USER_NOTE;
            case "IPF.Task":
                return SYNC_FOLDER_TYPE_USER_TASK;
			case "IPF.Journal":
				return SYNC_FOLDER_TYPE_USER_JOURNAL;
			default:
				return SYNC_FOLDER_TYPE_UNKNOWN;
        }
	}
};
?>
