<?php

declare(strict_types=1);

namespace Sabre\DAV\Sharing;

use Sabre\DAV\Exception\BadRequest;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\INode;
use Sabre\DAV\PropFind;
use Sabre\DAV\Server;
use Sabre\DAV\ServerPlugin;
use Sabre\DAV\Xml\Element\Sharee;
use Sabre\DAV\Xml\Property;
use Sabre\HTTP\RequestInterface;
use Sabre\HTTP\ResponseInterface;

/**
 * This plugin implements HTTP requests and properties related to:.
 *
 * draft-pot-webdav-resource-sharing
 *
 * This specification allows people to share webdav resources with others.
 *
 * @copyright Copyright (C) 2007-2015 fruux GmbH. (https://fruux.com/)
 * @author Evert Pot (http://evertpot.com/)
 * @license http://sabre.io/license/ Modified BSD License
 */
class Plugin extends ServerPlugin
{
    const ACCESS_NOTSHARED = 0;
    const ACCESS_SHAREDOWNER = 1;
    const ACCESS_READ = 2;
    const ACCESS_READWRITE = 3;
    const ACCESS_NOACCESS = 4;

    const INVITE_NORESPONSE = 1;
    const INVITE_ACCEPTED = 2;
    const INVITE_DECLINED = 3;
    const INVITE_INVALID = 4;

    /**
     * Reference to SabreDAV server object.
     *
     * @var 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 array
     */
    public function getFeatures()
    {
        return ['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 '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
     */
    public function initialize(Server $server)
    {
        $this->server = $server;

        $server->xml->elementMap['{DAV:}share-resource'] = 'Sabre\\DAV\\Xml\\Request\\ShareResource';

        array_push(
            $server->protectedProperties,
            '{DAV:}share-mode'
        );

        $server->on('method:POST', [$this, 'httpPost']);
        $server->on('propFind', [$this, 'propFind']);
        $server->on('getSupportedPrivilegeSet', [$this, 'getSupportedPrivilegeSet']);
        $server->on('onHTMLActionsPanel', [$this, 'htmlActionsPanel']);
        $server->on('onBrowserPostAction', [$this, 'browserPostAction']);
    }

    /**
     * Updates the list of sharees on a shared resource.
     *
     * The sharees  array is a list of people that are to be added modified
     * or removed in the list of shares.
     *
     * @param string   $path
     * @param Sharee[] $sharees
     */
    public function shareResource($path, array $sharees)
    {
        $node = $this->server->tree->getNodeForPath($path);

        if (!$node instanceof ISharedNode) {
            throw new Forbidden('Sharing is not allowed on this node');
        }

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

        // If there's no ACL support, we allow everything
        if ($acl) {
            $acl->checkPrivileges($path, '{DAV:}share');
        }

        foreach ($sharees as $sharee) {
            // We're going to attempt to get a local principal uri for a share
            // href by emitting the getPrincipalByUri event.
            $principal = null;
            $this->server->emit('getPrincipalByUri', [$sharee->href, &$principal]);
            $sharee->principal = $principal;
        }
        $node->updateInvites($sharees);
    }

    /**
     * This event is triggered when properties are requested for nodes.
     *
     * This allows us to inject any sharings-specific properties.
     *
     * @param PropFind $propFind
     * @param INode    $node
     */
    public function propFind(PropFind $propFind, INode $node)
    {
        if ($node instanceof ISharedNode) {
            $propFind->handle('{DAV:}share-access', function () use ($node) {
                return new Property\ShareAccess($node->getShareAccess());
            });
            $propFind->handle('{DAV:}invite', function () use ($node) {
                return new Property\Invite($node->getInvites());
            });
            $propFind->handle('{DAV:}share-resource-uri', function () use ($node) {
                return new Property\Href($node->getShareResourceUri());
            });
        }
    }

    /**
     * We intercept this to handle POST requests on shared resources.
     *
     * @param RequestInterface  $request
     * @param ResponseInterface $response
     *
     * @return bool|null
     */
    public function httpPost(RequestInterface $request, ResponseInterface $response)
    {
        $path = $request->getPath();
        $contentType = $request->getHeader('Content-Type');
        if (null === $contentType) {
            return;
        }

        // We're only interested in the davsharing content type.
        if (false === strpos($contentType, 'application/davsharing+xml')) {
            return;
        }

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

        switch ($documentType) {
            case '{DAV:}share-resource':

                $this->shareResource($path, $message->sharees);
                $response->setStatus(200);
                // 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;

            default:
                throw new BadRequest('Unexpected document type: '.$documentType.' for this Content-Type');
        }
    }

    /**
     * This method is triggered whenever a subsystem reqeuests the privileges
     * hat are supported on a particular node.
     *
     * We need to add a number of privileges for scheduling purposes.
     *
     * @param INode $node
     * @param array $supportedPrivilegeSet
     */
    public function getSupportedPrivilegeSet(INode $node, array &$supportedPrivilegeSet)
    {
        if ($node instanceof ISharedNode) {
            $supportedPrivilegeSet['{DAV:}share'] = [
                'abstract' => false,
                'aggregates' => [],
            ];
        }
    }

    /**
     * Returns a bunch of meta-data about the plugin.
     *
     * Providing this information is optional, and is mainly displayed by the
     * Browser plugin.
     *
     * The description key in the returned array may contain html and will not
     * be sanitized.
     *
     * @return array
     */
    public function getPluginInfo()
    {
        return [
            'name' => $this->getPluginName(),
            'description' => 'This plugin implements WebDAV resource sharing',
            'link' => 'https://github.com/evert/webdav-sharing',
        ];
    }

    /**
     * This method is used to generate HTML output for the
     * DAV\Browser\Plugin.
     *
     * @param INode  $node
     * @param string $output
     * @param string $path
     *
     * @return bool|null
     */
    public function htmlActionsPanel(INode $node, &$output, $path)
    {
        if (!$node instanceof ISharedNode) {
            return;
        }

        $aclPlugin = $this->server->getPlugin('acl');
        if ($aclPlugin) {
            if (!$aclPlugin->checkPrivileges($path, '{DAV:}share', \Sabre\DAVACL\Plugin::R_PARENT, false)) {
                // Sharing is not permitted, we will not draw this interface.
                return;
            }
        }

        $output .= '<tr><td colspan="2"><form method="post" action="">
            <h3>Share this resource</h3>
            <input type="hidden" name="sabreAction" value="share" />
            <label>Share with (uri):</label> <input type="text" name="href" placeholder="mailto:user@example.org"/><br />
            <label>Access</label>
                <select name="access">
                    <option value="readwrite">Read-write</option>
                    <option value="read">Read-only</option>
                    <option value="no-access">Revoke access</option>
                </select><br />
             <input type="submit" value="share" />
            </form>
            </td></tr>';
    }

    /**
     * This method is triggered for POST actions generated by the browser
     * plugin.
     *
     * @param string $path
     * @param string $action
     * @param array  $postVars
     */
    public function browserPostAction($path, $action, $postVars)
    {
        if ('share' !== $action) {
            return;
        }

        if (empty($postVars['href'])) {
            throw new BadRequest('The "href" POST parameter is required');
        }
        if (empty($postVars['access'])) {
            throw new BadRequest('The "access" POST parameter is required');
        }

        $accessMap = [
            'readwrite' => self::ACCESS_READWRITE,
            'read' => self::ACCESS_READ,
            'no-access' => self::ACCESS_NOACCESS,
        ];

        if (!isset($accessMap[$postVars['access']])) {
            throw new BadRequest('The "access" POST must be readwrite, read or no-access');
        }
        $sharee = new Sharee([
            'href' => $postVars['href'],
            'access' => $accessMap[$postVars['access']],
        ]);

        $this->shareResource(
            $path,
            [$sharee]
        );

        return false;
    }
}
