Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
38.71% covered (danger)
38.71%
60 / 155
18.18% covered (danger)
18.18%
6 / 33
CRAP
0.00% covered (danger)
0.00%
0 / 1
Tus_File
38.56% covered (danger)
38.56%
59 / 153
18.18% covered (danger)
18.18%
6 / 33
1108.02
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
72.00% covered (warning)
72.00%
18 / 25
0.00% covered (danger)
0.00%
0 / 1
9.40
 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                if ( $this->offset > $total_bytes ) {
419                    throw new \Out_Of_Range_Exception( 'The uploaded file is corrupt.' );
420                }
421
422                if ( $this->offset === $total_bytes ) {
423                    break;
424                }
425            }
426        } finally {
427            $this->close( $input );
428            $this->close( $output );
429
430            try {
431                $this->cache->set( $key, array( 'offset' => $this->offset ) );
432            } catch ( \Throwable $e ) {
433                Logger::log( 'error', $e );
434            }
435        }
436
437        return $this->offset;
438    }
439
440    /**
441     * Open file in given mode.
442     *
443     * @param string $file_path The file path.
444     * @param string $mode The mode.
445     *
446     * @return resource
447     * @throws File_Exception Exc.
448     * @throws InvalidArgumentException If argument is invalid.
449     */
450    public function open( $file_path, $mode ) {
451        if ( ! is_string( $file_path ) ) {
452            throw new InvalidArgumentException( '$file_path needs to be a string' );
453        }
454        if ( ! is_string( $mode ) ) {
455            throw new InvalidArgumentException( '$mode needs to be a string' );
456        }
457        $this->exists( $file_path, $mode );
458
459        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen, WordPress.PHP.NoSilencedErrors.Discouraged
460        $ptr = @fopen( $file_path, $mode );
461
462        if ( false === $ptr ) {
463            Logger::log( 'error', "Unable to open file at $file_path." );
464            throw new File_Exception( 'Unable to open file.' );
465        }
466
467        return $ptr;
468    }
469
470    /**
471     * Check if file to read exists.
472     *
473     * @param string $file_path The file path.
474     * @param string $mode The mode.
475     *
476     * @return bool
477     * @throws File_Exception File.
478     * @throws InvalidArgumentException If argument is invalid.
479     */
480    public function exists( $file_path, $mode = self::READ_BINARY ) {
481        if ( ! is_string( $file_path ) ) {
482            throw new InvalidArgumentException( '$file_path needs to be a string' );
483        }
484        if ( self::INPUT_STREAM === $file_path ) {
485            return true;
486        }
487
488        if ( self::READ_BINARY === $mode && ! file_exists( $file_path ) ) {
489            throw new File_Exception( 'File not found.' );
490        }
491
492        return true;
493    }
494
495    /**
496     * Move file pointer to given offset using fseek.
497     *
498     * @param resource $handle The handle.
499     * @param int      $offset The offset.
500     * @param int      $whence The whence.
501     *
502     * @throws File_Exception Exc.
503     *
504     * @return int
505     */
506    public function seek( $handle, $offset, $whence = SEEK_SET ) {
507        $offset   = $this->ensure_integer( $offset );
508        $position = fseek( $handle, $offset, $whence );
509
510        if ( -1 === $position ) {
511            throw new File_Exception( 'Cannot move pointer to desired position.' );
512        }
513
514        return $position;
515    }
516
517    /**
518     * Read data from file.
519     *
520     * @param resource $handle The handle.
521     * @param int      $chunk_size Chunk size.
522     *
523     * @return string
524     * @throws File_Exception If no data is read.
525     */
526    public function read( $handle, $chunk_size ) {
527        $chunk_size = $this->ensure_integer( $chunk_size );
528        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fread
529        $data = fread( $handle, $chunk_size );
530
531        if ( false === $data ) {
532            throw new File_Exception( 'Cannot read file.' );
533        }
534
535        return (string) $data;
536    }
537
538    /**
539     * Write data to file.
540     *
541     * @param resource $handle The file handle.
542     * @param string   $data The data to write.
543     * @param int|null $length Possibly the length of the data.
544     *
545     * @throws File_Exception When can't write.
546     *
547     * @return int
548     */
549    public function write( $handle, $data, $length = null ) {
550        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
551        $bytes_written = \is_numeric( $length ) ? fwrite( $handle, $data, intval( $length ) ) : fwrite( $handle, $data );
552
553        if ( false === $bytes_written ) {
554            throw new File_Exception( 'Cannot write to a file.' );
555        }
556
557        return $bytes_written;
558    }
559
560    /**
561     * Merge 2 or more files.
562     *
563     * @param array $files File data with meta info.
564     *
565     * @return int
566     * @throws File_Exception When the file to be merged is not found.
567     */
568    public function merge( array $files ) {
569        $destination = $this->get_file_path();
570        $first_file  = array_shift( $files );
571
572        // First partial file can directly be copied.
573        $this->copy( $first_file['file_path'], $destination );
574
575        $this->offset    = $first_file['offset'];
576        $this->file_size = filesize( $first_file['file_path'] );
577
578        $handle = $this->open( $destination, self::APPEND_BINARY );
579
580        foreach ( $files as $file ) {
581            if ( ! file_exists( $file['file_path'] ) ) {
582                throw new File_Exception( 'File to be merged not found.' );
583            }
584
585            $this->file_size += $this->write( $handle, $this->get_wp_filesystem()->get_contents( $file['file_path'] ) );
586
587            $this->offset += $file['offset'];
588        }
589
590        $this->close( $handle );
591
592        return $this->file_size;
593    }
594
595    /**
596     * Copy file from source to destination.
597     *
598     * @param string $source The source.
599     * @param string $destination The destination.
600     *
601     * @return bool
602     * @throws File_Exception If copy fails.
603     */
604    public function copy( $source, $destination ) {
605        $status = copy( $source, $destination );
606
607        if ( ! $status ) {
608            Logger::log( 'error', sprintf( 'Cannot copy source (%s) to destination (%s).', $source, $destination ) );
609            throw new File_Exception( 'Cannot copy source file to destination file.' );
610        }
611
612        return $status;
613    }
614
615    /**
616     * Delete file and/or folder.
617     *
618     * @param array $files The files.
619     * @param bool  $folder The folder.
620     *
621     * @return bool
622     */
623    public function delete( array $files, $folder = false ) {
624        $status = $this->delete_files( $files );
625
626        if ( $status && $folder ) {
627            return $this->get_wp_filesystem()->rmdir( \dirname( current( $files ) ) );
628        }
629
630        return $status;
631    }
632
633    /**
634     * Delete multiple files.
635     *
636     * @param array $files The files.
637     *
638     * @return bool
639     */
640    public function delete_files( array $files ) {
641        if ( empty( $files ) ) {
642            return false;
643        }
644
645        $status = true;
646
647        foreach ( $files as $file ) {
648            if ( $this->get_wp_filesystem()->exists( $file ) ) {
649                $r      = $this->get_wp_filesystem()->delete( $file );
650                $status = $status && $r;
651            }
652        }
653
654        return $status;
655    }
656
657    /**
658     * Close file.
659     *
660     * @param mixed $handle The handle.
661     *
662     * @return bool
663     */
664    public function close( $handle ) {
665        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
666        return fclose( $handle );
667    }
668
669    /**
670     * Get the wp filesystem.
671     *
672     * @return \WP_Filesystem_Base|null
673     */
674    private function get_wp_filesystem() {
675        global $wp_filesystem;
676
677        if ( ! isset( $wp_filesystem ) ) {
678            require_once ABSPATH . '/wp-admin/includes/file.php';
679            WP_Filesystem();
680        }
681
682        return $wp_filesystem;
683    }
684}