FlysystemStreamWrapper   F
last analyzed

Complexity

Total Complexity 103

Size/Duplication

Total Lines 969
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 9

Test Coverage

Coverage 100%

Importance

Changes 0
Metric Value
wmc 103
lcom 1
cbo 9
dl 0
loc 969
ccs 277
cts 277
cp 1
rs 1.631
c 0
b 0
f 0

42 Methods

Rating   Name   Duplication   Size   Complexity  
A stream_eof() 0 4 1
A register() 0 12 3
A unregister() 0 10 2
A unregisterAll() 0 6 2
A getRegisteredProtocols() 0 4 1
A streamWrapperExists() 0 4 1
A registerPlugins() 0 14 1
A dir_closedir() 0 6 1
A dir_opendir() 0 28 4
A dir_readdir() 0 7 2
A dir_rewinddir() 0 6 1
A mkdir() 0 6 1
A rename() 0 7 1
A rmdir() 0 6 1
A stream_cast() 0 4 1
A stream_close() 0 11 2
A stream_flush() 0 22 3
A stream_lock() 0 18 4
B stream_metadata() 0 29 6
A stream_open() 0 17 3
A stream_read() 0 8 2
A stream_seek() 0 4 1
B stream_set_option() 0 26 7
A stream_stat() 0 17 3
A stream_tell() 0 8 2
A stream_truncate() 0 10 2
A stream_write() 0 22 5
A unlink() 0 6 1
A url_stat() 0 17 4
B getStream() 0 25 6
A getWritableStream() 0 12 2
A getAppendStream() 0 8 2
A getXStream() 0 12 2
A ensureWritableHandle() 0 14 3
A getProtocol() 0 4 1
A getTarget() 0 10 3
A getConfiguration() 0 4 2
A getFilesystem() 0 10 2
A invoke() 0 11 3
A triggerError() 0 23 4
A openLockHandle() 0 26 2
A releaseLock() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like FlysystemStreamWrapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FlysystemStreamWrapper, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Twistor;
4
5
use League\Flysystem\AdapterInterface;
6
use League\Flysystem\FileNotFoundException;
7
use League\Flysystem\FilesystemInterface;
8
use League\Flysystem\Util;
9
use Twistor\Flysystem\Exception\TriggerErrorException;
10
use Twistor\Flysystem\Plugin\ForcedRename;
11
use Twistor\Flysystem\Plugin\Mkdir;
12
use Twistor\Flysystem\Plugin\Rmdir;
13
use Twistor\Flysystem\Plugin\Stat;
14
use Twistor\Flysystem\Plugin\Touch;
15
16
/**
17
 * An adapter for Flysystem to a PHP stream wrapper.
18
 */
19
class FlysystemStreamWrapper
20
{
21
    /**
22
     * A flag to tell FlysystemStreamWrapper::url_stat() to ignore the size.
23
     *
24
     * @var int
25
     */
26
    const STREAM_URL_IGNORE_SIZE = 8;
27
28
    /**
29
     * The registered filesystems.
30
     *
31
     * @var \League\Flysystem\FilesystemInterface[]
32
     */
33
    protected static $filesystems = [];
34
35
    /**
36
     * Optional configuration.
37
     *
38
     * @var array
39
     */
40
    protected static $config = [];
41
42
    /**
43
     * The default configuration.
44
     *
45
     * @var array
46
     */
47
    protected static $defaultConfiguration = [
48
        'permissions' => [
49
            'dir' => [
50
                'private' => 0700,
51
                'public' => 0755,
52
            ],
53
            'file' => [
54
                'private' => 0600,
55
                'public' => 0644,
56
            ],
57
        ],
58
        'metadata' => ['timestamp', 'size', 'visibility'],
59
        'public_mask' => 0044,
60
    ];
61
62
    /**
63
     * The number of bytes that have been written since the last flush.
64
     *
65
     * @var int
66
     */
67
    protected $bytesWritten = 0;
68
69
    /**
70
     * The filesystem of the current stream wrapper.
71
     *
72
     * @var \League\Flysystem\FilesystemInterface
73
     */
74
    protected $filesystem;
75
76
    /**
77
     * A generic resource handle.
78
     *
79
     * @var resource|bool
80
     */
81
    protected $handle;
82
83
    /**
84
     * Whether the handle is in append mode.
85
     *
86
     * @var bool
87
     */
88
    protected $isAppendMode = false;
89
90
    /**
91
     * Whether the handle is read-only.
92
     *
93
     * The stream returned from Flysystem may not actually be read-only, This
94
     * ensures read-only behavior.
95
     *
96
     * @var bool
97
     */
98
    protected $isReadOnly = false;
99
100
    /**
101
     * Whether the handle is write-only.
102
     *
103
     * @var bool
104
     */
105
    protected $isWriteOnly = false;
106
107
    /**
108
     * A directory listing.
109
     *
110
     * @var array
111
     */
112
    protected $listing;
113
114
    /**
115
     * Whether this handle has been verified writable.
116
     *
117
     * @var bool
118
     */
119
    protected $needsCowCheck = false;
120
121
    /**
122
     * Whether the handle should be flushed.
123
     *
124
     * @var bool
125
     */
126
    protected $needsFlush = false;
127
128
    /**
129
     * The handle used for calls to stream_lock.
130
     *
131
     * @var resource
132
     */
133
    protected $lockHandle;
134
135
    /**
136
     * If stream_set_write_buffer() is called, the arguments.
137
     *
138
     * @var int
139
     */
140
    protected $streamWriteBuffer;
141
142
    /**
143
     * Instance URI (stream).
144
     *
145
     * A stream is referenced as "protocol://target".
146
     *
147
     * @var string
148
     */
149
    protected $uri;
150
151
    /**
152
     * Registers the stream wrapper protocol if not already registered.
153
     *
154
     * @param string              $protocol      The protocol.
155
     * @param FilesystemInterface $filesystem    The filesystem.
156
     * @param array|null          $configuration Optional configuration.
157
     * @param int                 $flags         Should be set to STREAM_IS_URL if protocol is a URL protocol. Default is 0, local stream.
158
     *
159
     * @return bool True if the protocol was registered, false if not.
160
     */
161 210
    public static function register($protocol, FilesystemInterface $filesystem, array $configuration = null, $flags = 0)
162
    {
163 210
        if (static::streamWrapperExists($protocol)) {
164 3
            return false;
165
        }
166
167 210
        static::$config[$protocol] = $configuration ?: static::$defaultConfiguration;
168 210
        static::registerPlugins($protocol, $filesystem);
169 210
        static::$filesystems[$protocol] = $filesystem;
170
171 210
        return stream_wrapper_register($protocol, __CLASS__, $flags);
172
    }
173
174
    /**
175
     * Unregisters a stream wrapper.
176
     *
177
     * @param string $protocol The protocol.
178
     *
179
     * @return bool True if the protocol was unregistered, false if not.
180
     */
181 210
    public static function unregister($protocol)
182
    {
183 210
        if ( ! static::streamWrapperExists($protocol)) {
184 3
            return false;
185
        }
186
187 210
        unset(static::$filesystems[$protocol]);
188
189 210
        return stream_wrapper_unregister($protocol);
190
    }
191
192
    /**
193
     * Unregisters all controlled stream wrappers.
194
     */
195 210
    public static function unregisterAll()
196
    {
197 210
        foreach (static::getRegisteredProtocols() as $protocol) {
198 207
            static::unregister($protocol);
199 70
        }
200 210
    }
201
202
    /**
203
     * @return array The list of registered protocols.
204
     */
205 210
    public static function getRegisteredProtocols()
206
    {
207 210
        return array_keys(static::$filesystems);
208
    }
209
210
    /**
211
     * Determines if a protocol is registered.
212
     *
213
     * @param string $protocol The protocol to check.
214
     *
215
     * @return bool True if it is registered, false if not.
216
     */
217 210
    protected static function streamWrapperExists($protocol)
218
    {
219 210
        return in_array($protocol, stream_get_wrappers(), true);
220
    }
221
222
    /**
223
     * Registers plugins on the filesystem.
224
     * @param string $protocol
225
     * @param FilesystemInterface $filesystem
226
     */
227 210
    protected static function registerPlugins($protocol, FilesystemInterface $filesystem)
228
    {
229 210
        $filesystem->addPlugin(new ForcedRename());
230 210
        $filesystem->addPlugin(new Mkdir());
231 210
        $filesystem->addPlugin(new Rmdir());
232
233 210
        $stat = new Stat(
234 210
            static::$config[$protocol]['permissions'],
235 210
            static::$config[$protocol]['metadata']
236 70
        );
237
238 210
        $filesystem->addPlugin($stat);
239 210
        $filesystem->addPlugin(new Touch());
240 210
    }
241
242
    /**
243
     * Closes the directory handle.
244
     *
245
     * @return bool True on success, false on failure.
246
     */
247 12
    public function dir_closedir()
248
    {
249 12
        unset($this->listing);
250
251 12
        return true;
252
    }
253
254
    /**
255
     * Opens a directory handle.
256
     *
257
     * @param string $uri     The URL that was passed to opendir().
258
     * @param int    $options Whether or not to enforce safe_mode (0x04).
259
     *
260
     * @return bool True on success, false on failure.
261
     */
262 18
    public function dir_opendir($uri, $options)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
263
    {
264 18
        $this->uri = $uri;
265
266 18
        $path = Util::normalizePath($this->getTarget());
267
268 18
        $this->listing = $this->invoke($this->getFilesystem(), 'listContents', [$path], 'opendir');
269
270 18
        if ($this->listing === false) {
271 6
            return false;
272
        }
273
274 12
        if ( ! $dirlen = strlen($path)) {
275 6
            return true;
276
        }
277
278
        // Remove the separator /.
279 6
        $dirlen++;
280
281
        // Remove directory prefix.
282 6
        foreach ($this->listing as $delta => $item) {
283 6
            $this->listing[$delta]['path'] = substr($item['path'], $dirlen);
284 2
        }
285
286 6
        reset($this->listing);
287
288 6
        return true;
289
    }
290
291
    /**
292
     * Reads an entry from directory handle.
293
     *
294
     * @return string|bool The next filename, or false if there is no next file.
295
     */
296 12
    public function dir_readdir()
297
    {
298 12
        $current = current($this->listing);
299 12
        next($this->listing);
300
301 12
        return $current ? $current['path'] : false;
302
    }
303
304
    /**
305
     * Rewinds the directory handle.
306
     *
307
     * @return bool True on success, false on failure.
308
     */
309 12
    public function dir_rewinddir()
310
    {
311 12
        reset($this->listing);
312
313 12
        return true;
314
    }
315
316
    /**
317
     * Creates a directory.
318
     *
319
     * @param string $uri
320
     * @param int    $mode
321
     * @param int    $options
322
     *
323
     * @return bool True on success, false on failure.
324
     */
325 60
    public function mkdir($uri, $mode, $options)
326
    {
327 60
        $this->uri = $uri;
328
329 60
        return $this->invoke($this->getFilesystem(), 'mkdir', [$this->getTarget(), $mode, $options]);
330
    }
331
332
    /**
333
     * Renames a file or directory.
334
     *
335
     * @param string $uri_from
336
     * @param string $uri_to
337
     *
338
     * @return bool True on success, false on failure.
339
     */
340 36
    public function rename($uri_from, $uri_to)
341
    {
342 36
        $this->uri = $uri_from;
343 36
        $args = [$this->getTarget($uri_from), $this->getTarget($uri_to)];
344
345 36
        return $this->invoke($this->getFilesystem(), 'forcedRename', $args, 'rename');
346
    }
347
348
    /**
349
     * Removes a directory.
350
     *
351
     * @param string $uri
352
     * @param int    $options
353
     *
354
     * @return bool True on success, false on failure.
355
     */
356 18
    public function rmdir($uri, $options)
357
    {
358 18
        $this->uri = $uri;
359
360 18
        return $this->invoke($this->getFilesystem(), 'rmdir', [$this->getTarget(), $options]);
361
    }
362
363
    /**
364
     * Retrieves the underlying resource.
365
     *
366
     * @param int $cast_as
367
     *
368
     * @return resource|bool The stream resource used by the wrapper, or false.
369
     */
370 6
    public function stream_cast($cast_as)
371
    {
372 6
        return $this->handle;
373
    }
374
375
    /**
376
     * Closes the resource.
377
     */
378 84
    public function stream_close()
379
    {
380
        // PHP 7 doesn't call flush automatically anymore for truncate() or when
381
        // writing an empty file. We need to ensure that the handle gets pushed
382
        // as needed in that case. This will be a no-op for php 5.
383 84
        $this->stream_flush();
384
385 84
        if (is_resource($this->handle)) {
386 84
            fclose($this->handle);
387 28
        }
388 84
    }
389
390
    /**
391
     * Tests for end-of-file on a file pointer.
392
     *
393
     * @return bool True if the file is at the end, false if not.
394
     */
395 60
    public function stream_eof()
396
    {
397 60
        return feof($this->handle);
398
    }
399
400
    /**
401
     * Flushes the output.
402
     *
403
     * @return bool True on success, false on failure.
404
     */
405 84
    public function stream_flush()
406
    {
407 84
        if ( ! $this->needsFlush) {
408 76
            return true;
409
        }
410
411 84
        $this->needsFlush = false;
412 84
        $this->bytesWritten = 0;
413
414
        // Calling putStream() will rewind our handle. flush() shouldn't change
415
        // the position of the file.
416 84
        $pos = ftell($this->handle);
417
418 84
        $args = [$this->getTarget(), $this->handle];
419 84
        $success = $this->invoke($this->getFilesystem(), 'putStream', $args, 'fflush');
420
421 84
        if (is_resource($this->handle)) {
422 84
            fseek($this->handle, $pos);
423 28
        }
424
425 84
        return $success;
426
    }
427
428
    /**
429
     * Advisory file locking.
430
     *
431
     * @param int $operation
432
     *
433
     * @return bool True on success, false on failure.
434
     */
435 6
    public function stream_lock($operation)
436
    {
437 6
        $operation = (int) $operation;
438
439 6
        if (($operation & \LOCK_UN) === \LOCK_UN) {
440 6
            return $this->releaseLock($operation);
441
        }
442
443
        // If the caller calls flock() twice, there's no reason to re-create the
444
        // lock handle.
445 6
        if (is_resource($this->lockHandle)) {
446 6
            return flock($this->lockHandle, $operation);
447
        }
448
449 6
        $this->lockHandle = $this->openLockHandle();
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->openLockHandle() can also be of type false. However, the property $lockHandle is declared as type resource. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
450
451 6
        return is_resource($this->lockHandle) && flock($this->lockHandle, $operation);
452
    }
453
454
    /**
455
     * Changes stream options.
456
     *
457
     * @param string $uri
458
     * @param int    $option
459
     * @param mixed  $value
460
     *
461
     * @return bool True on success, false on failure.
462
     */
463 39
    public function stream_metadata($uri, $option, $value)
464
    {
465 39
        $this->uri = $uri;
466
467
        switch ($option) {
468 39
            case STREAM_META_ACCESS:
469 15
                $permissions = octdec(substr(decoct($value), -4));
470 15
                $is_public = $permissions & $this->getConfiguration('public_mask');
471 15
                $visibility =  $is_public ? AdapterInterface::VISIBILITY_PUBLIC : AdapterInterface::VISIBILITY_PRIVATE;
472
473
                try {
474 15
                    return $this->getFilesystem()->setVisibility($this->getTarget(), $visibility);
475 9
                } catch (\LogicException $e) {
476
                    // The adapter doesn't support visibility.
477 7
                } catch (\Exception $e) {
478 6
                    $this->triggerError('chmod', $e);
479
480 6
                    return false;
481
                }
482
483 3
                return true;
484
485 27
            case STREAM_META_TOUCH:
486 27
                return $this->invoke($this->getFilesystem(), 'touch', [$this->getTarget()]);
487
488 2
            default:
489 6
                return false;
490 2
        }
491
    }
492
493
    /**
494
     * Opens file or URL.
495
     *
496
     * @param string $uri
497
     * @param string $mode
498
     * @param int    $options
499
     * @param string &$opened_path
500
     *
501
     * @return bool True on success, false on failure.
502
     */
503 102
    public function stream_open($uri, $mode, $options, &$opened_path)
504
    {
505 102
        $this->uri = $uri;
506 102
        $path = $this->getTarget();
507
508 102
        $this->isReadOnly = StreamUtil::modeIsReadOnly($mode);
509 102
        $this->isWriteOnly = StreamUtil::modeIsWriteOnly($mode);
510 102
        $this->isAppendMode = StreamUtil::modeIsAppendable($mode);
511
512 102
        $this->handle = $this->invoke($this, 'getStream', [$path, $mode], 'fopen');
513
514 102
        if ($this->handle && $options & STREAM_USE_PATH) {
515 6
            $opened_path = $path;
516 2
        }
517
518 102
        return is_resource($this->handle);
519
    }
520
521
    /**
522
     * Reads from stream.
523
     *
524
     * @param int $count
525
     *
526
     * @return string The bytes read.
527
     */
528 60
    public function stream_read($count)
529
    {
530 60
        if ($this->isWriteOnly) {
531 6
            return '';
532
        }
533
534 60
        return fread($this->handle, $count);
535
    }
536
537
    /**
538
     * Seeks to specific location in a stream.
539
     *
540
     * @param int $offset
541
     * @param int $whence
542
     *
543
     * @return bool True on success, false on failure.
544
     */
545 24
    public function stream_seek($offset, $whence = SEEK_SET)
546
    {
547 24
        return fseek($this->handle, $offset, $whence) === 0;
548
    }
549
550
    /**
551
     * Changes stream options.
552
     *
553
     * @param int $option
554
     * @param int $arg1
555
     * @param int $arg2
556
     *
557
     * @return bool True on success, false on failure.
558
     */
559 6
    public function stream_set_option($option, $arg1, $arg2)
560
    {
561
        switch ($option) {
562 6
            case STREAM_OPTION_BLOCKING:
563
                // This works for the local adapter. It doesn't do anything for
564
                // memory streams.
565 6
                return stream_set_blocking($this->handle, $arg1);
566
567 6
            case STREAM_OPTION_READ_TIMEOUT:
568 6
                return  stream_set_timeout($this->handle, $arg1, $arg2);
569
570 6
            case STREAM_OPTION_READ_BUFFER:
571 6
                if ($arg1 === STREAM_BUFFER_NONE) {
572 6
                    return stream_set_read_buffer($this->handle, 0) === 0;
573
                }
574
575 6
                return stream_set_read_buffer($this->handle, $arg2) === 0;
576
577 6
            case STREAM_OPTION_WRITE_BUFFER:
578 6
                $this->streamWriteBuffer = $arg1 === STREAM_BUFFER_NONE ? 0 : $arg2;
579
580 6
                return true;
581
        }
582
583 6
        return false;
584
    }
585
586
    /**
587
     * Retrieves information about a file resource.
588
     *
589
     * @return array A similar array to fstat().
590
     *
591
     * @see fstat()
592
     */
593 66
    public function stream_stat()
594
    {
595
        // Get metadata from original file.
596 66
        $stat = $this->url_stat($this->uri, static::STREAM_URL_IGNORE_SIZE | STREAM_URL_STAT_QUIET) ?: [];
597
598
        // Newly created file.
599 66
        if (empty($stat['mode'])) {
600 6
            $stat['mode'] = 0100000 + $this->getConfiguration('permissions')['file']['public'];
601 6
            $stat[2] = $stat['mode'];
602 2
        }
603
604
        // Use the size of our handle, since it could have been written to or
605
        // truncated.
606 66
        $stat['size'] = $stat[7] = StreamUtil::getSize($this->handle);
607
608 66
        return $stat;
609
    }
610
611
    /**
612
     * Retrieves the current position of a stream.
613
     *
614
     * @return int The current position of the stream.
615
     */
616 24
    public function stream_tell()
617
    {
618 24
        if ($this->isAppendMode) {
619 6
            return 0;
620
        }
621
622 18
        return ftell($this->handle);
623
    }
624
625
    /**
626
     * Truncates the stream.
627
     *
628
     * @param int $new_size
629
     *
630
     * @return bool True on success, false on failure.
631
     */
632 12
    public function stream_truncate($new_size)
633
    {
634 12
        if ($this->isReadOnly) {
635 6
            return false;
636
        }
637 12
        $this->needsFlush = true;
638 12
        $this->ensureWritableHandle();
639
640 12
        return ftruncate($this->handle, $new_size);
641
    }
642
643
    /**
644
     * Writes to the stream.
645
     *
646
     * @param string $data
647
     *
648
     * @return int The number of bytes that were successfully stored.
649
     */
650 66
    public function stream_write($data)
651
    {
652 66
        if ($this->isReadOnly) {
653 6
            return 0;
654
        }
655 66
        $this->needsFlush = true;
656 66
        $this->ensureWritableHandle();
657
658
        // Enforce append semantics.
659 66
        if ($this->isAppendMode) {
660 6
            StreamUtil::trySeek($this->handle, 0, SEEK_END);
661 2
        }
662
663 66
        $written = fwrite($this->handle, $data);
664 66
        $this->bytesWritten += $written;
665
666 66
        if (isset($this->streamWriteBuffer) && $this->bytesWritten >= $this->streamWriteBuffer) {
667 6
            $this->stream_flush();
668 2
        }
669
670 66
        return $written;
671
    }
672
673
    /**
674
     * Deletes a file.
675
     *
676
     * @param string $uri
677
     *
678
     * @return bool True on success, false on failure.
679
     */
680 12
    public function unlink($uri)
681
    {
682 12
        $this->uri = $uri;
683
684 12
        return $this->invoke($this->getFilesystem(), 'delete', [$this->getTarget()], 'unlink');
685
    }
686
687
    /**
688
     * Retrieves information about a file.
689
     *
690
     * @param string $uri
691
     * @param int    $flags
692
     *
693
     * @return array|false Output similar to stat().
694
     *
695
     * @see stat()
696
     */
697 90
    public function url_stat($uri, $flags)
698
    {
699 90
        $this->uri = $uri;
700
701
        try {
702 90
            return $this->getFilesystem()->stat($this->getTarget(), $flags);
703 36
        } catch (FileNotFoundException $e) {
704
            // File doesn't exist.
705 30
            if ( ! ($flags & STREAM_URL_STAT_QUIET)) {
706 22
                $this->triggerError('stat', $e);
707 2
            }
708 16
        } catch (\Exception $e) {
709 6
            $this->triggerError('stat', $e);
710
        }
711
712 36
        return false;
713
    }
714
715
    /**
716
     * Returns a stream for a given path and mode.
717
     *
718
     * @param string $path The path to open.
719
     * @param string $mode The mode to open the stream in.
720
     *
721
     * @return resource|bool The file handle, or false.
722
     *
723
     * @throws \League\Flysystem\FileNotFoundException
724
     */
725 102
    protected function getStream($path, $mode)
726
    {
727 102
        switch ($mode[0]) {
728 102
            case 'r':
729 66
                $this->needsCowCheck = true;
730
731 66
                return $this->getFilesystem()->readStream($path);
732
733 96
            case 'w':
734 78
                $this->needsFlush = true;
735
736 78
                return fopen('php://temp', 'w+b');
737
738 24
            case 'a':
739 6
                return $this->getAppendStream($path);
740
741 24
            case 'x':
742 12
                return $this->getXStream($path);
743
744 12
            case 'c':
745 6
                return $this->getWritableStream($path);
746 2
        }
747
748 6
        return false;
749
    }
750
751
    /**
752
     * Returns a writable stream for a given path and mode.
753
     *
754
     * @param string $path The path to open.
755
     *
756
     * @return resource|bool The file handle, or false.
757
     */
758 6
    protected function getWritableStream($path)
759
    {
760
        try {
761 6
            $handle = $this->getFilesystem()->readStream($path);
762 6
            $this->needsCowCheck = true;
763 6
        } catch (FileNotFoundException $e) {
764 6
            $handle = fopen('php://temp', 'w+b');
765 6
            $this->needsFlush = true;
766
        }
767
768 6
        return $handle;
769
    }
770
771
    /**
772
     * Returns an appendable stream for a given path and mode.
773
     *
774
     * @param string $path The path to open.
775
     *
776
     * @return resource|bool The file handle, or false.
777
     */
778 6
    protected function getAppendStream($path)
779
    {
780 6
        if ($handle = $this->getWritableStream($path)) {
781 6
            StreamUtil::trySeek($handle, 0, SEEK_END);
782 2
        }
783
784 6
        return $handle;
785
    }
786
787
    /**
788
     * Returns a writable stream for a given path and mode.
789
     *
790
     * Triggers a warning if the file exists.
791
     *
792
     * @param string $path The path to open.
793
     *
794
     * @return resource|bool The file handle, or false.
795
     */
796 12
    protected function getXStream($path)
797
    {
798 12
        if ($this->getFilesystem()->has($path)) {
799 6
            trigger_error('fopen(): failed to open stream: File exists', E_USER_WARNING);
800
801 6
            return false;
802
        }
803
804 6
        $this->needsFlush = true;
805
806 6
        return fopen('php://temp', 'w+b');
807
    }
808
809
    /**
810
     * Guarantees that the handle is writable.
811
     */
812 66
    protected function ensureWritableHandle()
813
    {
814 66
        if ( ! $this->needsCowCheck) {
815 66
            return;
816
        }
817
818 18
        $this->needsCowCheck = false;
819
820 18
        if (StreamUtil::isWritable($this->handle)) {
821 9
            return;
822
        }
823
824 9
        $this->handle = StreamUtil::copy($this->handle);
825 9
    }
826
827
    /**
828
     * Returns the protocol from the internal URI.
829
     *
830
     * @return string The protocol.
831
     */
832 195
    protected function getProtocol()
833
    {
834 195
        return substr($this->uri, 0, strpos($this->uri, '://'));
835
    }
836
837
    /**
838
     * Returns the local writable target of the resource within the stream.
839
     *
840
     * @param string|null $uri The URI.
841
     *
842
     * @return string The path appropriate for use with Flysystem.
843
     */
844 201
    protected function getTarget($uri = null)
845
    {
846 201
        if ( ! isset($uri)) {
847 195
            $uri = $this->uri;
848 65
        }
849
850 201
        $target = substr($uri, strpos($uri, '://') + 3);
851
852 201
        return $target === false ? '' : $target;
853
    }
854
855
    /**
856
     * Returns the configuration.
857
     *
858
     * @param string|null $key The optional configuration key.
859
     *
860
     * @return array The requested configuration.
861
     */
862 21
    protected function getConfiguration($key = null)
863
    {
864 21
        return $key ? static::$config[$this->getProtocol()][$key] : static::$config[$this->getProtocol()];
865
    }
866
867
    /**
868
     * Returns the filesystem.
869
     *
870
     * @return \League\Flysystem\FilesystemInterface The filesystem object.
871
     */
872 195
    protected function getFilesystem()
873
    {
874 195
        if (isset($this->filesystem)) {
875 66
            return $this->filesystem;
876
        }
877
878 195
        $this->filesystem = static::$filesystems[$this->getProtocol()];
879
880 195
        return $this->filesystem;
881
    }
882
883
    /**
884
     * Calls a method on an object, catching any exceptions.
885
     *
886
     * @param object      $object    The object to call the method on.
887
     * @param string      $method    The method name.
888
     * @param array       $args      The arguments to the method.
889
     * @param string|null $errorname The name of the calling function.
890
     *
891
     * @return mixed|false The return value of the call, or false on failure.
892
     */
893 183
    protected function invoke($object, $method, array $args, $errorname = null)
894
    {
895
        try {
896 183
            return call_user_func_array([$object, $method], $args);
897 78
        } catch (\Exception $e) {
898 78
            $errorname = $errorname ?: $method;
899 78
            $this->triggerError($errorname, $e);
900
        }
901
902 78
        return false;
903
    }
904
905
    /**
906
     * Calls trigger_error(), printing the appropriate message.
907
     *
908
     * @param string     $function
909
     * @param \Exception $e
910
     */
911 96
    protected function triggerError($function, \Exception $e)
912
    {
913 96
        if ($e instanceof TriggerErrorException) {
914 30
            trigger_error($e->formatMessage($function), E_USER_WARNING);
915
916 30
            return;
917
        }
918
919 72
        switch (get_class($e)) {
920 72
            case 'League\Flysystem\FileNotFoundException':
921 42
                trigger_error(sprintf('%s(): No such file or directory', $function), E_USER_WARNING);
922
923 42
                return;
924
925 30
            case 'League\Flysystem\RootViolationException':
926 6
                trigger_error(sprintf('%s(): Cannot remove the root directory', $function), E_USER_WARNING);
927
928 6
                return;
929 8
        }
930
931
        // Don't allow any exceptions to leak.
932 24
        trigger_error($e->getMessage(), E_USER_WARNING);
933 24
    }
934
935
    /**
936
     * Creates an advisory lock handle.
937
     *
938
     * @return resource|false
939
     */
940 6
    protected function openLockHandle()
941
    {
942
        // PHP allows periods, '.', to be scheme names. Normalize the scheme
943
        // name to something that won't cause problems. Also, avoid problems
944
        // with case-insensitive filesystems. We use bin2hex() rather than a
945
        // hashing function since most scheme names are small, and bin2hex()
946
        // only doubles the string length.
947 6
        $sub_dir = bin2hex($this->getProtocol());
948
949
        // Since we're flattening out whole filesystems, at least create a
950
        // sub-directory for each scheme to attempt to reduce the number of
951
        // files per directory.
952 6
        $temp_dir = sys_get_temp_dir() . '/flysystem-stream-wrapper/' . $sub_dir;
953
954
        // Race free directory creation. If @mkdir() fails, fopen() will fail
955
        // later, so there's no reason to test again.
956 6
        ! is_dir($temp_dir) && @mkdir($temp_dir, 0777, true);
957
958
        // Normalize paths so that locks are consistent.
959
        // We are using sha1() to avoid the file name limits, and case
960
        // insensitivity on Windows. This is not security sensitive.
961 6
        $lock_key = sha1(Util::normalizePath($this->getTarget()));
962
963
        // Relay the lock to a real filesystem lock.
964 6
        return fopen($temp_dir . '/' . $lock_key, 'c');
965
    }
966
967
    /**
968
     * Releases the advisory lock.
969
     *
970
     * @param int $operation
971
     *
972
     * @return bool
973
     *
974
     * @see FlysystemStreamWrapper::stream_lock()
975
     */
976 6
    protected function releaseLock($operation)
977
    {
978 6
        $exists = is_resource($this->lockHandle);
979
980 6
        $success = $exists && flock($this->lockHandle, $operation);
981
982 6
        $exists && fclose($this->lockHandle);
983 6
        $this->lockHandle = null;
984
985 6
        return $success;
986
    }
987
}
988