Implement #015359: access.php - new MatchOrder=host_uri
[tinyz:tinyz.git] / kernel / private / classes / webdav / ezwebdavcontentbackend.php
1 <?php
2 //
3 // This is the eZWebDAVContentBackend class. Manages WebDAV sessions.
4 // Based on the eZ Components Webdav component.
5 //
6 // Created on: <14-Jul-2008 15:15:15 as>
7 //
8 // ## BEGIN COPYRIGHT, LICENSE AND WARRANTY NOTICE ##
9 // SOFTWARE NAME: eZ Publish
10 // SOFTWARE RELEASE: 4.1.x
11 // COPYRIGHT NOTICE: Copyright (C) 1999-2010 eZ Systems AS
12 // SOFTWARE LICENSE: GNU General Public License v2.0
13 // NOTICE: >
14 //   This program is free software; you can redistribute it and/or
15 //   modify it under the terms of version 2.0  of the GNU General
16 //   Public License as published by the Free Software Foundation.
17 //
18 //   This program is distributed in the hope that it will be useful,
19 //   but WITHOUT ANY WARRANTY; without even the implied warranty of
20 //   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
21 //   GNU General Public License for more details.
22 //
23 //   You should have received a copy of version 2.0 of the GNU General
24 //   Public License along with this program; if not, write to the Free
25 //   Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
26 //   MA 02110-1301, USA.
27 //
28 //
29 // ## END COPYRIGHT, LICENSE AND WARRANTY NOTICE ##
30 //
31
32 /*!
33   \class eZWebDAVContentBackend ezwebdavcontentbackend.php
34   \ingroup eZWebDAV
35   \brief Provides access to eZ Publish kernel using WebDAV.
36          Based on the eZ Components Webdav component.
37
38   @todo Replace direct path manipulation with path factory from ezcWebdav
39   @todo Fix appendLogEntry to write in only one log file
40   @todo Fix using [0] for content object attributes (could be another index in some classes)
41   @todo Add lock/unlock calls in setProperty and removeProperty
42   @todo Use PathPrefix, PathPrefixExclude (site.ini) and StartNode (webdav.ini) in all functions where necessary
43   @todo Remove all todos.
44
45 */
46
47 /**
48  * WebDAV backend for eZ Publish, based on eZ Components Webdav component.
49  */
50 class eZWebDAVContentBackend extends ezcWebdavSimpleBackend implements ezcWebdavLockBackend
51 {
52     /**
53      * The name of the content folder in eZ Publish.
54      */
55     const VIRTUAL_CONTENT_FOLDER_NAME = 'Content';
56
57     /**
58      * The name of the media folder in eZ Publish.
59      */
60     const VIRTUAL_MEDIA_FOLDER_NAME = 'Media';
61
62     /**
63      * The ini file which holds settings for WebDAV.
64      */
65     const WEBDAV_INI_FILE = "webdav.ini";
66
67     /**
68      * Mimetype for directories.
69      */
70     const DIRECTORY_MIMETYPE = 'httpd/unix-directory';
71
72     /**
73      * Mimetype for eZ Publish objects which don't have a mimetype.
74      */
75     const DEFAULT_MIMETYPE = "application/octet-stream";
76
77     /**
78      * Default size in bytes for eZ Publish objects which don't have a size.
79      */
80     const DEFAULT_SIZE = 0;
81
82     /**
83      * Names of live properties from the DAV: namespace which will be handled
84      * live, and should not be stored like dead properties.
85      *
86      * @var array(string)
87      */
88     protected $handledLiveProperties = array(
89         'getcontentlength',
90         'getlastmodified',
91         'creationdate',
92         'displayname',
93         'getetag',
94         'getcontenttype',
95         'resourcetype',
96         'supportedlock',
97         'lockdiscovery',
98     );
99
100     /**
101      * Contains an array with classes that are considered folder.
102      *
103      * @var array(string)
104      */
105     protected $FolderClasses = null;
106
107     /**
108      * The list of available sites.
109      *
110      * @var array(string)
111      */
112     protected $availableSites = array();
113
114     /**
115      * Holds the retrieved nodes to allow for faster retrieval on subsequent requests.
116      *
117      * @var array(string=>array())
118      */
119     protected $cachedNodes = array();
120
121     /**
122      * Holds the retrieved properties to allow for faster retrieval on subsequent requests.
123      *
124      * @var array(string=>array())
125      */
126     protected $cachedProperties = array();
127
128     /**
129      * Specifies weather to log with appendLogEntry().
130      *
131      * Value defined in webdav.ini, read in appendLogEntry().
132      *
133      * @var bool
134      */
135     protected static $useLogging;
136
137     /**
138      * Creates a new backend instance.
139      */
140     public function __construct()
141     {
142         // @as @todo check how to make Article class to be handled as a resource (document) instead of a collection
143         $webdavINI = eZINI::instance( self::WEBDAV_INI_FILE );
144
145         $folderClasses = array();
146         if ( $webdavINI->hasGroup( 'GeneralSettings' ) and
147              $webdavINI->hasVariable( 'GeneralSettings', 'FolderClasses' ) )
148         {
149             $folderClasses = $webdavINI->variable( 'GeneralSettings', 'FolderClasses' );
150         }
151         $this->FolderClasses = $folderClasses;
152
153         $ini = eZINI::instance();
154         $this->availableSites = $ini->variable( 'SiteSettings', 'SiteList' );
155     }
156
157     /**
158      * Locks the backend.
159      *
160      * Tries to lock the backend. If the lock is already owned by this process,
161      * locking is successful. If $timeout is reached before a lock could be
162      * acquired, an {@link ezcWebdavLockTimeoutException} is thrown. Waits
163      * $waitTime microseconds between attempts to lock the backend.
164      * 
165      * @param int $waitTime 
166      * @param int $timeout 
167      * @return void
168      */
169     public function lock( $waitTime, $timeout )
170     {
171         // @as @todo implement locking with eZ Publish functionality (object states)
172     }
173
174     /**
175      * Removes the lock.
176      * 
177      * @return void
178      */
179     public function unlock()
180     {
181         // @as @todo implement locking with eZ Publish functionality (object states)
182     }
183
184     /**
185      * Wait and get lock for complete directory tree.
186      *
187      * Acquire lock for the complete tree for read or write operations. This
188      * does not implement any priorities for operations, or check if several
189      * read operation may run in parallel. The plain locking should / could be
190      * extended by something more sophisticated.
191      *
192      * If the tree already has been locked, the method waits until the lock can
193      * be acquired.
194      *
195      * The optional second parameter $readOnly indicates wheather a read only
196      * lock should be acquired. This may be used by extended implementations,
197      * but it is not used in this implementation.
198      *
199      * @param bool $readOnly
200      */
201     protected function acquireLock( $readOnly = false )
202     {
203         // @as @todo implement locking with eZ Publish functionality (object states)
204     }
205
206     /**
207      * Free lock.
208      *
209      * Frees the lock after the operation has been finished.
210      */
211     protected function freeLock()
212     {
213         // @as @todo implement locking with eZ Publish functionality (object states)
214     }
215
216     /**
217      * Returns all child nodes.
218      *
219      * Get all nodes from the resource identified by $source up to the given
220      * depth. Reuses the method {@link getCollectionMembers()}, but you may
221      * want to overwrite this implementation by somethings which fits better
222      * with your backend.
223      *
224      * @param string $source
225      * @param int $depth
226      * @return array(ezcWebdavResource|ezcWebdavCollection)
227      */
228     protected function getNodes( $requestUri, $depth )
229     {
230         $source = $requestUri;
231         $nodeInfo = $this->getNodeInfo( $requestUri );
232
233         if ( !$nodeInfo['nodeExists'] )
234         {
235             return array();
236         }
237
238         // No special handling for plain resources
239         if ( !$nodeInfo['isCollection'] )
240         {
241             return array( new ezcWebdavResource( $source, $this->getAllProperties( $source ) ) );
242         }
243
244         // For zero depth just return the collection
245         if ( $depth === ezcWebdavRequest::DEPTH_ZERO )
246         {
247             return array( new ezcWebdavCollection( $source, $this->getAllProperties( $source ) ) );
248         }
249
250         $nodes = array( new ezcWebdavCollection( $source, $this->getAllProperties( $source ) ) );
251         $recurseCollections = array( $source );
252
253         // Collect children for all collections listed in $recurseCollections.
254         for ( $i = 0; $i < count( $recurseCollections ); ++$i )
255         {
256             $source = $recurseCollections[$i];
257
258             // add the slash at the end of the path if it is missing
259             if ( $source{strlen( $source ) - 1} !== '/' )
260             {
261                 $source .= '/';
262             }
263             $children = $this->getCollectionMembers( $source, $depth );
264
265             foreach ( $children as $child )
266             {
267                 $nodes[] = $child;
268
269                 // Check if we should recurse deeper, and add collections to
270                 // processing list in this case.
271                 if ( $child instanceof ezcWebdavCollection
272                      && $depth === ezcWebdavRequest::DEPTH_INFINITY
273                      && $child->path !== $source ) // @as added for recursive DEPTH_INFINITY
274                 {
275                     $recurseCollections[] = $child->path;
276                 }
277             }
278         }
279
280         return $nodes;
281     }
282
283     /**
284      * Returns the contents of a resource.
285      *
286      * This method returns the content of the resource identified by $path as a
287      * string.
288      *
289      * @param string $target
290      * @return string
291      */
292     protected function getResourceContents( $target )
293     {
294         $result = array();
295         $fullPath = $target;
296         $target = $this->splitFirstPathElement( $fullPath, $currentSite );
297
298         $data = $this->getVirtualFolderData( $result, $currentSite, $target, $fullPath );
299
300         if ( isset( $data['file'] ) )
301         {
302             return file_get_contents( $data['file'] );
303         }
304         return false;
305     }
306
307     /**
308      * Returns members of collection.
309      *
310      * Returns an array with the members of the collection identified by $path.
311      * The returned array can contain {@link ezcWebdavCollection}, and {@link
312      * ezcWebdavResource} instances and might also be empty, if the collection
313      * has no members.
314      *
315      * Added $depth.
316      *
317      * @param string $path
318      * @param int $depth Added by @as
319      * @return array(ezcWebdavResource|ezcWebdavCollection)
320      */
321     protected function getCollectionMembers( $path, $depth = ezcWebdavRequest::DEPTH_INFINITY )
322     {
323         $properties = $this->handledLiveProperties;
324         $fullPath = $path;
325         $collection = $this->splitFirstPathElement( $fullPath, $currentSite );
326
327         if ( !$currentSite )
328         {
329             // Display the root which contains a list of sites
330             $entries = $this->fetchSiteListContent( $fullPath, $depth, $properties );
331         }
332         else
333         {
334             $entries = $this->getVirtualFolderCollection( $currentSite, $collection, $fullPath, $depth, $properties );
335         }
336
337         $contents = array();
338
339         foreach ( $entries as $entry )
340         {
341             // prevent infinite recursion
342             if ( $path === $entry['href'] )
343             {
344                 continue;
345             }
346
347             if ( $entry['mimetype'] === self::DIRECTORY_MIMETYPE )
348             {
349                 // Add collection without any children
350                 $contents[] = new ezcWebdavCollection( $entry['href'], $this->getAllProperties( $path ) );
351             }
352             else
353             {
354                 // If this is not a collection, don't leave a trailing '/'
355                 // on the href. If you do, Goliath gets confused.
356                 $entry['href'] = rtrim( $entry['href'], '/' );
357
358                 // Add files without content
359                 $contents[] = new ezcWebdavResource( $entry['href'], $this->getAllProperties( $path ) );
360             }
361         }
362
363         return $contents;
364     }
365
366     /**
367      * Returns an array with information about the node with path $path.
368      *
369      * The returned array is of this form:
370      * <code>
371      * array( 'nodeExists' => boolean, 'isCollection' => boolean )
372      * </code>
373      *
374      * @param string $path
375      * @return array(string=>boolean)
376      */
377     protected function getNodeInfo( $requestUri, $source = null )
378     {
379         $path = ( $source === null ) ? $requestUri : $source;
380
381         $fullPath = $path;
382         $target = $this->splitFirstPathElement( $path, $currentSite );
383
384         if ( !$currentSite )
385         {
386             $data = $this->fetchSiteListContent( $fullPath, 0, array() );
387             $data = $data[0];
388             $data['nodeExists'] = true;
389             $data['isCollection'] = true;
390         }
391         else
392         {
393             if ( !in_array( $currentSite, $this->availableSites ) )
394             {
395                 $data = array();
396                 $data['nodeExists'] = false;
397                 $data['isCollection'] = false;
398             }
399             else
400             {
401                 if ( $target === "" )
402                 {
403                     $data = $this->fetchVirtualSiteContent( $fullPath, $currentSite, 0, array() );
404                 }
405                 else if ( in_array( $target, array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) ) )
406                 {
407                     $data = $this->fetchContainerNodeInfo( $fullPath, $currentSite, $target );
408                 }
409                 else
410                 {
411                     $data = $this->getCollectionContent( $fullPath, 0, array() );
412                 }
413
414                 if ( is_array( $data ) )
415                 {
416                     $data = $data[0];
417                     $data['nodeExists'] = true;
418                     $data['isCollection'] = ( $data['mimetype'] === self::DIRECTORY_MIMETYPE );
419                     $data['href'] = $fullPath; // @as @todo move this hack to correct function
420                 }
421                 else
422                 {
423                     $data = array();
424                     $data['nodeExists'] = false;
425                     $data['isCollection'] = false;
426                 }
427             }
428         }
429         return $data;
430     }
431
432     /**
433      * Returns a property of a resource.
434      * 
435      * Returns the property with the given $propertyName, from the resource
436      * identified by $path. You may optionally define a $namespace to receive
437      * the property from.
438      *
439      * @param string $path 
440      * @param string $propertyName 
441      * @param string $namespace 
442      * @return ezcWebdavProperty
443      */
444     public function getProperty( $path, $propertyName, $namespace = 'DAV:' )
445     {
446         $storage = $this->getPropertyStorage( $path );
447
448         // Handle dead propreties
449         if ( $namespace !== 'DAV:' )
450         {
451             $properties = $storage->getAllProperties();
452             return $properties[$namespace][$name];
453         }
454
455         if ( !isset( $this->cachedProperties[$path] ) )
456         {
457             $this->cachedProperties[$path] = $this->getNodeInfo( $path );
458         }
459
460         $item = $this->cachedProperties[$path];
461
462         // Handle live properties
463         switch ( $propertyName )
464         {
465             case 'getcontentlength':
466                 $property = new ezcWebdavGetContentLengthProperty();
467                 $mimetype = isset( $item['mimetype'] ) ? $item['mimetype'] : self::DEFAULT_MIMETYPE;
468                 $size = isset( $item['size'] ) ? $item['size'] : self::DEFAULT_SIZE;
469                 $property->length = ( $mimetype === self::DIRECTORY_MIMETYPE ) ?
470                     ezcWebdavGetContentLengthProperty::COLLECTION :
471                     (string) $size;
472                 break;
473
474             case 'getlastmodified':
475                 $property = new ezcWebdavGetLastModifiedProperty();
476                 $timestamp = isset( $item['mtime'] ) ? $item['mtime'] : time();
477                 $property->date = new ezcWebdavDateTime( '@' . $timestamp );
478                 break;
479
480             case 'creationdate':
481                 $property = new ezcWebdavCreationDateProperty();
482                 $timestamp = isset( $item['ctime'] ) ? $item['ctime'] : time();
483                 $property->date = new ezcWebdavDateTime( '@' . $timestamp );
484                 break;
485
486             case 'displayname':
487                 $property = new ezcWebdavDisplayNameProperty();
488                 $property->displayName = isset( $item['name'] ) ? $item['name'] : 'Unknown displayname';
489                 break;
490
491             case 'getcontenttype':
492                 $property = new ezcWebdavGetContentTypeProperty();
493                 $property->mime = isset( $item['mimetype'] ) ? $item['mimetype'] : self::DEFAULT_MIMETYPE;
494                 break;
495
496             case 'getetag':
497                 $property = new ezcWebdavGetEtagProperty();
498                 $mimetype = isset( $item['mimetype'] ) ? $item['mimetype'] : self::DEFAULT_MIMETYPE;
499                 $size = isset( $item['size'] ) ? $item['size'] : self::DEFAULT_SIZE;
500                 $size = ( $mimetype === self::DIRECTORY_MIMETYPE ) ?
501                     ezcWebdavGetContentLengthProperty::COLLECTION :
502                     (string) $size;
503                 $timestamp = isset( $item['mtime'] ) ? $item['mtime'] : time();
504                 $property->etag = md5( $path . $size . date( 'c', $timestamp ) );
505                 break;
506
507             case 'resourcetype':
508                 $property = new ezcWebdavResourceTypeProperty();
509                 $mimetype = isset( $item['mimetype'] ) ? $item['mimetype'] : self::DEFAULT_MIMETYPE;
510                 $property->type = ( $mimetype === self::DIRECTORY_MIMETYPE ) ?
511                     ezcWebdavResourceTypeProperty::TYPE_COLLECTION : 
512                     ezcWebdavResourceTypeProperty::TYPE_RESOURCE;
513                 break;
514
515             case 'supportedlock':
516                 $property = new ezcWebdavSupportedLockProperty();
517                 break;
518
519             case 'lockdiscovery':
520                 $property = new ezcWebdavLockDiscoveryProperty();
521                 break;
522
523             default:
524                 // Handle all other live properties like dead properties
525                 $properties = $storage->getAllProperties();
526                 $property = $properties['DAV:'][$name]; // @as (need to figure $namespace)
527                 break;
528         }
529
530         return $property;
531     }
532
533     /**
534      * Returns all properties for a resource.
535      * 
536      * Returns all properties for the resource identified by $path as a {@link
537      * ezcWebdavBasicPropertyStorage}.
538      *
539      * @param string $path 
540      * @return ezcWebdavPropertyStorage
541      */
542     public function getAllProperties( $path )
543     {
544         $storage = $this->getPropertyStorage( $path );
545
546         // Add all live properties to stored properties
547         foreach ( $this->handledLiveProperties as $property )
548         {
549             $storage->attach(
550                 $this->getProperty( $path, $property )
551             );
552         }
553
554         return $storage;
555     }
556
557     /**
558      * Returns the property storage for a resource.
559      *
560      * Returns the {@link ezcWebdavPropertyStorage} instance containing the
561      * properties for the resource identified by $path.
562      * 
563      * @param string $path 
564      * @return ezcWebdavBasicPropertyStorage
565      */
566     protected function getPropertyStorage( $path )
567     {
568         $storage = new ezcWebdavBasicPropertyStorage();
569
570         // @todo implement property storage
571         return $storage;
572     }
573
574     /**
575      * Returns if a resource exists.
576      *
577      * Returns if a the resource identified by $path exists.
578      * 
579      * @param string $path 
580      * @return bool
581      */
582     protected function nodeExists( $path )
583     {
584         if ( !isset( $this->cachedNodes[$path] ) )
585         {
586             $this->cachedNodes[$path] = $this->getNodeInfo( $path );
587         }
588
589         return $this->cachedNodes[$path]['nodeExists'];
590     }
591
592     /**
593      * Returns if resource is a collection.
594      *
595      * Returns if the resource identified by $path is a collection resource
596      * (true) or a non-collection one (false).
597      * 
598      * @param string $path 
599      * @return bool
600      */
601     protected function isCollection( $path )
602     {
603         if ( !isset( $this->cachedNodes[$path] ) )
604         {
605             $this->cachedNodes[$path] = $this->getNodeInfo( $path );
606         }
607
608         return $this->cachedNodes[$path]['isCollection'];
609     }
610
611     /**
612      * Serves GET requests.
613      *
614      * The method receives a {@link ezcWebdavGetRequest} object containing all
615      * relevant information obout the clients request and will return an {@link
616      * ezcWebdavErrorResponse} instance on error or an instance of {@link
617      * ezcWebdavGetResourceResponse} or {@link ezcWebdavGetCollectionResponse}
618      * on success, depending on the type of resource that is referenced by the
619      * request.
620      *
621      * This method acquires the internal lock of the backend, dispatches to
622      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
623      * lock afterwards.
624      *
625      * @param ezcWebdavGetRequest $request
626      * @return ezcWebdavResponse
627      */
628     public function get( ezcWebdavGetRequest $request )
629     {
630         $this->acquireLock( true );
631         $return = parent::get( $request );
632         $this->freeLock();
633
634         return $return;
635     }
636
637     /**
638      * Serves HEAD requests.
639      *
640      * The method receives a {@link ezcWebdavHeadRequest} object containing all
641      * relevant information obout the clients request and will return an {@link
642      * ezcWebdavErrorResponse} instance on error or an instance of {@link
643      * ezcWebdavHeadResponse} on success.
644      *
645      * This method acquires the internal lock of the backend, dispatches to
646      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
647      * lock afterwards.
648      * 
649      * @param ezcWebdavHeadRequest $request
650      * @return ezcWebdavResponse
651      */
652     public function head( ezcWebdavHeadRequest $request )
653     {
654         $this->acquireLock( true );
655         $return = parent::head( $request );
656         $this->freeLock();
657
658         return $return;
659     }
660
661     /**
662      * Serves PROPFIND requests.
663      * 
664      * The method receives a {@link ezcWebdavPropFindRequest} object containing
665      * all relevant information obout the clients request and will either
666      * return an instance of {@link ezcWebdavErrorResponse} to indicate an error
667      * or a {@link ezcWebdavPropFindResponse} on success. If the referenced
668      * resource is a collection or if some properties produced errors, an
669      * instance of {@link ezcWebdavMultistatusResponse} may be returned.
670      *
671      * The {@link ezcWebdavPropFindRequest} object contains a definition to
672      * find one or more properties of a given collection or non-collection
673      * resource.
674      *
675      * This method acquires the internal lock of the backend, dispatches to
676      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
677      * lock afterwards.
678      *
679      * This method is an overwrite of the propFind method from
680      * ezcWebdavSimpleBackend, a hack necessary to permit correct
681      * output of eZ Publish nodes. The array of ezcWebdavPropFindResponse
682      * objects returned by ezcWebdavSimpleBackend::propFind is iterated and
683      * the paths of the nodes in the ezcWebdavPropFindResponse objects
684      * are encoded properly, in order to be displayed correctly in WebDAV
685      * clients. The encoding is from the ini setting Charset in
686      * [CharacterSettings] in i18n.ini.
687      *
688      * The code for coding is taken from eZWebDAVServer::outputCollectionContent().
689      *
690      * @param ezcWebdavPropFindRequest $request
691      * @return ezcWebdavResponse
692      */
693     public function propFind( ezcWebdavPropFindRequest $request )
694     {
695         $ini = eZINI::instance( 'i18n.ini' );
696         $dataCharset = $ini->variable( 'CharacterSettings', 'Charset' );
697         $xmlCharset = 'utf-8';
698
699         $this->acquireLock( true );
700         $return = parent::propFind( $request );
701         if ( isset( $return->responses ) && is_array( $return->responses ) )
702         {
703             foreach ( $return->responses as $response )
704             {
705                 $href = $response->node->path;
706                 $pathArray = explode( '/', self::recode( $href, $dataCharset, $xmlCharset ) );
707                 $encodedPath = '/';
708
709                 foreach ( $pathArray as $pathElement )
710                 {
711                     if ( $pathElement != '' )
712                     {
713                         $encodedPath .= rawurlencode( $pathElement );
714                         $encodedPath .= '/';
715                     }
716                 }
717                 $encodedPath = rtrim( $encodedPath, '/' );
718                 $response->node->path = $encodedPath;
719             }
720         }
721         $this->freeLock();
722
723         return $return;
724     }
725
726     /**
727      * Serves PROPPATCH requests.
728      *
729      * The method receives a {@link ezcWebdavPropPatchRequest} object
730      * containing all relevant information obout the clients request and will
731      * return an instance of {@link ezcWebdavErrorResponse} on error or a
732      * {@link ezcWebdavPropPatchResponse} response on success. If the
733      * referenced resource is a collection or if only some properties produced
734      * errors, an instance of {@link ezcWebdavMultistatusResponse} may be
735      * returned.
736      *
737      * This method acquires the internal lock of the backend, dispatches to
738      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
739      * lock afterwards.
740      *
741      * @param ezcWebdavPropPatchRequest $request
742      * @return ezcWebdavResponse
743      */
744     public function propPatch( ezcWebdavPropPatchRequest $request )
745     {
746         $this->acquireLock();
747         $return = parent::propPatch( $request );
748         $this->freeLock();
749
750         return $return;
751     }
752
753     /**
754      * Stores properties for a resource.
755      *
756      * Creates a new property storage file and stores the properties given for
757      * the resource identified by $path.  This depends on the affected resource
758      * and the actual properties in the property storage.
759      *
760      * @param string $path
761      * @param ezcWebdavBasicPropertyStorage $storage
762      */
763     protected function storeProperties( $path, ezcWebdavBasicPropertyStorage $storage )
764     {
765         // @as @todo implement storing properties
766         return true;
767     }
768
769     /**
770      * Manually sets a property on a resource.
771      *
772      * Sets the given $propertyBackup for the resource identified by $path.
773      *
774      * @param string $path
775      * @param ezcWebdavProperty $property
776      * @return bool
777      */
778     public function setProperty( $path, ezcWebdavProperty $property )
779     {
780         if ( !in_array( $property->name, $this->handledLiveProperties, true ) )
781         {
782             return false;
783         }
784
785         // @as @todo implement setting properties
786         // @todo implement locking and unlocking based on the code
787         // lock:
788         // replace 30607 with your object ID
789         // $object = eZContentObject::fetch( 30607 );
790         // $stateGroup = eZContentObjectStateGroup::fetchByIdentifier( 'ez_lock' );
791         // $state = eZContentObjectState::fetchByIdentifier( 'locked', $stateGroup->attribute( 'id' ) );
792         // $object->assignState( $state );
793
794         // unlock:
795         // $state = eZContentObjectState::fetchByIdentifier( 'not_locked', $stateGroup->attribute( 'id' ) );
796         // $object->assignState( $state );
797
798         // Get namespace property storage
799         $storage = new ezcWebdavBasicPropertyStorage();
800
801         // Attach property to store
802         $storage->attach( $property );
803
804         // Store document back
805         $this->storeProperties( $path, $storage );
806
807         return true;
808     }
809
810     /**
811      * Manually removes a property from a resource.
812      *
813      * Removes the given $property form the resource identified by $path.
814      *
815      * @param string $path
816      * @param ezcWebdavProperty $property
817      * @return bool
818      */
819     public function removeProperty( $path, ezcWebdavProperty $property )
820     {
821         // @as @todo implement removing properties
822         return true;
823     }
824
825     /**
826      * Resets the property storage for a resource.
827      *
828      * Discards the current {@link ezcWebdavPropertyStorage} of the resource
829      * identified by $path and replaces it with the given $properties.
830      *
831      * @param string $path
832      * @param ezcWebdavPropertyStorage $storage
833      * @return bool
834      */
835     public function resetProperties( $path, ezcWebdavPropertyStorage $storage )
836     {
837         $this->storeProperties( $path, $storage );
838     }
839
840     /**
841      * Serves PUT requests.
842      *
843      * The method receives a {@link ezcWebdavPutRequest} objects containing all
844      * relevant information obout the clients request and will return an
845      * instance of {@link ezcWebdavErrorResponse} on error or {@link
846      * ezcWebdavPutResponse} on success.
847      *
848      * This method acquires the internal lock of the backend, dispatches to
849      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
850      * lock afterwards.
851      *
852      * @param ezcWebdavPutRequest $request
853      * @return ezcWebdavResponse
854      */
855     public function put( ezcWebdavPutRequest $request )
856     {
857         $this->acquireLock();
858         $return = parent::put( $request );
859         $this->freeLock();
860
861         return $return;
862     }
863
864     /**
865      * Creates a new resource.
866      *
867      * Creates a new resource at the given $path, optionally with the given
868      * content. If $content is empty, an empty resource will be created.
869      *
870      * @param string $path
871      * @param string $content
872      */
873     protected function createResource( $path, $content = null )
874     {
875         // the creation of the resource is done in setResourceContents()
876     }
877
878     /**
879      * Sets the contents of a resource.
880      *
881      * This method replaces the content of the resource identified by $path
882      * with the submitted $content.
883      *
884      * @param string $path
885      * @param string $content
886      */
887     protected function setResourceContents( $path, $content )
888     {
889         // Attempt to get file/resource sent from client/browser.
890         $tempFile = $this->storeUploadedFile( $path, $content );
891         eZWebDAVContentBackend::appendLogEntry( 'SetResourceContents:' . $path . ';' . $tempFile );
892
893         // If there was an actual file:
894         if ( !$tempFile )
895         {
896             return false; // @as self::FAILED_FORBIDDEN;
897         }
898
899         // Attempt to do something with it (copy/whatever).
900         $fullPath = $path;
901         $target = $this->splitFirstPathElement( $path, $currentSite );
902
903         if ( !$currentSite )
904         {
905             return false; // @as self::FAILED_FORBIDDEN;
906         }
907
908         $result = $this->putVirtualFolderData( $currentSite, $target, $tempFile, $fullPath );
909
910         unlink( $tempFile );
911         eZDir::cleanupEmptyDirectories( dirname( $tempFile ) );
912
913         return $result;
914     }
915
916     /**
917      * Serves DELETE requests.
918      *
919      * The method receives a {@link ezcWebdavDeleteRequest} objects containing
920      * all relevant information obout the clients request and will return an
921      * instance of {@link ezcWebdavErrorResponse} on error or {@link
922      * ezcWebdavDeleteResponse} on success.
923      *
924      * This method acquires the internal lock of the backend, dispatches to
925      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
926      * lock afterwards.
927      *
928      * @param ezcWebdavDeleteRequest $request
929      * @return ezcWebdavResponse
930      */
931     public function delete( ezcWebdavDeleteRequest $request )
932     {
933         $this->acquireLock();
934         $return = parent::delete( $request );
935         $this->freeLock();
936
937         return $return;
938     }
939
940     /**
941      * Returns if everything below a path can be deleted recursively.
942      *
943      * Checks files and directories recursively and returns if everything can
944      * be deleted.  Returns an empty array if no errors occured, and an array
945      * with the files which caused errors otherwise.
946      *
947      * @param string $source
948      * @return array
949      */
950     public function checkDeleteRecursive( $target )
951     {
952         $errors = array();
953
954         $fullPath = $target;
955         $target = $this->splitFirstPathElement( $target, $currentSite );
956
957         if ( !$currentSite )
958         {
959             // Cannot delete root folder
960             return array(
961                 new ezcWebdavErrorResponse(
962                     ezcWebdavResponse::STATUS_403,
963                     $fullPath
964                 ),
965             );
966         }
967
968         if ( $target === "" )
969         {
970             // Cannot delete entries in site list
971             return array(
972                 new ezcWebdavErrorResponse(
973                     ezcWebdavResponse::STATUS_403,
974                     $fullPath
975                 ),
976             );
977         }
978
979         return $errors;
980     }
981
982     /**
983      * Deletes everything below a path.
984      *
985      * Deletes the resource identified by $path recursively. Returns an
986      * instance of {@link ezcWebdavErrorResponse} if the deletion failed, and
987      * null on success.
988      *
989      * In case performDelete() was called from a MOVE operation, it does not
990      * delete anything, because the move() function in ezcWebdavSimpleBackend
991      * first calls performDelete(), which deletes the destination if the source
992      * and destination are the same in URL alias terms.
993      *
994      * @param string $path
995      * @return ezcWebdavErrorResponse
996      */
997     protected function performDelete( $target )
998     {
999         switch ( $_SERVER['REQUEST_METHOD'] )
1000         {
1001             case 'MOVE':
1002                 // in case performDelete() was called from a MOVE operation,
1003                 // do not delete anything, because the move() function in
1004                 // ezcWebdavSimpleBackend first calls performDelete(), which
1005                 // deletes the destination if the source and destination
1006                 // are the same in URL alias terms
1007                 return null;
1008
1009             default:
1010                 $errors = $this->checkDeleteRecursive( $target );
1011                 break;
1012         }
1013
1014         // If an error will occur return the proper status. We return
1015         // multistatus in any case.
1016         if ( count( $errors ) > 0 )
1017         {
1018             return new ezcWebdavMultistatusResponse(
1019                 $errors
1020             );
1021         }
1022
1023         $fullPath = $target;
1024         $target = $this->splitFirstPathElement( $target, $currentSite );
1025
1026         $status = $this->deleteVirtualFolder( $currentSite, $target, $fullPath );
1027         // @as @todo return based on $status
1028
1029         // Return success
1030         return null;
1031     }
1032
1033     /**
1034      * Serves COPY requests.
1035      *
1036      * The method receives a {@link ezcWebdavCopyRequest} objects containing
1037      * all relevant information obout the clients request and will return an
1038      * instance of {@link ezcWebdavErrorResponse} on error or {@link
1039      * ezcWebdavCopyResponse} on success. If only some operations failed, this
1040      * method may return an instance of {@link ezcWebdavMultistatusResponse}.
1041      *
1042      * This method acquires the internal lock of the backend, dispatches to
1043      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
1044      * lock afterwards.
1045      *
1046      * @param ezcWebdavCopyRequest $request
1047      * @return ezcWebdavResponse
1048      */
1049     public function copy( ezcWebdavCopyRequest $request )
1050     {
1051         $this->acquireLock();
1052         $return = parent::copy( $request );
1053         $this->freeLock();
1054
1055         return $return;
1056     }
1057
1058     /**
1059      * Copies resources recursively from one path to another.
1060      *
1061      * Copies the resourced identified by $fromPath recursively to $toPath with
1062      * the given $depth, where $depth is one of {@link
1063      * ezcWebdavRequest::DEPTH_ZERO}, {@link ezcWebdavRequest::DEPTH_ONE},
1064      * {@link ezcWebdavRequest::DEPTH_INFINITY}.
1065      *
1066      * Returns an array with {@link ezcWebdavErrorResponse}s for all subtrees,
1067      * where the copy operation failed. Errors for subsequent resources in a
1068      * subtree should be ommitted.
1069      *
1070      * If an empty array is return, the operation has been completed
1071      * successfully.
1072      *
1073      * In case performCopy() was called from a MOVE operation, do a real move
1074      * operation, because the move() function from ezcWebdavSimpleBackend
1075      * calls performCopy() and performDelete().
1076      *
1077      * @param string $fromPath
1078      * @param string $toPath
1079      * @param int $depth
1080      * @return array(ezcWebdavErrorResponse)
1081      */
1082     protected function performCopy( $source, $destination, $depth = ezcWebdavRequest::DEPTH_INFINITY )
1083     {
1084         switch ( $_SERVER['REQUEST_METHOD'] )
1085         {
1086             case 'MOVE':
1087                 // in case performCopy() was called from a MOVE operation,
1088                 // do a real move operation, because the move() function
1089                 // from ezcWebdavSimpleBackend calls performCopy() and
1090                 // performDelete()
1091                 $errors = $this->moveRecursive( $source, $destination, $depth );
1092                 break;
1093
1094             case 'COPY':
1095                 $errors = $this->copyRecursive( $source, $destination, $depth );
1096                 break;
1097
1098             default:
1099                 $errors = $this->moveRecursive( $source, $destination, $depth );
1100                 break;
1101         }
1102
1103         // Transform errors
1104         foreach ( $errors as $nr => $error )
1105         {
1106             $errors[$nr] = new ezcWebdavErrorResponse(
1107                 ezcWebdavResponse::STATUS_423,
1108                 $error
1109             );
1110         }
1111
1112         // Copy dead properties
1113         $storage = $this->getPropertyStorage( $source );
1114         $this->storeProperties( $destination, $storage );
1115
1116         // Updateable live properties are updated automagically, because they
1117         // are regenerated on request on base of the file they affect. So there
1118         // is no reason to keep them "alive".
1119
1120         return $errors;
1121     }
1122
1123     /**
1124      * Recursively copy a file or directory.
1125      *
1126      * Recursively copy a file or directory in $source to the given
1127      * $destination. If a $depth is given, the operation will stop as soon as
1128      * the given recursion depth is reached. A depth of -1 means no limit,
1129      * while a depth of 0 means, that only the current file or directory will
1130      * be copied, without any recursion.
1131      *
1132      * Returns an empty array if no errors occured, and an array with the files
1133      * which caused errors otherwise.
1134      *
1135      * @param string $source
1136      * @param string $destination
1137      * @param int $depth
1138      * @return array
1139      */
1140     public function copyRecursive( $source, $destination, $depth = ezcWebdavRequest::DEPTH_INFINITY )
1141     {
1142         $errors = array();
1143         $fullSource = $source;
1144         $fullDestination = $destination;
1145         $source = $this->splitFirstPathElement( $source, $sourceSite );
1146         $destination = $this->splitFirstPathElement( $destination, $destinationSite );
1147
1148         if ( $sourceSite !== $destinationSite )
1149         {
1150             // We do not support copying from one site to another yet
1151             // TODO: Check if the sites are using the same db,
1152             //       if so allow the copy as a simple object copy
1153             //       If not we will have to do an object export from
1154             //       $sourceSite and import it in $destinationSite
1155             return array(); // @as self::FAILED_FORBIDDEN;
1156         }
1157
1158         if ( !$sourceSite or
1159              !$destinationSite )
1160         {
1161             // Cannot copy entries in site list
1162             return array( $fullSource ); // @as self::FAILED_FORBIDDEN;
1163         }
1164
1165         $this->copyVirtualFolder( $sourceSite, $destinationSite,
1166                                   $source, $destination );
1167
1168         if ( $depth === ezcWebdavRequest::DEPTH_ZERO || !$this->isCollection( $fullSource ) )
1169         {
1170             // Do not recurse (any more)
1171             return array();
1172         }
1173
1174         // Recurse
1175         $nodes = $this->getCollectionMembers( $fullSource );
1176         foreach ( $nodes as $node )
1177         {
1178             if ( $node->path === $fullSource . '/' )
1179             {
1180                 // skip the current node which was copied with copyVirtualFolder() above
1181                 continue;
1182             }
1183
1184             $newDepth = ( $depth !== ezcWebdavRequest::DEPTH_ONE ) ? $depth : ezcWebdavRequest::DEPTH_ZERO;
1185             $errors = array_merge(
1186                 $errors,
1187                 $this->copyRecursive( $node->path,
1188                                       $fullDestination . '/' . $this->getProperty( $node->path, 'displayname' )->displayName,
1189                                       $newDepth ) );
1190         }
1191
1192         return $errors;
1193     }
1194
1195     /**
1196      * Recursively move a file or directory.
1197      *
1198      * Recursively move a file or directory in $source to the given
1199      * $destination. If a $depth is given, the operation will stop as soon as
1200      * the given recursion depth is reached. A depth of -1 means no limit,
1201      * while a depth of 0 means, that only the current file or directory will
1202      * be copied, without any recursion.
1203      *
1204      * Returns an empty array if no errors occured, and an array with the files
1205      * which caused errors otherwise.
1206      *
1207      * @param string $source
1208      * @param string $destination
1209      * @param int $depth
1210      * @return array
1211      */
1212     public function moveRecursive( $source, $destination, $depth = ezcWebdavRequest::DEPTH_INFINITY )
1213     {
1214         $errors = array();
1215         $fullSource = $source;
1216         $fullDestination = $destination;
1217         $source = $this->splitFirstPathElement( $source, $sourceSite );
1218         $destination = $this->splitFirstPathElement( $destination, $destinationSite );
1219
1220         if ( $sourceSite !== $destinationSite )
1221         {
1222             // We do not support copying from one site to another yet
1223             // TODO: Check if the sites are using the same db,
1224             //       if so allow the copy as a simple object copy
1225             //       If not we will have to do an object export from
1226             //       $sourceSite and import it in $destinationSite
1227             return array(); // @as self::FAILED_FORBIDDEN;
1228         }
1229
1230         if ( !$sourceSite or
1231              !$destinationSite )
1232         {
1233             // Cannot copy entries in site list
1234             return array( $fullSource ); // @as self::FAILED_FORBIDDEN;
1235         }
1236
1237         $this->moveVirtualFolder( $sourceSite, $destinationSite,
1238                                   $source, $destination,
1239                                   $fullSource, $fullDestination );
1240
1241         if ( $depth === ezcWebdavRequest::DEPTH_ZERO || !$this->isCollection( $fullSource ) )
1242         {
1243             // Do not recurse (any more)
1244             return array();
1245         }
1246
1247         // Recurse
1248         $nodes = $this->getCollectionMembers( $fullSource );
1249         foreach ( $nodes as $node )
1250         {
1251             if ( $node->path === $fullSource . '/' )
1252             {
1253                 // skip the current node which was copied with copyVirtualFolder() above
1254                 continue;
1255             }
1256
1257             $newDepth = ( $depth !== ezcWebdavRequest::DEPTH_ONE ) ? $depth : ezcWebdavRequest::DEPTH_ZERO;
1258             $errors = array_merge(
1259                 $errors,
1260                 $this->moveRecursive( $node->path,
1261                                       $fullDestination . '/' . $this->getProperty( $node->path, 'displayname' )->displayName,
1262                                       $newDepth ) );
1263         }
1264
1265         return $errors;
1266     }
1267
1268     /**
1269      * Serves MOVE requests.
1270      *
1271      * The method receives a {@link ezcWebdavMoveRequest} objects containing
1272      * all relevant information obout the clients request and will return an
1273      * instance of {@link ezcWebdavErrorResponse} on error or {@link
1274      * ezcWebdavMoveResponse} on success. If only some operations failed, this
1275      * method may return an instance of {@link ezcWebdavMultistatusResponse}.
1276      *
1277      * This method acquires the internal lock of the backend, dispatches to
1278      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
1279      * lock afterwards.
1280      *
1281      * @param ezcWebdavMoveRequest $request
1282      * @return ezcWebdavResponse
1283      */
1284     public function move( ezcWebdavMoveRequest $request )
1285     {
1286         $this->acquireLock();
1287         $return = parent::move( $request );
1288         $this->freeLock();
1289
1290         return $return;
1291     }
1292
1293     /**
1294      * Serves MKCOL (make collection) requests.
1295      *
1296      * The method receives a {@link ezcWebdavMakeCollectionRequest} objects
1297      * containing all relevant information obout the clients request and will
1298      * return an instance of {@link ezcWebdavErrorResponse} on error or {@link
1299      * ezcWebdavMakeCollectionResponse} on success.
1300      *
1301      * This method acquires the internal lock of the backend, dispatches to
1302      * {@link ezcWebdavSimpleBackend} to perform the operation and releases the
1303      * lock afterwards.
1304      *
1305      * @param ezcWebdavMakeCollectionRequest $request
1306      * @return ezcWebdavResponse
1307      */
1308     public function makeCollection( ezcWebdavMakeCollectionRequest $request )
1309     {
1310         $this->acquireLock();
1311         $return = parent::makeCollection( $request );
1312         $this->freeLock();
1313
1314         return $return;
1315     }
1316
1317
1318     /**
1319      * Required method to serve OPTIONS requests.
1320      * 
1321      * The method receives a {@link ezcWebdavOptionsRequest} object containing all
1322      * relevant information obout the clients request and should either return
1323      * an error by returning an {@link ezcWebdavErrorResponse} object, or any
1324      * other {@link ezcWebdavResponse} objects.
1325      *
1326      * @param ezcWebdavOptionsRequest $request
1327      * @return ezcWebdavResponse
1328      */
1329     public function options( ezcWebdavOptionsRequest $request )
1330     {
1331         $response = new ezcWebdavOptionsResponse( '1' );
1332
1333         // Always allowed
1334         $allowed = 'GET, HEAD, PROPFIND, PROPPATCH, OPTIONS, ';
1335
1336         // Check if modifications are allowed
1337         if ( $this instanceof ezcWebdavBackendChange )
1338         {
1339             $allowed .= 'DELETE, COPY, MOVE, ';
1340         }
1341
1342         // Check if MKCOL is allowed
1343         if ( $this instanceof ezcWebdavBackendMakeCollection )
1344         {
1345             $allowed .= 'MKCOL, ';
1346         }
1347
1348         // Check if PUT is allowed
1349         if ( $this instanceof ezcWebdavBackendPut )
1350         {
1351             $allowed .= 'PUT, ';
1352         }
1353
1354         // Check if LOCK and UNLOCK are allowed
1355         if ( $this instanceof ezcWebdavLockBackend )
1356         {
1357             $allowed .= 'LOCK, UNLOCK, ';
1358         }
1359
1360         $response->setHeader( 'Allow', substr( $allowed, 0, -2 ) );
1361
1362         return $response;
1363     }
1364
1365
1366     // eZ Publish functionality -----------------------------------------
1367
1368
1369     /**
1370      * Sets the current site.
1371      *
1372      * From eZ Publish.
1373      *
1374      * @param string $site Eg. 'plain_site_user'
1375      * @todo remove or move in another class?
1376      */
1377     public function setCurrentSite( $site )
1378     {
1379         eZWebDAVContentBackend::appendLogEntry( __FUNCTION__ . '1:' . $site );
1380         $access = array( 'name' => $site,
1381                          'type' => eZSiteAccess::TYPE_STATIC );
1382
1383         $access = eZSiteAccess::change( $access );
1384         eZWebDAVContentBackend::appendLogEntry( __FUNCTION__ . '2:' . $site );
1385
1386         eZDebugSetting::writeDebug( 'kernel-siteaccess', $access, 'current siteaccess' );
1387
1388         // Clear/flush global database instance.
1389         $nullVar = null;
1390         eZDB::setInstance( $nullVar );
1391     }
1392
1393     /**
1394      * Detects a possible/valid site-name in start of a path.
1395      *
1396      * From eZ Publish.
1397      *
1398      * @param string $path Eg. '/plain_site_user/Content/Folder1/file1.txt'
1399      * @return string The name of the site that was detected (eg. 'plain_site_user')
1400      *                or false if not site could be detected
1401      * @todo remove or move in another class?
1402      */
1403     public function currentSiteFromPath( $path )
1404     {
1405         $indexDir = eZSys::indexDir();
1406
1407         // Remove indexDir if used in non-virtualhost mode.
1408         if ( preg_match( "#^" . preg_quote( $indexDir ) . "(.+)$#", $path, $matches ) )
1409         {
1410             $path = $matches[1];
1411         }
1412
1413         foreach ( $this->availableSites as $site )
1414         {
1415             // Check if given path starts with this site-name, if so: return it.
1416             if ( preg_match( "#^/" . preg_quote( $site ) . "(.*)$#", $path, $matches ) )
1417             {
1418                 return $site;
1419             }
1420         }
1421
1422         return false;
1423     }
1424
1425     /**
1426      * Gathers information about a given node specified as parameter.
1427      *
1428      * The format of the returned array is:
1429      * <code>
1430      * array( 'name' => node name (eg. 'Group picture'),
1431      *        'size' => storage size of the_node in bytes (eg. 57123),
1432      *        'mimetype' => mime type of the node (eg. 'image/jpeg'),
1433      *        'ctime' => creation time as timestamp,
1434      *        'mtime' => latest modification time as timestamp,
1435      *        'href' => the path to the node (eg. '/plain_site_user/Content/Folder1/file1.jpg')
1436      * </code>
1437      *
1438      * @param string $target Eg. '/plain_site_user/Content/Folder1/file1.jpg
1439      * @param eZContentObject &$node The node corresponding to $target
1440      * @return array(string=>mixed)
1441      * @todo remove/replace .ini calls, eZContentUpload, eZMimeType, eZSys RequestURI
1442      * @todo handle articles as files
1443      */
1444     protected function fetchNodeInfo( $target, &$node )
1445     {
1446         // When finished, we'll return an array of attributes/properties.
1447         $entry = array();
1448
1449         $classIdentifier = $node->attribute( 'class_identifier' );
1450
1451         $object = $node->attribute( 'object' );
1452
1453         // By default, everything is displayed as a folder:
1454         // Trim the name of the node, it is in some cases whitespace in eZ Publish
1455         $name = trim( $node->attribute( 'name' ) );
1456
1457         // @as 2009-03-09: return node_id as displayname in case name is missing
1458         // displayname is not actually used by WebDAV clients
1459         $entry["name"] = ( $name !== '' && $name !== NULL ) ? $name : $node->attribute( 'node_id' );
1460         $entry["size"] = 0;
1461         $entry["mimetype"] = self::DIRECTORY_MIMETYPE;
1462         eZWebDAVContentBackend::appendLogEntry( 'FetchNodeInfo:' . $node->attribute( 'name' ) . '/' . $node->urlAlias() );
1463
1464         // @todo handle articles as files
1465         // if ( $classIdentifier === 'article' )
1466         // {
1467         //     $entry["mimetype"] = 'application/ms-word';
1468         // }
1469
1470         $entry["ctime"] = $object->attribute( 'published' );
1471         $entry["mtime"] = $object->attribute( 'modified' );
1472
1473         $upload = new eZContentUpload();
1474         $info = $upload->objectFileInfo( $object );
1475
1476         $suffix = '';
1477         $class = $object->contentClass();
1478         $isObjectFolder = $this->isObjectFolder( $object, $class );
1479
1480         if ( $isObjectFolder )
1481         {
1482             // We do nothing, the default is to see it as a folder
1483         }
1484         else if ( $info )
1485         {
1486             $filePath = $info['filepath'];
1487             $entry['filepath'] = $filePath;
1488             $entry["mimetype"] = false;
1489             $entry["size"] = false;
1490             if ( isset( $info['filesize'] ) )
1491             {
1492                 $entry['size'] = $info['filesize'];
1493             }
1494             if ( isset( $info['mime_type'] ) )
1495             {
1496                 $entry['mimetype'] = $info['mime_type'];
1497             }
1498
1499             // Fill in information from the actual file if they are missing.
1500             $file = eZClusterFileHandler::instance( $filePath );
1501             if ( !$entry['size'] and $file->exists() )
1502             {
1503                 $entry["size"] = $file->size();
1504             }
1505             if ( !$entry['mimetype']  )
1506             {
1507                 $mimeInfo = eZMimeType::findByURL( $filePath );
1508                 $entry["mimetype"] = $mimeInfo['name'];
1509
1510                 $suffix = $mimeInfo['suffix'];
1511
1512                 if ( strlen( $suffix ) > 0 )
1513                 {
1514                     $entry["name"] .= '.' . $suffix;
1515                 }
1516             }
1517             else
1518             {
1519                 // eZMimeType returns first suffix in its list
1520                 // this could be another one than the original file extension
1521                 // so let's try to get the suffix from the file path first
1522                 $suffix = eZFile::suffix( $filePath );
1523                 if ( !$suffix )
1524                 {
1525                     $mimeInfo = eZMimeType::findByName( $entry['mimetype'] );
1526                     $suffix = $mimeInfo['suffix'];
1527                 }
1528
1529                 if ( strlen( $suffix ) > 0 )
1530                 {
1531                     $entry["name"] .= '.' . $suffix;
1532                 }
1533             }
1534
1535             if ( $file->exists() )
1536             {
1537                 $entry["ctime"] = $file->mtime();
1538                 $entry["mtime"] = $file->mtime();
1539             }
1540         }
1541         else
1542         {
1543             // Here we only show items as folders if they have
1544             // is_container set to true, otherwise it's an unknown binary file
1545             if ( !$class->attribute( 'is_container' ) )
1546             {
1547                 $entry['mimetype'] = 'application/octet-stream';
1548             }
1549         }
1550
1551         $scriptURL = $target;
1552         if ( strlen( $scriptURL ) > 0 and $scriptURL[ strlen( $scriptURL ) - 1 ] !== "/" )
1553         {
1554             $scriptURL .= "/";
1555         }
1556
1557         $trimmedScriptURL = trim( $scriptURL, '/' );
1558         $scriptURLParts = explode( '/', $trimmedScriptURL );
1559
1560         $urlPartCount = count( $scriptURLParts );
1561
1562         if ( $urlPartCount >= 2 )
1563         {
1564             // one of the virtual folders
1565             // or inside one of the virtual folders
1566             $siteAccess = $scriptURLParts[0];
1567             $virtualFolder = $scriptURLParts[1];
1568
1569             // only when the virtual folder is Content we need to add its path to the start URL
1570             // the paths of other top level folders (like Media) are included in URL aliases of their descending nodes
1571             if ( $virtualFolder === self::virtualContentFolderName() )
1572             {
1573                 $startURL = '/' . $siteAccess . '/' . $virtualFolder . '/';
1574             }
1575             else
1576             {
1577                 $startURL = '/' . $siteAccess . '/';
1578             }
1579         }
1580         else
1581         {
1582             // site access level
1583             $startURL = $scriptURL;
1584         }
1585
1586         // Set the href attribute (note that it doesn't just equal the name).
1587         if ( !isset( $entry['href'] ) )
1588         {
1589             if ( strlen( $suffix ) > 0 )
1590             {
1591                 $suffix = '.' . $suffix;
1592             }
1593
1594             $entry["href"] = $startURL . $node->urlAlias() . $suffix;
1595         }
1596
1597         // Return array of attributes/properties (name, size, mime, times, etc.).
1598         return $entry;
1599     }
1600
1601     /**
1602      * Handles data retrival on the virtual folder level.
1603      *
1604      * The format of the returned array is the same as $result:
1605      * <code>
1606      * array( 'isFile' => bool,
1607      *        'isCollection' => bool,
1608      *        'file' => path to the storage file which contain the contents );
1609      * </code>
1610      *
1611      * @param array(string=>mixed) $result
1612      * @param string $currentSite Eg. 'plain_site_user'
1613      * @param string $target Eg. 'Content/Folder1/file1.txt'
1614      * @param string $fullPath Eg. '/plain_site_user/Content/Folder1/file1.txt'
1615      * @return array(string=>mixed) Or false if an error appeared
1616      */
1617     protected function getVirtualFolderData( $result, $currentSite, $target, $fullPath )
1618     {
1619         $target = $this->splitFirstPathElement( $target, $virtualFolder );
1620         if ( !$target )
1621         {
1622             // The rest in the virtual folder does not have any data
1623             return false; // self::FAILED_NOT_FOUND;
1624         }
1625
1626         if ( !in_array( $virtualFolder, array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) ) )
1627         {
1628             return false; // self::FAILED_NOT_FOUND;
1629         }
1630
1631         if ( $virtualFolder === self::virtualContentFolderName() or
1632              $virtualFolder === self::virtualMediaFolderName() )
1633         {
1634             return $this->getContentNodeData( $result, $currentSite, $virtualFolder, $target, $fullPath );
1635         }
1636         return false; // self::FAILED_NOT_FOUND;
1637     }
1638
1639     /**
1640      * Handles data retrival on the content tree level.
1641      *
1642      * The format of the returned array is the same as $result:
1643      * <code>
1644      * array( 'isFile' => bool,
1645      *        'isCollection' => bool,
1646      *        'file' => path to the storage file which contain the contents );
1647      * </code>
1648      *
1649      * @param array(string=>mixed) $result
1650      * @param string $currentSite Eg. 'plain_site_user'
1651      * @param string $virtualFolder Eg. 'Content'
1652      * @param string $target Eg. 'Folder1/file1.txt'
1653      * @param string $fullPath Eg. '/plain_site_user/Content/Folder1/file1.txt' 
1654      * @return array(string=>mixed) Or false if an error appeared
1655      * @todo remove/replace eZContentUpload
1656      */
1657     protected function getContentNodeData( $result, $currentSite, $virtualFolder, $target, $fullPath )
1658     {
1659         // Attempt to fetch the node the client wants to get.
1660         $nodePath = $this->internalNodePath( $virtualFolder, $target );
1661         eZWebDAVContentBackend::appendLogEntry( __FUNCTION__ . " Path:" . $nodePath );
1662         $node = $this->fetchNodeByTranslation( $nodePath );
1663         $info = $this->fetchNodeInfo( $fullPath, $node );
1664
1665         // Proceed only if the node is valid:
1666         if ( $node === null )
1667         {
1668             return $result;
1669         }
1670
1671         // Can we fetch the contents of the node
1672         if ( !$node->canRead() )
1673         {
1674             return false; // @as self::FAILED_FORBIDDEN;
1675         }
1676
1677         $object = $node->attribute( 'object' );
1678
1679         foreach ( $info as $key => $value )
1680         {
1681             $result[$key] = $value;
1682         }
1683
1684         $upload = new eZContentUpload();
1685         $info = $upload->objectFileInfo( $object );
1686
1687         if ( $info )
1688         {
1689             $result['file'] = $info['filepath'];
1690         }
1691         else
1692         {
1693             $class = $object->contentClass();
1694             if ( $this->isObjectFolder( $object, $class ) )
1695             {
1696                 $result['isCollection'] = true;
1697             }
1698
1699         }
1700         return $result;
1701     }
1702
1703     /**
1704      * Produces the collection content.
1705      *
1706      * Builds either the virtual start folder with the virtual content folder
1707      * in it (and additional files). OR: if we're browsing within the content
1708      * folder: it gets the content of the target/given folder.
1709      *
1710      * @param string $collection Eg. '/plain_site_user/Content/Folder1'
1711      * @param int $depth One of -1 (infinite), 0, 1
1712      * @param array(string) $properties Currently not used
1713      * @return array(string=>array())
1714      */
1715     protected function getCollectionContent( $collection, $depth = false, $properties = false )
1716     {
1717         $fullPath = $collection;
1718         $collection = $this->splitFirstPathElement( $collection, $currentSite );
1719
1720         if ( !$currentSite )
1721         {
1722             // Display the root which contains a list of sites
1723             $entries = $this->fetchSiteListContent( $fullPath, $depth, $properties );
1724             return $entries;
1725         }
1726
1727         return $this->getVirtualFolderCollection( $currentSite, $collection, $fullPath, $depth, $properties );
1728     }
1729
1730     /**
1731      * Same as fetchVirtualSiteContent(), but only one entry is returned
1732      * (Content or Media).
1733      *
1734      * An entry in the the returned array is of this form:
1735      * <code>
1736      * array( 'name' => node name (eg. 'Group picture'),
1737      *        'size' => storage size of the_node in bytes (eg. 57123),
1738      *        'mimetype' => mime type of the node (eg. 'image/jpeg'),
1739      *        'ctime' => creation time as timestamp,
1740      *        'mtime' => latest modification time as timestamp,
1741      *        'href' => the path to the node (eg. '/plain_site_user/Content/Folder1/file1.jpg')
1742      * </code>
1743      *
1744      * @param string $fullPath Eg. '/plain_site_user/Content/Folder1'
1745      * @param string $site Eg. 'plain_site_user'
1746      * @param string $nodeName Eg. 'Folder1'
1747      * @return array(array(string=>mixed))
1748      */
1749     protected function fetchContainerNodeInfo( $fullPath, $site, $nodeName )
1750     {
1751         $contentEntry             = array();
1752         $contentEntry["name"]     = $nodeName;
1753         $contentEntry["size"]     = 0;
1754         $contentEntry["mimetype"] = self::DIRECTORY_MIMETYPE;
1755         $contentEntry["ctime"]    = filectime( 'settings/siteaccess/' . $site );
1756         $contentEntry["mtime"]    = filemtime( 'settings/siteaccess/' . $site );
1757         $contentEntry["href"]     = $fullPath;
1758
1759         $entries[] = $contentEntry;
1760
1761         return $entries;
1762     }
1763
1764     /**
1765      * Builds and returns the content of the virtual start folder for a site.
1766      *
1767      * The virtual startfolder is an intermediate step between the site-list
1768      * and actual content. This directory contains the "content" folder which
1769      * leads to the site's actual content.
1770      *
1771      * An entry in the the returned array is of this form:
1772      * <code>
1773      * array( 'name' => node name (eg. 'Group picture'),
1774      *        'size' => storage size of the_node in bytes (eg. 57123),
1775      *        'mimetype' => mime type of the node (eg. 'image/jpeg'),
1776      *        'ctime' => creation time as timestamp,
1777      *        'mtime' => latest modification time as timestamp,
1778      *        'href' => the path to the node (eg. '/plain_site_user/Content/Folder1/file1.jpg')
1779      * </code>
1780      *
1781      * @param string $target Eg. '/plain_site_user/Content/Folder1'
1782      * @param string $site Eg. 'plain_site_user
1783      * @param string $depth One of -1 (infinite), 0, 1
1784      * @param array(string) $properties Currently not used
1785      * @return array(array(string=>mixed))
1786      */
1787     protected function fetchVirtualSiteContent( $target, $site, $depth, $properties )
1788     {
1789         $requestUri = $target;
1790
1791         // Always add the current collection
1792         $contentEntry = array();
1793         $scriptURL = $requestUri;
1794         if ( $scriptURL{strlen( $scriptURL ) - 1} !== '/' )
1795         {
1796             $scriptURL .= "/";
1797         }
1798         $contentEntry["name"]     = $scriptURL;
1799         $contentEntry["size"]     = 0;
1800         $contentEntry["mimetype"] = self::DIRECTORY_MIMETYPE;
1801         $contentEntry["ctime"]    = filectime( 'settings/siteaccess/' . $site );
1802         $contentEntry["mtime"]    = filemtime( 'settings/siteaccess/' . $site );
1803         $contentEntry["href"]     = $requestUri;
1804         $entries[] = $contentEntry;
1805
1806         $defctime = $contentEntry['ctime'];
1807         $defmtime = $contentEntry['mtime'];
1808
1809         if ( $depth > 0 )
1810         {
1811             $scriptURL = $requestUri;
1812             if ( $scriptURL{strlen( $scriptURL ) - 1} !== '/' )
1813             {
1814                 $scriptURL .= "/";
1815             }
1816
1817             // Set up attributes for the virtual content folder:
1818             foreach ( array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) as $name )
1819             {
1820                 $entry             = array();
1821                 $entry["name"]     = $name;
1822                 $entry["size"]     = 0;
1823                 $entry["mimetype"] = self::DIRECTORY_MIMETYPE;
1824                 $entry["ctime"]    = $defctime;
1825                 $entry["mtime"]    = $defmtime;
1826                 $entry["href"]     = $scriptURL . $name;
1827
1828                 $entries[]         = $entry;
1829             }
1830         }
1831
1832         return $entries;
1833     }
1834
1835     /**
1836      * Handles collections on the virtual folder level, if no virtual folder
1837      * elements are accessed it lists the virtual folders.
1838      *
1839      * An entry in the the returned array is of this form:
1840      * <code>
1841      * array( 'name' => node name (eg. 'Group picture'),
1842      *        'size' => storage size of the_node in bytes (eg. 57123),
1843      *        'mimetype' => mime type of the node (eg. 'image/jpeg'),
1844      *        'ctime' => creation time as timestamp,
1845      *        'mtime' => latest modification time as timestamp,
1846      *        'href' => the path to the node (eg. '/plain_site_user/Content/Folder1/file1.jpg')
1847      * </code>
1848      *
1849      * @param string $site Eg. 'plain_site_user
1850      * @param string $collection Eg. 'Folder1'
1851      * @param string $fullPath Eg. '/plain_site_user/Content/Folder1'
1852      * @param string $depth One of -1 (infinite), 0, 1
1853      * @param array(string) $properties Currently not used
1854      * @return array(array(string=>mixed))
1855      */
1856     protected function getVirtualFolderCollection( $currentSite, $collection, $fullPath, $depth, $properties )
1857     {
1858         if ( !$collection )
1859         {
1860             // We are inside a site so we display the virtual folder for the site
1861             $entries = $this->fetchVirtualSiteContent( $fullPath, $currentSite, $depth, $properties );
1862             return $entries;
1863         }
1864
1865         $collection = $this->splitFirstPathElement( $collection, $virtualFolder );
1866
1867         if ( !in_array( $virtualFolder, array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) ) )
1868         {
1869             return false; // self::FAILED_NOT_FOUND;
1870         }
1871
1872         // added by @ds 2008-12-07 to fix problems with IE6 SP2
1873         $ini = eZINI::instance();
1874         $prefixAdded = false;
1875         $prefix = $ini->hasVariable( 'SiteAccessSettings', 'PathPrefix' ) &&
1876                       $ini->variable( 'SiteAccessSettings', 'PathPrefix' ) != '' ? eZURLAliasML::cleanURL( $ini->variable( 'SiteAccessSettings', 'PathPrefix' ) ) : false;
1877
1878         if ( $prefix )
1879         {
1880             $escapedPrefix = preg_quote( $prefix, '#' );
1881             // Only prepend the path prefix if it's not already the first element of the url.
1882             if ( !preg_match( "#^$escapedPrefix(/.*)?$#i", $collection )  )
1883             {
1884                 $exclude = $ini->hasVariable( 'SiteAccessSettings', 'PathPrefixExclude' )
1885                            ? $ini->variable( 'SiteAccessSettings', 'PathPrefixExclude' )
1886                            : false;
1887                 $breakInternalURI = false;
1888                 foreach ( $exclude as $item )
1889                 {
1890                     $escapedItem = preg_quote( $item, '#' );
1891                     if ( preg_match( "#^$escapedItem(/.*)?$#i", $collection )  )
1892                     {
1893                         $breakInternalURI = true;
1894                         break;
1895                     }
1896                 }
1897
1898                 if ( !$breakInternalURI )
1899                 {
1900                     $collection = $prefix . '/' . $collection;
1901                     $prefixAdded = true;
1902                 }
1903             }
1904         }
1905
1906         return $this->getContentTreeCollection( $currentSite, $virtualFolder, $collection, $fullPath, $depth, $properties );
1907     }
1908
1909     /**
1910      * Handles collections on the content tree level.
1911      *
1912      * Depending on the virtual folder we will generate a node path url and fetch
1913      * the nodes for that path.
1914      *
1915      * An entry in the the returned array is of this form:
1916      * <code>
1917      * array( 'name' => node name (eg. 'Folder1'),
1918      *        'size' => storage size of the_node in bytes (eg. 4096 for collections),
1919      *        'mimetype' => mime type of the node (eg. 'httpd/unix-directory'),
1920      *        'ctime' => creation time as timestamp,
1921      *        'mtime' => latest modification time as timestamp,
1922      *        'href' => the path to the node (eg. '/plain_site_user/Content/Folder1/')
1923      * </code>
1924      *
1925      * @param string $site Eg. 'plain_site_user
1926      * @param string $virtualFolder Eg. 'Content'
1927      * @param string $collection Eg. 'Folder1'
1928      * @param string $fullPath Eg. '/plain_site_user/Content/Folder1/'
1929      * @param string $depth One of -1 (infinite), 0, 1
1930      * @param array(string) $properties Currently not used
1931      * @return array(array(string=>mixed))
1932      */
1933     protected function getContentTreeCollection( $currentSite, $virtualFolder, $collection, $fullPath, $depth, $properties )
1934     {
1935         $nodePath = $this->internalNodePath( $virtualFolder, $collection );
1936         $node = $this->fetchNodeByTranslation( $nodePath );
1937
1938         if ( !$node )
1939         {
1940             return false; // self::FAILED_NOT_FOUND;
1941         }
1942
1943         // Can we list the children of the node?
1944         if ( !$node->canRead() )
1945         {
1946             return false; // self::FAILED_FORBIDDEN;
1947         }
1948
1949         $entries = $this->fetchContentList( $fullPath, $node, $nodePath, $depth, $properties );
1950         return $entries;
1951     }
1952
1953     /**
1954      * Gets and returns the content of an actual node.
1955      *
1956      * List of other nodes belonging to the target node (one level below it)
1957      * will be returned as well.
1958      *
1959      * An entry in the the returned array is of this form:
1960      * <code>
1961      * array( 'name' => node name (eg. 'Content'),
1962      *        'size' => storage size of the_node in bytes (eg. 4096 for collections),
1963      *        'mimetype' => mime type of the node (eg. 'httpd/unix-directory'),
1964      *        'ctime' => creation time as timestamp,
1965      *        'mtime' => latest modification time as timestamp,
1966      *        'href' => the path to the node (eg. '/plain_site_user/Content/')
1967      * </code>
1968      *
1969      * @param string $fullPath Eg. '/plain_site_user/Content/'
1970      * @param eZContentObject &$node The note corresponding to $fullPath
1971      * @param string $target Eg. 'Content'
1972      * @param string $depth One of -1 (infinite), 0, 1
1973      * @param array(string) $properties Currently not used
1974      * @return array(array(string=>mixed))
1975      */
1976     protected function fetchContentList( $fullPath, &$node, $target, $depth, $properties )
1977     {
1978         // We'll return an array of entries (which is an array of attributes).
1979         $entries = array();
1980
1981         if ( $depth === ezcWebdavRequest::DEPTH_ONE || $depth === ezcWebdavRequest::DEPTH_INFINITY )
1982             // @as added || $depth === ezcWebdavRequest::DEPTH_INFINITY
1983         {
1984             // Get all the children of the target node.
1985             $subTree = $node->subTree( array ( 'Depth' => 1 ) );
1986
1987             // Build the entries array by going through all the
1988             // nodes in the subtree and getting their attributes:
1989             foreach ( $subTree as $someNode )
1990             {
1991                 $entries[] = $this->fetchNodeInfo( $fullPath, $someNode );
1992             }
1993         }
1994
1995         // Always include the information about the current level node
1996         $thisNodeInfo = $this->fetchNodeInfo( $fullPath, $node );
1997
1998         $scriptURL = $fullPath;
1999         if ( $scriptURL{strlen( $scriptURL ) - 1} !== '/' )
2000         {
2001             $scriptURL .= "/";
2002         }
2003
2004         $thisNodeInfo["href"] = $scriptURL;
2005         $entries[] = $thisNodeInfo;
2006
2007         // Return the content of the target.
2008         return $entries;
2009     }
2010
2011     /**
2012      * Builds a content-list of available sites and returns it.
2013      *
2014      * An entry in the the returned array is of this form:
2015      * <code>
2016      * array( 'name' => node name (eg. 'plain_site_user'),
2017      *        'size' => storage size of the_node in bytes (eg. 4096 for collections),
2018      *        'mimetype' => mime type of the node (eg. 'httpd/unix-directory'),
2019      *        'ctime' => creation time as timestamp,
2020      *        'mtime' => latest modification time as timestamp,
2021      *        'href' => the path to the node (eg. '/plain_site_user/')
2022      * </code>
2023      *
2024      * @param string $target Eg. '/'
2025      * @param string $depth One of -1 (infinite), 0, 1
2026      * @param array(string) $properties Currently not used
2027      * @return array(array(string=>mixed))
2028      */
2029     protected function fetchSiteListContent( $target, $depth, $properties )
2030     {
2031         // At the end: we'll return an array of entry-arrays.
2032         $entries = array();
2033
2034         // An entry consists of several attributes (name, size, etc).
2035         $contentEntry = array();
2036         $entries = array();
2037
2038         // add a slash at the end of the path, if it is missing
2039         $scriptURL = $target;
2040         if ( $scriptURL{strlen( $scriptURL ) - 1} !== '/' )
2041         {
2042             $scriptURL .= "/";
2043         }
2044
2045         $thisNodeInfo["href"] = $scriptURL;
2046
2047         // Set up attributes for the virtual site-list folder:
2048         $contentEntry["name"]     = '/';
2049         $contentEntry["href"]     = $scriptURL;
2050         $contentEntry["size"]     = 0;
2051         $contentEntry["mimetype"] = self::DIRECTORY_MIMETYPE;
2052         $contentEntry["ctime"]    = filectime( 'var' );
2053         $contentEntry["mtime"]    = filemtime( 'var' );
2054
2055         $entries[] = $contentEntry;
2056
2057         if ( $depth > 0 )
2058         {
2059             // For all available sites:
2060             foreach ( $this->availableSites as $site )
2061             {
2062                 // Set up attributes for the virtual site-list folder:
2063                 $contentEntry["name"]     = $scriptURL . $site . '/'; // @as added '/'
2064                 $contentEntry["size"]     = 0;
2065                 $contentEntry["mimetype"] = self::DIRECTORY_MIMETYPE;
2066                 $contentEntry["ctime"]    = filectime( 'settings/siteaccess/' . $site );
2067                 $contentEntry["mtime"]    = filemtime( 'settings/siteaccess/' . $site );
2068
2069                 if ( $target === '/' )
2070                 {
2071                     $contentEntry["href"] = $contentEntry["name"];
2072                 }
2073                 else
2074                 {
2075                     $contentEntry["href"] = $scriptURL . $contentEntry["name"];
2076                 }
2077
2078                 $entries[] = $contentEntry;
2079             }
2080         }
2081
2082         return $entries;
2083     }
2084
2085     /**
2086      * Attempts to fetch a possible node by translating the provided
2087      * string/path to a node-number.
2088      *
2089      * @param string $nodePathString Eg. 'Folder1/file1.txt'
2090      * @return eZContentObject Eg. the node of 'Folder1/file1.txt'
2091      */
2092     protected function fetchNodeByTranslation( $nodePathString )
2093     {
2094         // Get rid of possible extensions, remove .jpeg .txt .html etc..
2095         $nodePathString = $this->fileBasename( $nodePathString );
2096
2097         // Strip away last slash
2098         if ( strlen( $nodePathString ) > 0 and
2099              $nodePathString[strlen( $nodePathString ) - 1] === '/' )
2100         {
2101             $nodePathString = substr( $nodePathString, 0, strlen( $nodePathString ) - 1 );
2102         }
2103
2104         if ( strlen( $nodePathString ) > 0 )
2105         {
2106             $nodePathString = eZURLAliasML::convertPathToAlias( $nodePathString );
2107         }
2108
2109         $nodeID = eZURLAliasML::fetchNodeIDByPath( $nodePathString );
2110         if ( $nodeID == 2 )
2111         {
2112             // added by @ds 2008-12-07 to fix problems with IE6 SP2
2113             $ini = eZINI::instance( 'webdav.ini' );
2114             if ( $ini->hasVariable( 'GeneralSettings', 'StartNode' ) )
2115             {
2116                 $nodeID = $ini->variable( 'GeneralSettings', 'StartNode' );
2117             }
2118         }
2119         elseif ( $nodeID )
2120         {
2121         }
2122         else
2123         {
2124             return false;
2125         }
2126
2127         // Attempt to fetch the node.
2128         $node = eZContentObjectTreeNode::fetch( $nodeID );
2129
2130         // Return the node.
2131         return $node;
2132     }
2133
2134     /**
2135      * Attempts to fetch a possible node by translating the provided
2136      * string/path to a node-number.
2137      *
2138      * The last section of the path is removed before the actual
2139      * translation: hence, the PARENT node is returned.
2140      *
2141      * @param string $nodePathString Eg. 'Folder1/file1.txt'
2142      * @return eZContentObject Eg. the node of 'Folder1'
2143      */
2144     protected function fetchParentNodeByTranslation( $nodePathString )
2145     {
2146         // Strip extensions. E.g. .jpg
2147         $nodePathString = $this->fileBasename( $nodePathString );
2148
2149         // Strip away last slash
2150         if ( strlen( $nodePathString ) > 0 and
2151              $nodePathString[strlen( $nodePathString ) - 1] === '/' )
2152         {
2153             $nodePathString = substr( $nodePathString, 0, strlen( $nodePathString ) - 1 );
2154         }
2155
2156         $nodePathString = $this->splitLastPathElement( $nodePathString, $element );
2157
2158         if ( strlen( $nodePathString ) === 0 )
2159         {
2160             $nodePathString = '/';
2161         }
2162
2163         $nodePathString = eZURLAliasML::convertPathToAlias( $nodePathString );
2164
2165         // Attempt to translate the URL to something like "/content/view/full/84".
2166         $translateResult = eZURLAliasML::translate( $nodePathString );
2167
2168         // handle redirects
2169         while ( $nodePathString === 'error/301' )
2170         {
2171             $nodePathString = $translateResult;
2172
2173             $translateResult = eZURLAliasML::translate( $nodePathString );
2174         }
2175
2176         // Get the ID of the node (which is the last part of the translated path).
2177         if ( preg_match( "#^content/view/full/([0-9]+)$#", $nodePathString, $matches ) )
2178         {
2179             $nodeID = $matches[1];
2180         }
2181         else
2182         {
2183             $ini = eZINI::instance( 'webdav.ini' );
2184             if ( $ini->hasVariable( 'GeneralSettings', 'StartNode' ) )
2185             {
2186                 $nodeID = $ini->variable( 'GeneralSettings', 'StartNode' );
2187             }
2188         }
2189
2190         // Attempt to fetch the node.
2191         $node = eZContentObjectTreeNode::fetch( $nodeID );
2192
2193         // Return the node.
2194         return $node;
2195     }
2196
2197     /**
2198      * Creates a new collection (folder) at the given path $target.
2199      *
2200      * @param string $target Eg. '/plain_site_user/Content/Folder1'
2201      * @return bool
2202      */
2203     protected function createCollection( $target )
2204     {
2205         $target = $this->splitFirstPathElement( $target, $currentSite );
2206
2207         if ( !$currentSite )
2208         {
2209             // Site list cannot get new entries
2210             return false; // @as self::FAILED_FORBIDDEN;
2211         }
2212
2213         return $this->mkcolVirtualFolder( $currentSite, $target );
2214     }
2215
2216     /**
2217      * Handles collection creation on the virtual folder level.
2218      *
2219      * It will check if the target is below a content folder in which it
2220      * calls mkcolContent().
2221      *
2222      * @param string $currentSite Eg. 'plain_site_user'
2223      * @param string $target Eg. 'Content/Folder1'
2224      * @return bool
2225      */
2226     protected function mkcolVirtualFolder( $currentSite, $target )
2227     {
2228         $target = $this->splitFirstPathElement( $target, $virtualFolder );
2229
2230         if ( !in_array( $virtualFolder, array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) ) )
2231         {
2232             return false; // @as self::FAILED_NOT_FOUND;
2233         }
2234
2235         if ( !$target )
2236         {
2237             // We have reached the end of the path
2238             // We do not allow 'mkcol' operations for the virtual folder.
2239             return false; // @as self::FAILED_FORBIDDEN;
2240         }
2241
2242         if ( $virtualFolder === self::virtualContentFolderName() or
2243              $virtualFolder === self::virtualMediaFolderName() )
2244         {
2245             return $this->mkcolContent( $currentSite, $virtualFolder, $target );
2246         }
2247
2248         return false; // @as self::FAILED_FORBIDDEN;
2249     }
2250
2251     /**
2252      * Handles collection creation on the content tree level.
2253      *
2254      * It will try to find the parent node of the wanted placement and
2255      * create a new collection (folder etc.) as a child.
2256      *
2257      * @param string $currentSite Eg. 'plain_site_user'
2258      * @param string $virtualFolder Eg. 'Content'
2259      * @param string $target Eg. 'Folder1'
2260      * @return bool
2261      */
2262     protected function mkcolContent( $currentSite, $virtualFolder, $target )
2263     {
2264         $nodePath = $this->internalNodePath( $virtualFolder, $target );
2265         $node = $this->fetchNodeByTranslation( $nodePath );
2266         if ( $node )
2267         {
2268             return false; // @as self::FAILED_EXISTS;
2269         }
2270
2271         $parentNode = $this->fetchParentNodeByTranslation( $nodePath );
2272
2273         if ( !$parentNode )
2274         {
2275             return false; // @as self::FAILED_NOT_FOUND;
2276         }
2277
2278         // Can we create a collection in the parent node
2279         if ( !$parentNode->canRead() )
2280         {
2281             return false; // @as self::FAILED_FORBIDDEN;
2282         }
2283
2284         return $this->createFolder( $parentNode, $nodePath );
2285     }
2286
2287     /**
2288      * Creates a new folder under the given $target path.
2289      *
2290      * @param eZContentObject $parentNode
2291      * @param string $target Eg. 'Folder1'
2292      * @return bool
2293      */
2294     protected function createFolder( $parentNode, $target )
2295     {
2296         // Grab settings from the ini file:
2297         $webdavINI = eZINI::instance( self::WEBDAV_INI_FILE );
2298         $folderClassID = $webdavINI->variable( 'FolderSettings', 'FolderClass' );
2299         $languageCode = eZContentObject::defaultLanguage();
2300
2301         $contentObject = eZContentObject::createWithNodeAssignment( $parentNode, $folderClassID, $languageCode );
2302         if ( $contentObject )
2303         {
2304             $db = eZDB::instance();
2305             $db->begin();
2306             $version = $contentObject->version( 1 );
2307             $version->setAttribute( 'status', eZContentObjectVersion::STATUS_DRAFT );
2308             $version->store();
2309
2310             $contentObjectID = $contentObject->attribute( 'id' );
2311             $contentObjectAttributes = $version->contentObjectAttributes();
2312
2313             // @todo @as avoid using [0] (could be another index in some classes)
2314             $contentObjectAttributes[0]->setAttribute( 'data_text', basename( $target ) );
2315             $contentObjectAttributes[0]->store();
2316             $db->commit();
2317
2318             $operationResult = eZOperationHandler::execute( 'content', 'publish', array( 'object_id' => $contentObjectID,
2319                                                                                          'version' => 1 ) );
2320             return true; // @as self::OK_CREATED;
2321         }
2322         else
2323         {
2324             return false; // @as self::FAILED_FORBIDDEN;
2325         }
2326     }
2327
2328     /**
2329      * Handles deletion on the virtual folder level.
2330      *
2331      * It will check if the target is below a content folder in which it calls
2332      * deleteContent().
2333      *
2334      * @param string $currentSite Eg. 'plain_site_user'
2335      * @param string $target Eg. 'Content/Folder1/file1.txt'
2336      * @return bool
2337      */
2338     protected function deleteVirtualFolder( $currentSite, $target )
2339     {
2340         $target = $this->splitFirstPathElement( $target, $virtualFolder );
2341
2342         if ( !in_array( $virtualFolder, array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) ) )
2343         {
2344             return false; // @as self::FAILED_NOT_FOUND;
2345         }
2346
2347         if ( !$target )
2348         {
2349             // We have reached the end of the path
2350             // We do not allow 'delete' operations for the virtual folder.
2351             return false; // @as self::FAILED_FORBIDDEN;
2352         }
2353
2354         if ( $virtualFolder === self::virtualContentFolderName() or
2355              $virtualFolder === self::virtualMediaFolderName() )
2356         {
2357             return $this->deleteContent( $currentSite, $virtualFolder, $target );
2358         }
2359
2360         return false; // @as self::FAILED_FORBIDDEN;
2361     }
2362
2363     /**
2364      * Handles deletion on the content tree level.
2365      *
2366      * It will try to find the node with the path $target and then try to
2367      * remove it (ie. move to trash) if the user is allowed.
2368      *
2369      * @param string $currentSite Eg. 'plain_site_user'
2370      * @param string $virtualFolder Eg. 'Content'
2371      * @param string $target Eg. 'Folder1/file1.txt'
2372      * @return bool
2373      */
2374     protected function deleteContent( $currentSite, $virtualFolder, $target )
2375     {
2376         $nodePath = $this->internalNodePath( $virtualFolder, $target );
2377         $node = $this->fetchNodeByTranslation( $nodePath );
2378
2379         if ( $node === null )
2380         {
2381             return false; // @as self::FAILED_NOT_FOUND;
2382         }
2383
2384         // Can we delete the node?
2385         if ( !$node->canRead() or
2386              !$node->canRemove() )
2387         {
2388             return false; // @as self::FAILED_FORBIDDEN;
2389         }
2390
2391         // added by @as: use the content.ini setting for handling delete: 'delete' or 'trash'
2392         // default is 'trash'
2393         $contentINI = eZINI::instance( 'content.ini' );
2394         $removeAction = $contentINI->hasVariable( 'RemoveSettings', 'DefaultRemoveAction' ) ?
2395                         $contentINI->variable( 'RemoveSettings', 'DefaultRemoveAction' ) : 'trash';
2396
2397         if ( $removeAction !== 'trash' && $removeAction !== 'delete' )
2398         {
2399             // default remove action is to trash
2400             $removeAction = 'trash';
2401         }
2402
2403         $moveToTrash = ( $removeAction === 'trash' ) ? true : false;
2404
2405         $node->removeNodeFromTree( $moveToTrash );
2406         return true; // @as self::OK;
2407     }
2408
2409     /**
2410      * Handles data storage on the content tree level.
2411      *
2412      * It will check if the target is below a content folder in which it calls
2413      * putContentData().
2414      *
2415      * @param string $currentSite Eg. 'plain_site_user'
2416      * @param string $target Eg. 'Content/Folder1/file1.txt'
2417      * @param string $tempFile The temporary file holding the contents
2418      * @return bool
2419      */
2420     protected function putVirtualFolderData( $currentSite, $target, $tempFile )
2421     {
2422         $target = $this->splitFirstPathElement( $target, $virtualFolder );
2423
2424         if ( !$target )
2425         {
2426             // We have reached the end of the path
2427             // We do not allow 'put' operations for the virtual folder.
2428             return false; // @as self::FAILED_FORBIDDEN;
2429         }
2430
2431         if ( !in_array( $virtualFolder, array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) ) )
2432         {
2433             return false; // @as self::FAILED_CONFLICT;
2434         }
2435
2436         if ( $virtualFolder === self::virtualContentFolderName() or
2437              $virtualFolder === self::virtualMediaFolderName() )
2438         {
2439             $result = $this->putContentData( $currentSite, $virtualFolder, $target, $tempFile );
2440             return $result;
2441         }
2442
2443         return false; // @as self::FAILED_FORBIDDEN;
2444     }
2445
2446     /**
2447      * Handles data storage on the content tree level.
2448      *
2449      * It will try to find the parent node of the wanted placement and
2450      * create a new object with data from $tempFile.
2451      *
2452      * @param string $currentSite Eg. 'plain_site_user'
2453      * @param string $virtualFolder Eg. 'Content'
2454      * @param string $target Eg. 'Folder1/file1.txt'
2455      * @param string $tempFile The temporary file holding the contents
2456      * @return bool
2457      * @todo remove/replace eZContentUpload
2458      */
2459     protected function putContentData( $currentSite, $virtualFolder, $target, $tempFile )
2460     {
2461         $nodePath = $this->internalNodePath( $virtualFolder, $target );
2462
2463         $parentNode = $this->fetchParentNodeByTranslation( $nodePath );
2464         if ( $parentNode === null )
2465         {
2466             // The node does not exist, so we cannot put the file
2467             return false; // @as self::FAILED_CONFLICT;
2468         }
2469
2470         // Can we put content in the parent node
2471         if ( !$parentNode->canRead() )
2472         {
2473             return false; // @as self::FAILED_FORBIDDEN;
2474         }
2475
2476         $parentNodeID = $parentNode->attribute( 'node_id' );
2477
2478         $existingNode = $this->fetchNodeByTranslation( $nodePath );
2479
2480         $upload = new eZContentUpload();
2481
2482         if ( !$upload->handleLocalFile( $result, $tempFile, $parentNodeID, $existingNode ) )
2483         {
2484             if ( $result['status'] === eZContentUpload::STATUS_PERMISSION_DENIED )
2485             {
2486                 return false; // @as self::FAILED_FORBIDDEN;
2487             }
2488             else
2489             {
2490                 return false; // @as self::FAILED_UNSUPPORTED;
2491             }
2492         }
2493
2494         return true; // @as self::OK_CREATED;
2495     }
2496
2497     /**
2498      * Handles copying on the virtual folder level.
2499      *
2500      * It will check if the target is below a content folder in which it
2501      * calls copyContent().
2502      *
2503      * @param string $sourceSite Eg. 'plain_site_user'
2504      * @param string $destinationSite Eg. 'plain_site_user'
2505      * @param string $source Eg. 'Content/Folder1/file1.txt'
2506      * @param string $destination Eg. 'Content/Folder2/file1.txt'
2507      * @return bool
2508      */
2509     protected function copyVirtualFolder( $sourceSite, $destinationSite,
2510                                           $source, $destination )
2511     {
2512         $source = $this->splitFirstPathElement( $source, $sourceVFolder );
2513         $destination = $this->splitFirstPathElement( $destination, $destinationVFolder );
2514
2515         if ( !in_array( $sourceVFolder, array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) ) )
2516         {
2517             return false; // @as self::FAILED_NOT_FOUND;
2518         }
2519
2520         if ( !in_array( $destinationVFolder, array( self::virtualContentFolderName(), self::virtualMediaFolderName() ) ) )
2521         {
2522             return false; // @as self::FAILED_NOT_FOUND;
2523         }
2524
2525         if ( !$source or
2526              !$destination )
2527         {
2528             // We have reached the end of the path for source or destination
2529             // We do not allow 'copy' operations for the virtual folder (from or to)
2530             return false; // @as self::FAILED_FORBIDDEN;
2531         }
2532
2533         if ( ( $sourceVFolder === self::virtualContentFolderName() or
2534                $sourceVFolder === self::virtualMediaFolderName() ) and
2535              ( $destinationVFolder === self::virtualContentFolderName() or
2536                $destinationVFolder === self::virtualMediaFolderName() ) )
2537         {
2538             return $this->copyContent( $sourceSite, $destinationSite,
2539                                        $sourceVFolder, $destinationVFolder,
2540                                        $source, $destination );
2541         }
2542
2543         return false; // @as self::FAILED_FORBIDDEN;
2544     }
2545
2546     /**
2547      * Copies the node specified by the path $source to $destination.
2548      *
2549      * @param string $sourceSite Eg. 'plain_site_user'
2550      * @param string $destinationSite Eg. 'plain_site_user'
2551      * @param string $sourceVFolder Eg. 'Content'
2552      * @param string $destinationVFolder Eg. 'Content'
2553      * @param string $source Eg. 'Folder1/file1.txt'
2554      * @param string $destination Eg. 'Folder2/file1.txt'
2555      * @return bool
2556      */
2557     protected function copyContent( $sourceSite, $destinationSite,
2558                                     $sourceVFolder, $destinationVFolder,
2559                                     $source, $destination )
2560     {
2561         $nodePath = $this->internalNodePath( $sourceVFolder, $source );
2562         $destinationNodePath = $this->internalNodePath( $destinationVFolder, $destination );
2563
2564         // Get rid of possible extensions, remove .jpeg .txt .html etc..
2565         $source = $this->fileBasename( $source );
2566
2567         $sourceNode = $this->fetchNodeByTranslation( $nodePath );
2568
2569         if ( !$sourceNode )
2570         {
2571             return false; // @as self::FAILED_NOT_FOUND;
2572         }
2573
2574         $object = $sourceNode->attribute( 'object' );
2575         $classID = $object->attribute( 'contentclass_id' );
2576
2577         // Get rid of possible extensions, remove .jpeg .txt .html etc..
2578         $destination = $this->fileBasename( $destination );
2579
2580         $destinationNode = $this->fetchNodeByTranslation( $destinationNodePath );
2581
2582         // @as @todo check if this is needed
2583         // if ( $destinationNode )
2584         // {
2585         //     return false; // @as self::FAILED_EXISTS;
2586         // }
2587
2588         $destinationNode = $this->fetchParentNodeByTranslation( $destinationNodePath );
2589
2590         if ( !$destinationNode )
2591         {
2592             return false; // @as self::FAILED_NOT_FOUND;
2593         }
2594
2595         // Can we move the node to $destinationNode
2596         if ( !$destinationNode->canMoveTo( $classID ) )
2597         {
2598             return false; // @as self::FAILED_FORBIDDEN;
2599         }
2600
2601         $srcParentPath = $this->splitLastPathElement( $nodePath, $srcNodeName );
2602         $dstParentPath = $this->splitLastPathElement( $destinationNodePath, $dstNodeName );
2603         if ( $srcParentPath === $dstParentPath )
2604         {
2605             // @todo check if copy in the same folder is correct
2606             return $this->copyObjectSameDirectory( $sourceNode, $destinationNode, $this->fileBasename( $dstNodeName ) );
2607         }
2608         else
2609         {
2610             // @todo check if copy in different folders is correct
2611             return $this->copyObject( $sourceNode, $destinationNode );
2612         }
2613
2614         /*
2615         // Todo: add lookup of the name setting for the current object
2616         $contentObjectID = $object->attribute( 'id' );
2617         $contentObjectAttributes =& $object->contentObjectAttributes();
2618         // @todo @as avoid using [0] (could be another index in some classes)
2619         $contentObjectAttributes[0]->setAttribute( 'data_text', basename( $destination ) );
2620         $contentObjectAttributes[0]->store();
2621
2622         $operationResult = eZOperationHandler::execute( 'content', 'publish', array( 'object_id' => $contentObjectID, 'version' => 1 ) );
2623         $object->store();
2624         */
2625
2626         // @todo Unreachable code?
2627         return true; // @as self::OK_CREATED;
2628     }
2629
2630     /**
2631      * Copies the specified object as a child to the node $newParentNode.
2632      *
2633      * @param eZContentObject $object
2634      * @param eZContentObject $newParentNode
2635      * @return bool
2636      */
2637     protected function copyObject( $object, $newParentNode )
2638     {
2639         $object = $object->ContentObject;
2640         $newParentNodeID = $newParentNode->attribute( 'node_id' );
2641         $classID = $object->attribute( 'contentclass_id' );
2642