<?php
/*
 * Copyright 2005 - 2009  Zarafa B.V.
 * 
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License, version 3, 
 * as published by the Free Software Foundation with the following additional 
 * term according to sec. 7:
 *  
 * According to sec. 7 of the GNU Affero General Public License, version
 * 3, the terms of the AGPL are supplemented with the following terms:
 * 
 * "Zarafa" is a registered trademark of Zarafa B.V. The licensing of
 * the Program under the AGPL does not imply a trademark license.
 * Therefore any rights, title and interest in our trademarks remain
 * entirely with us.
 * 
 * However, if you propagate an unmodified version of the Program you are
 * allowed to use the term "Zarafa" to indicate that you distribute the
 * Program. Furthermore you may use our trademarks where it is necessary
 * to indicate the intended purpose of a product or service provided you
 * use it in accordance with honest practices in industrial or commercial
 * matters.  If you want to propagate modified versions of the Program
 * under the name "Zarafa" or "Zarafa Server", you may only do so if you
 * have a written permission by Zarafa B.V. (to acquire a permission
 * please contact Zarafa at trademark@zarafa.com).
 * 
 * The interactive user interface of the software displays an attribution
 * notice containing the term "Zarafa" and/or the logo of Zarafa.
 * Interactive user interfaces of unmodified and modified versions must
 * display Appropriate Legal Notices according to sec. 5 of the GNU
 * Affero General Public License, version 3, when you propagate
 * unmodified or modified versions of the Program. In accordance with
 * sec. 7 b) of the GNU Affero General Public License, version 3, these
 * Appropriate Legal Notices must retain the logo of Zarafa or display
 * the words "Initial Development by Zarafa" if the display of the logo
 * is not reasonably feasible for technical reasons."
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *  
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 */

?>
<?php
	/**
	 * ListModule
	 * Superclass of every module, which retreives a MAPI message list. It 
	 * extends the Module class.
	 */
	class ListModule extends Module
	{
		/**
		 * @var array list of columns which are selected in the previous request.
		 */
		var $properties;
		
		/**
		 * @var array list of columns which are shown in the table view.
		 */
		var $tablecolumns;
		
		/**
		 * @var array list of columns which are shown in the table view.
		 */
		var $newcolumns;
		
		/**
		 * @var array sort.
		 */
		var $sort;
		
		/**
		 * @var int startrow in the table.
		 */
		var $start;
		
		/**
		 * @var array contains (when needed) a restriction used when searching
		 */
		var $searchRestriction;

		/**
		 * @var bool contains check whether a search result is listed or just the contents of a normal folder
		 */
		var $searchFolderList;

		/**
		 * Constructor
		 * @param int $id unique id.
		 * @param array $data list of all actions.
		 */
		function ListModule($id, $data, $events = false)
		{
			$this->start = 0;
			$this->searchRestriction = false;
			$this->searchFolderList = false;
			$this->sort = array();
			
			parent::Module($id, $data, $events);
		}
		
		/**
		 * Executes all the actions in the $data variable.
		 * @return boolean true on success of false on fialure.
		 */
		function execute()
		{
			$result = false;
			
			foreach($this->data as $action)
			{
				if(isset($action["attributes"]) && isset($action["attributes"]["type"])) {
					$store = $this->getActionStore($action);
					$parententryid = $this->getActionParentEntryID($action);
					$entryid = $this->getActionEntryID($action);
				
					switch($action["attributes"]["type"])
					{
						case "list":
							$result = $this->messageList($store, $entryid, $action);
							break;
						case "save":
							$result = $this->save($store, $parententryid, $action);
							break;
						case "read_flag":
							$result = $this->setReadFlag($store, $entryid, $action);
							break;
						case "delete":
							$result = $this->delete($store, $parententryid, $entryid, $action);
							break;
						case "copy":
							$result = $this->copy($store, $parententryid, $entryid, $action);
							break;
                        case "cancelInvitation":
                            $GLOBALS["operations"]->cancelInvitation($store, $entryid, $action);
                            break;
					}
				}
			}
			
			return $result;
		}
		
		/**
		 * Function which retrieves a list of messages in a folder
		 * @param object $store MAPI Message Store Object
		 * @param string $entryid entryid of the folder
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure		 		 
		 */
		function messageList($store, $entryid, $action)
		{
			$this->searchFolderList = false; // Set to indicate this is not the search result, but a normal folder content
			$result = false;

			if($store && $entryid) {

				// Restriction
				$this->parseSearchRestriction($action);

				// Sort
				$this->parseSortOrder($action, null, true);
				
				// List columns visible
				$this->parseVisibleColumns($action);
				
				// Create the data array, which will be send back to the client
				$data = array();
				$data["attributes"] = array("type" => "list");
				$data["column"] = $this->tablecolumns;
				
				$data["insertcolumn"] = $this->insertcolumns;
				
				$firstSortColumn = reset(array_keys($this->sort)); // get first key of the sort array
				$data["sort"] = array();
				$data["sort"]["attributes"] = array();
				$data["sort"]["attributes"]["direction"] = (isset($this->sort[$firstSortColumn]) && $this->sort[$firstSortColumn] == TABLE_SORT_ASCEND) ? "asc" : "desc";
				$data["sort"]["_content"] = array_search($firstSortColumn, $this->properties);
				
				// Get the table and merge the arrays
				$data = array_merge($data, $GLOBALS["operations"]->getTable($store, $entryid, $this->properties, $this->sort, $this->start, false, $this->searchRestriction));
				
				array_push($this->responseData["action"], $data);
				$GLOBALS["bus"]->addData($this->responseData);

				$result = true;
			}
			
			return $result;
		}

		function parseSearchRestriction($action)
		{
			// TODO: let javascript generate the MAPI restriction just as with the rules

			if(isset($action["restriction"])) {
				if(isset($action["restriction"]["start"])) {
					// Set start variable
					$this->start = (int) $action["restriction"]["start"];
				}
				if(isset($action["restriction"]["search"])) {
					// if the restriction is a associative array, it means that only one property is requested
					// so we must build an non-associative array arround it
					if (is_assoc_array($action["restriction"]["search"])){
						$action["restriction"]["search"] = Array($action["restriction"]["search"]);
					}

					$res_or = Array();
					foreach($action["restriction"]["search"] as $i=>$search){
						$prop = false;
						// convert search property to MAPI property
						switch($search["property"]){
							case "subject":
								$prop = PR_SUBJECT;
								break;
							case "body":
								$prop = PR_BODY;
								break;
							case "to":
								$prop = PR_DISPLAY_TO;
								break;
							case "cc":
								$prop = PR_DISPLAY_CC;
								break;
							case "sender_name":
								$prop = PR_SENDER_NAME;
								break;
							case "sender_email":
								$prop = PR_SENDER_EMAIL_ADDRESS;
								break;
							case "sent_representing_name":
								$prop = PR_SENT_REPRESENTING_NAME;
								break;
							case "sent_representing_email":
								$prop = PR_SENT_REPRESENTING_EMAIL_ADDRESS;
								break;
						}
						// build restriction
						if ($prop !== false){
							array_push($res_or, Array(RES_AND,
													Array(
														Array(RES_EXIST, // check first if the property exists
															Array(ULPROPTAG=>$prop
															)
														),
														Array(RES_CONTENT,
															Array(FUZZYLEVEL => FL_SUBSTRING|FL_IGNORECASE,
																ULPROPTAG=>$prop,
																VALUE => utf8_to_windows1252($search["value"])
															)
														)
													)
												)
								); // array_push: $res_or
						}
					}
					if (count($res_or)>0){
						$this->searchRestriction = Array(RES_OR,$res_or);
					}else{
						$this->searchRestriction = false;
					}
				}else{
					$this->searchRestriction = false;
				}
			}
		}
		
		/**
		 * Parses the incoming sort request and builds a MAPI sort order. Normally
		 * properties are mapped from the XML to MAPI by the standard $this->properties mapping. However,
		 * if you want other mappings, you can specify them in the optional $map mapping.
		 * 
		 * $allow_multi_instance is used for creating multiple instance of MV property related items.
		 */
		function parseSortOrder($action, $map = false, $allow_multi_instance = false)
		{
			if(isset($action["sort"]) && isset($action["sort"]["column"])) {
				$this->sort = array();
				
				// Unshift MVI_FLAG of MV properties. So the table is not sort on it anymore.
				// Otherwise the server would generate multiple rows for one item (categories).
				foreach($this->properties as $id => $property)
				{
					switch(mapi_prop_type($property)) 
					{
						case (PT_MV_STRING8 | MVI_FLAG):
						case (PT_MV_LONG | MVI_FLAG):
							$this->properties[$id] = $this->properties[$id] &~ MV_INSTANCE;
							break;
					}
				}
				
				// Loop through the sort columns
				foreach($action["sort"]["column"] as $column)
				{
					if(isset($column["attributes"]) && isset($column["attributes"]["direction"])) {
						if(isset($this->properties[$column["_content"]]) || ($map && isset($map[$column["_content"]]))) {
							if($map && isset($map[$column["_content"]])) 
								$property = $map[$column["_content"]];
							else
								$property = $this->properties[$column["_content"]];
							
							// Check if column is a MV property
							switch(mapi_prop_type($property)) 
							{
								case PT_MV_STRING8:
								case PT_MV_LONG:
									// Set MVI_FLAG.
									// The server will generate multiple rows for one item (for example: categories)
									if($allow_multi_instance){
										$this->properties[$column["_content"]] = $this->properties[$column["_content"]] | MVI_FLAG;
									}
									$property = $this->properties[$column["_content"]];
									break;
							}

							// Set sort direction
							switch(strtolower($column["attributes"]["direction"]))
							{
								default:
								case "asc":
									$this->sort[$property] = TABLE_SORT_ASCEND;
									break;
								case "desc":
									$this->sort[$property] = TABLE_SORT_DESCEND;
									break;
							}
						}
					}
				}
			}
			
		}
		
		/**
		 * Generates the output sort order to send to the client so it can show sorting in the
		 * column headers. Takes an optional $map when mapping columns to something other than their
		 * standard MAPI property. (same $map as parseSortOrder).
		 *
		 */
		function generateSortOrder($map) {
			$data = array();
			
			$firstSortColumn = array_shift(array_keys($this->sort));
			$data = array();
			$data["attributes"] = array();
			$data["attributes"]["direction"] = (isset($this->sort[$firstSortColumn]) && $this->sort[$firstSortColumn] == TABLE_SORT_ASCEND) ? "asc":"desc";
			$data["_content"] = array_search($firstSortColumn, $map) ? array_search($firstSortColumn, $map) : array_search($firstSortColumn, $this->properties);
			
			return $data;
		}

		function parseVisibleColumns($action) 
		{
			if(isset($action["columns"]) && isset($action["columns"]["column"])) {
				// Loop through the columns
				foreach($action["columns"]["column"] as $column)
				{
					if(isset($column["attributes"]) && isset($column["attributes"]["action"])) {
						$tablecolumnkey = $this->getColumn($this->tablecolumns, $column["_content"]);

						// Add or delete the column
						if($tablecolumnkey !== false) {
							switch(strtolower($column["attributes"]["action"]))
							{
								case "add":
									$this->tablecolumns[$tablecolumnkey]["visible"] = true;

									if(isset($column["attributes"]["order"])) {
										$this->tablecolumns[$tablecolumnkey]["order"] = (int) $column["attributes"]["order"];
									}
									break;
								case "delete":
									$this->tablecolumns[$tablecolumnkey]["visible"] = false;
									break;
							}
						}
					}
				}
			}

		}
		
		/**
		 * Function which saves an item.
		 * @param object $store MAPI Message Store Object
		 * @param string $parententryid entryid of the folder
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure		 		 
		 */
		function save($store, $parententryid, $action)
		{
			$result = false;

			if($store && $parententryid && isset($action["props"])) {
				$messageProps = array();
				$result = $GLOBALS["operations"]->saveMessage($store, $parententryid, Conversion::mapXML2MAPI($this->properties, $action["props"]), null, "", $messageProps);
	
				if($result) {
					$GLOBALS["bus"]->notify($this->entryid, TABLE_SAVE, $messageProps);
				}
			}
			
			return $result;
		}
		
		/**
		 * Function which sets the PR_MESSAGE_FLAGS property of an item.
		 * @param object $store MAPI Message Store Object
		 * @param string $entryid entryid of the item
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure		 		 
		 */
		function setReadFlag($store, $entryid, $action)
		{
			$result = false;
			
			if($store && $entryid) {
				$flags = "read,noreceipt";
				if(isset($action["flag"])) {
					$flags = $action["flag"];
				}
			
				$props = array();
				$result = $GLOBALS["operations"]->setMessageFlag($store, $entryid, $flags, $props);
	
				if($result) {
					$GLOBALS["bus"]->notify($this->entryid, TABLE_SAVE, $props);
				}
			}
			
			return $result;
		}
		
		/**
		 * Function which deletes one or more items.
		 * @param object $store MAPI Message Store Object
		 * @param string $parententryid entryid of the folder
		 * @param array $entryid list of entryids which will be deleted		 
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure
		 */
		function delete($store, $parententryid, $entryids, $action)
		{
			$result = false;
			
			if($store && $parententryid && $entryids) {
				$props = array();
				$props[PR_PARENT_ENTRYID] = $parententryid;
				$props[PR_ENTRYID] = $entryids;

				$storeprops = mapi_getprops($store, array(PR_ENTRYID));
				$props[PR_STORE_ENTRYID] = $storeprops[PR_ENTRYID];
				
				$result = $GLOBALS["operations"]->deleteMessages($store, $parententryid, $entryids);

				if($result) {
					$GLOBALS["bus"]->notify($this->entryid, TABLE_DELETE, $props);

					// because we don't have real notifications and we know that most of the time deleted items are going to
					// the "Deleted Items"-folder, we notify the bus that that folder is changed to update the counters
					$msgprops = mapi_getprops($store, array(PR_IPM_WASTEBASKET_ENTRYID));
					if ($msgprops[PR_IPM_WASTEBASKET_ENTRYID]!=$this->entryid){ // only when we are not deleting within the trash itself

						$props[PR_PARENT_ENTRYID] = $msgprops[PR_IPM_WASTEBASKET_ENTRYID];
						$GLOBALS["bus"]->notify(bin2hex($msgprops[PR_IPM_WASTEBASKET_ENTRYID]), TABLE_SAVE, $props);
					}
				}else{
					$data = array();
					$data["attributes"] = array("type" => "failed");
					$data["action_type"] = 'delete';
	
					array_push($this->responseData["action"], $data);
					$GLOBALS["bus"]->addData($this->responseData);
				}
			}
		
			return $result;
		}
		
		/**
		 * Function which copies or moves one or more items.
		 * @param object $store MAPI Message Store Object
		 * @param string $parententryid entryid of the folder
		 * @param array $entryid list of entryids which will be copied or moved		 
		 * @param array $action the action data, sent by the client
		 * @return boolean true on success or false on failure
		 */
		function copy($store, $parententryid, $entryids, $action)
		{
			$result = false;
			
			if($store && $parententryid && $entryids) {
				$dest_store = $store;
				if(isset($action["destinationstore"])) {
					$dest_storeentryid = hex2bin($action["destinationstore"]);
					$dest_store = $GLOBALS["mapisession"]->openMessageStore($dest_storeentryid);
				}
				
				$dest_folderentryid = false;
				if(isset($action["destinationfolder"])) {
					$dest_folderentryid = hex2bin($action["destinationfolder"]);
				}

				$moveMessages = false;
				if(isset($action["movemessages"])) {
					$moveMessages = true;
				}

				$props = array();
				$props[PR_PARENT_ENTRYID] = $parententryid;
				$props[PR_ENTRYID] = $entryids;			
				
				$storeprops = mapi_getprops($store, array(PR_ENTRYID));
				$props[PR_STORE_ENTRYID] = $storeprops[PR_ENTRYID];
				
				$result = $GLOBALS["operations"]->copyMessages($store, $parententryid, $dest_store, $dest_folderentryid, $entryids, $moveMessages);
				
				if($result) {
					if($moveMessages) {
						$GLOBALS["bus"]->notify($this->entryid, TABLE_DELETE, $props);
					}
					
					$props[PR_PARENT_ENTRYID] = $dest_folderentryid;
					$props[PR_STORE_ENTRYID] = $dest_storeentryid;
					$GLOBALS["bus"]->notify(bin2hex($dest_folderentryid), TABLE_SAVE, $props);
				}
			}
			
			return $result;
		}

		/**
		 * Function which sets the column properties. This function is used for setting
		 * column information, like name and if it is visible in the client view.		 
		 * @param array $columns reference to an array, which the column will be added
		 * @param string $id id of the column
		 * @param boolean $visible true - column is visible, false - column is not visible		 
		 * @param integer $order the order in which the columns are visible
		 * @param string $name the name of the column
		 * @param string $title the title of the column
		 * @param string $type the type of column
		 * @param integer $length the length of the column (pixels)	
		 */
		function addColumn(&$columns, $id, $visible, $order = false, $name = false, $title = false, $length = false, $type = false)
		{
			$column = array();
			
			$column["id"] = $id;
			$column["visible"] = $visible;
			
			if($order !== false && $name && $title) {
				$column["name"] = $name;
				$column["title"] = $title;
				$column["order"] = $order;
				
				if($length) {
					$column["length"] = $length;
				}
			}
			if ($type)
				$column["type"] = $type;
			
			array_push($columns, $column);
		}

		
		/**
		 * Function which sets the column properties. This function is used for setting
		 * column information, like name and if it is visible in the client view.		 
		 * @param array 	$columns 	reference to an array, which the column will be added
		 * @param string 	$id 		id of the input type fields
		 * @param string 	$type 		the type of column
		 * @param boolean 	$visible 	true - column is visible, false - column is not visible		 
		 * @param boolean 	$readonly	true - column is readonly, false - column is not editable
		 * @param integer 	$order 		the order in which the columns are visible
		 * @param string 	$name 		the id` of the corresponding column in the header row
		 * @param string 	$title 		the title of the column
		 * @param integer 	$length 	the length of the column (pixels)
		 */
		function addInputColumn(&$columns, $id, $type, $visible, $readonly, $order = false, $name = false, $title = false, $length = false)
		{
			$column = array();
			
			$column["id"] = $id;
			$column["type"] = $type;
			$column["visible"] = $visible;
			$column["readonly"] = $readonly;
			$column["name"] = $name;
			
			if($order !== false) {
				$column["title"] = $title;				
				$column["order"] = $order;
			}	
			
			if($length) {				
				$column["length"] = $length;
			}						
			array_push($columns, $column);
		}		
		
		/**
		 * Function which returns the column array key (0, 1, 2, ...) of a column.		 
		 * @param array $columns list of columns
		 * @param string $id id of the column
		 * @return integer key in the $columns array (0, 1, 2, ...)	
		 */
		function getColumn($columns, $id)
		{
			$key = false;
			
			foreach($columns as $columnkey => $column)
			{
				if($column["id"] == $id) {
					$key = $columnkey;
				}
			}
			
			return $key;
		}
		
		/**
		 * If an event elsewhere has occurred, it enters in this methode. This method
		 * executes one ore more actions, depends on the event.
		 * @param int $event Event.
		 * @param string $entryid Entryid.
		 * @param array $data array of data.
		 */
		function update($event, $entryid, $props)
		{
			$this->reset();

			switch($event)
			{
				case TABLE_SAVE:
					$data = array();
					$data["attributes"] = array("type" => "item");

					if(isset($props[PR_STORE_ENTRYID])) {
						$store = $GLOBALS["mapisession"]->openMessageStore($props[PR_STORE_ENTRYID]);
						
						if(isset($props[PR_ENTRYID])) {
							$data["item"] = $GLOBALS["operations"]->getMessageProps($store, $GLOBALS["operations"]->openMessage($store, $props[PR_ENTRYID]), $this->properties);
						}
					}
					
					array_push($this->responseData["action"], $data);
					break;
				case TABLE_DELETE:
					// When user has used searchfolder the entryID of that folder should be used.
					if(!$this->searchFolderList){
						$folderEntryID = (isset($props[PR_PARENT_ENTRYID]))?$props[PR_PARENT_ENTRYID]:false;
					}else{
						$folderEntryID = (isset($this->searchFolder))?hex2bin($this->searchFolder):false;
					}

					if(isset($props[PR_ENTRYID]) && $folderEntryID) {
						// Get items, which are shown under the table.
						$store = $GLOBALS["mapisession"]->openMessageStore($props[PR_STORE_ENTRYID]);
						
						$deletedrows = 1;
						if(is_array($props[PR_ENTRYID])) {
							$deletedrows = count($props[PR_ENTRYID]);
						}

						$newItemsStart = ($this->start + (int)$GLOBALS["settings"]->get("global/rowcount", 50)) - $deletedrows;
						$newItems = $GLOBALS["operations"]->getTable($store, $folderEntryID, $this->properties, $this->sort, $newItemsStart, $deletedrows, $this->searchRestriction);
						
						if(count($newItems["item"]) > 0) {
							$data = array();
							$data["delete"] = 1;
							if(!$this->searchFolderList){
								$data["attributes"] = array ("type" => "item");
							}else{
								$data["attributes"] = array("type" => "item", "searchfolder" => $this->searchFolder);
							}
							$data["item"] = $newItems["item"];
						
							array_push($this->responseData["action"], $data);
						}
						
						$data = array();
						$data["attributes"] = array("type" => "delete");
						$data["page"] = $newItems["page"];
						$data["page"]["start"] = $this->start;
						$data["page"]["rowcount"] = $GLOBALS["settings"]->get("global/rowcount", 50);
						$data["parent_entryid"] = bin2hex($folderEntryID);
						
						if(is_array($props[PR_ENTRYID])) {
							$data["entryid"] = array();
							
							foreach($props[PR_ENTRYID] as $entryid)
							{
								array_push($data["entryid"], bin2hex($entryid));
							}
						} else {
							$data["entryid"] = bin2hex($props[PR_ENTRYID]);
						}
						
						array_push($this->responseData["action"], $data);
					}
					break;
			}
			
			$GLOBALS["bus"]->addData($this->responseData);
		}
	}
?>
