866 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			866 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| 
 | |
| namespace Aerex\Taskwarrior;
 | |
| 
 | |
| use Sabre\DAV\Exception\BadRequest;
 | |
| use Sabre\VObject;
 | |
| use Sabre\HTTP\RequestInterface;
 | |
| use Sabre\HTTP\ResponseInterface;
 | |
| use Sabre\Xml\ParseException;
 | |
| use Aerex\Taskwarrior\TaskwarriorVCalendarManger;
 | |
| use DavidBadura\Taskwarrior\TaskManager;
 | |
| use DavidBadura\Taskwarrior\Task;
 | |
| use DavidBadura\Taskwarrior\Recurring;
 | |
| use DavidBadura\Taskwarrior\Annotation;
 | |
| 
 | |
| /**
 | |
|  * The plugin to interact with Baikal and Taskwarrior
 | |
|  *
 | |
|  */
 | |
| class Taskwarrior extends ServerPlugin {
 | |
| 
 | |
|     /**
 | |
|      * Reference to server object.
 | |
|      *
 | |
|      * @var Server
 | |
|      */
 | |
|     protected $server;
 | |
|     private $TWCalManager;
 | |
| 
 | |
|     /**
 | |
|      * Sets up the plugin
 | |
|      *
 | |
|      * @param Server $server
 | |
|      * @return void
 | |
|      */
 | |
|     function initialize(Server $server) {
 | |
| 
 | |
|         $this->server = $server;
 | |
|         $this->$TWCalManager = new TaskwarriorCalendarEvent();
 | |
|         $server->on('method:GET',       [$this, 'httpGet']);
 | |
|         $server->on('method:OPTIONS',   [$this, 'httpOptions']);
 | |
|         $server->on('method:HEAD',      [$this, 'httpHead']);
 | |
|         $server->on('method:DELETE',    [$this, 'httpDelete']);
 | |
|         $server->on('method:PROPFIND',  [$this, 'httpPropFind']);
 | |
|         $server->on('method:PROPPATCH', [$this, 'httpPropPatch']);
 | |
|         $server->on('method:PUT',       [$this, 'httpPut']);
 | |
|         $server->on('method:MKCOL',     [$this, 'httpMkcol']);
 | |
|         $server->on('method:MOVE',      [$this, 'httpMove']);
 | |
|         $server->on('method:COPY',      [$this, 'httpCopy']);
 | |
|         $server->on('method:REPORT',    [$this, 'httpReport']);
 | |
| 
 | |
|         $server->on('propPatch',        [$this, 'propPatchProtectedPropertyCheck'], 90);
 | |
|         $server->on('propPatch',        [$this, 'propPatchNodeUpdate'], 200);
 | |
|         $server->on('propFind',         [$this, 'propFind']);
 | |
|         $server->on('propFind',         [$this, 'propFindNode'], 120);
 | |
|         $server->on('propFind',         [$this, 'propFindLate'], 200);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Returns a plugin name.
 | |
|      *
 | |
|      * Using this name other plugins will be able to access other plugins
 | |
|      * using DAV\Server::getPlugin
 | |
|      *
 | |
|      * @return string
 | |
|      */
 | |
|     function getPluginName() {
 | |
| 
 | |
|         return 'taskwarrior';
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method will parse a calendar object and create an new task in taskwarrior
 | |
|      *
 | |
|      * @param VCalendar $vCal parsed calendar object
 | |
|      */
 | |
|     function processCalendarEventForTaskwarrior(VCalendar $vCal){
 | |
|       try {
 | |
|         $TWCalManager->buildCalendarEvent($VCal->VEVENT);
 | |
|         $TWCalManager->buildToDoEvent($VCal->VTODO);
 | |
|       } catch(Exception $e){
 | |
|           throw new BadRequest($e->getMessage(), null, $e);
 | |
|       }
 | |
|     }
 | |
|     /**
 | |
|      * * This method is triggered whenever there was a calendar object gets
 | |
|      * * created or updated.
 | |
|      * *
 | |
|      * * @param RequestInterface $request HTTP request
 | |
|      * * @param ResponseInterface $response HTTP Response
 | |
|      * * @param VCalendar $vCal Parsed iCalendar object
 | |
|      * * @param mixed $calendarPath Path to calendar collection
 | |
|      * * @param mixed $modified The iCalendar object has been touched.
 | |
|      * * @param mixed $isNew Whether this was a new item or we're updating one
 | |
|      * * @return void
 | |
|      *                                                        */
 | |
|     function calendarObjectChange(RequestInterface $request, ResponseInterface $response, VCalendar $vCal, $calendarPath, &$modified, $isNew) {
 | |
|       $calendarNode = $this->server->tree->getNodeForPath($calendarPath);
 | |
|       $addresses = $this->getAddressesForPrincipal(
 | |
|         $calendarNode->getOwner()
 | |
|       );
 | |
|       if ($isNew) {
 | |
|         try {
 | |
|           processCalendarEventForTaskwarrior($vCal);
 | |
|         } catch(Exception\BadRequest $e){
 | |
|           $response->setStatus(200);
 | |
|           $response->setBody('');
 | |
|           $response->setHeader('Content-Type', 'text/plain');
 | |
|           $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode());
 | |
|         }
 | |
|       }
 | |
|     }    
 | |
|     /**
 | |
|      * This is the default implementation for the GET method.
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpGet(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $path = $request->getPath();
 | |
|       $node = $this->server->tree->getNodeForPath($path);
 | |
| 
 | |
|       if (!$node instanceof IFile) return;
 | |
| 
 | |
|       $body = $node->get();
 | |
| 
 | |
|       echo($body);
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP OPTIONS
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpOptions(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $methods = $this->server->getAllowedMethods($request->getPath());
 | |
|       echo('httpOptions ' . $response);
 | |
| 
 | |
|       $response->setHeader('Allow', strtoupper(implode(', ', $methods)));
 | |
|       $features = ['1', '3', 'extended-mkcol'];
 | |
| 
 | |
|       foreach ($this->server->getPlugins() as $plugin) {
 | |
|         $features = array_merge($features, $plugin->getFeatures());
 | |
|       }
 | |
| 
 | |
|       $response->setHeader('DAV', implode(', ', $features));
 | |
|       $response->setHeader('MS-Author-Via', 'DAV');
 | |
|       $response->setHeader('Accept-Ranges', 'bytes');
 | |
|       $response->setHeader('Content-Length', '0');
 | |
|       $response->setStatus(200);
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP HEAD
 | |
|      *
 | |
|      * This method is normally used to take a peak at a url, and only get the
 | |
|      * HTTP response headers, without the body. This is used by clients to
 | |
|      * determine if a remote file was changed, so they can use a local cached
 | |
|      * version, instead of downloading it again
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpHead(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       // This is implemented by changing the HEAD request to a GET request,
 | |
|       // and dropping the response body.
 | |
|       $subRequest = clone $request;
 | |
|       $subRequest->setMethod('GET');
 | |
| 
 | |
|       try {
 | |
|         $this->server->invokeMethod($subRequest, $response, false);
 | |
|         $response->setBody('');
 | |
|       } catch (Exception\NotImplemented $e) {
 | |
|         // Some clients may do HEAD requests on collections, however, GET
 | |
|         // requests and HEAD requests _may_ not be defined on a collection,
 | |
|         // which would trigger a 501.
 | |
|         // This breaks some clients though, so we're transforming these
 | |
|         // 501s into 200s.
 | |
|         $response->setStatus(200);
 | |
|         $response->setBody('');
 | |
|         $response->setHeader('Content-Type', 'text/plain');
 | |
|         $response->setHeader('X-Sabre-Real-Status', $e->getHTTPCode());
 | |
|       }
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP Delete
 | |
|      *
 | |
|      * The HTTP delete method, deletes a given uri
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return void
 | |
|      */
 | |
|     function httpDelete(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $path = $request->getPath();
 | |
|       echo('httpDelete.path ' . $path);
 | |
| 
 | |
|       if (!$this->server->emit('beforeUnbind', [$path])) return false;
 | |
|       $this->server->tree->delete($path);
 | |
|       $this->server->emit('afterUnbind', [$path]);
 | |
| 
 | |
|       $response->setStatus(204);
 | |
|       $response->setHeader('Content-Length', '0');
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * WebDAV PROPFIND
 | |
|      *
 | |
|      * This WebDAV method requests information about an uri resource, or a list of resources
 | |
|      * If a client wants to receive the properties for a single resource it will add an HTTP Depth: header with a 0 value
 | |
|      * If the value is 1, it means that it also expects a list of sub-resources (e.g.: files in a directory)
 | |
|      *
 | |
|      * The request body contains an XML data structure that has a list of properties the client understands
 | |
|      * The response body is also an xml document, containing information about every uri resource and the requested properties
 | |
|      *
 | |
|      * It has to return a HTTP 207 Multi-status status code
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return void
 | |
|      */
 | |
|     function httpPropFind(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $path = $request->getPath();
 | |
| 
 | |
|       $requestBody = $request->getBodyAsString();
 | |
|       if (strlen($requestBody)) {
 | |
|         try {
 | |
|           $propFindXml = $this->server->xml->expect('{DAV:}propfind', $requestBody);
 | |
|         } catch (ParseException $e) {
 | |
|           throw new BadRequest($e->getMessage(), null, $e);
 | |
|         }
 | |
|       } else {
 | |
|         $propFindXml = new Xml\Request\PropFind();
 | |
|         $propFindXml->allProp = true;
 | |
|         $propFindXml->properties = [];
 | |
|       }
 | |
| 
 | |
|       $depth = $this->server->getHTTPDepth(1);
 | |
|       // The only two options for the depth of a propfind is 0 or 1 - as long as depth infinity is not enabled
 | |
|       if (!$this->server->enablePropfindDepthInfinity && $depth != 0) $depth = 1;
 | |
| 
 | |
|       $newProperties = $this->server->getPropertiesForPath($path, $propFindXml->properties, $depth);
 | |
| 
 | |
|       // This is a multi-status response
 | |
|       $response->setStatus(207);
 | |
|       $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
 | |
|       $response->setHeader('Vary', 'Brief,Prefer');
 | |
| 
 | |
|       // Normally this header is only needed for OPTIONS responses, however..
 | |
|       // iCal seems to also depend on these being set for PROPFIND. Since
 | |
|       // this is not harmful, we'll add it.
 | |
|       $features = ['1', '3', 'extended-mkcol'];
 | |
|       foreach ($this->server->getPlugins() as $plugin) {
 | |
|         $features = array_merge($features, $plugin->getFeatures());
 | |
|       }
 | |
|       $response->setHeader('DAV', implode(', ', $features));
 | |
| 
 | |
|       $prefer = $this->server->getHTTPPrefer();
 | |
|       $minimal = $prefer['return'] === 'minimal';
 | |
| 
 | |
|       $data = $this->server->generateMultiStatus($newProperties, $minimal);
 | |
|       echo('httppropfind ' . $data);
 | |
| 
 | |
|       $response->setBody($data);
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * WebDAV PROPPATCH
 | |
|      *
 | |
|      * This method is called to update properties on a Node. The request is an XML body with all the mutations.
 | |
|      * In this XML body it is specified which properties should be set/updated and/or deleted
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpPropPatch(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $path = $request->getPath();
 | |
| 
 | |
|       try {
 | |
|         $propPatch = $this->server->xml->expect('{DAV:}propertyupdate', $request->getBody());
 | |
|       } catch (ParseException $e) {
 | |
|         throw new BadRequest($e->getMessage(), null, $e);
 | |
|       }
 | |
|       $newProperties = $propPatch->properties;
 | |
| 
 | |
|       $result = $this->server->updateProperties($path, $newProperties);
 | |
| 
 | |
|       $prefer = $this->server->getHTTPPrefer();
 | |
|       $response->setHeader('Vary', 'Brief,Prefer');
 | |
| 
 | |
|       if ($prefer['return'] === 'minimal') {
 | |
| 
 | |
|         // If return-minimal is specified, we only have to check if the
 | |
|         // request was succesful, and don't need to return the
 | |
|         // multi-status.
 | |
|         $ok = true;
 | |
|         foreach ($result as $prop => $code) {
 | |
|           if ((int)$code > 299) {
 | |
|             $ok = false;
 | |
|           }
 | |
|         }
 | |
| 
 | |
|         if ($ok) {
 | |
| 
 | |
|           $response->setStatus(204);
 | |
|           return false;
 | |
| 
 | |
|         }
 | |
| 
 | |
|       }
 | |
| 
 | |
|       $response->setStatus(207);
 | |
|       $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
 | |
| 
 | |
| 
 | |
|       // Reorganizing the result for generateMultiStatus
 | |
|       $multiStatus = [];
 | |
|       foreach ($result as $propertyName => $code) {
 | |
|         if (isset($multiStatus[$code])) {
 | |
|           $multiStatus[$code][$propertyName] = null;
 | |
|         } else {
 | |
|           $multiStatus[$code] = [$propertyName => null];
 | |
|         }
 | |
|       }
 | |
|       $multiStatus['href'] = $path;
 | |
| 
 | |
|       $response->setBody(
 | |
|         $this->server->generateMultiStatus([$multiStatus])
 | |
|       );
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP PUT method
 | |
|      *
 | |
|      * This HTTP method updates a file, or creates a new one.
 | |
|      *
 | |
|      * If a new resource was created, a 201 Created status code should be returned. If an existing resource is updated, it's a 204 No Content
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpPut(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $body = $request->getBodyAsStream();
 | |
|       $path = $request->getPath();
 | |
| 
 | |
|       // Intercepting Content-Range
 | |
|       if ($request->getHeader('Content-Range')) {
 | |
|             /*
 | |
|                An origin server that allows PUT on a given target resource MUST send
 | |
|                a 400 (Bad Request) response to a PUT request that contains a
 | |
|                Content-Range header field.
 | |
| 
 | |
|                Reference: http://tools.ietf.org/html/rfc7231#section-4.3.4
 | |
|              */
 | |
|         throw new Exception\BadRequest('Content-Range on PUT requests are forbidden.');
 | |
|       }
 | |
| 
 | |
|       // Intercepting the Finder problem
 | |
|       if (($expected = $request->getHeader('X-Expected-Entity-Length')) && $expected > 0) {
 | |
| 
 | |
|             /*
 | |
|             Many webservers will not cooperate well with Finder PUT requests,
 | |
|             because it uses 'Chunked' transfer encoding for the request body.
 | |
| 
 | |
|             The symptom of this problem is that Finder sends files to the
 | |
|             server, but they arrive as 0-length files in PHP.
 | |
| 
 | |
|             If we don't do anything, the user might think they are uploading
 | |
|             files successfully, but they end up empty on the server. Instead,
 | |
|             we throw back an error if we detect this.
 | |
| 
 | |
|             The reason Finder uses Chunked, is because it thinks the files
 | |
|             might change as it's being uploaded, and therefore the
 | |
|             Content-Length can vary.
 | |
| 
 | |
|             Instead it sends the X-Expected-Entity-Length header with the size
 | |
|             of the file at the very start of the request. If this header is set,
 | |
|             but we don't get a request body we will fail the request to
 | |
|             protect the end-user.
 | |
|              */
 | |
| 
 | |
|         // Only reading first byte
 | |
|         $firstByte = fread($body, 1);
 | |
|         if (strlen($firstByte) !== 1) {
 | |
|           throw new Exception\Forbidden('This server is not compatible with OS/X finder. Consider using a different WebDAV client or webserver.');
 | |
|         }
 | |
| 
 | |
|         // The body needs to stay intact, so we copy everything to a
 | |
|         // temporary stream.
 | |
| 
 | |
|         $newBody = fopen('php://temp', 'r+');
 | |
|         fwrite($newBody, $firstByte);
 | |
|         stream_copy_to_stream($body, $newBody);
 | |
|         rewind($newBody);
 | |
| 
 | |
|         $body = $newBody;
 | |
| 
 | |
|       }
 | |
| 
 | |
|       if ($this->server->tree->nodeExists($path)) {
 | |
| 
 | |
|         $node = $this->server->tree->getNodeForPath($path);
 | |
| 
 | |
|         // If the node is a collection, we'll deny it
 | |
|         if (!($node instanceof IFile)) throw new Exception\Conflict('PUT is not allowed on non-files.');
 | |
| 
 | |
|         if (!$this->server->updateFile($path, $body, $etag)) {
 | |
|           return false;
 | |
|         }
 | |
| 
 | |
|         $response->setHeader('Content-Length', '0');
 | |
|         if ($etag) $response->setHeader('ETag', $etag);
 | |
|         $response->setStatus(204);
 | |
| 
 | |
|       } else {
 | |
| 
 | |
|         $etag = null;
 | |
|         // If we got here, the resource didn't exist yet.
 | |
|         if (!$this->server->createFile($path, $body, $etag)) {
 | |
|           // For one reason or another the file was not created.
 | |
|           return false;
 | |
|         }
 | |
| 
 | |
|         $response->setHeader('Content-Length', '0');
 | |
|         if ($etag) $response->setHeader('ETag', $etag);
 | |
|         $response->setStatus(201);
 | |
| 
 | |
|       }
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
| 
 | |
|     /**
 | |
|      * WebDAV MKCOL
 | |
|      *
 | |
|      * The MKCOL method is used to create a new collection (directory) on the server
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpMkcol(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $requestBody = $request->getBodyAsString();
 | |
|       $path = $request->getPath();
 | |
| 
 | |
|       if ($requestBody) {
 | |
| 
 | |
|         $contentType = $request->getHeader('Content-Type');
 | |
|         if (strpos($contentType, 'application/xml') !== 0 && strpos($contentType, 'text/xml') !== 0) {
 | |
| 
 | |
|           // We must throw 415 for unsupported mkcol bodies
 | |
|           throw new Exception\UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
 | |
| 
 | |
|         }
 | |
| 
 | |
|         try {
 | |
|           $mkcol = $this->server->xml->expect('{DAV:}mkcol', $requestBody);
 | |
|         } catch (\Sabre\Xml\ParseException $e) {
 | |
|           throw new Exception\BadRequest($e->getMessage(), null, $e);
 | |
|         }
 | |
| 
 | |
|         $properties = $mkcol->getProperties();
 | |
| 
 | |
|         if (!isset($properties['{DAV:}resourcetype']))
 | |
|           throw new Exception\BadRequest('The mkcol request must include a {DAV:}resourcetype property');
 | |
| 
 | |
|         $resourceType = $properties['{DAV:}resourcetype']->getValue();
 | |
|         unset($properties['{DAV:}resourcetype']);
 | |
| 
 | |
|       } else {
 | |
| 
 | |
|         $properties = [];
 | |
|         $resourceType = ['{DAV:}collection'];
 | |
| 
 | |
|       }
 | |
| 
 | |
|       $mkcol = new MkCol($resourceType, $properties);
 | |
| 
 | |
|       $result = $this->server->createCollection($path, $mkcol);
 | |
| 
 | |
|       if (is_array($result)) {
 | |
|         $response->setStatus(207);
 | |
|         $response->setHeader('Content-Type', 'application/xml; charset=utf-8');
 | |
| 
 | |
|         $response->setBody(
 | |
|           $this->server->generateMultiStatus([$result])
 | |
|         );
 | |
| 
 | |
|       } else {
 | |
|         $response->setHeader('Content-Length', '0');
 | |
|         $response->setStatus(201);
 | |
|       }
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * WebDAV HTTP MOVE method
 | |
|      *
 | |
|      * This method moves one uri to a different uri. A lot of the actual request processing is done in getCopyMoveInfo
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpMove(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $path = $request->getPath();
 | |
| 
 | |
|       $moveInfo = $this->server->getCopyAndMoveInfo($request);
 | |
| 
 | |
|       if ($moveInfo['destinationExists']) {
 | |
| 
 | |
|         if (!$this->server->emit('beforeUnbind', [$moveInfo['destination']])) return false;
 | |
| 
 | |
|       }
 | |
|       if (!$this->server->emit('beforeUnbind', [$path])) return false;
 | |
|       if (!$this->server->emit('beforeBind', [$moveInfo['destination']])) return false;
 | |
|       if (!$this->server->emit('beforeMove', [$path, $moveInfo['destination']])) return false;
 | |
| 
 | |
|       if ($moveInfo['destinationExists']) {
 | |
| 
 | |
|         $this->server->tree->delete($moveInfo['destination']);
 | |
|         $this->server->emit('afterUnbind', [$moveInfo['destination']]);
 | |
| 
 | |
|       }
 | |
| 
 | |
|       $this->server->tree->move($path, $moveInfo['destination']);
 | |
| 
 | |
|       // Its important afterMove is called before afterUnbind, because it
 | |
|       // allows systems to transfer data from one path to another.
 | |
|       // PropertyStorage uses this. If afterUnbind was first, it would clean
 | |
|       // up all the properties before it has a chance.
 | |
|       $this->server->emit('afterMove', [$path, $moveInfo['destination']]);
 | |
|       $this->server->emit('afterUnbind', [$path]);
 | |
|       $this->server->emit('afterBind', [$moveInfo['destination']]);
 | |
| 
 | |
|       // If a resource was overwritten we should send a 204, otherwise a 201
 | |
|       $response->setHeader('Content-Length', '0');
 | |
|       $response->setStatus($moveInfo['destinationExists'] ? 204 : 201);
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * WebDAV HTTP COPY method
 | |
|      *
 | |
|      * This method copies one uri to a different uri, and works much like the MOVE request
 | |
|      * A lot of the actual request processing is done in getCopyMoveInfo
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpCopy(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $path = $request->getPath();
 | |
| 
 | |
|       $copyInfo = $this->server->getCopyAndMoveInfo($request);
 | |
| 
 | |
|       if (!$this->server->emit('beforeBind', [$copyInfo['destination']])) return false;
 | |
|       if ($copyInfo['destinationExists']) {
 | |
|         if (!$this->server->emit('beforeUnbind', [$copyInfo['destination']])) return false;
 | |
|         $this->server->tree->delete($copyInfo['destination']);
 | |
|       }
 | |
| 
 | |
|       $this->server->tree->copy($path, $copyInfo['destination']);
 | |
|       $this->server->emit('afterBind', [$copyInfo['destination']]);
 | |
| 
 | |
|       // If a resource was overwritten we should send a 204, otherwise a 201
 | |
|       $response->setHeader('Content-Length', '0');
 | |
|       $response->setStatus($copyInfo['destinationExists'] ? 204 : 201);
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * HTTP REPORT method implementation
 | |
|      *
 | |
|      * Although the REPORT method is not part of the standard WebDAV spec (it's from rfc3253)
 | |
|      * It's used in a lot of extensions, so it made sense to implement it into the core.
 | |
|      *
 | |
|      * @param RequestInterface $request
 | |
|      * @param ResponseInterface $response
 | |
|      * @return bool
 | |
|      */
 | |
|     function httpReport(RequestInterface $request, ResponseInterface $response) {
 | |
| 
 | |
|       $path = $request->getPath();
 | |
| 
 | |
|       $result = $this->server->xml->parse(
 | |
|         $request->getBody(),
 | |
|         $request->getUrl(),
 | |
|         $rootElementName
 | |
|       );
 | |
| 
 | |
|       if ($this->server->emit('report', [$rootElementName, $result, $path])) {
 | |
| 
 | |
|         // If emit returned true, it means the report was not supported
 | |
|         throw new Exception\ReportNotSupported();
 | |
| 
 | |
|       }
 | |
| 
 | |
|       // Sending back false will interupt the event chain and tell the server
 | |
|       // we've handled this method.
 | |
|       return false;
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method is called during property updates.
 | |
|      *
 | |
|      * Here we check if a user attempted to update a protected property and
 | |
|      * ensure that the process fails if this is the case.
 | |
|      *
 | |
|      * @param string $path
 | |
|      * @param PropPatch $propPatch
 | |
|      * @return void
 | |
|      */
 | |
|     function propPatchProtectedPropertyCheck($path, PropPatch $propPatch) {
 | |
| 
 | |
|       // Comparing the mutation list to the list of propetected properties.
 | |
|       $mutations = $propPatch->getMutations();
 | |
| 
 | |
|       $protected = array_intersect(
 | |
|         $this->server->protectedProperties,
 | |
|         array_keys($mutations)
 | |
|       );
 | |
| 
 | |
|       if ($protected) {
 | |
|         $propPatch->setResultCode($protected, 403);
 | |
|       }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method is called during property updates.
 | |
|      *
 | |
|      * Here we check if a node implements IProperties and let the node handle
 | |
|      * updating of (some) properties.
 | |
|      *
 | |
|      * @param string $path
 | |
|      * @param PropPatch $propPatch
 | |
|      * @return void
 | |
|      */
 | |
|     function propPatchNodeUpdate($path, PropPatch $propPatch) {
 | |
| 
 | |
|       // This should trigger a 404 if the node doesn't exist.
 | |
|       $node = $this->server->tree->getNodeForPath($path);
 | |
| 
 | |
|       if ($node instanceof IProperties) {
 | |
|         $node->propPatch($propPatch);
 | |
|       }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method is called when properties are retrieved.
 | |
|      *
 | |
|      * Here we add all the default properties.
 | |
|      *
 | |
|      * @param PropFind $propFind
 | |
|      * @param INode $node
 | |
|      * @return void
 | |
|      */
 | |
|     function propFind(PropFind $propFind, INode $node) {
 | |
| 
 | |
|       $propFind->handle('{DAV:}getlastmodified', function() use ($node) {
 | |
|         $lm = $node->getLastModified();
 | |
|         if ($lm) {
 | |
|           return new Xml\Property\GetLastModified($lm);
 | |
|         }
 | |
|       });
 | |
| 
 | |
|       if ($node instanceof IFile) {
 | |
|         $propFind->handle('{DAV:}getcontentlength', [$node, 'getSize']);
 | |
|         $propFind->handle('{DAV:}getetag', [$node, 'getETag']);
 | |
|         $propFind->handle('{DAV:}getcontenttype', [$node, 'getContentType']);
 | |
|       }
 | |
| 
 | |
|       if ($node instanceof IQuota) {
 | |
|         $quotaInfo = null;
 | |
|         $propFind->handle('{DAV:}quota-used-bytes', function() use (&$quotaInfo, $node) {
 | |
|           $quotaInfo = $node->getQuotaInfo();
 | |
|           return $quotaInfo[0];
 | |
|         });
 | |
|         $propFind->handle('{DAV:}quota-available-bytes', function() use (&$quotaInfo, $node) {
 | |
|           if (!$quotaInfo) {
 | |
|             $quotaInfo = $node->getQuotaInfo();
 | |
|           }
 | |
|           return $quotaInfo[1];
 | |
|         });
 | |
|       }
 | |
| 
 | |
|       $propFind->handle('{DAV:}supported-report-set', function() use ($propFind) {
 | |
|         $reports = [];
 | |
|         foreach ($this->server->getPlugins() as $plugin) {
 | |
|           $reports = array_merge($reports, $plugin->getSupportedReportSet($propFind->getPath()));
 | |
|         }
 | |
|         return new Xml\Property\SupportedReportSet($reports);
 | |
|       });
 | |
|       $propFind->handle('{DAV:}resourcetype', function() use ($node) {
 | |
|         return new Xml\Property\ResourceType($this->server->getResourceTypeForNode($node));
 | |
|       });
 | |
|       $propFind->handle('{DAV:}supported-method-set', function() use ($propFind) {
 | |
|         return new Xml\Property\SupportedMethodSet(
 | |
|           $this->server->getAllowedMethods($propFind->getPath())
 | |
|         );
 | |
|       });
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * Fetches properties for a node.
 | |
|      *
 | |
|      * This event is called a bit later, so plugins have a chance first to
 | |
|      * populate the result.
 | |
|      *
 | |
|      * @param PropFind $propFind
 | |
|      * @param INode $node
 | |
|      * @return void
 | |
|      */
 | |
|     function propFindNode(PropFind $propFind, INode $node) {
 | |
| 
 | |
|       if ($node instanceof IProperties && $propertyNames = $propFind->get404Properties()) {
 | |
| 
 | |
|         $nodeProperties = $node->getProperties($propertyNames);
 | |
|         foreach ($propertyNames as $propertyName) {
 | |
|           if (array_key_exists($propertyName, $nodeProperties)) {
 | |
|             $propFind->set($propertyName, $nodeProperties[$propertyName], 200);
 | |
|           }
 | |
|         }
 | |
| 
 | |
|       }
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * This method is called when properties are retrieved.
 | |
|      *
 | |
|      * This specific handler is called very late in the process, because we
 | |
|      * want other systems to first have a chance to handle the properties.
 | |
|      *
 | |
|      * @param PropFind $propFind
 | |
|      * @param INode $node
 | |
|      * @return void
 | |
|      */
 | |
|     function propFindLate(PropFind $propFind, INode $node) {
 | |
| 
 | |
|       $propFind->handle('{http://calendarserver.org/ns/}getctag', function() use ($propFind) {
 | |
| 
 | |
|         // If we already have a sync-token from the current propFind
 | |
|         // request, we can re-use that.
 | |
|         $val = $propFind->get('{http://sabredav.org/ns}sync-token');
 | |
|         if ($val) return $val;
 | |
| 
 | |
|         $val = $propFind->get('{DAV:}sync-token');
 | |
|         if ($val && is_scalar($val)) {
 | |
|           return $val;
 | |
|         }
 | |
|         if ($val && $val instanceof Xml\Property\Href) {
 | |
|           return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
 | |
|         }
 | |
| 
 | |
|         // If we got here, the earlier two properties may simply not have
 | |
|         // been part of the earlier request. We're going to fetch them.
 | |
|         $result = $this->server->getProperties($propFind->getPath(), [
 | |
|           '{http://sabredav.org/ns}sync-token',
 | |
|           '{DAV:}sync-token',
 | |
|         ]);
 | |
| 
 | |
|         if (isset($result['{http://sabredav.org/ns}sync-token'])) {
 | |
|           return $result['{http://sabredav.org/ns}sync-token'];
 | |
|         }
 | |
|         if (isset($result['{DAV:}sync-token'])) {
 | |
|           $val = $result['{DAV:}sync-token'];
 | |
|           if (is_scalar($val)) {
 | |
|             return $val;
 | |
|           } elseif ($val instanceof Xml\Property\Href) {
 | |
|             return substr($val->getHref(), strlen(Sync\Plugin::SYNCTOKEN_PREFIX));
 | |
|           }
 | |
|         }
 | |
| 
 | |
|       });
 | |
| 
 | |
|     }
 | |
| 
 | |
|     /**
 | |
|      * 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
 | |
|      */
 | |
|     function getPluginInfo() {
 | |
| 
 | |
|       return [
 | |
|         'name'        => $this->getPluginName(),
 | |
|         'description' => 'The Core plugin provides syncronization between tasks and iCAL events',
 | |
|         'link'        => null,
 | |
|       ];
 | |
| 
 | |
|     }
 | |
| }
 |