<?php

/**
 * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
 * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
 * SPDX-License-Identifier: AGPL-3.0-only
 */
namespace OCA\DAV\DAV\Sharing;

use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\Connector\Sabre\Auth;
use OCA\DAV\DAV\Sharing\Xml\Invite;
use OCA\DAV\DAV\Sharing\Xml\ShareRequest;
use OCP\AppFramework\Http;
use OCP\IConfig;
use OCP\IRequest;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;

class Plugin extends ServerPlugin {
	public const NS_OWNCLOUD = 'http://owncloud.org/ns';
	public const NS_NEXTCLOUD = 'http://nextcloud.com/ns';

	/**
	 * Plugin constructor.
	 *
	 * @param Auth $auth
	 * @param IRequest $request
	 * @param IConfig $config
	 */
	public function __construct(
		private Auth $auth,
		private IRequest $request,
		private IConfig $config,
	) {
	}

	/**
	 * Reference to SabreDAV server object.
	 *
	 * @var \Sabre\DAV\Server
	 */
	protected $server;

	/**
	 * This method should return a list of server-features.
	 *
	 * This is for example 'versioning' and is added to the DAV: header
	 * in an OPTIONS response.
	 *
	 * @return string[]
	 */
	public function getFeatures() {
		return ['oc-resource-sharing'];
	}

	/**
	 * Returns a plugin name.
	 *
	 * Using this name other plugins will be able to access other plugins
	 * using Sabre\DAV\Server::getPlugin
	 *
	 * @return string
	 */
	public function getPluginName() {
		return 'oc-resource-sharing';
	}

	/**
	 * This initializes the plugin.
	 *
	 * This function is called by Sabre\DAV\Server, after
	 * addPlugin is called.
	 *
	 * This method should set up the required event subscriptions.
	 *
	 * @param Server $server
	 * @return void
	 */
	public function initialize(Server $server) {
		$this->server = $server;
		$this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}share'] = ShareRequest::class;
		$this->server->xml->elementMap['{' . Plugin::NS_OWNCLOUD . '}invite'] = Invite::class;

		$this->server->on('method:POST', [$this, 'httpPost']);
		$this->server->on('propFind', [$this, 'propFind']);
	}

	/**
	 * We intercept this to handle POST requests on a dav resource.
	 *
	 * @param RequestInterface $request
	 * @param ResponseInterface $response
	 * @return null|false
	 */
	public function httpPost(RequestInterface $request, ResponseInterface $response) {
		$path = $request->getPath();

		// Only handling xml
		$contentType = (string)$request->getHeader('Content-Type');
		if (!str_contains($contentType, 'application/xml') && !str_contains($contentType, 'text/xml')) {
			return;
		}

		// Making sure the node exists
		try {
			$node = $this->server->tree->getNodeForPath($path);
		} catch (NotFound $e) {
			return;
		}

		$requestBody = $request->getBodyAsString();

		// If this request handler could not deal with this POST request, it
		// will return 'null' and other plugins get a chance to handle the
		// request.
		//
		// However, we already requested the full body. This is a problem,
		// because a body can only be read once. This is why we preemptively
		// re-populated the request body with the existing data.
		$request->setBody($requestBody);

		$message = $this->server->xml->parse($requestBody, $request->getUrl(), $documentType);

		switch ($documentType) {

			// Dealing with the 'share' document, which modified invitees on a
			// calendar.
			case '{' . self::NS_OWNCLOUD . '}share':

				// We can only deal with IShareableCalendar objects
				if (!$node instanceof IShareable) {
					return;
				}

				$this->server->transactionType = 'post-oc-resource-share';

				// Getting ACL info
				$acl = $this->server->getPlugin('acl');

				// If there's no ACL support, we allow everything
				if ($acl) {
					/** @var \Sabre\DAVACL\Plugin $acl */
					$acl->checkPrivileges($path, '{DAV:}write');

					$limitSharingToOwner = $this->config->getAppValue('dav', 'limitAddressBookAndCalendarSharingToOwner', 'no') === 'yes';
					$isOwner = $acl->getCurrentUserPrincipal() === $node->getOwner();
					if ($limitSharingToOwner && !$isOwner) {
						return;
					}
				}

				$node->updateShares($message->set, $message->remove);

				$response->setStatus(Http::STATUS_OK);
				// Adding this because sending a response body may cause issues,
				// and I wanted some type of indicator the response was handled.
				$response->setHeader('X-Sabre-Status', 'everything-went-well');

				// Breaking the event chain
				return false;
		}
	}

	/**
	 * This event is triggered when properties are requested for a certain
	 * node.
	 *
	 * This allows us to inject any properties early.
	 *
	 * @param PropFind $propFind
	 * @param INode $node
	 * @return void
	 */
	public function propFind(PropFind $propFind, INode $node) {
		if ($node instanceof CalendarHome && $propFind->getDepth() === 1) {
			$backend = $node->getCalDAVBackend();
			if ($backend instanceof CalDavBackend) {
				$calendars = $node->getChildren();
				$calendars = array_filter($calendars, function (INode $node) {
					return $node instanceof IShareable;
				});
				/** @var int[] $resourceIds */
				$resourceIds = array_map(function (IShareable $node) {
					return $node->getResourceId();
				}, $calendars);
				$backend->preloadShares($resourceIds);
			}
		}
		if ($node instanceof IShareable) {
			$propFind->handle('{' . Plugin::NS_OWNCLOUD . '}invite', function () use ($node) {
				return new Invite(
					$node->getShares()
				);
			});
		}
	}
}
