12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000 |
- <?php
- /**
- * Main DAV server class
- *
- * @package Sabre
- * @subpackage DAV
- * @copyright Copyright (C) 2007-2012 Rooftop Solutions. All rights reserved.
- * @author Evert Pot (http://www.rooftopsolutions.nl/)
- * @license http://code.google.com/p/sabredav/wiki/License Modified BSD License
- */
- class Sabre_DAV_Server {
- /**
- * Inifinity is used for some request supporting the HTTP Depth header and indicates that the operation should traverse the entire tree
- */
- const DEPTH_INFINITY = -1;
- /**
- * Nodes that are files, should have this as the type property
- */
- const NODE_FILE = 1;
- /**
- * Nodes that are directories, should use this value as the type property
- */
- const NODE_DIRECTORY = 2;
- /**
- * XML namespace for all SabreDAV related elements
- */
- const NS_SABREDAV = 'http://sabredav.org/ns';
- /**
- * The tree object
- *
- * @var Sabre_DAV_Tree
- */
- public $tree;
- /**
- * The base uri
- *
- * @var string
- */
- protected $baseUri = null;
- /**
- * httpResponse
- *
- * @var Sabre_HTTP_Response
- */
- public $httpResponse;
- /**
- * httpRequest
- *
- * @var Sabre_HTTP_Request
- */
- public $httpRequest;
- /**
- * The list of plugins
- *
- * @var array
- */
- protected $plugins = array();
- /**
- * This array contains a list of callbacks we should call when certain events are triggered
- *
- * @var array
- */
- protected $eventSubscriptions = array();
- /**
- * This is a default list of namespaces.
- *
- * If you are defining your own custom namespace, add it here to reduce
- * bandwidth and improve legibility of xml bodies.
- *
- * @var array
- */
- public $xmlNamespaces = array(
- 'DAV:' => 'd',
- 'http://sabredav.org/ns' => 's',
- );
- /**
- * The propertymap can be used to map properties from
- * requests to property classes.
- *
- * @var array
- */
- public $propertyMap = array(
- '{DAV:}resourcetype' => 'Sabre_DAV_Property_ResourceType',
- );
- public $protectedProperties = array(
- // RFC4918
- '{DAV:}getcontentlength',
- '{DAV:}getetag',
- '{DAV:}getlastmodified',
- '{DAV:}lockdiscovery',
- '{DAV:}resourcetype',
- '{DAV:}supportedlock',
- // RFC4331
- '{DAV:}quota-available-bytes',
- '{DAV:}quota-used-bytes',
- // RFC3744
- '{DAV:}supported-privilege-set',
- '{DAV:}current-user-privilege-set',
- '{DAV:}acl',
- '{DAV:}acl-restrictions',
- '{DAV:}inherited-acl-set',
- );
- /**
- * This is a flag that allow or not showing file, line and code
- * of the exception in the returned XML
- *
- * @var bool
- */
- public $debugExceptions = false;
- /**
- * This property allows you to automatically add the 'resourcetype' value
- * based on a node's classname or interface.
- *
- * The preset ensures that {DAV:}collection is automaticlly added for nodes
- * implementing Sabre_DAV_ICollection.
- *
- * @var array
- */
- public $resourceTypeMapping = array(
- 'Sabre_DAV_ICollection' => '{DAV:}collection',
- );
- /**
- * If this setting is turned off, SabreDAV's version number will be hidden
- * from various places.
- *
- * Some people feel this is a good security measure.
- *
- * @var bool
- */
- static public $exposeVersion = true;
- /**
- * Sets up the server
- *
- * If a Sabre_DAV_Tree object is passed as an argument, it will
- * use it as the directory tree. If a Sabre_DAV_INode is passed, it
- * will create a Sabre_DAV_ObjectTree and use the node as the root.
- *
- * If nothing is passed, a Sabre_DAV_SimpleCollection is created in
- * a Sabre_DAV_ObjectTree.
- *
- * If an array is passed, we automatically create a root node, and use
- * the nodes in the array as top-level children.
- *
- * @param Sabre_DAV_Tree|Sabre_DAV_INode|null $treeOrNode The tree object
- */
- public function __construct($treeOrNode = null) {
- if ($treeOrNode instanceof Sabre_DAV_Tree) {
- $this->tree = $treeOrNode;
- } elseif ($treeOrNode instanceof Sabre_DAV_INode) {
- $this->tree = new Sabre_DAV_ObjectTree($treeOrNode);
- } elseif (is_array($treeOrNode)) {
- // If it's an array, a list of nodes was passed, and we need to
- // create the root node.
- foreach($treeOrNode as $node) {
- if (!($node instanceof Sabre_DAV_INode)) {
- throw new Sabre_DAV_Exception('Invalid argument passed to constructor. If you\'re passing an array, all the values must implement Sabre_DAV_INode');
- }
- }
- $root = new Sabre_DAV_SimpleCollection('root', $treeOrNode);
- $this->tree = new Sabre_DAV_ObjectTree($root);
- } elseif (is_null($treeOrNode)) {
- $root = new Sabre_DAV_SimpleCollection('root');
- $this->tree = new Sabre_DAV_ObjectTree($root);
- } else {
- throw new Sabre_DAV_Exception('Invalid argument passed to constructor. Argument must either be an instance of Sabre_DAV_Tree, Sabre_DAV_INode, an array or null');
- }
- $this->httpResponse = new Sabre_HTTP_Response();
- $this->httpRequest = new Sabre_HTTP_Request();
- }
- /**
- * Starts the DAV Server
- *
- * @return void
- */
- public function exec() {
- try {
- $this->invokeMethod($this->httpRequest->getMethod(), $this->getRequestUri());
- } catch (Exception $e) {
- $DOM = new DOMDocument('1.0','utf-8');
- $DOM->formatOutput = true;
- $error = $DOM->createElementNS('DAV:','d:error');
- $error->setAttribute('xmlns:s',self::NS_SABREDAV);
- $DOM->appendChild($error);
- $error->appendChild($DOM->createElement('s:exception',get_class($e)));
- $error->appendChild($DOM->createElement('s:message',htmlentities($e->getMessage())));
- if ($this->debugExceptions) {
- $error->appendChild($DOM->createElement('s:file',$e->getFile()));
- $error->appendChild($DOM->createElement('s:line',$e->getLine()));
- $error->appendChild($DOM->createElement('s:code',$e->getCode()));
- $error->appendChild($DOM->createElement('s:stacktrace',$e->getTraceAsString()));
- }
- if (self::$exposeVersion) {
- $error->appendChild($DOM->createElement('s:sabredav-version',Sabre_DAV_Version::VERSION));
- }
- if($e instanceof Sabre_DAV_Exception) {
- $httpCode = $e->getHTTPCode();
- $e->serialize($this,$error);
- $headers = $e->getHTTPHeaders($this);
- } else {
- $httpCode = 500;
- $headers = array();
- }
- $headers['Content-Type'] = 'application/xml; charset=utf-8';
- $this->httpResponse->sendStatus($httpCode);
- $this->httpResponse->setHeaders($headers);
- $this->httpResponse->sendBody($DOM->saveXML());
- }
- }
- /**
- * Sets the base server uri
- *
- * @param string $uri
- * @return void
- */
- public function setBaseUri($uri) {
- // If the baseUri does not end with a slash, we must add it
- if ($uri[strlen($uri)-1]!=='/')
- $uri.='/';
- $this->baseUri = $uri;
- }
- /**
- * Returns the base responding uri
- *
- * @return string
- */
- public function getBaseUri() {
- if (is_null($this->baseUri)) $this->baseUri = $this->guessBaseUri();
- return $this->baseUri;
- }
- /**
- * This method attempts to detect the base uri.
- * Only the PATH_INFO variable is considered.
- *
- * If this variable is not set, the root (/) is assumed.
- *
- * @return string
- */
- public function guessBaseUri() {
- $pathInfo = $this->httpRequest->getRawServerValue('PATH_INFO');
- $uri = $this->httpRequest->getRawServerValue('REQUEST_URI');
- // If PATH_INFO is found, we can assume it's accurate.
- if (!empty($pathInfo)) {
- // We need to make sure we ignore the QUERY_STRING part
- if ($pos = strpos($uri,'?'))
- $uri = substr($uri,0,$pos);
- // PATH_INFO is only set for urls, such as: /example.php/path
- // in that case PATH_INFO contains '/path'.
- // Note that REQUEST_URI is percent encoded, while PATH_INFO is
- // not, Therefore they are only comparable if we first decode
- // REQUEST_INFO as well.
- $decodedUri = Sabre_DAV_URLUtil::decodePath($uri);
- // A simple sanity check:
- if(substr($decodedUri,strlen($decodedUri)-strlen($pathInfo))===$pathInfo) {
- $baseUri = substr($decodedUri,0,strlen($decodedUri)-strlen($pathInfo));
- return rtrim($baseUri,'/') . '/';
- }
- throw new Sabre_DAV_Exception('The REQUEST_URI ('. $uri . ') did not end with the contents of PATH_INFO (' . $pathInfo . '). This server might be misconfigured.');
- }
- // The last fallback is that we're just going to assume the server root.
- return '/';
- }
- /**
- * Adds a plugin to the server
- *
- * For more information, console the documentation of Sabre_DAV_ServerPlugin
- *
- * @param Sabre_DAV_ServerPlugin $plugin
- * @return void
- */
- public function addPlugin(Sabre_DAV_ServerPlugin $plugin) {
- $this->plugins[$plugin->getPluginName()] = $plugin;
- $plugin->initialize($this);
- }
- /**
- * Returns an initialized plugin by it's name.
- *
- * This function returns null if the plugin was not found.
- *
- * @param string $name
- * @return Sabre_DAV_ServerPlugin
- */
- public function getPlugin($name) {
- if (isset($this->plugins[$name]))
- return $this->plugins[$name];
- // This is a fallback and deprecated.
- foreach($this->plugins as $plugin) {
- if (get_class($plugin)===$name) return $plugin;
- }
- return null;
- }
- /**
- * Returns all plugins
- *
- * @return array
- */
- public function getPlugins() {
- return $this->plugins;
- }
- /**
- * Subscribe to an event.
- *
- * When the event is triggered, we'll call all the specified callbacks.
- * It is possible to control the order of the callbacks through the
- * priority argument.
- *
- * This is for example used to make sure that the authentication plugin
- * is triggered before anything else. If it's not needed to change this
- * number, it is recommended to ommit.
- *
- * @param string $event
- * @param callback $callback
- * @param int $priority
- * @return void
- */
- public function subscribeEvent($event, $callback, $priority = 100) {
- if (!isset($this->eventSubscriptions[$event])) {
- $this->eventSubscriptions[$event] = array();
- }
- while(isset($this->eventSubscriptions[$event][$priority])) $priority++;
- $this->eventSubscriptions[$event][$priority] = $callback;
- ksort($this->eventSubscriptions[$event]);
- }
- /**
- * Broadcasts an event
- *
- * This method will call all subscribers. If one of the subscribers returns false, the process stops.
- *
- * The arguments parameter will be sent to all subscribers
- *
- * @param string $eventName
- * @param array $arguments
- * @return bool
- */
- public function broadcastEvent($eventName,$arguments = array()) {
- if (isset($this->eventSubscriptions[$eventName])) {
- foreach($this->eventSubscriptions[$eventName] as $subscriber) {
- $result = call_user_func_array($subscriber,$arguments);
- if ($result===false) return false;
- }
- }
- return true;
- }
- /**
- * Handles a http request, and execute a method based on its name
- *
- * @param string $method
- * @param string $uri
- * @return void
- */
- public function invokeMethod($method, $uri) {
- $method = strtoupper($method);
- if (!$this->broadcastEvent('beforeMethod',array($method, $uri))) return;
- // Make sure this is a HTTP method we support
- $internalMethods = array(
- 'OPTIONS',
- 'GET',
- 'HEAD',
- 'DELETE',
- 'PROPFIND',
- 'MKCOL',
- 'PUT',
- 'PROPPATCH',
- 'COPY',
- 'MOVE',
- 'REPORT'
- );
- if (in_array($method,$internalMethods)) {
- call_user_func(array($this,'http' . $method), $uri);
- } else {
- if ($this->broadcastEvent('unknownMethod',array($method, $uri))) {
- // Unsupported method
- throw new Sabre_DAV_Exception_NotImplemented('There was no handler found for this "' . $method . '" method');
- }
- }
- }
- // {{{ HTTP Method implementations
- /**
- * HTTP OPTIONS
- *
- * @param string $uri
- * @return void
- */
- protected function httpOptions($uri) {
- $methods = $this->getAllowedMethods($uri);
- $this->httpResponse->setHeader('Allow',strtoupper(implode(', ',$methods)));
- $features = array('1','3', 'extended-mkcol');
- foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
- $this->httpResponse->setHeader('DAV',implode(', ',$features));
- $this->httpResponse->setHeader('MS-Author-Via','DAV');
- $this->httpResponse->setHeader('Accept-Ranges','bytes');
- if (self::$exposeVersion) {
- $this->httpResponse->setHeader('X-Sabre-Version',Sabre_DAV_Version::VERSION);
- }
- $this->httpResponse->setHeader('Content-Length',0);
- $this->httpResponse->sendStatus(200);
- }
- /**
- * HTTP GET
- *
- * This method simply fetches the contents of a uri, like normal
- *
- * @param string $uri
- * @return bool
- */
- protected function httpGet($uri) {
- $node = $this->tree->getNodeForPath($uri,0);
- if (!$this->checkPreconditions(true)) return false;
- if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_NotImplemented('GET is only implemented on File objects');
- $body = $node->get();
- // Converting string into stream, if needed.
- if (is_string($body)) {
- $stream = fopen('php://temp','r+');
- fwrite($stream,$body);
- rewind($stream);
- $body = $stream;
- }
- /*
- * TODO: getetag, getlastmodified, getsize should also be used using
- * this method
- */
- $httpHeaders = $this->getHTTPHeaders($uri);
- /* ContentType needs to get a default, because many webservers will otherwise
- * default to text/html, and we don't want this for security reasons.
- */
- if (!isset($httpHeaders['Content-Type'])) {
- $httpHeaders['Content-Type'] = 'application/octet-stream';
- }
- if (isset($httpHeaders['Content-Length'])) {
- $nodeSize = $httpHeaders['Content-Length'];
- // Need to unset Content-Length, because we'll handle that during figuring out the range
- unset($httpHeaders['Content-Length']);
- } else {
- $nodeSize = null;
- }
- $this->httpResponse->setHeaders($httpHeaders);
- $range = $this->getHTTPRange();
- $ifRange = $this->httpRequest->getHeader('If-Range');
- $ignoreRangeHeader = false;
- // If ifRange is set, and range is specified, we first need to check
- // the precondition.
- if ($nodeSize && $range && $ifRange) {
- // if IfRange is parsable as a date we'll treat it as a DateTime
- // otherwise, we must treat it as an etag.
- try {
- $ifRangeDate = new DateTime($ifRange);
- // It's a date. We must check if the entity is modified since
- // the specified date.
- if (!isset($httpHeaders['Last-Modified'])) $ignoreRangeHeader = true;
- else {
- $modified = new DateTime($httpHeaders['Last-Modified']);
- if($modified > $ifRangeDate) $ignoreRangeHeader = true;
- }
- } catch (Exception $e) {
- // It's an entity. We can do a simple comparison.
- if (!isset($httpHeaders['ETag'])) $ignoreRangeHeader = true;
- elseif ($httpHeaders['ETag']!==$ifRange) $ignoreRangeHeader = true;
- }
- }
- // We're only going to support HTTP ranges if the backend provided a filesize
- if (!$ignoreRangeHeader && $nodeSize && $range) {
- // Determining the exact byte offsets
- if (!is_null($range[0])) {
- $start = $range[0];
- $end = $range[1]?$range[1]:$nodeSize-1;
- if($start >= $nodeSize)
- throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The start offset (' . $range[0] . ') exceeded the size of the entity (' . $nodeSize . ')');
- if($end < $start) throw new Sabre_DAV_Exception_RequestedRangeNotSatisfiable('The end offset (' . $range[1] . ') is lower than the start offset (' . $range[0] . ')');
- if($end >= $nodeSize) $end = $nodeSize-1;
- } else {
- $start = $nodeSize-$range[1];
- $end = $nodeSize-1;
- if ($start<0) $start = 0;
- }
- // New read/write stream
- $newStream = fopen('php://temp','r+');
- stream_copy_to_stream($body, $newStream, $end-$start+1, $start);
- rewind($newStream);
- $this->httpResponse->setHeader('Content-Length', $end-$start+1);
- $this->httpResponse->setHeader('Content-Range','bytes ' . $start . '-' . $end . '/' . $nodeSize);
- $this->httpResponse->sendStatus(206);
- $this->httpResponse->sendBody($newStream);
- } else {
- if ($nodeSize) $this->httpResponse->setHeader('Content-Length',$nodeSize);
- $this->httpResponse->sendStatus(200);
- $this->httpResponse->sendBody($body);
- }
- }
- /**
- * 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 string $uri
- * @return void
- */
- protected function httpHead($uri) {
- $node = $this->tree->getNodeForPath($uri);
- /* This information is only collection for File objects.
- * Ideally we want to throw 405 Method Not Allowed for every
- * non-file, but MS Office does not like this
- */
- if ($node instanceof Sabre_DAV_IFile) {
- $headers = $this->getHTTPHeaders($this->getRequestUri());
- if (!isset($headers['Content-Type'])) {
- $headers['Content-Type'] = 'application/octet-stream';
- }
- $this->httpResponse->setHeaders($headers);
- }
- $this->httpResponse->sendStatus(200);
- }
- /**
- * HTTP Delete
- *
- * The HTTP delete method, deletes a given uri
- *
- * @param string $uri
- * @return void
- */
- protected function httpDelete($uri) {
- if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
- $this->tree->delete($uri);
- $this->broadcastEvent('afterUnbind',array($uri));
- $this->httpResponse->sendStatus(204);
- $this->httpResponse->setHeader('Content-Length','0');
- }
- /**
- * 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 string $uri
- * @return void
- */
- protected function httpPropfind($uri) {
- // $xml = new Sabre_DAV_XMLReader(file_get_contents('php://input'));
- $requestedProperties = $this->parsePropfindRequest($this->httpRequest->getBody(true));
- $depth = $this->getHTTPDepth(1);
- // The only two options for the depth of a propfind is 0 or 1
- if ($depth!=0) $depth = 1;
- $newProperties = $this->getPropertiesForPath($uri,$requestedProperties,$depth);
- // This is a multi-status response
- $this->httpResponse->sendStatus(207);
- $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
- // 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 = array('1','3', 'extended-mkcol');
- foreach($this->plugins as $plugin) $features = array_merge($features,$plugin->getFeatures());
- $this->httpResponse->setHeader('DAV',implode(', ',$features));
- $data = $this->generateMultiStatus($newProperties);
- $this->httpResponse->sendBody($data);
- }
- /**
- * 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 string $uri
- * @return void
- */
- protected function httpPropPatch($uri) {
- $newProperties = $this->parsePropPatchRequest($this->httpRequest->getBody(true));
- $result = $this->updateProperties($uri, $newProperties);
- $this->httpResponse->sendStatus(207);
- $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
- $this->httpResponse->sendBody(
- $this->generateMultiStatus(array($result))
- );
- }
- /**
- * 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 string $uri
- * @return bool
- */
- protected function httpPut($uri) {
- $body = $this->httpRequest->getBody();
- // Intercepting Content-Range
- if ($this->httpRequest->getHeader('Content-Range')) {
- /**
- Content-Range is dangerous for PUT requests: PUT per definition
- stores a full resource. draft-ietf-httpbis-p2-semantics-15 says
- in section 7.6:
- An origin server SHOULD reject any PUT request that contains a
- Content-Range header field, since it might be misinterpreted as
- partial content (or might be partial content that is being mistakenly
- PUT as a full representation). Partial content updates are possible
- by targeting a separately identified resource with state that
- overlaps a portion of the larger resource, or by using a different
- method that has been specifically defined for partial updates (for
- example, the PATCH method defined in [RFC5789]).
- This clarifies RFC2616 section 9.6:
- The recipient of the entity MUST NOT ignore any Content-*
- (e.g. Content-Range) headers that it does not understand or implement
- and MUST return a 501 (Not Implemented) response in such cases.
- OTOH is a PUT request with a Content-Range currently the only way to
- continue an aborted upload request and is supported by curl, mod_dav,
- Tomcat and others. Since some clients do use this feature which results
- in unexpected behaviour (cf PEAR::HTTP_WebDAV_Client 1.0.1), we reject
- all PUT requests with a Content-Range for now.
- */
- throw new Sabre_DAV_Exception_NotImplemented('PUT with Content-Range is not allowed.');
- }
- // Intercepting the Finder problem
- if (($expected = $this->httpRequest->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 Sabre_DAV_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->tree->nodeExists($uri)) {
- $node = $this->tree->getNodeForPath($uri);
- // Checking If-None-Match and related headers.
- if (!$this->checkPreconditions()) return;
- // If the node is a collection, we'll deny it
- if (!($node instanceof Sabre_DAV_IFile)) throw new Sabre_DAV_Exception_Conflict('PUT is not allowed on non-files.');
- if (!$this->broadcastEvent('beforeWriteContent',array($uri, $node, &$body))) return false;
- $etag = $node->put($body);
- $this->broadcastEvent('afterWriteContent',array($uri, $node));
- $this->httpResponse->setHeader('Content-Length','0');
- if ($etag) $this->httpResponse->setHeader('ETag',$etag);
- $this->httpResponse->sendStatus(204);
- } else {
- $etag = null;
- // If we got here, the resource didn't exist yet.
- if (!$this->createFile($this->getRequestUri(),$body,$etag)) {
- // For one reason or another the file was not created.
- return;
- }
- $this->httpResponse->setHeader('Content-Length','0');
- if ($etag) $this->httpResponse->setHeader('ETag', $etag);
- $this->httpResponse->sendStatus(201);
- }
- }
- /**
- * WebDAV MKCOL
- *
- * The MKCOL method is used to create a new collection (directory) on the server
- *
- * @param string $uri
- * @return void
- */
- protected function httpMkcol($uri) {
- $requestBody = $this->httpRequest->getBody(true);
- if ($requestBody) {
- $contentType = $this->httpRequest->getHeader('Content-Type');
- if (strpos($contentType,'application/xml')!==0 && strpos($contentType,'text/xml')!==0) {
- // We must throw 415 for unsupported mkcol bodies
- throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must have an xml Content-Type');
- }
- $dom = Sabre_DAV_XMLUtil::loadDOMDocument($requestBody);
- if (Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild)!=='{DAV:}mkcol') {
- // We must throw 415 for unsupported mkcol bodies
- throw new Sabre_DAV_Exception_UnsupportedMediaType('The request body for the MKCOL request must be a {DAV:}mkcol request construct.');
- }
- $properties = array();
- foreach($dom->firstChild->childNodes as $childNode) {
- if (Sabre_DAV_XMLUtil::toClarkNotation($childNode)!=='{DAV:}set') continue;
- $properties = array_merge($properties, Sabre_DAV_XMLUtil::parseProperties($childNode, $this->propertyMap));
- }
- if (!isset($properties['{DAV:}resourcetype']))
- throw new Sabre_DAV_Exception_BadRequest('The mkcol request must include a {DAV:}resourcetype property');
- $resourceType = $properties['{DAV:}resourcetype']->getValue();
- unset($properties['{DAV:}resourcetype']);
- } else {
- $properties = array();
- $resourceType = array('{DAV:}collection');
- }
- $result = $this->createCollection($uri, $resourceType, $properties);
- if (is_array($result)) {
- $this->httpResponse->sendStatus(207);
- $this->httpResponse->setHeader('Content-Type','application/xml; charset=utf-8');
- $this->httpResponse->sendBody(
- $this->generateMultiStatus(array($result))
- );
- } else {
- $this->httpResponse->setHeader('Content-Length','0');
- $this->httpResponse->sendStatus(201);
- }
- }
- /**
- * 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 string $uri
- * @return void
- */
- protected function httpMove($uri) {
- $moveInfo = $this->getCopyAndMoveInfo();
- // If the destination is part of the source tree, we must fail
- if ($moveInfo['destination']==$uri)
- throw new Sabre_DAV_Exception_Forbidden('Source and destination uri are identical.');
- if ($moveInfo['destinationExists']) {
- if (!$this->broadcastEvent('beforeUnbind',array($moveInfo['destination']))) return false;
- $this->tree->delete($moveInfo['destination']);
- $this->broadcastEvent('afterUnbind',array($moveInfo['destination']));
- }
- if (!$this->broadcastEvent('beforeUnbind',array($uri))) return false;
- if (!$this->broadcastEvent('beforeBind',array($moveInfo['destination']))) return false;
- $this->tree->move($uri,$moveInfo['destination']);
- $this->broadcastEvent('afterUnbind',array($uri));
- $this->broadcastEvent('afterBind',array($moveInfo['destination']));
- // If a resource was overwritten we should send a 204, otherwise a 201
- $this->httpResponse->setHeader('Content-Length','0');
- $this->httpResponse->sendStatus($moveInfo['destinationExists']?204:201);
- }
- /**
- * 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 string $uri
- * @return bool
- */
- protected function httpCopy($uri) {
- $copyInfo = $this->getCopyAndMoveInfo();
- // If the destination is part of the source tree, we must fail
- if ($copyInfo['destination']==$uri)
- throw new Sabre_DAV_Exception_Forbidden('Source and destination uri are identical.');
- if ($copyInfo['destinationExists']) {
- if (!$this->broadcastEvent('beforeUnbind',array($copyInfo['destination']))) return false;
- $this->tree->delete($copyInfo['destination']);
- }
- if (!$this->broadcastEvent('beforeBind',array($copyInfo['destination']))) return false;
- $this->tree->copy($uri,$copyInfo['destination']);
- $this->broadcastEvent('afterBind',array($copyInfo['destination']));
- // If a resource was overwritten we should send a 204, otherwise a 201
- $this->httpResponse->setHeader('Content-Length','0');
- $this->httpResponse->sendStatus($copyInfo['destinationExists']?204:201);
- }
- /**
- * 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 string $uri
- * @return void
- */
- protected function httpReport($uri) {
- $body = $this->httpRequest->getBody(true);
- $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
- $reportName = Sabre_DAV_XMLUtil::toClarkNotation($dom->firstChild);
- if ($this->broadcastEvent('report',array($reportName,$dom, $uri))) {
- // If broadcastEvent returned true, it means the report was not supported
- throw new Sabre_DAV_Exception_ReportNotImplemented();
- }
- }
- // }}}
- // {{{ HTTP/WebDAV protocol helpers
- /**
- * Returns an array with all the supported HTTP methods for a specific uri.
- *
- * @param string $uri
- * @return array
- */
- public function getAllowedMethods($uri) {
- $methods = array(
- 'OPTIONS',
- 'GET',
- 'HEAD',
- 'DELETE',
- 'PROPFIND',
- 'PUT',
- 'PROPPATCH',
- 'COPY',
- 'MOVE',
- 'REPORT'
- );
- // The MKCOL is only allowed on an unmapped uri
- try {
- $this->tree->getNodeForPath($uri);
- } catch (Sabre_DAV_Exception_NotFound $e) {
- $methods[] = 'MKCOL';
- }
- // We're also checking if any of the plugins register any new methods
- foreach($this->plugins as $plugin) $methods = array_merge($methods, $plugin->getHTTPMethods($uri));
- array_unique($methods);
- return $methods;
- }
- /**
- * Gets the uri for the request, keeping the base uri into consideration
- *
- * @return string
- */
- public function getRequestUri() {
- return $this->calculateUri($this->httpRequest->getUri());
- }
- /**
- * Calculates the uri for a request, making sure that the base uri is stripped out
- *
- * @param string $uri
- * @throws Sabre_DAV_Exception_Forbidden A permission denied exception is thrown whenever there was an attempt to supply a uri outside of the base uri
- * @return string
- */
- public function calculateUri($uri) {
- if ($uri[0]!='/' && strpos($uri,'://')) {
- $uri = parse_url($uri,PHP_URL_PATH);
- }
- $uri = str_replace('//','/',$uri);
- if (strpos($uri,$this->getBaseUri())===0) {
- return trim(Sabre_DAV_URLUtil::decodePath(substr($uri,strlen($this->getBaseUri()))),'/');
- // A special case, if the baseUri was accessed without a trailing
- // slash, we'll accept it as well.
- } elseif ($uri.'/' === $this->getBaseUri()) {
- return '';
- } else {
- throw new Sabre_DAV_Exception_Forbidden('Requested uri (' . $uri . ') is out of base uri (' . $this->getBaseUri() . ')');
- }
- }
- /**
- * Returns the HTTP depth header
- *
- * This method returns the contents of the HTTP depth request header. If the depth header was 'infinity' it will return the Sabre_DAV_Server::DEPTH_INFINITY object
- * It is possible to supply a default depth value, which is used when the depth header has invalid content, or is completely non-existent
- *
- * @param mixed $default
- * @return int
- */
- public function getHTTPDepth($default = self::DEPTH_INFINITY) {
- // If its not set, we'll grab the default
- $depth = $this->httpRequest->getHeader('Depth');
- if (is_null($depth)) return $default;
- if ($depth == 'infinity') return self::DEPTH_INFINITY;
- // If its an unknown value. we'll grab the default
- if (!ctype_digit($depth)) return $default;
- return (int)$depth;
- }
- /**
- * Returns the HTTP range header
- *
- * This method returns null if there is no well-formed HTTP range request
- * header or array($start, $end).
- *
- * The first number is the offset of the first byte in the range.
- * The second number is the offset of the last byte in the range.
- *
- * If the second offset is null, it should be treated as the offset of the last byte of the entity
- * If the first offset is null, the second offset should be used to retrieve the last x bytes of the entity
- *
- * @return array|null
- */
- public function getHTTPRange() {
- $range = $this->httpRequest->getHeader('range');
- if (is_null($range)) return null;
- // Matching "Range: bytes=1234-5678: both numbers are optional
- if (!preg_match('/^bytes=([0-9]*)-([0-9]*)$/i',$range,$matches)) return null;
- if ($matches[1]==='' && $matches[2]==='') return null;
- return array(
- $matches[1]!==''?$matches[1]:null,
- $matches[2]!==''?$matches[2]:null,
- );
- }
- /**
- * Returns information about Copy and Move requests
- *
- * This function is created to help getting information about the source and the destination for the
- * WebDAV MOVE and COPY HTTP request. It also validates a lot of information and throws proper exceptions
- *
- * The returned value is an array with the following keys:
- * * destination - Destination path
- * * destinationExists - Whether or not the destination is an existing url (and should therefore be overwritten)
- *
- * @return array
- */
- public function getCopyAndMoveInfo() {
- // Collecting the relevant HTTP headers
- if (!$this->httpRequest->getHeader('Destination')) throw new Sabre_DAV_Exception_BadRequest('The destination header was not supplied');
- $destination = $this->calculateUri($this->httpRequest->getHeader('Destination'));
- $overwrite = $this->httpRequest->getHeader('Overwrite');
- if (!$overwrite) $overwrite = 'T';
- if (strtoupper($overwrite)=='T') $overwrite = true;
- elseif (strtoupper($overwrite)=='F') $overwrite = false;
- // We need to throw a bad request exception, if the header was invalid
- else throw new Sabre_DAV_Exception_BadRequest('The HTTP Overwrite header should be either T or F');
- list($destinationDir) = Sabre_DAV_URLUtil::splitPath($destination);
- try {
- $destinationParent = $this->tree->getNodeForPath($destinationDir);
- if (!($destinationParent instanceof Sabre_DAV_ICollection)) throw new Sabre_DAV_Exception_UnsupportedMediaType('The destination node is not a collection');
- } catch (Sabre_DAV_Exception_NotFound $e) {
- // If the destination parent node is not found, we throw a 409
- throw new Sabre_DAV_Exception_Conflict('The destination node is not found');
- }
- try {
- $destinationNode = $this->tree->getNodeForPath($destination);
- // If this succeeded, it means the destination already exists
- // we'll need to throw precondition failed in case overwrite is false
- if (!$overwrite) throw new Sabre_DAV_Exception_PreconditionFailed('The destination node already exists, and the overwrite header is set to false','Overwrite');
- } catch (Sabre_DAV_Exception_NotFound $e) {
- // Destination didn't exist, we're all good
- $destinationNode = false;
- }
- // These are the three relevant properties we need to return
- return array(
- 'destination' => $destination,
- 'destinationExists' => $destinationNode==true,
- 'destinationNode' => $destinationNode,
- );
- }
- /**
- * Returns a list of properties for a path
- *
- * This is a simplified version getPropertiesForPath.
- * if you aren't interested in status codes, but you just
- * want to have a flat list of properties. Use this method.
- *
- * @param string $path
- * @param array $propertyNames
- */
- public function getProperties($path, $propertyNames) {
- $result = $this->getPropertiesForPath($path,$propertyNames,0);
- return $result[0][200];
- }
- /**
- * A kid-friendly way to fetch properties for a node's children.
- *
- * The returned array will be indexed by the path of the of child node.
- * Only properties that are actually found will be returned.
- *
- * The parent node will not be returned.
- *
- * @param string $path
- * @param array $propertyNames
- * @return array
- */
- public function getPropertiesForChildren($path, $propertyNames) {
- $result = array();
- foreach($this->getPropertiesForPath($path,$propertyNames,1) as $k=>$row) {
- // Skipping the parent path
- if ($k === 0) continue;
- $result[$row['href']] = $row[200];
- }
- return $result;
- }
- /**
- * Returns a list of HTTP headers for a particular resource
- *
- * The generated http headers are based on properties provided by the
- * resource. The method basically provides a simple mapping between
- * DAV property and HTTP header.
- *
- * The headers are intended to be used for HEAD and GET requests.
- *
- * @param string $path
- * @return array
- */
- public function getHTTPHeaders($path) {
- $propertyMap = array(
- '{DAV:}getcontenttype' => 'Content-Type',
- '{DAV:}getcontentlength' => 'Content-Length',
- '{DAV:}getlastmodified' => 'Last-Modified',
- '{DAV:}getetag' => 'ETag',
- );
- $properties = $this->getProperties($path,array_keys($propertyMap));
- $headers = array();
- foreach($propertyMap as $property=>$header) {
- if (!isset($properties[$property])) continue;
- if (is_scalar($properties[$property])) {
- $headers[$header] = $properties[$property];
- // GetLastModified gets special cased
- } elseif ($properties[$property] instanceof Sabre_DAV_Property_GetLastModified) {
- $headers[$header] = Sabre_HTTP_Util::toHTTPDate($properties[$property]->getTime());
- }
- }
- return $headers;
- }
- /**
- * Returns a list of properties for a given path
- *
- * The path that should be supplied should have the baseUrl stripped out
- * The list of properties should be supplied in Clark notation. If the list is empty
- * 'allprops' is assumed.
- *
- * If a depth of 1 is requested child elements will also be returned.
- *
- * @param string $path
- * @param array $propertyNames
- * @param int $depth
- * @return array
- */
- public function getPropertiesForPath($path, $propertyNames = array(), $depth = 0) {
- if ($depth!=0) $depth = 1;
- $returnPropertyList = array();
- $parentNode = $this->tree->getNodeForPath($path);
- $nodes = array(
- $path => $parentNode
- );
- if ($depth==1 && $parentNode instanceof Sabre_DAV_ICollection) {
- foreach($this->tree->getChildren($path) as $childNode)
- $nodes[$path . '/' . $childNode->getName()] = $childNode;
- }
- // If the propertyNames array is empty, it means all properties are requested.
- // We shouldn't actually return everything we know though, and only return a
- // sensible list.
- $allProperties = count($propertyNames)==0;
- foreach($nodes as $myPath=>$node) {
- $currentPropertyNames = $propertyNames;
- $newProperties = array(
- '200' => array(),
- '404' => array(),
- );
- if ($allProperties) {
- // Default list of propertyNames, when all properties were requested.
- $currentPropertyNames = array(
- '{DAV:}getlastmodified',
- '{DAV:}getcontentlength',
- '{DAV:}resourcetype',
- '{DAV:}quota-used-bytes',
- '{DAV:}quota-available-bytes',
- '{DAV:}getetag',
- '{DAV:}getcontenttype',
- );
- }
- // If the resourceType was not part of the list, we manually add it
- // and mark it for removal. We need to know the resourcetype in order
- // to make certain decisions about the entry.
- // WebDAV dictates we should add a / and the end of href's for collections
- $removeRT = false;
- if (!in_array('{DAV:}resourcetype',$currentPropertyNames)) {
- $currentPropertyNames[] = '{DAV:}resourcetype';
- $removeRT = true;
- }
- $result = $this->broadcastEvent('beforeGetProperties',array($myPath, $node, &$currentPropertyNames, &$newProperties));
- // If this method explicitly returned false, we must ignore this
- // node as it is inaccessible.
- if ($result===false) continue;
- if (count($currentPropertyNames) > 0) {
- if ($node instanceof Sabre_DAV_IProperties)
- $newProperties['200'] = $newProperties[200] + $node->getProperties($currentPropertyNames);
- }
- foreach($currentPropertyNames as $prop) {
- if (isset($newProperties[200][$prop])) continue;
- switch($prop) {
- case '{DAV:}getlastmodified' : if ($node->getLastModified()) $newProperties[200][$prop] = new Sabre_DAV_Property_GetLastModified($node->getLastModified()); break;
- case '{DAV:}getcontentlength' :
- if ($node instanceof Sabre_DAV_IFile) {
- $size = $node->getSize();
- if (!is_null($size)) {
- $newProperties[200][$prop] = (int)$node->getSize();
- }
- }
- break;
- case '{DAV:}quota-used-bytes' :
- if ($node instanceof Sabre_DAV_IQuota) {
- $quotaInfo = $node->getQuotaInfo();
- $newProperties[200][$prop] = $quotaInfo[0];
- }
- break;
- case '{DAV:}quota-available-bytes' :
- if ($node instanceof Sabre_DAV_IQuota) {
- $quotaInfo = $node->getQuotaInfo();
- $newProperties[200][$prop] = $quotaInfo[1];
- }
- break;
- case '{DAV:}getetag' : if ($node instanceof Sabre_DAV_IFile && $etag = $node->getETag()) $newProperties[200][$prop] = $etag; break;
- case '{DAV:}getcontenttype' : if ($node instanceof Sabre_DAV_IFile && $ct = $node->getContentType()) $newProperties[200][$prop] = $ct; break;
- case '{DAV:}supported-report-set' :
- $reports = array();
- foreach($this->plugins as $plugin) {
- $reports = array_merge($reports, $plugin->getSupportedReportSet($myPath));
- }
- $newProperties[200][$prop] = new Sabre_DAV_Property_SupportedReportSet($reports);
- break;
- case '{DAV:}resourcetype' :
- $newProperties[200]['{DAV:}resourcetype'] = new Sabre_DAV_Property_ResourceType();
- foreach($this->resourceTypeMapping as $className => $resourceType) {
- if ($node instanceof $className) $newProperties[200]['{DAV:}resourcetype']->add($resourceType);
- }
- break;
- }
- // If we were unable to find the property, we will list it as 404.
- if (!$allProperties && !isset($newProperties[200][$prop])) $newProperties[404][$prop] = null;
- }
- $this->broadcastEvent('afterGetProperties',array(trim($myPath,'/'),&$newProperties));
- $newProperties['href'] = trim($myPath,'/');
- // Its is a WebDAV recommendation to add a trailing slash to collectionnames.
- // Apple's iCal also requires a trailing slash for principals (rfc 3744).
- // Therefore we add a trailing / for any non-file. This might need adjustments
- // if we find there are other edge cases.
- if ($myPath!='' && isset($newProperties[200]['{DAV:}resourcetype']) && count($newProperties[200]['{DAV:}resourcetype']->getValue())>0) $newProperties['href'] .='/';
- // If the resourcetype property was manually added to the requested property list,
- // we will remove it again.
- if ($removeRT) unset($newProperties[200]['{DAV:}resourcetype']);
- $returnPropertyList[] = $newProperties;
- }
- return $returnPropertyList;
- }
- /**
- * This method is invoked by sub-systems creating a new file.
- *
- * Currently this is done by HTTP PUT and HTTP LOCK (in the Locks_Plugin).
- * It was important to get this done through a centralized function,
- * allowing plugins to intercept this using the beforeCreateFile event.
- *
- * This method will return true if the file was actually created
- *
- * @param string $uri
- * @param resource $data
- * @param string $etag
- * @return bool
- */
- public function createFile($uri,$data, &$etag = null) {
- list($dir,$name) = Sabre_DAV_URLUtil::splitPath($uri);
- if (!$this->broadcastEvent('beforeBind',array($uri))) return false;
- $parent = $this->tree->getNodeForPath($dir);
- if (!$this->broadcastEvent('beforeCreateFile',array($uri, &$data, $parent))) return false;
- $etag = $parent->createFile($name,$data);
- $this->tree->markDirty($dir);
- $this->broadcastEvent('afterBind',array($uri));
- $this->broadcastEvent('afterCreateFile',array($uri, $parent));
- return true;
- }
- /**
- * This method is invoked by sub-systems creating a new directory.
- *
- * @param string $uri
- * @return void
- */
- public function createDirectory($uri) {
- $this->createCollection($uri,array('{DAV:}collection'),array());
- }
- /**
- * Use this method to create a new collection
- *
- * The {DAV:}resourcetype is specified using the resourceType array.
- * At the very least it must contain {DAV:}collection.
- *
- * The properties array can contain a list of additional properties.
- *
- * @param string $uri The new uri
- * @param array $resourceType The resourceType(s)
- * @param array $properties A list of properties
- * @return array|null
- */
- public function createCollection($uri, array $resourceType, array $properties) {
- list($parentUri,$newName) = Sabre_DAV_URLUtil::splitPath($uri);
- // Making sure {DAV:}collection was specified as resourceType
- if (!in_array('{DAV:}collection', $resourceType)) {
- throw new Sabre_DAV_Exception_InvalidResourceType('The resourceType for this collection must at least include {DAV:}collection');
- }
- // Making sure the parent exists
- try {
- $parent = $this->tree->getNodeForPath($parentUri);
- } catch (Sabre_DAV_Exception_NotFound $e) {
- throw new Sabre_DAV_Exception_Conflict('Parent node does not exist');
- }
- // Making sure the parent is a collection
- if (!$parent instanceof Sabre_DAV_ICollection) {
- throw new Sabre_DAV_Exception_Conflict('Parent node is not a collection');
- }
- // Making sure the child does not already exist
- try {
- $parent->getChild($newName);
- // If we got here.. it means there's already a node on that url, and we need to throw a 405
- throw new Sabre_DAV_Exception_MethodNotAllowed('The resource you tried to create already exists');
- } catch (Sabre_DAV_Exception_NotFound $e) {
- // This is correct
- }
- if (!$this->broadcastEvent('beforeBind',array($uri))) return;
- // There are 2 modes of operation. The standard collection
- // creates the directory, and then updates properties
- // the extended collection can create it directly.
- if ($parent instanceof Sabre_DAV_IExtendedCollection) {
- $parent->createExtendedCollection($newName, $resourceType, $properties);
- } else {
- // No special resourcetypes are supported
- if (count($resourceType)>1) {
- throw new Sabre_DAV_Exception_InvalidResourceType('The {DAV:}resourcetype you specified is not supported here.');
- }
- $parent->createDirectory($newName);
- $rollBack = false;
- $exception = null;
- $errorResult = null;
- if (count($properties)>0) {
- try {
- $errorResult = $this->updateProperties($uri, $properties);
- if (!isset($errorResult[200])) {
- $rollBack = true;
- }
- } catch (Sabre_DAV_Exception $e) {
- $rollBack = true;
- $exception = $e;
- }
- }
- if ($rollBack) {
- if (!$this->broadcastEvent('beforeUnbind',array($uri))) return;
- $this->tree->delete($uri);
- // Re-throwing exception
- if ($exception) throw $exception;
- return $errorResult;
- }
- }
- $this->tree->markDirty($parentUri);
- $this->broadcastEvent('afterBind',array($uri));
- }
- /**
- * This method updates a resource's properties
- *
- * The properties array must be a list of properties. Array-keys are
- * property names in clarknotation, array-values are it's values.
- * If a property must be deleted, the value should be null.
- *
- * Note that this request should either completely succeed, or
- * completely fail.
- *
- * The response is an array with statuscodes for keys, which in turn
- * contain arrays with propertynames. This response can be used
- * to generate a multistatus body.
- *
- * @param string $uri
- * @param array $properties
- * @return array
- */
- public function updateProperties($uri, array $properties) {
- // we'll start by grabbing the node, this will throw the appropriate
- // exceptions if it doesn't.
- $node = $this->tree->getNodeForPath($uri);
- $result = array(
- 200 => array(),
- 403 => array(),
- 424 => array(),
- );
- $remainingProperties = $properties;
- $hasError = false;
- // Running through all properties to make sure none of them are protected
- if (!$hasError) foreach($properties as $propertyName => $value) {
- if(in_array($propertyName, $this->protectedProperties)) {
- $result[403][$propertyName] = null;
- unset($remainingProperties[$propertyName]);
- $hasError = true;
- }
- }
- if (!$hasError) {
- // Allowing plugins to take care of property updating
- $hasError = !$this->broadcastEvent('updateProperties',array(
- &$remainingProperties,
- &$result,
- $node
- ));
- }
- // If the node is not an instance of Sabre_DAV_IProperties, every
- // property is 403 Forbidden
- if (!$hasError && count($remainingProperties) && !($node instanceof Sabre_DAV_IProperties)) {
- $hasError = true;
- foreach($properties as $propertyName=> $value) {
- $result[403][$propertyName] = null;
- }
- $remainingProperties = array();
- }
- // Only if there were no errors we may attempt to update the resource
- if (!$hasError) {
- if (count($remainingProperties)>0) {
- $updateResult = $node->updateProperties($remainingProperties);
- if ($updateResult===true) {
- // success
- foreach($remainingProperties as $propertyName=>$value) {
- $result[200][$propertyName] = null;
- }
- } elseif ($updateResult===false) {
- // The node failed to update the properties for an
- // unknown reason
- foreach($remainingProperties as $propertyName=>$value) {
- $result[403][$propertyName] = null;
- }
- } elseif (is_array($updateResult)) {
- // The node has detailed update information
- // We need to merge the results with the earlier results.
- foreach($updateResult as $status => $props) {
- if (is_array($props)) {
- if (!isset($result[$status]))
- $result[$status] = array();
- $result[$status] = array_merge($result[$status], $updateResult[$status]);
- }
- }
- } else {
- throw new Sabre_DAV_Exception('Invalid result from updateProperties');
- }
- $remainingProperties = array();
- }
- }
- foreach($remainingProperties as $propertyName=>$value) {
- // if there are remaining properties, it must mean
- // there's a dependency failure
- $result[424][$propertyName] = null;
- }
- // Removing empty array values
- foreach($result as $status=>$props) {
- if (count($props)===0) unset($result[$status]);
- }
- $result['href'] = $uri;
- return $result;
- }
- /**
- * This method checks the main HTTP preconditions.
- *
- * Currently these are:
- * * If-Match
- * * If-None-Match
- * * If-Modified-Since
- * * If-Unmodified-Since
- *
- * The method will return true if all preconditions are met
- * The method will return false, or throw an exception if preconditions
- * failed. If false is returned the operation should be aborted, and
- * the appropriate HTTP response headers are already set.
- *
- * Normally this method will throw 412 Precondition Failed for failures
- * related to If-None-Match, If-Match and If-Unmodified Since. It will
- * set the status to 304 Not Modified for If-Modified_since.
- *
- * If the $handleAsGET argument is set to true, it will also return 304
- * Not Modified for failure of the If-None-Match precondition. This is the
- * desired behaviour for HTTP GET and HTTP HEAD requests.
- *
- * @param bool $handleAsGET
- * @return bool
- */
- public function checkPreconditions($handleAsGET = false) {
- $uri = $this->getRequestUri();
- $node = null;
- $lastMod = null;
- $etag = null;
- if ($ifMatch = $this->httpRequest->getHeader('If-Match')) {
- // If-Match contains an entity tag. Only if the entity-tag
- // matches we are allowed to make the request succeed.
- // If the entity-tag is '*' we are only allowed to make the
- // request succeed if a resource exists at that url.
- try {
- $node = $this->tree->getNodeForPath($uri);
- } catch (Sabre_DAV_Exception_NotFound $e) {
- throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified and the resource did not exist','If-Match');
- }
- // Only need to check entity tags if they are not *
- if ($ifMatch!=='*') {
- // There can be multiple etags
- $ifMatch = explode(',',$ifMatch);
- $haveMatch = false;
- foreach($ifMatch as $ifMatchItem) {
- // Stripping any extra spaces
- $ifMatchItem = trim($ifMatchItem,' ');
- $etag = $node->getETag();
- if ($etag===$ifMatchItem) {
- $haveMatch = true;
- }
- }
- if (!$haveMatch) {
- throw new Sabre_DAV_Exception_PreconditionFailed('An If-Match header was specified, but none of the specified the ETags matched.','If-Match');
- }
- }
- }
- if ($ifNoneMatch = $this->httpRequest->getHeader('If-None-Match')) {
- // The If-None-Match header contains an etag.
- // Only if the ETag does not match the current ETag, the request will succeed
- // The header can also contain *, in which case the request
- // will only succeed if the entity does not exist at all.
- $nodeExists = true;
- if (!$node) {
- try {
- $node = $this->tree->getNodeForPath($uri);
- } catch (Sabre_DAV_Exception_NotFound $e) {
- $nodeExists = false;
- }
- }
- if ($nodeExists) {
- $haveMatch = false;
- if ($ifNoneMatch==='*') $haveMatch = true;
- else {
- // There might be multiple etags
- $ifNoneMatch = explode(',', $ifNoneMatch);
- $etag = $node->getETag();
- foreach($ifNoneMatch as $ifNoneMatchItem) {
- // Stripping any extra spaces
- $ifNoneMatchItem = trim($ifNoneMatchItem,' ');
- if ($etag===$ifNoneMatchItem) $haveMatch = true;
- }
- }
- if ($haveMatch) {
- if ($handleAsGET) {
- $this->httpResponse->sendStatus(304);
- return false;
- } else {
- throw new Sabre_DAV_Exception_PreconditionFailed('An If-None-Match header was specified, but the ETag matched (or * was specified).','If-None-Match');
- }
- }
- }
- }
- if (!$ifNoneMatch && ($ifModifiedSince = $this->httpRequest->getHeader('If-Modified-Since'))) {
- // The If-Modified-Since header contains a date. We
- // will only return the entity if it has been changed since
- // that date. If it hasn't been changed, we return a 304
- // header
- // Note that this header only has to be checked if there was no If-None-Match header
- // as per the HTTP spec.
- $date = Sabre_HTTP_Util::parseHTTPDate($ifModifiedSince);
- if ($date) {
- if (is_null($node)) {
- $node = $this->tree->getNodeForPath($uri);
- }
- $lastMod = $node->getLastModified();
- if ($lastMod) {
- $lastMod = new DateTime('@' . $lastMod);
- if ($lastMod <= $date) {
- $this->httpResponse->sendStatus(304);
- $this->httpResponse->setHeader('Last-Modified', Sabre_HTTP_Util::toHTTPDate($lastMod));
- return false;
- }
- }
- }
- }
- if ($ifUnmodifiedSince = $this->httpRequest->getHeader('If-Unmodified-Since')) {
- // The If-Unmodified-Since will allow allow the request if the
- // entity has not changed since the specified date.
- $date = Sabre_HTTP_Util::parseHTTPDate($ifUnmodifiedSince);
- // We must only check the date if it's valid
- if ($date) {
- if (is_null($node)) {
- $node = $this->tree->getNodeForPath($uri);
- }
- $lastMod = $node->getLastModified();
- if ($lastMod) {
- $lastMod = new DateTime('@' . $lastMod);
- if ($lastMod > $date) {
- throw new Sabre_DAV_Exception_PreconditionFailed('An If-Unmodified-Since header was specified, but the entity has been changed since the specified date.','If-Unmodified-Since');
- }
- }
- }
- }
- return true;
- }
- // }}}
- // {{{ XML Readers & Writers
- /**
- * Generates a WebDAV propfind response body based on a list of nodes
- *
- * @param array $fileProperties The list with nodes
- * @return string
- */
- public function generateMultiStatus(array $fileProperties) {
- $dom = new DOMDocument('1.0','utf-8');
- //$dom->formatOutput = true;
- $multiStatus = $dom->createElement('d:multistatus');
- $dom->appendChild($multiStatus);
- // Adding in default namespaces
- foreach($this->xmlNamespaces as $namespace=>$prefix) {
- $multiStatus->setAttribute('xmlns:' . $prefix,$namespace);
- }
- foreach($fileProperties as $entry) {
- $href = $entry['href'];
- unset($entry['href']);
- $response = new Sabre_DAV_Property_Response($href,$entry);
- $response->serialize($this,$multiStatus);
- }
- return $dom->saveXML();
- }
- /**
- * This method parses a PropPatch request
- *
- * PropPatch changes the properties for a resource. This method
- * returns a list of properties.
- *
- * The keys in the returned array contain the property name (e.g.: {DAV:}displayname,
- * and the value contains the property value. If a property is to be removed the value
- * will be null.
- *
- * @param string $body xml body
- * @return array list of properties in need of updating or deletion
- */
- public function parsePropPatchRequest($body) {
- //We'll need to change the DAV namespace declaration to something else in order to make it parsable
- $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
- $newProperties = array();
- foreach($dom->firstChild->childNodes as $child) {
- if ($child->nodeType !== XML_ELEMENT_NODE) continue;
- $operation = Sabre_DAV_XMLUtil::toClarkNotation($child);
- if ($operation!=='{DAV:}set' && $operation!=='{DAV:}remove') continue;
- $innerProperties = Sabre_DAV_XMLUtil::parseProperties($child, $this->propertyMap);
- foreach($innerProperties as $propertyName=>$propertyValue) {
- if ($operation==='{DAV:}remove') {
- $propertyValue = null;
- }
- $newProperties[$propertyName] = $propertyValue;
- }
- }
- return $newProperties;
- }
- /**
- * This method parses the PROPFIND request and returns its information
- *
- * This will either be a list of properties, or an empty array; in which case
- * an {DAV:}allprop was requested.
- *
- * @param string $body
- * @return array
- */
- public function parsePropFindRequest($body) {
- // If the propfind body was empty, it means IE is requesting 'all' properties
- if (!$body) return array();
- $dom = Sabre_DAV_XMLUtil::loadDOMDocument($body);
- $elem = $dom->getElementsByTagNameNS('urn:DAV','propfind')->item(0);
- return array_keys(Sabre_DAV_XMLUtil::parseProperties($elem));
- }
- // }}}
- }
|