<?php

/**
 * ownCloud
 *
 * @copyright (C) 2014 ownCloud, Inc.
 *
 * @author Sam Tuke <samtuke@owncloud.org>
 * @author Bjoern Schiessle <schiessle@owncloud.com>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or any later version.
 *
 * This library 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 library.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

namespace OCA\Encryption;

use OC\Files\Filesystem;

/**
 * Class for hook specific logic
 */
class Hooks {

	// file for which we want to rename the keys after the rename operation was successful
	private static $renamedFiles = array();
	// file for which we want to delete the keys after the delete operation was successful
	private static $deleteFiles = array();
	// file for which we want to delete the keys after the delete operation was successful
	private static $unmountedFiles = array();

	/**
	 * Startup encryption backend upon user login
	 * @note This method should never be called for users using client side encryption
	 */
	public static function login($params) {

		if (\OCP\App::isEnabled('files_encryption') === false) {
			return true;
		}


		$l = new \OC_L10N('files_encryption');

		$view = new \OC\Files\View('/');

		// ensure filesystem is loaded
		if (!\OC\Files\Filesystem::$loaded) {
			\OC_Util::setupFS($params['uid']);
		}

		$privateKey = \OCA\Encryption\Keymanager::getPrivateKey($view, $params['uid']);

		// if no private key exists, check server configuration
		if (!$privateKey) {
			//check if all requirements are met
			if (!Helper::checkRequirements() || !Helper::checkConfiguration()) {
				$error_msg = $l->t("Missing requirements.");
				$hint = $l->t('Please make sure that OpenSSL together with the PHP extension is enabled and configured properly. For now, the encryption app has been disabled.');
				\OC_App::disable('files_encryption');
				\OCP\Util::writeLog('Encryption library', $error_msg . ' ' . $hint, \OCP\Util::ERROR);
				\OCP\Template::printErrorPage($error_msg, $hint);
			}
		}

		$util = new Util($view, $params['uid']);

		// setup user, if user not ready force relogin
		if (Helper::setupUser($util, $params['password']) === false) {
			return false;
		}

		$session = $util->initEncryption($params);

		// Check if first-run file migration has already been performed
		$ready = false;
		$migrationStatus = $util->getMigrationStatus();
		if ($migrationStatus === Util::MIGRATION_OPEN && $session !== false) {
			$ready = $util->beginMigration();
		} elseif ($migrationStatus === Util::MIGRATION_IN_PROGRESS) {
			// refuse login as long as the initial encryption is running
			sleep(5);
			\OCP\User::logout();
			return false;
		}

		$result = true;

		// If migration not yet done
		if ($ready) {

			// Encrypt existing user files
			try {
				$result = $util->encryptAll('/' . $params['uid'] . '/' . 'files');
			} catch (\Exception $ex) {
				\OCP\Util::writeLog('Encryption library', 'Initial encryption failed! Error: ' . $ex->getMessage(), \OCP\Util::FATAL);
				$result = false;
			}

			if ($result) {
				\OC_Log::write(
						'Encryption library', 'Encryption of existing files belonging to "' . $params['uid'] . '" completed'
						, \OC_Log::INFO
					);
				// Register successful migration in DB
				$util->finishMigration();
			} else  {
				\OCP\Util::writeLog('Encryption library', 'Initial encryption failed!', \OCP\Util::FATAL);
				$util->resetMigrationStatus();
				\OCP\User::logout();
			}
		}

		return $result;
	}

	/**
	 * remove keys from session during logout
	 */
	public static function logout() {
		$session = new \OCA\Encryption\Session(new \OC\Files\View());
		$session->removeKeys();
	}

	/**
	 * setup encryption backend upon user created
	 * @note This method should never be called for users using client side encryption
	 */
	public static function postCreateUser($params) {

		if (\OCP\App::isEnabled('files_encryption')) {
			$view = new \OC\Files\View('/');
			$util = new Util($view, $params['uid']);
			Helper::setupUser($util, $params['password']);
		}
	}

	/**
	 * cleanup encryption backend upon user deleted
	 * @note This method should never be called for users using client side encryption
	 */
	public static function postDeleteUser($params) {

		if (\OCP\App::isEnabled('files_encryption')) {
			Keymanager::deletePublicKey(new \OC\Files\View(), $params['uid']);
		}
	}

	/**
	 * If the password can't be changed within ownCloud, than update the key password in advance.
	 */
	public static function preSetPassphrase($params) {
		if (\OCP\App::isEnabled('files_encryption')) {
			if ( ! \OC_User::canUserChangePassword($params['uid']) ) {
				self::setPassphrase($params);
			}
		}
	}

	/**
	 * Change a user's encryption passphrase
	 * @param array $params keys: uid, password
	 */
	public static function setPassphrase($params) {
		if (\OCP\App::isEnabled('files_encryption') === false) {
			return true;
		}

		// Only attempt to change passphrase if server-side encryption
		// is in use (client-side encryption does not have access to
		// the necessary keys)
		if (Crypt::mode() === 'server') {

			$view = new \OC\Files\View('/');
			$session = new \OCA\Encryption\Session($view);

			// Get existing decrypted private key
			$privateKey = $session->getPrivateKey();

			if ($params['uid'] === \OCP\User::getUser() && $privateKey) {

				// Encrypt private key with new user pwd as passphrase
				$encryptedPrivateKey = Crypt::symmetricEncryptFileContent($privateKey, $params['password'], Helper::getCipher());

				// Save private key
				if ($encryptedPrivateKey) {
					Keymanager::setPrivateKey($encryptedPrivateKey, \OCP\User::getUser());
				} else {
					\OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR);
				}

				// NOTE: Session does not need to be updated as the
				// private key has not changed, only the passphrase
				// used to decrypt it has changed


			} else { // admin changed the password for a different user, create new keys and reencrypt file keys

				$user = $params['uid'];
				$util = new Util($view, $user);
				$recoveryPassword = isset($params['recoveryPassword']) ? $params['recoveryPassword'] : null;

				// we generate new keys if...
				// ...we have a recovery password and the user enabled the recovery key
				// ...encryption was activated for the first time (no keys exists)
				// ...the user doesn't have any files
				if (($util->recoveryEnabledForUser() && $recoveryPassword)
						|| !$util->userKeysExists()
						|| !$view->file_exists($user . '/files')) {

					// backup old keys
					$util->backupAllKeys('recovery');

					$newUserPassword = $params['password'];

					// make sure that the users home is mounted
					\OC\Files\Filesystem::initMountPoints($user);

					$keypair = Crypt::createKeypair();

					// Disable encryption proxy to prevent recursive calls
					$proxyStatus = \OC_FileProxy::$enabled;
					\OC_FileProxy::$enabled = false;

					// Save public key
					Keymanager::setPublicKey($keypair['publicKey'], $user);

					// Encrypt private key with new password
					$encryptedKey = \OCA\Encryption\Crypt::symmetricEncryptFileContent($keypair['privateKey'], $newUserPassword, Helper::getCipher());
					if ($encryptedKey) {
						Keymanager::setPrivateKey($encryptedKey, $user);

						if ($recoveryPassword) { // if recovery key is set we can re-encrypt the key files
							$util = new Util($view, $user);
							$util->recoverUsersFiles($recoveryPassword);
						}
					} else {
						\OCP\Util::writeLog('files_encryption', 'Could not update users encryption password', \OCP\Util::ERROR);
					}

					\OC_FileProxy::$enabled = $proxyStatus;
				}
			}
		}
	}

	/**
	 * after password reset we create a new key pair for the user
	 *
	 * @param array $params
	 */
	public static function postPasswordReset($params) {
		$uid = $params['uid'];
		$password = $params['password'];

		$util = new Util(new \OC\Files\View(), $uid);
		$util->replaceUserKeys($password);
	}

	/*
	 * check if files can be encrypted to every user.
	 */
	/**
	 * @param array $params
	 */
	public static function preShared($params) {

		if (\OCP\App::isEnabled('files_encryption') === false) {
			return true;
		}

		$l = new \OC_L10N('files_encryption');
		$users = array();
		$view = new \OC\Files\View('/');

		switch ($params['shareType']) {
			case \OCP\Share::SHARE_TYPE_USER:
				$users[] = $params['shareWith'];
				break;
			case \OCP\Share::SHARE_TYPE_GROUP:
				$users = \OC_Group::usersInGroup($params['shareWith']);
				break;
		}

		$notConfigured = array();
		foreach ($users as $user) {
			if (!Keymanager::publicKeyExists($view, $user)) {
				$notConfigured[] = $user;
			}
		}

		if (count($notConfigured) > 0) {
			$params['run'] = false;
			$params['error'] = $l->t('Following users are not set up for encryption:') . ' ' . join(', ' , $notConfigured);
		}

	}

	/**
	 * update share keys if a file was shared
	 */
	public static function postShared($params) {

		if (\OCP\App::isEnabled('files_encryption') === false) {
			return true;
		}

		if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {

			$path = \OC\Files\Filesystem::getPath($params['fileSource']);

			self::updateKeyfiles($path);
		}
	}

	/**
	 * update keyfiles and share keys recursively
	 *
	 * @param string $path to the file/folder
	 */
	private static function updateKeyfiles($path) {
		$view = new \OC\Files\View('/');
		$userId = \OCP\User::getUser();
		$session = new \OCA\Encryption\Session($view);
		$util = new Util($view, $userId);
		$sharingEnabled = \OCP\Share::isEnabled();

		$mountManager = \OC\Files\Filesystem::getMountManager();
		$mount = $mountManager->find('/' . $userId . '/files' . $path);
		$mountPoint = $mount->getMountPoint();

		// if a folder was shared, get a list of all (sub-)folders
		if ($view->is_dir('/' . $userId . '/files' . $path)) {
			$allFiles = $util->getAllFiles($path, $mountPoint);
		} else {
			$allFiles = array($path);
		}

		foreach ($allFiles as $path) {
			$usersSharing = $util->getSharingUsersArray($sharingEnabled, $path);
			$util->setSharedFileKeyfiles($session, $usersSharing, $path);
		}
	}

	/**
	 * unshare file/folder from a user with whom you shared the file before
	 */
	public static function postUnshare($params) {

		if (\OCP\App::isEnabled('files_encryption') === false) {
			return true;
		}

		if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') {

			$view = new \OC\Files\View('/');
			$userId = \OCP\User::getUser();
			$util = new Util($view, $userId);
			$path = \OC\Files\Filesystem::getPath($params['fileSource']);

			// for group shares get a list of the group members
			if ($params['shareType'] === \OCP\Share::SHARE_TYPE_GROUP) {
				$userIds = \OC_Group::usersInGroup($params['shareWith']);
			} else {
				if ($params['shareType'] === \OCP\Share::SHARE_TYPE_LINK) {
					$userIds = array($util->getPublicShareKeyId());
				} else {
					$userIds = array($params['shareWith']);
				}
			}

			$mountManager = \OC\Files\Filesystem::getMountManager();
			$mount = $mountManager->find('/' . $userId . '/files' . $path);
			$mountPoint = $mount->getMountPoint();

			// if we unshare a folder we need a list of all (sub-)files
			if ($params['itemType'] === 'folder') {
				$allFiles = $util->getAllFiles($path, $mountPoint);
			} else {
				$allFiles = array($path);
			}

			foreach ($allFiles as $path) {

				// check if the user still has access to the file, otherwise delete share key
				$sharingUsers = $util->getSharingUsersArray(true, $path);

				// Unshare every user who no longer has access to the file
				$delUsers = array_diff($userIds, $sharingUsers);
				$keyPath = Keymanager::getKeyPath($view, $util, $path);

				// delete share key
				Keymanager::delShareKey($view, $delUsers, $keyPath, $userId, $path);
			}

		}
	}

	/**
	 * mark file as renamed so that we know the original source after the file was renamed
	 * @param array $params with the old path and the new path
	 */
	public static function preRename($params) {
		self::preRenameOrCopy($params, 'rename');
	}

	/**
	 * mark file as copied so that we know the original source after the file was copied
	 * @param array $params with the old path and the new path
	 */
	public static function preCopy($params) {
		self::preRenameOrCopy($params, 'copy');
	}

	private static function preRenameOrCopy($params, $operation) {
		$user = \OCP\User::getUser();
		$view = new \OC\Files\View('/');
		$util = new Util($view, $user);

		// we only need to rename the keys if the rename happens on the same mountpoint
		// otherwise we perform a stream copy, so we get a new set of keys
		$mp1 = $view->getMountPoint('/' . $user . '/files/' . $params['oldpath']);
		$mp2 = $view->getMountPoint('/' . $user . '/files/' . $params['newpath']);

		$oldKeysPath = Keymanager::getKeyPath($view, $util, $params['oldpath']);

		if ($mp1 === $mp2) {
			self::$renamedFiles[$params['oldpath']] = array(
				'operation' => $operation,
				'oldKeysPath' => $oldKeysPath,
				);
		} else {
			self::$renamedFiles[$params['oldpath']] = array(
				'operation' => 'cleanup',
				'oldKeysPath' => $oldKeysPath,
				);
		}
	}

	/**
	 * after a file is renamed/copied, rename/copy its keyfile and share-keys also fix the file size and fix also the sharing
	 *
	 * @param array $params array with oldpath and newpath
	 */
	public static function postRenameOrCopy($params) {

		if (\OCP\App::isEnabled('files_encryption') === false) {
			return true;
		}

		$view = new \OC\Files\View('/');
		$userId = \OCP\User::getUser();
		$util = new Util($view, $userId);

		if (isset(self::$renamedFiles[$params['oldpath']]['operation']) &&
				isset(self::$renamedFiles[$params['oldpath']]['oldKeysPath'])) {
			$operation = self::$renamedFiles[$params['oldpath']]['operation'];
			$oldKeysPath = self::$renamedFiles[$params['oldpath']]['oldKeysPath'];
			unset(self::$renamedFiles[$params['oldpath']]);
			if ($operation === 'cleanup') {
				return $view->unlink($oldKeysPath);
			}
		} else {
			\OCP\Util::writeLog('Encryption library', "can't get path and owner from the file before it was renamed", \OCP\Util::DEBUG);
			return false;
		}

		list($ownerNew, $pathNew) = $util->getUidAndFilename($params['newpath']);

		if ($util->isSystemWideMountPoint($pathNew)) {
			$newKeysPath =  'files_encryption/keys/' . $pathNew;
		} else {
			$newKeysPath = $ownerNew . '/files_encryption/keys/' . $pathNew;
		}

		// create  key folders if it doesn't exists
		if (!$view->file_exists(dirname($newKeysPath))) {
			$view->mkdir(dirname($newKeysPath));
		}

		$view->$operation($oldKeysPath, $newKeysPath);

		// update sharing-keys
		self::updateKeyfiles($params['newpath']);
	}

	/**
	 * set migration status and the init status back to '0' so that all new files get encrypted
	 * if the app gets enabled again
	 * @param array $params contains the app ID
	 */
	public static function preDisable($params) {
		if ($params['app'] === 'files_encryption') {

			\OC_Preferences::deleteAppFromAllUsers('files_encryption');

			$session = new \OCA\Encryption\Session(new \OC\Files\View('/'));
			$session->setInitialized(\OCA\Encryption\Session::NOT_INITIALIZED);
		}
	}

	/**
	 * set the init status to 'NOT_INITIALIZED' (0) if the app gets enabled
	 * @param array $params contains the app ID
	 */
	public static function postEnable($params) {
		if ($params['app'] === 'files_encryption') {
			$session = new \OCA\Encryption\Session(new \OC\Files\View('/'));
			$session->setInitialized(\OCA\Encryption\Session::NOT_INITIALIZED);
		}
	}

	/**
	 * if the file was really deleted we remove the encryption keys
	 * @param array $params
	 * @return boolean|null
	 */
	public static function postDelete($params) {

		$path = $params[\OC\Files\Filesystem::signal_param_path];

		if (!isset(self::$deleteFiles[$path])) {
			return true;
		}

		$deletedFile = self::$deleteFiles[$path];
		$keyPath = $deletedFile['keyPath'];

		// we don't need to remember the file any longer
		unset(self::$deleteFiles[$path]);

		$view = new \OC\Files\View('/');

		// return if the file still exists and wasn't deleted correctly
		if ($view->file_exists('/' . \OCP\User::getUser() . '/files/' . $path)) {
			return true;
		}

		// Delete keyfile & shareKey so it isn't orphaned
		$view->unlink($keyPath);

	}

	/**
	 * remember the file which should be deleted and it's owner
	 * @param array $params
	 * @return boolean|null
	 */
	public static function preDelete($params) {
		$view = new \OC\Files\View('/');
		$path = $params[\OC\Files\Filesystem::signal_param_path];

		// skip this method if the trash bin is enabled or if we delete a file
		// outside of /data/user/files
		if (\OCP\App::isEnabled('files_trashbin')) {
			return true;
		}

		$util = new Util($view, \OCP\USER::getUser());

		$keysPath = Keymanager::getKeyPath($view, $util, $path);

		self::$deleteFiles[$path] = array(
			'keyPath' => $keysPath);
	}

	/**
	 * unmount file from yourself
	 * remember files/folders which get unmounted
	 */
	public static function preUnmount($params) {
		$view = new \OC\Files\View('/');
		$user = \OCP\User::getUser();
		$path = $params[\OC\Files\Filesystem::signal_param_path];

		$util = new Util($view, $user);
		list($owner, $ownerPath) = $util->getUidAndFilename($path);

		$keysPath = Keymanager::getKeyPath($view, $util, $path);

		self::$unmountedFiles[$path] = array(
			'keyPath' => $keysPath,
			'owner' => $owner,
			'ownerPath' => $ownerPath
		);
	}

	/**
	 * unmount file from yourself
	 */
	public static function postUnmount($params) {

		$path = $params[\OC\Files\Filesystem::signal_param_path];
		$user = \OCP\User::getUser();

		if (!isset(self::$unmountedFiles[$path])) {
			return true;
		}

		$umountedFile = self::$unmountedFiles[$path];
		$keyPath = $umountedFile['keyPath'];
		$owner = $umountedFile['owner'];
		$ownerPath = $umountedFile['ownerPath'];

		$view = new \OC\Files\View();

		// we don't need to remember the file any longer
		unset(self::$unmountedFiles[$path]);

		// check if the user still has access to the file, otherwise delete share key
		$sharingUsers = \OCP\Share::getUsersSharingFile($path, $user);
		if (!in_array(\OCP\User::getUser(), $sharingUsers['users'])) {
			Keymanager::delShareKey($view, array(\OCP\User::getUser()), $keyPath, $owner, $ownerPath);
		}
	}

}
