<?php

/**
 * AttachmentState.
 *
 * A wrapper around the State class for handling the Attachment State file.
 * This class supplies operators for correctly managing the State contents.
 */
class AttachmentState {
	/**
	 * The basedir in which the attachments for all sessions are found.
	 */
	private $basedir;

	/**
	 * The directory in which the attachments for the current session are found.
	 */
	private $sessiondir;

	/**
	 * The directory in which the session files are created.
	 */
	private $attachmentdir = 'attachments';

	/**
	 * The State object which refers to the Attachments state.
	 */
	private $state;

	/**
	 * List of attachment files from the $state.
	 */
	private $files;

	/**
	 * List of deleted attachment files from the $state.
	 */
	private $deleteattachment;

	/**
	 * List of aborted attachment files while uploading is going on.
	 */
	private $abortedattachment;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->basedir = TMP_PATH . DIRECTORY_SEPARATOR . $this->attachmentdir;
		$this->sessiondir = $this->basedir . DIRECTORY_SEPARATOR . session_id();

		$this->state = new State('attachments');
	}

	/**
	 * Open the session file.
	 *
	 * The session file is opened and locked so that other processes can not access the state information
	 */
	public function open() {
		if (!is_dir($this->sessiondir)) {
			mkdir($this->sessiondir, 0755, true /* recursive */);
		}

		$this->state->open();
		$this->files = $this->state->read('files');
		$this->deleteattachment = $this->state->read('deleteattachment');
		$this->abortedattachment = $this->state->read('abortedattachment');

		// Check if any aborted attachments are pending to be removed
		if (!empty($this->abortedattachment)) {
			// Remove aborted attachments
			$this->removeAbortedAttachments();
		}
	}

	/**
	 * Checks if any information regarding attachments is stored in this attachment state.
	 *
	 * @param string $message_id The unique identifier for referencing the attachments for a single message
	 *
	 * @return bool True to indicate if any changes needed to save in message else false
	 */
	public function isChangesPending($message_id) {
		$files = $this->getAttachmentFiles($message_id);
		$deletedfiles = $this->getDeletedAttachments($message_id);

		if (empty($files) && empty($deletedfiles)) {
			return false;
		}

		return true;
	}

	/**
	 * Obtain the folder in which the attachments for the current session
	 * can be found. All handling of attachments will be isolated to this
	 * folder to prevent any other user to be able to access the attachment
	 * files of another user.
	 *
	 * @return string The foldername in which the attachments can be found
	 */
	private function getAttachmentFolder() {
		return $this->sessiondir;
	}

	/**
	 * Obtain the full path for a new filename on the harddisk
	 * This uses tempnam to generate a new filename in the default
	 * attachments folder.
	 *
	 * @param string $filename The file for which the temporary path is requested
	 *
	 * @return string The full path to the attachment file
	 */
	public function getAttachmentTmpPath($filename) {
		$attachmentPath = tempnam($this->getAttachmentFolder(), mb_basename($filename));

		// Convert in UTF-8 properly if any Malformed UTF-8 characters.
		return mb_convert_encoding($attachmentPath, 'UTF-8');
	}

	/**
	 * Obtain the full path of the given filename on the harddisk.
	 *
	 * @param string $filename The file for which the path is requested
	 *
	 * @return string The full path to the attachment file
	 */
	public function getAttachmentPath($filename) {
		return $this->getAttachmentFolder() . DIRECTORY_SEPARATOR . mb_basename($filename);
	}

	/**
	 * Check if attachment file which belongs to the given $message_id
	 * and is identified by the given $attachid is an embedded attachment.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $attachid   The unique identifier for referencing the
	 *                           attachment
	 *
	 * @return bool True if attachment is an embedded else false
	 */
	public function isEmbeddedAttachment($message_id, $attachid) {
		$attach = $this->getAttachmentFile($message_id, $attachid);

		if ($attach !== false) {
			if ($attach['sourcetype'] === 'embedded') {
				return true;
			}
		}

		return false;
	}

	/**
	 * Function which identifies whether an attachment is an inline or normal attachment.
	 *
	 * @param MAPIAttach $attachment MAPI attachment Object
	 *
	 * @return return true if attachment was inline attachment else return false
	 */
	public function isInlineAttachment($attachment) {
		$props = mapi_attach_getprops($attachment, [PR_ATTACH_CONTENT_ID, PR_ATTACHMENT_HIDDEN, PR_ATTACH_FLAGS]);
		$isInlineAttachmentFlag = (isset($props[PR_ATTACH_FLAGS]) && $props[PR_ATTACH_FLAGS] & 4) ? true : false;

		return isset($props[PR_ATTACH_CONTENT_ID]) && $isInlineAttachmentFlag;
	}

	/**
	 * Function which identifies whether an attachment is contact photo or normal attachment.
	 *
	 * @param MAPIAttach $attachment MAPI attachment Object
	 *
	 * @return return true if attachment is contact photo else return false
	 */
	public function isContactPhoto($attachment) {
		$attachmentProps = mapi_attach_getprops($attachment, [PR_ATTACHMENT_CONTACTPHOTO, PR_ATTACHMENT_HIDDEN]);
		$isHidden = $attachmentProps[PR_ATTACHMENT_HIDDEN] ?? false;
		$isAttachmentContactPhoto = $attachmentProps[PR_ATTACHMENT_CONTACTPHOTO] ?? false;

		return $isAttachmentContactPhoto && $isHidden;
	}

	/**
	 * Add a file which was uploaded to the server by moving it to the attachments directory,
	 * and then register (addAttachmentFile) it.
	 *
	 * @param string $message_id   The unique identifier for referencing the
	 *                             attachments for a single message
	 * @param string $filename     The filename of the attachment
	 * @param string $uploadedfile The file which was uploaded and will be moved to the attachments directory
	 * @param array  $fileinfo     The attachment data
	 *
	 * @return The attachment identifier to be used for referencing the file in the tmp folder
	 */
	public function addUploadedAttachmentFile($message_id, $filename, $uploadedfile, $fileinfo) {
		// Create the destination path, the attachment must
		// be placed in the attachment folder with a unique name.
		$filepath = $this->getAttachmentTmpPath($filename);

		// Obtain the generated filename
		$tmpname = mb_basename($filepath);

		move_uploaded_file($uploadedfile, $filepath);

		$this->addAttachmentFile($message_id, $tmpname, $fileinfo);

		return $tmpname;
	}

	/**
	 * Move a file from an alternative location to the attachments directory,
	 * this will call addAttachmentFile to register the attachment to the state.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $filename   The filename of the attachment
	 * @param string $sourcefile The path of the file to move to the attachments directory
	 * @param array  $fileinfo   The attachment data
	 *
	 * @return The attachment identifier to be used for referencing the file in the tmp folder
	 */
	public function addProvidedAttachmentFile($message_id, $filename, $sourcefile, $fileinfo) {
		// Create the destination path, the attachment must
		// be placed in the attachment folder with a unique name.
		$filepath = $this->getAttachmentTmpPath($filename);

		// Obtain the generated filename
		$tmpname = mb_basename($filepath);

		// Move the uploaded file to tmpname location
		rename($sourcefile, $filepath);

		$this->addAttachmentFile($message_id, $tmpname, $fileinfo);

		return $tmpname;
	}

	/**
	 * Add information regarding embedded attachment to attachment state.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param array  $fileinfo   The attachment data
	 *
	 * @return The attachment identifier to be used for referencing the file in the tmp folder
	 */
	public function addEmbeddedAttachment($message_id, $fileinfo) {
		// generate a random number to be used as unique id of attachment
		$tmpname = md5(random_int(0, mt_getrandmax()));

		$this->addAttachmentFile($message_id, $tmpname, $fileinfo);

		return $tmpname;
	}

	/**
	 * Delete a file which was previously uploaded to the attachments directory,
	 * this will call removeAttachmentFile to unregister the attachment from the state.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $filename   The filename of the attachment to delete
	 * @param string $attachID   The unique identifier for referencing attachment
	 */
	public function deleteUploadedAttachmentFile($message_id, $filename, $attachID) {
		// Create the destination path, the attachment has
		// previously been placed in the attachment folder
		$filepath = $this->getAttachmentPath($filename);
		if (is_file($filepath)) {
			unlink($filepath);
		}

		$this->removeAttachmentFile($message_id, mb_basename($filepath), $attachID);
	}

	/**
	 * Obtain all files which were registered for the given $message_id.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 *
	 * @return array The array of attachments
	 */
	public function getAttachmentFiles($message_id) {
		if ($this->files && isset($this->files[$message_id])) {
			return $this->files[$message_id];
		}

		return false;
	}

	/**
	 * Obtain a single attachment file which belongs to the given $message_id
	 * and is identified by the given $attachid.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $attachid   The unique identifier for referencing the
	 *                           attachment
	 *
	 * @return array The attachment description for the requested attachment
	 */
	public function getAttachmentFile($message_id, $attachid) {
		if ($this->files && isset($this->files[$message_id], $this->files[$message_id][$attachid])) {
			return $this->files[$message_id][$attachid];
		}

		return false;
	}

	/**
	 * Add an attachment file to the message.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $name       The name of the new attachment
	 * @param array  $attachment The attachment data
	 */
	public function addAttachmentFile($message_id, $name, $attachment) {
		if (!$this->files) {
			$this->files = [$message_id => []];
		}
		elseif (!isset($this->files[$message_id])) {
			$this->files[$message_id] = [];
		}

		$this->files[$message_id][$name] = $attachment;
	}

	/**
	 * Remove an attachment file from the message.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $name       The name of the attachment
	 * @param string $attachID   The unique identifier for referencing attachment
	 */
	public function removeAttachmentFile($message_id, $name, $attachID) {
		if ($this->files && isset($this->files[$message_id])) {
			unset($this->files[$message_id][$name]);
		}

		// if attachID is supplied then user is trying to remove the attachment which is
		// still uploading
		if ($attachID) {
			$found = false;
			if (!empty($this->files)) {
				// Loop over and find the attachment from attachID and remove the same
				foreach ($this->files as $tmpDir => $attachment) {
					foreach ($this->files[$tmpDir] as $tmpName => $attachmentVal) {
						if ($this->files[$tmpDir][$tmpName]['attach_id'] === $attachID) {
							$found = true;
							$filepath = $this->getAttachmentPath($tmpName);
							if (is_file($filepath)) {
								unlink($filepath);
							}
							unset($this->files[$tmpDir][$tmpName]);
						}
					}
				}
			}

			if (!$found) {
				// if files array is empty or attachment is not found then just remember
				// this particular attachment, which will be removed when attachment_state
				// will be opened again.
				$this->addAbortedAttachment($message_id, $attachID);

				return;
			}
		}
	}

	/**
	 * Remove all attachment files from the message.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 */
	public function clearAttachmentFiles($message_id) {
		if ($this->files) {
			unset($this->files[$message_id]);
		}
	}

	/**
	 * Obtain all files which were removed for the given $message_id.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 *
	 * @return array The array of attachments
	 */
	public function getDeletedAttachments($message_id) {
		if ($this->deleteattachment && isset($this->deleteattachment[$message_id])) {
			return $this->deleteattachment[$message_id];
		}

		return false;
	}

	/**
	 * Add a deleted attachment file to the message.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $name       The name of the attachment
	 */
	public function addDeletedAttachment($message_id, $name) {
		if (!$this->deleteattachment) {
			$this->deleteattachment = [$message_id => []];
		}
		elseif (!isset($this->deleteattachment[$message_id])) {
			$this->deleteattachment[$message_id] = [];
		}

		$this->deleteattachment[$message_id][] = $name;
	}

	/**
	 * Remove a deleted attachment from the message.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $name       The name of the attachment
	 */
	public function removeDeletedAttachment($message_id, $name) {
		if ($this->deleteattachment && isset($this->deleteattachment[$message_id])) {
			$index = array_search($name, $this->deleteattachment[$message_id]);
			if ($index !== false) {
				unset($this->deleteattachment[$message_id][$index]);
				$this->deleteattachment[$message_id] = array_values($this->deleteattachment[$message_id]);
			}
		}
	}

	/**
	 * Add an aborted attachment file to the 'abortedattachment' array.
	 * So that it can be removed from message while it is available in 'files' array.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 * @param string $attachID   The unique identifier for referencing attachment
	 */
	public function addAbortedAttachment($message_id, $attachID) {
		if (!$this->abortedattachment) {
			$this->abortedattachment = [$message_id => []];
		}
		elseif (!isset($this->abortedattachment[$message_id])) {
			$this->abortedattachment[$message_id] = [];
		}

		$this->abortedattachment[$message_id][] = $attachID;
	}

	/**
	 * Remove an aborted attachment from the message and files list.
	 */
	public function removeAbortedAttachments() {
		if (empty($this->files)) {
			return;
		}

		// Loop over and find the attachment from attachID and remove the same
		foreach ($this->files as $tmpDir => $attachment) {
			foreach ($this->files[$tmpDir] as $tmpName => $attachmentVal) {
				if (!isset($this->abortedattachment[$tmpDir])) {
					continue;
				}

				// check if the aborted attachID is present in files array
				$index = array_search($this->files[$tmpDir][$tmpName]['attach_id'], $this->abortedattachment[$tmpDir]);
				if ($index !== false) {
					// Remove attachment from the list of aborted attachments.
					unset($this->abortedattachment[$tmpDir][$index]);

					// If respective file is still there in tmp directory then remove
					$filepath = $this->getAttachmentPath($tmpName);
					if (is_file($filepath)) {
						unlink($filepath);
					}

					// Remove attachment from state as well
					unset($this->files[$tmpDir][$tmpName]);
				}
			}
		}
	}

	/**
	 * Remove all deleted attachment files from the message.
	 *
	 * @param string $message_id The unique identifier for referencing the
	 *                           attachments for a single message
	 */
	public function clearDeletedAttachments($message_id) {
		if ($this->deleteattachment) {
			unset($this->deleteattachment[$message_id]);
		}
	}

	/**
	 * Close the state and flush all information back
	 * to the State file on the disk.
	 */
	public function close() {
		$this->state->write('files', $this->files, false);
		$this->state->write('deleteattachment', $this->deleteattachment, false);
		$this->state->write('abortedattachment', $this->abortedattachment, false);
		$this->state->flush();
		$this->state->close();
	}

	/**
	 * Cleans all old attachments in the attachment directory.
	 *
	 * @param int $maxLifeTime the maximum allowed age of files in seconds
	 */
	public function clean($maxLifeTime = UPLOADED_ATTACHMENT_MAX_LIFETIME) {
		cleanTemp($this->basedir, $maxLifeTime);

		// remove base directory also
		if (is_dir($this->sessiondir)) {
			/*
			 * FIXME: We should remove the folder when it is not needed,
			 * Because of WA-6523 this is creating an issue, so need to fix this properly
			 */
			// Directory should be empty to remove it, So first remove all files in the directory
			/*if(function_exists("glob")) {
				array_map('unlink', glob($this->sessiondir . "/*"));
			}*/
			rmdir($this->sessiondir);
		}
	}
}
