Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
37.25% covered (danger)
37.25%
57 / 153
18.18% covered (danger)
18.18%
6 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
Tus_File
37.09% covered (danger)
37.09%
56 / 151
18.18% covered (danger)
18.18%
6 / 33
1150.75
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 ensure_integer
66.67% covered (warning)
66.67%
2 / 3
0.00% covered (danger)
0.00%
0 / 1
2.15
 set_meta
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
2.01
 set_name
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_name
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_file_size
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_file_size
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_key
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 get_key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_checksum
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_checksum
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_offset
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_offset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_location
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_location
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_file_path
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_file_path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_upload_metadata
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 get_input_stream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 set_input_stream
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 details
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
2
 upload
65.22% covered (warning)
65.22%
15 / 23
0.00% covered (danger)
0.00%
0 / 1
9.06
 open
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
5.02
 exists
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
6.97
 seek
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 read
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
2.03
 write
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 merge
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
12
 copy
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 delete
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 delete_files
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
30
 close
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_wp_filesystem
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Main
4 *
5 * @package VideoPressUploader
6 **/
7
8namespace VideoPressUploader;
9
10use InvalidArgumentException;
11
12// Avoid direct calls to this file.
13if ( ! defined( 'ABSPATH' ) ) {
14    die( 0 );
15}
16
17/**
18 * Class Tus_File.
19 *
20 * @package VideoPressUploader
21 */
22class Tus_File {
23    const CHUNK_SIZE    = 8192; // 8 kilobytes.
24    const INPUT_STREAM  = 'php://input';
25    const READ_BINARY   = 'rb';
26    const APPEND_BINARY = 'ab';
27
28    /**
29     * The input stream.
30     *
31     * @var string
32     */
33    protected static $input_stream = self::INPUT_STREAM;
34
35    /**
36     * The key.
37     *
38     * @var string The key.
39     */
40    protected $key;
41
42    /**
43     * The file checksum.
44     *
45     * @var string
46     */
47    protected $checksum;
48
49    /**
50     * The file name.
51     *
52     * @var string
53     */
54    protected $name;
55
56    /**
57     * The cache we are using.
58     *
59     * @var Tus_Abstract_Cache
60     */
61    protected $cache;
62
63    /**
64     * The current file offset.
65     *
66     * @var int
67     */
68    protected $offset;
69
70    /**
71     * The location.
72     *
73     * @var string
74     */
75    protected $location;
76
77    /**
78     * The file path.
79     *
80     * @var string
81     */
82    protected $file_path;
83
84    /**
85     * The file size.
86     *
87     * @var int
88     */
89    protected $file_size;
90
91    /**
92     * The upload metadata.
93     *
94     * @var array
95     */
96    private $upload_metadata = array();
97
98    /**
99     * File constructor.
100     *
101     * @param string|null             $name Name.
102     * @param Tus_Abstract_Cache|null $cache Cache.
103     */
104    public function __construct( $name = null, $cache = null ) {
105        $this->name  = $name;
106        $this->cache = $cache;
107    }
108
109    /**
110     * Returns an integer if it's castable. Otherwise it throws
111     *
112     * @param string|int $number Number to cast.
113     * @throws InvalidArgumentException If argument is invalid.
114     * @return int
115     */
116    public function ensure_integer( $number ) {
117        if ( ! is_numeric( $number ) ) {
118            throw new InvalidArgumentException( 'argument needs to be an integer. Check stacktrace' );
119        }
120        return (int) $number;
121    }
122
123    /**
124     * Set file meta.
125     *
126     * @param int    $offset Offset.
127     * @param int    $file_size File size.
128     * @param string $file_path File path.
129     * @param string $location Location.
130     *
131     * @throws InvalidArgumentException If argument is invalid.
132     * @return $this
133     */
134    public function set_meta( $offset, $file_size, $file_path, $location = null ) {
135        $offset    = $this->ensure_integer( $offset );
136        $file_size = $this->ensure_integer( $file_size );
137
138        if ( ! is_string( $file_path ) ) {
139            throw new InvalidArgumentException( '$file_path needs to be a string' );
140        }
141
142        $this->offset    = absint( $offset );
143        $this->file_size = absint( $file_size );
144        $this->file_path = $file_path;
145        $this->location  = $location;
146
147        return $this;
148    }
149
150    /**
151     * Set name.
152     *
153     * @param string $name Name.
154     *
155     * @throws InvalidArgumentException If argument is invalid.
156     *
157     * @return $this
158     */
159    public function set_name( $name ) {
160        if ( ! is_string( $name ) ) {
161            throw new InvalidArgumentException( '$name needs to be a string' );
162        }
163        $this->name = $name;
164
165        return $this;
166    }
167
168    /**
169     * Get name.
170     *
171     * @return string
172     */
173    public function get_name() {
174        return $this->name;
175    }
176
177    /**
178     * Set file size.
179     *
180     * @param int $size The size.
181     *
182     * @throws InvalidArgumentException If argument is invalid.
183     * @return Tus_File
184     */
185    public function set_file_size( $size ) {
186        $size            = $this->ensure_integer( $size );
187        $this->file_size = $size;
188
189        return $this;
190    }
191
192    /**
193     * Get file size.
194     *
195     * @return int
196     */
197    public function get_file_size() {
198        return $this->file_size;
199    }
200
201    /**
202     * Set key.
203     *
204     * @param string $key The key.
205     *
206     * @throws InvalidArgumentException If argument is invalid.
207     * @return Tus_File
208     */
209    public function set_key( $key ) {
210        if ( ! is_string( $key ) ) {
211            throw new InvalidArgumentException( '$key needs to be a string' );
212        }
213        $this->key = $key;
214
215        return $this;
216    }
217
218    /**
219     * Get key.
220     *
221     * @return string
222     */
223    public function get_key() {
224        return $this->key;
225    }
226
227    /**
228     * Set checksum.
229     *
230     * @param string $checksum The checksum.
231     *
232     * @throws InvalidArgumentException If argument is invalid.
233     * @return Tus_File
234     */
235    public function set_checksum( $checksum ) {
236        if ( ! is_string( $checksum ) ) {
237            throw new InvalidArgumentException( '$checksum needs to be a string' );
238        }
239        $this->checksum = $checksum;
240
241        return $this;
242    }
243
244    /**
245     * Get checksum.
246     *
247     * @return string
248     */
249    public function get_checksum() {
250        return $this->checksum;
251    }
252
253    /**
254     * Set offset.
255     *
256     * @param int $offset The offset.
257     *
258     * @throws InvalidArgumentException If argument is invalid.
259     * @return self
260     */
261    public function set_offset( $offset ) {
262        $offset       = $this->ensure_integer( $offset );
263        $this->offset = absint( $offset );
264
265        return $this;
266    }
267
268    /**
269     * Get offset.
270     *
271     * @return int
272     */
273    public function get_offset() {
274        return $this->offset;
275    }
276
277    /**
278     * Set location.
279     *
280     * @param string $location The location.
281     *
282     * @throws InvalidArgumentException If argument is invalid.
283     * @return self
284     */
285    public function set_location( $location ) {
286        if ( ! is_string( $location ) ) {
287            throw new InvalidArgumentException( '$location needs to be a string' );
288        }
289        $this->location = $location;
290
291        return $this;
292    }
293
294    /**
295     * Get location.
296     *
297     * @return string
298     */
299    public function get_location() {
300        return $this->location;
301    }
302
303    /**
304     * Set absolute file location.
305     *
306     * @param string $path The path.
307     *
308     * @return Tus_File
309     */
310    public function set_file_path( $path ) {
311        $this->file_path = $path;
312
313        return $this;
314    }
315
316    /**
317     * Get absolute location.
318     *
319     * @return string
320     */
321    public function get_file_path() {
322        return $this->file_path;
323    }
324
325    /**
326     * Set the upload meta.
327     *
328     * @param array $metadata The metadata.
329     *
330     * @return Tus_File
331     */
332    public function set_upload_metadata( array $metadata ) {
333        $this->upload_metadata = $metadata;
334
335        return $this;
336    }
337
338    /**
339     * Get input stream.
340     *
341     * @return string
342     */
343    public function get_input_stream() {
344        return self::$input_stream;
345    }
346
347    /**
348     * Set input stream. Useful for testing.
349     *
350     * @param string $stream The stream.
351     *
352     * @return void
353     */
354    public static function set_input_stream( $stream ) {
355        self::$input_stream = $stream;
356    }
357
358    /**
359     * Get file meta.
360     *
361     * @return array
362     * @throws \Exception If date fails.
363     */
364    public function details() {
365        $now = Tus_Date_Utils::date_utc();
366        $ttl = $this->cache->get_ttl();
367
368        return array(
369            'name'       => $this->name,
370            'size'       => $this->file_size,
371            'offset'     => $this->offset,
372            'checksum'   => $this->checksum,
373            'location'   => $this->location,
374            'file_path'  => $this->file_path,
375            'metadata'   => $this->upload_metadata,
376            'created_at' => $now->format( Tus_Abstract_Cache::RFC_7231 ),
377            'expires_at' => Tus_Date_Utils::add_seconds( $now, $ttl )->format( Tus_Abstract_Cache::RFC_7231 ),
378        );
379    }
380
381    /**
382     * Upload file to server.
383     *
384     * @param int $total_bytes The total bytes of the file.
385     *
386     * @return int
387     * @throws \Out_Of_Range_Exception Various exceptions.
388     * @throws \Connection_Exception Various exceptions.
389     * @throws File_Exception Various exceptions.
390     */
391    public function upload( $total_bytes ) {
392        if ( $this->offset === $total_bytes ) {
393            return $this->offset;
394        }
395
396        try {
397                    $input  = $this->open( $this->get_input_stream(), self::READ_BINARY );
398                    $output = $this->open( $this->get_file_path(), self::APPEND_BINARY );
399                    $key    = $this->get_key();
400        } catch ( File_Exception $fe ) {
401            Logger::log( 'error', $fe );
402            throw new File_Exception( 'Upload failed.' );
403        }
404
405        try {
406            $this->seek( $output, $this->offset );
407
408            while ( ! feof( $input ) ) {
409                if ( CONNECTION_NORMAL !== connection_status() ) {
410                    throw new \Connection_Exception( 'Connection aborted by user.' );
411                }
412
413                $data  = $this->read( $input, self::CHUNK_SIZE );
414                $bytes = $this->write( $output, $data, self::CHUNK_SIZE );
415
416                $this->offset += $bytes;
417
418                $this->cache->set( $key, array( 'offset' => $this->offset ) );
419
420                if ( $this->offset > $total_bytes ) {
421                    throw new \Out_Of_Range_Exception( 'The uploaded file is corrupt.' );
422                }
423
424                if ( $this->offset === $total_bytes ) {
425                    break;
426                }
427            }
428        } finally {
429            $this->close( $input );
430            $this->close( $output );
431        }
432
433        return $this->offset;
434    }
435
436    /**
437     * Open file in given mode.
438     *
439     * @param string $file_path The file path.
440     * @param string $mode The mode.
441     *
442     * @return resource
443     * @throws File_Exception Exc.
444     * @throws InvalidArgumentException If argument is invalid.
445     */
446    public function open( $file_path, $mode ) {
447        if ( ! is_string( $file_path ) ) {
448            throw new InvalidArgumentException( '$file_path needs to be a string' );
449        }
450        if ( ! is_string( $mode ) ) {
451            throw new InvalidArgumentException( '$mode needs to be a string' );
452        }
453        $this->exists( $file_path, $mode );
454
455        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.PHP.NoSilencedErrors.Discouraged
456        $ptr = @fopen( $file_path, $mode );
457
458        if ( false === $ptr ) {
459            Logger::log( 'error', "Unable to open file at $file_path." );
460            throw new File_Exception( 'Unable to open file.' );
461        }
462
463        return $ptr;
464    }
465
466    /**
467     * Check if file to read exists.
468     *
469     * @param string $file_path The file path.
470     * @param string $mode The mode.
471     *
472     * @return bool
473     * @throws File_Exception File.
474     * @throws InvalidArgumentException If argument is invalid.
475     */
476    public function exists( $file_path, $mode = self::READ_BINARY ) {
477        if ( ! is_string( $file_path ) ) {
478            throw new InvalidArgumentException( '$file_path needs to be a string' );
479        }
480        if ( self::INPUT_STREAM === $file_path ) {
481            return true;
482        }
483
484        if ( self::READ_BINARY === $mode && ! file_exists( $file_path ) ) {
485            throw new File_Exception( 'File not found.' );
486        }
487
488        return true;
489    }
490
491    /**
492     * Move file pointer to given offset using fseek.
493     *
494     * @param resource $handle The handle.
495     * @param int      $offset The offset.
496     * @param int      $whence The whence.
497     *
498     * @throws File_Exception Exc.
499     *
500     * @return int
501     */
502    public function seek( $handle, $offset, $whence = SEEK_SET ) {
503        $offset   = $this->ensure_integer( $offset );
504        $position = fseek( $handle, $offset, $whence );
505
506        if ( -1 === $position ) {
507            throw new File_Exception( 'Cannot move pointer to desired position.' );
508        }
509
510        return $position;
511    }
512
513    /**
514     * Read data from file.
515     *
516     * @param resource $handle The handle.
517     * @param int      $chunk_size Chunk size.
518     *
519     * @return string
520     * @throws File_Exception If no data is read.
521     */
522    public function read( $handle, $chunk_size ) {
523        $chunk_size = $this->ensure_integer( $chunk_size );
524        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
525        $data = fread( $handle, $chunk_size );
526
527        if ( false === $data ) {
528            throw new File_Exception( 'Cannot read file.' );
529        }
530
531        return (string) $data;
532    }
533
534    /**
535     * Write data to file.
536     *
537     * @param resource $handle The file handle.
538     * @param string   $data The data to write.
539     * @param int|null $length Possibly the length of the data.
540     *
541     * @throws File_Exception When can't write.
542     *
543     * @return int
544     */
545    public function write( $handle, $data, $length = null ) {
546        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
547        $bytes_written = \is_numeric( $length ) ? fwrite( $handle, $data, intval( $length ) ) : fwrite( $handle, $data );
548
549        if ( false === $bytes_written ) {
550            throw new File_Exception( 'Cannot write to a file.' );
551        }
552
553        return $bytes_written;
554    }
555
556    /**
557     * Merge 2 or more files.
558     *
559     * @param array $files File data with meta info.
560     *
561     * @return int
562     * @throws File_Exception When the file to be merged is not found.
563     */
564    public function merge( array $files ) {
565        $destination = $this->get_file_path();
566        $first_file  = array_shift( $files );
567
568        // First partial file can directly be copied.
569        $this->copy( $first_file['file_path'], $destination );
570
571        $this->offset    = $first_file['offset'];
572        $this->file_size = filesize( $first_file['file_path'] );
573
574        $handle = $this->open( $destination, self::APPEND_BINARY );
575
576        foreach ( $files as $file ) {
577            if ( ! file_exists( $file['file_path'] ) ) {
578                throw new File_Exception( 'File to be merged not found.' );
579            }
580
581            $this->file_size += $this->write( $handle, $this->get_wp_filesystem()->get_contents( $file['file_path'] ) );
582
583            $this->offset += $file['offset'];
584        }
585
586        $this->close( $handle );
587
588        return $this->file_size;
589    }
590
591    /**
592     * Copy file from source to destination.
593     *
594     * @param string $source The source.
595     * @param string $destination The destination.
596     *
597     * @return bool
598     * @throws File_Exception If copy fails.
599     */
600    public function copy( $source, $destination ) {
601        $status = copy( $source, $destination );
602
603        if ( false === $status ) {
604            Logger::log( 'error', sprintf( 'Cannot copy source (%s) to destination (%s).', $source, $destination ) );
605            throw new File_Exception( 'Cannot copy source file to destination file.' );
606        }
607
608        return $status;
609    }
610
611    /**
612     * Delete file and/or folder.
613     *
614     * @param array $files The files.
615     * @param bool  $folder The folder.
616     *
617     * @return bool
618     */
619    public function delete( array $files, $folder = false ) {
620        $status = $this->delete_files( $files );
621
622        if ( $status && $folder ) {
623            return $this->get_wp_filesystem()->rmdir( \dirname( current( $files ) ) );
624        }
625
626        return $status;
627    }
628
629    /**
630     * Delete multiple files.
631     *
632     * @param array $files The files.
633     *
634     * @return bool
635     */
636    public function delete_files( array $files ) {
637        if ( empty( $files ) ) {
638            return false;
639        }
640
641        $status = true;
642
643        foreach ( $files as $file ) {
644            if ( $this->get_wp_filesystem()->exists( $file ) ) {
645                $r      = $this->get_wp_filesystem()->delete( $file );
646                $status = $status && $r;
647            }
648        }
649
650        return $status;
651    }
652
653    /**
654     * Close file.
655     *
656     * @param mixed $handle The handle.
657     *
658     * @return bool
659     */
660    public function close( $handle ) {
661        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
662        return fclose( $handle );
663    }
664
665    /**
666     * Get the wp filesystem.
667     *
668     * @return \WP_Filesystem_Base|null
669     */
670    private function get_wp_filesystem() {
671        global $wp_filesystem;
672
673        if ( ! isset( $wp_filesystem ) ) {
674            require_once ABSPATH . '/wp-admin/includes/file.php';
675            WP_Filesystem();
676        }
677
678        return $wp_filesystem;
679    }
680}