1<?php
2
3/**
4 * Hoa
5 *
6 *
7 * @license
8 *
9 * New BSD License
10 *
11 * Copyright © 2007-2017, Hoa community. All rights reserved.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions are met:
15 *     * Redistributions of source code must retain the above copyright
16 *       notice, this list of conditions and the following disclaimer.
17 *     * Redistributions in binary form must reproduce the above copyright
18 *       notice, this list of conditions and the following disclaimer in the
19 *       documentation and/or other materials provided with the distribution.
20 *     * Neither the name of the Hoa nor the names of its contributors may be
21 *       used to endorse or promote products derived from this software without
22 *       specific prior written permission.
23 *
24 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
25 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
26 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
27 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS AND CONTRIBUTORS BE
28 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
29 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
30 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
33 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
34 * POSSIBILITY OF SUCH DAMAGE.
35 */
36
37namespace Hoa\Protocol
38{
39
40/**
41 * Class \Hoa\Protocol\Wrapper.
42 *
43 * Stream wrapper for the `hoa://` protocol.
44 *
45 * @copyright  Copyright © 2007-2017 Hoa community
46 * @license    New BSD License
47 */
48class Wrapper
49{
50    /**
51     * Opened stream.
52     *
53     * @var resource
54     */
55    private $_stream     = null;
56
57    /**
58     * Stream name (filename).
59     *
60     * @var string
61     */
62    private $_streamName = null;
63
64    /**
65     * Stream context (given by the streamWrapper class).
66     *
67     * @var resource
68     */
69    public $context      = null;
70
71
72
73    /**
74     * Get the real path of the given URL.
75     * Could return false if the path cannot be reached.
76     *
77     * @param   string  $path      Path (or URL).
78     * @param   bool    $exists    If true, try to find the first that exists,
79     * @return  mixed
80     */
81    public static function realPath($path, $exists = true)
82    {
83        return Node::getRoot()->resolve($path, $exists);
84    }
85
86    /**
87     * Retrieve the underlying resource.
88     *
89     * @param   int     $castAs    Can be STREAM_CAST_FOR_SELECT when
90     *                             stream_select() is calling stream_cast() or
91     *                             STREAM_CAST_AS_STREAM when stream_cast() is
92     *                             called for other uses.
93     * @return  resource
94     */
95    public function stream_cast($castAs)
96    {
97        return false;
98    }
99
100    /**
101     * Close a resource.
102     * This method is called in response to fclose().
103     * All resources that were locked, or allocated, by the wrapper should be
104     * released.
105     *
106     * @return  void
107     */
108    public function stream_close()
109    {
110        if (true === @fclose($this->getStream())) {
111            $this->_stream     = null;
112            $this->_streamName = null;
113        }
114
115        return;
116    }
117
118    /**
119     * Tests for end-of-file on a file pointer.
120     * This method is called in response to feof().
121     *
122     * access   public
123     * @return  bool
124     */
125    public function stream_eof()
126    {
127        return feof($this->getStream());
128    }
129
130    /**
131     * Flush the output.
132     * This method is called in respond to fflush().
133     * If we have cached data in our stream but not yet stored it into the
134     * underlying storage, we should do so now.
135     *
136     * @return  bool
137     */
138    public function stream_flush()
139    {
140        return fflush($this->getStream());
141    }
142
143    /**
144     * Advisory file locking.
145     * This method is called in response to flock(), when file_put_contents()
146     * (when flags contains LOCK_EX), stream_set_blocking() and when closing the
147     * stream (LOCK_UN).
148     *
149     * @param   int     $operation    Operation is one the following:
150     *                                  * LOCK_SH to acquire a shared lock (reader) ;
151     *                                  * LOCK_EX to acquire an exclusive lock (writer) ;
152     *                                  * LOCK_UN to release a lock (shared or exclusive) ;
153     *                                  * LOCK_NB if we don't want flock() to
154     *                                    block while locking (not supported on
155     *                                    Windows).
156     * @return  bool
157     */
158    public function stream_lock($operation)
159    {
160        return flock($this->getStream(), $operation);
161    }
162
163    /**
164     * Change stream options.
165     * This method is called to set metadata on the stream. It is called when
166     * one of the following functions is called on a stream URL: touch, chmod,
167     * chown or chgrp.
168     *
169     * @param   string    $path      The file path or URL to set metadata.
170     * @param   int       $option    One of the following constant:
171     *                                 * STREAM_META_TOUCH,
172     *                                 * STREAM_META_OWNER_NAME,
173     *                                 * STREAM_META_OWNER,
174     *                                 * STREAM_META_GROUP_NAME,
175     *                                 * STREAM_META_GROUP,
176     *                                 * STREAM_META_ACCESS.
177     * @param   mixed     $values    Arguments of touch, chmod, chown and chgrp.
178     * @return  bool
179     */
180    public function stream_metadata($path, $option, $values)
181    {
182        $path = static::realPath($path, false);
183
184        switch ($option) {
185            case STREAM_META_TOUCH:
186                $arity = count($values);
187
188                if (0 === $arity) {
189                    $out = touch($path);
190                } elseif (1 === $arity) {
191                    $out = touch($path, $values[0]);
192                } else {
193                    $out = touch($path, $values[0], $values[1]);
194                }
195
196                break;
197
198            case STREAM_META_OWNER_NAME:
199            case STREAM_META_OWNER:
200                $out = chown($path, $values);
201
202                break;
203
204            case STREAM_META_GROUP_NAME:
205            case STREAM_META_GROUP:
206                $out = chgrp($path, $values);
207
208                break;
209
210            case STREAM_META_ACCESS:
211                $out = chmod($path, $values);
212
213                break;
214
215            default:
216                $out = false;
217        }
218
219        return $out;
220    }
221
222    /**
223     * Open file or URL.
224     * This method is called immediately after the wrapper is initialized (f.e.
225     * by fopen() and file_get_contents()).
226     *
227     * @param   string  $path           Specifies the URL that was passed to the
228     *                                  original function.
229     * @param   string  $mode           The mode used to open the file, as
230     *                                  detailed for fopen().
231     * @param   int     $options        Holds additional flags set by the
232     *                                  streams API. It can hold one or more of
233     *                                  the following values OR'd together:
234     *                                    * STREAM_USE_PATH, if path is relative,
235     *                                      search for the resource using the
236     *                                      include_path;
237     *                                    * STREAM_REPORT_ERRORS, if this is
238     *                                    set, you are responsible for raising
239     *                                    errors using trigger_error during
240     *                                    opening the stream. If this is not
241     *                                    set, you should not raise any errors.
242     * @param   string  &$openedPath    If the $path is opened successfully, and
243     *                                  STREAM_USE_PATH is set in $options,
244     *                                  $openedPath should be set to the full
245     *                                  path of the file/resource that was
246     *                                  actually opened.
247     * @return  bool
248     */
249    public function stream_open($path, $mode, $options, &$openedPath)
250    {
251        $path = static::realPath($path, 'r' === $mode[0]);
252
253        if (Protocol::NO_RESOLUTION === $path) {
254            return false;
255        }
256
257        if (null === $this->context) {
258            $openedPath = fopen($path, $mode, $options & STREAM_USE_PATH);
259        } else {
260            $openedPath = fopen(
261                $path,
262                $mode,
263                $options & STREAM_USE_PATH,
264                $this->context
265            );
266        }
267
268        if (false === is_resource($openedPath)) {
269            return false;
270        }
271
272        $this->_stream     = $openedPath;
273        $this->_streamName = $path;
274
275        return true;
276    }
277
278    /**
279     * Read from stream.
280     * This method is called in response to fread() and fgets().
281     *
282     * @param   int     $count    How many bytes of data from the current
283     *                            position should be returned.
284     * @return  string
285     */
286    public function stream_read($count)
287    {
288        return fread($this->getStream(), $count);
289    }
290
291    /**
292     * Seek to specific location in a stream.
293     * This method is called in response to fseek().
294     * The read/write position of the stream should be updated according to the
295     * $offset and $whence.
296     *
297     * @param   int     $offset    The stream offset to seek to.
298     * @param   int     $whence    Possible values:
299     *                               * SEEK_SET to set position equal to $offset
300     *                                 bytes ;
301     *                               * SEEK_CUR to set position to current
302     *                                 location plus $offsete ;
303     *                               * SEEK_END to set position to end-of-file
304     *                                 plus $offset.
305     * @return  bool
306     */
307    public function stream_seek($offset, $whence = SEEK_SET)
308    {
309        return 0 === fseek($this->getStream(), $offset, $whence);
310    }
311
312    /**
313     * Retrieve information about a file resource.
314     * This method is called in response to fstat().
315     *
316     * @return  array
317     */
318    public function stream_stat()
319    {
320        return fstat($this->getStream());
321    }
322
323    /**
324     * Retrieve the current position of a stream.
325     * This method is called in response to ftell().
326     *
327     * @return  int
328     */
329    public function stream_tell()
330    {
331        return ftell($this->getStream());
332    }
333
334    /**
335     * Truncate a stream to a given length.
336     *
337     * @param   int     $size    Size.
338     * @return  bool
339     */
340    public function stream_truncate($size)
341    {
342        return ftruncate($this->getStream(), $size);
343    }
344
345    /**
346     * Write to stream.
347     * This method is called in response to fwrite().
348     *
349     * @param   string  $data    Should be stored into the underlying stream.
350     * @return  int
351     */
352    public function stream_write($data)
353    {
354        return fwrite($this->getStream(), $data);
355    }
356
357    /**
358     * Close directory handle.
359     * This method is called in to closedir().
360     * Any resources which were locked, or allocated, during opening and use of
361     * the directory stream should be released.
362     *
363     * @return  void
364     */
365    public function dir_closedir()
366    {
367        closedir($this->getStream());
368        $this->_stream     = null;
369        $this->_streamName = null;
370
371        return;
372    }
373
374    /**
375     * Open directory handle.
376     * This method is called in response to opendir().
377     *
378     * @param   string  $path       Specifies the URL that was passed to opendir().
379     * @param   int     $options    Whether or not to enforce safe_mode (0x04).
380     *                              It is not used here.
381     * @return  bool
382     */
383    public function dir_opendir($path, $options)
384    {
385        $path   = static::realPath($path);
386        $handle = null;
387
388        if (null === $this->context) {
389            $handle = @opendir($path);
390        } else {
391            $handle = @opendir($path, $this->context);
392        }
393
394        if (false === $handle) {
395            return false;
396        }
397
398        $this->_stream     = $handle;
399        $this->_streamName = $path;
400
401        return true;
402    }
403
404    /**
405     * Read entry from directory handle.
406     * This method is called in response to readdir().
407     *
408     * @return  mixed
409     */
410    public function dir_readdir()
411    {
412        return readdir($this->getStream());
413    }
414
415    /**
416     * Rewind directory handle.
417     * This method is called in response to rewinddir().
418     * Should reset the output generated by self::dir_readdir, i.e. the next
419     * call to self::dir_readdir should return the first entry in the location
420     * returned by self::dir_opendir.
421     *
422     * @return  void
423     */
424    public function dir_rewinddir()
425    {
426        return rewinddir($this->getStream());
427    }
428
429    /**
430     * Create a directory.
431     * This method is called in response to mkdir().
432     *
433     * @param   string  $path       Directory which should be created.
434     * @param   int     $mode       The value passed to mkdir().
435     * @param   int     $options    A bitwise mask of values.
436     * @return  bool
437     */
438    public function mkdir($path, $mode, $options)
439    {
440        if (null === $this->context) {
441            return mkdir(
442                static::realPath($path, false),
443                $mode,
444                $options | STREAM_MKDIR_RECURSIVE
445            );
446        }
447
448        return mkdir(
449            static::realPath($path, false),
450            $mode,
451            $options | STREAM_MKDIR_RECURSIVE,
452            $this->context
453        );
454    }
455
456    /**
457     * Rename a file or directory.
458     * This method is called in response to rename().
459     * Should attempt to rename $from to $to.
460     *
461     * @param   string  $from    The URL to current file.
462     * @param   string  $to      The URL which $from should be renamed to.
463     * @return  bool
464     */
465    public function rename($from, $to)
466    {
467        if (null === $this->context) {
468            return rename(static::realPath($from), static::realPath($to, false));
469        }
470
471        return rename(
472            static::realPath($from),
473            static::realPath($to, false),
474            $this->context
475        );
476    }
477
478    /**
479     * Remove a directory.
480     * This method is called in response to rmdir().
481     *
482     * @param   string  $path       The directory URL which should be removed.
483     * @param   int     $options    A bitwise mask of values. It is not used
484     *                              here.
485     * @return  bool
486     */
487    public function rmdir($path, $options)
488    {
489        if (null === $this->context) {
490            return rmdir(static::realPath($path));
491        }
492
493        return rmdir(static::realPath($path), $this->context);
494    }
495
496    /**
497     * Delete a file.
498     * This method is called in response to unlink().
499     *
500     * @param   string  $path    The file URL which should be deleted.
501     * @return  bool
502     */
503    public function unlink($path)
504    {
505        if (null === $this->context) {
506            return unlink(static::realPath($path));
507        }
508
509        return unlink(static::realPath($path), $this->context);
510    }
511
512    /**
513     * Retrieve information about a file.
514     * This method is called in response to all stat() related functions.
515     *
516     * @param   string  $path     The file URL which should be retrieve
517     *                            information about.
518     * @param   int     $flags    Holds additional flags set by the streams API.
519     *                            It can hold one or more of the following
520     *                            values OR'd together.
521     *                            STREAM_URL_STAT_LINK: for resource with the
522     *                            ability to link to other resource (such as an
523     *                            HTTP location: forward, or a filesystem
524     *                            symlink). This flag specified that only
525     *                            information about the link itself should be
526     *                            returned, not the resource pointed to by the
527     *                            link. This flag is set in response to calls to
528     *                            lstat(), is_link(), or filetype().
529     *                            STREAM_URL_STAT_QUIET: if this flag is set,
530     *                            our wrapper should not raise any errors. If
531     *                            this flag is not set, we are responsible for
532     *                            reporting errors using the trigger_error()
533     *                            function during stating of the path.
534     * @return  array
535     */
536    public function url_stat($path, $flags)
537    {
538        $path = static::realPath($path);
539
540        if (Protocol::NO_RESOLUTION === $path) {
541            if ($flags & STREAM_URL_STAT_QUIET) {
542                return 0;
543            } else {
544                return trigger_error(
545                    'Path ' . $path . ' cannot be resolved.',
546                    E_WARNING
547                );
548            }
549        }
550
551        if ($flags & STREAM_URL_STAT_LINK) {
552            return @lstat($path);
553        }
554
555        return @stat($path);
556    }
557
558    /**
559     * Get stream resource.
560     *
561     * @return  resource
562     */
563    public function getStream()
564    {
565        return $this->_stream;
566    }
567
568    /**
569     * Get stream name.
570     *
571     * @return  resource
572     */
573    public function getStreamName()
574    {
575        return $this->_streamName;
576    }
577}
578
579/**
580 * Register the `hoa://` protocol.
581 */
582stream_wrapper_register('hoa', Wrapper::class);
583
584}
585
586namespace
587{
588
589/**
590 * Alias of `Hoa\Protocol::resolve` method.
591 *
592 * @param   string  $path      Path to resolve.
593 * @param   bool    $exists    If `true`, try to find the first that exists,
594 *                             else return the first solution.
595 * @param   bool    $unfold    Return all solutions instead of one.
596 * @return  mixed
597 */
598if (!function_exists('resolve')) {
599    function resolve($path, $exists = true, $unfold = false)
600    {
601        return Hoa\Protocol::getInstance()->resolve($path, $exists, $unfold);
602    }
603}
604
605}
606