first commit
This commit is contained in:
commit
f239099d55
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
/vendor/
|
25
composer.json
Normal file
25
composer.json
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "aerex/taskwarrior",
|
||||||
|
"description": "A Baikal plugin for taskwarrior",
|
||||||
|
"type": "library",
|
||||||
|
"keywords": [
|
||||||
|
"task",
|
||||||
|
"taskwarrior",
|
||||||
|
"Baikal",
|
||||||
|
"sabre"
|
||||||
|
],
|
||||||
|
"require": {
|
||||||
|
"sabre/dav": "^3.2",
|
||||||
|
"davidbadura/taskwarrior": "^3.0",
|
||||||
|
"sabre/vobject": "^4.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit" : "> 4.8, <=6.0.0"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Aerex",
|
||||||
|
"email": "aerex@aerex.me"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
2850
composer.lock
generated
Normal file
2850
composer.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
865
libs/Plugin.php
Normal file
865
libs/Plugin.php
Normal file
@ -0,0 +1,865 @@
|
|||||||
|
<?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,
|
||||||
|
];
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
86
libs/TaskwarriorCalendarEvent.php
Normal file
86
libs/TaskwarriorCalendarEvent.php
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Aerex\Taskwarrior;
|
||||||
|
|
||||||
|
use Aerex\Taskwarrior\Config;
|
||||||
|
use DavidBadura\Taskwarrior\Taskwarrior;
|
||||||
|
use Sabre\VObject\Component\VCalendar;
|
||||||
|
|
||||||
|
class TaskwarriorCalendarEvent {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Config
|
||||||
|
*/
|
||||||
|
private $config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
private $taskrc;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
|
||||||
|
private $taskDataDir;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
|
||||||
|
private $taskBinFile;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Taskwarrior
|
||||||
|
*/
|
||||||
|
|
||||||
|
private $taskwarrior;
|
||||||
|
|
||||||
|
|
||||||
|
public function __construct(){
|
||||||
|
$this->config = new Config();
|
||||||
|
|
||||||
|
if($this->config.isNotValidConfiguration()){
|
||||||
|
$invalidConfigurationString = $this->config.invalidConfigurations();
|
||||||
|
|
||||||
|
$invalidConfigurationMessage = sprintf('The following configurations are invalid %s and' .
|
||||||
|
' the default configurations will be used', $invalidConfigurationString);
|
||||||
|
echo($invalidConfigurationMessage);
|
||||||
|
|
||||||
|
$this->config.setDefaults();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->taskrc = $this->config->getTaskRC();
|
||||||
|
$this->taskDataDir = $this->config->getTaskDataDir();
|
||||||
|
$this->taskBinFile = $this->config->getTaskBinFile();
|
||||||
|
|
||||||
|
$this->taskwarrior = $taskwarrior($this->taskrc,$this->taskDataDir, [], $this->taskBinFile);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public function buildCalendarEvent(VCalendar $vEvent){
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildToDoEvent(VCalendar $vToDo){
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
?>
|
40
test/PluginTest.php
Normal file
40
test/PluginTest.php
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Sabre\VObject;
|
||||||
|
use Aerex\Taskwarrior;
|
||||||
|
use DateTime;
|
||||||
|
use DateTimeZone;
|
||||||
|
|
||||||
|
|
||||||
|
class PluginTest extends \PHPUnit\Framework\TestCase {
|
||||||
|
|
||||||
|
protected $cal;
|
||||||
|
protected $pluginInstance;
|
||||||
|
|
||||||
|
function setup(){
|
||||||
|
$this->$cal = new Componet\VCalendar();
|
||||||
|
$pluginInstance = new Plugin();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function testCreateSimpleTask() {
|
||||||
|
$expectedTaskDescription = "Going to the Movies";
|
||||||
|
$expectedStartTime = new \DateTime("2018-03-13 09:33:00Z");
|
||||||
|
$expectedEndTime = new \DateTime("2018-03-13 10:45:00Z");
|
||||||
|
|
||||||
|
$vCalendarObjectEvent = $this->$cal->add('VEVENT', [
|
||||||
|
"UID" => "1ff0313e-1ffa-4a18-b8c1-449bddc9109c",
|
||||||
|
"SUMMARY" => $expectedTaskDescription,
|
||||||
|
"DTSTART" => $expectedStartTime,
|
||||||
|
"DTEND" => $expectedEndTime
|
||||||
|
], false);
|
||||||
|
|
||||||
|
|
||||||
|
$this->$pluginInstance->processCalendarEventForTaskwarrior($vCalendarObjectEvent);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
?>
|
Loading…
Reference in New Issue
Block a user