Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 234
0.00% covered (danger)
0.00%
0 / 37
CRAP
0.00% covered (danger)
0.00%
0 / 1
Tus_Client
0.00% covered (danger)
0.00%
0 / 234
0.00% covered (danger)
0.00%
0 / 37
8556
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 get_cache
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_cache_attribute
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 set_uploaded_video_details
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_uploaded_video_details
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 file
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
30
 get_file_path
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_file_name
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_file_name
0.00% covered (danger)
0.00%
0 / 1
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_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 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 add_metadata
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 remove_metadata
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 set_metadata
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 get_metadata
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_upload_metadata_header
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_key
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_url
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 set_checksum_algorithm
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 get_checksum_algorithm
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_expired
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 is_partial
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_partial_offset
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 seek
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 upload
0.00% covered (danger)
0.00%
0 / 13
0.00% covered (danger)
0.00%
0 / 1
56
 create
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 create_with_upload
0.00% covered (danger)
0.00%
0 / 54
0.00% covered (danger)
0.00%
0 / 1
210
 partial
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 get_offset
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
20
 send_patch_request
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
72
 handle_patch_exception
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
 get_data
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 get_upload_checksum_header
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 do_request
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 do_get_request
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 do_post_request
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Tus_Client
4 *
5 * @package VideoPressUploader
6 **/
7
8// phpcs:disable Generic.Commenting.DocComment.MissingShort
9// phpcs:disable Squiz.Commenting.FunctionComment.EmptyThrows
10// phpcs:disable Generic.Commenting.DocComment.MissingShort
11// phpcs:disable Squiz.Commenting.FunctionComment.MissingParamComment
12
13namespace VideoPressUploader;
14
15use InvalidArgumentException;
16use WP_Error;
17use WP_Http;
18
19/**
20 * Tus_Client
21 */
22class Tus_Client {
23
24    /** @const string Tus protocol version. */
25    const TUS_PROTOCOL_VERSION = '1.0.0';
26
27    /** @const string Upload type partial. */
28    const UPLOAD_TYPE_PARTIAL = 'partial';
29
30    /** @const string Upload type final. */
31    const UPLOAD_TYPE_FINAL = 'final';
32
33    /** @const string Name separator for partial upload. */
34    const PARTIAL_UPLOAD_NAME_SEPARATOR = '_';
35
36    /** @const string Upload type normal. */
37    const UPLOAD_TYPE_NORMAL = 'normal';
38
39    /** @const string Header Content Type */
40    const HEADER_CONTENT_TYPE = 'application/offset+octet-stream';
41
42    /** @const string Base API Uri */
43    const BASE_API_URL = 'https://public-api.wordpress.com/rest/v1.1/video-uploads/%d';
44
45    /** @var Tus_Abstract_Cache */
46    protected $cache;
47
48    /** @var string */
49    protected $file_path;
50
51    /** @var int */
52    protected $file_size = 0;
53
54    /** @var string */
55    protected $file_name;
56
57    /** @var string */
58    protected $key;
59
60    /** @var string */
61    protected $url;
62
63    /** @var string */
64    protected $checksum;
65
66    /** @var int */
67    protected $partial_offset = -1;
68
69    /** @var bool */
70    protected $partial = false;
71
72    /** @var string */
73    protected $checksum_algorithm = 'sha256';
74
75    /** @var array */
76    protected $metadata = array();
77
78    /** @var array */
79    protected $headers = array();
80
81    /**
82     * The details of the server response about the uploaded file
83     * VideoPress mod: Create new attribute
84     *
85     * @var array
86     */
87    protected $uploaded_video_details = null;
88
89    /**
90     * The API url composed by BASE_API_URL and the Blod ID.
91     *
92     * @var string
93     */
94    protected $api_url = null;
95
96    /**
97     * Tus_Client constructor.
98     *
99     * @param string $key The unique upload key identifier.
100     * @param string $upload_token The upload token retrieved from the server.
101     * @param int    $blog_id The current Jetpack Blog ID.
102     *
103     * @throws \ReflectionException
104     * @throws InvalidArgumentException
105     */
106    public function __construct( $key, $upload_token, $blog_id ) {
107
108        $this->key = $key;
109
110        $this->headers = array(
111            'Tus-Resumable'             => self::TUS_PROTOCOL_VERSION,
112            'x-videopress-upload-token' => $upload_token,
113        );
114
115        $this->api_url = sprintf( self::BASE_API_URL, (int) $blog_id );
116
117        $this->cache = new Transient_Store( $this->get_key() );
118    }
119
120    /**
121     * Get cache.
122     *
123     * @return Tus_Abstract_Cache
124     */
125    public function get_cache() {
126        return $this->cache;
127    }
128
129    /**
130     * Gets one attribute from the cache, if it exists.
131     *
132     * @param string $attribute The attribute name.
133     * @return mixed The attribute value if found or null.
134     */
135    public function get_cache_attribute( $attribute ) {
136        $cache_values = $this->get_cache()->get( $this->get_key() );
137        return ! empty( $cache_values[ $attribute ] ) ? $cache_values[ $attribute ] : null;
138    }
139
140    /**
141     * Sets the uploaded video details
142     *
143     * @param string $guid The guid of the created video.
144     * @param string $media_id The ID of the attachment created.
145     * @param string $upload_src The video URL.
146     * @return void
147     */
148    protected function set_uploaded_video_details( $guid, $media_id, $upload_src ) {
149        $this->uploaded_video_details = compact( 'guid', 'media_id', 'upload_src' );
150    }
151
152    /**
153     * Gets the details of the uploaded video
154     * VideoPress mod: Create new method
155     *
156     * @return array
157     */
158    public function get_uploaded_video_details() {
159        return $this->uploaded_video_details;
160    }
161
162    /**
163     * Set file properties.
164     *
165     * @param string      $file File path.
166     * @param string|null $name File name.
167     *
168     * @throws InvalidArgumentException
169     * @throws Tus_Exception
170     * @return Tus_Client
171     */
172    public function file( $file, $name = null ) {
173        if ( ! is_string( $file ) ) {
174            throw new InvalidArgumentException( '$file needs to be a string' );
175        }
176        $this->file_path = $file;
177
178        if ( ! file_exists( $file ) || ! is_readable( $file ) ) {
179            throw new Tus_Exception( 'Cannot read file: ' . $file );
180        }
181
182        $this->file_name = ! empty( $name ) ? basename( $this->file_path ) : '';
183        $this->file_size = filesize( $file );
184
185        $this->add_metadata( 'filename', $this->file_name );
186
187        return $this;
188    }
189
190    /**
191     * Get file path.
192     *
193     * @return string|null
194     */
195    public function get_file_path() {
196        return $this->file_path;
197    }
198
199    /**
200     * Set file name.
201     *
202     * @param string $name The file name.
203     *
204     * @throws InvalidArgumentException
205     * @return Tus_Client
206     */
207    public function set_file_name( $name ) {
208        if ( ! is_string( $name ) ) {
209            throw new InvalidArgumentException( '$name needs to be a string' );
210        }
211        $this->add_metadata( 'filename', $this->file_name = $name );
212
213        return $this;
214    }
215
216    /**
217     * Get file name.
218     *
219     * @return string|null
220     */
221    public function get_file_name() {
222        return $this->file_name;
223    }
224
225    /**
226     * Get file size.
227     *
228     * @return int
229     */
230    public function get_file_size() {
231        return $this->file_size;
232    }
233
234    /**
235     * Set checksum.
236     *
237     * @param string $checksum
238     *
239     * @throws InvalidArgumentException
240     * @return Tus_Client
241     */
242    public function set_checksum( $checksum ) {
243        if ( ! is_string( $checksum ) ) {
244            throw new InvalidArgumentException( '$checksum needs to be a string' );
245        }
246        $this->checksum = $checksum;
247
248        return $this;
249    }
250
251    /**
252     * Get checksum.
253     *
254     * @return string
255     */
256    public function get_checksum() {
257        if ( empty( $this->checksum ) ) {
258            $this->set_checksum( hash_file( $this->get_checksum_algorithm(), $this->get_file_path() ) );
259        }
260
261        return $this->checksum;
262    }
263
264    /**
265     * Add metadata.
266     *
267     * @param string $key
268     * @param string $value
269     *
270     * @throws InvalidArgumentException
271     * @return Tus_Client
272     */
273    public function add_metadata( $key, $value ) {
274        if ( ! is_string( $key ) || ! is_string( $value ) ) {
275            throw new InvalidArgumentException( '$key and $value need to be strings' );
276        }
277        $this->metadata[ $key ] = base64_encode( $value ); //phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
278
279        return $this;
280    }
281
282    /**
283     * Remove metadata.
284     *
285     * @param string $key
286     *
287     * @throws InvalidArgumentException
288     * @return Tus_Client
289     */
290    public function remove_metadata( $key ) {
291        if ( ! is_string( $key ) ) {
292            throw new InvalidArgumentException( '$key needs to be a string' );
293        }
294        unset( $this->metadata[ $key ] );
295
296        return $this;
297    }
298
299    /**
300     * Set metadata.
301     *
302     * @param array $items
303     *
304     * @return Tus_Client
305     */
306    public function set_metadata( array $items ) {
307        $items = array_map( 'base64_encode', $items );
308
309        $this->metadata = $items;
310
311        return $this;
312    }
313
314    /**
315     * Get metadata.
316     *
317     * @return array
318     */
319    public function get_metadata() {
320        return $this->metadata;
321    }
322
323    /**
324     * Get metadata for Upload-Metadata header.
325     *
326     * @return string
327     */
328    protected function get_upload_metadata_header() {
329        $metadata = array();
330
331        foreach ( $this->get_metadata() as $key => $value ) {
332            $metadata[] = "{$key} {$value}";
333        }
334
335        return implode( ',', $metadata );
336    }
337
338    /**
339     * Get key.
340     *
341     * @return string
342     */
343    public function get_key() {
344        return $this->key;
345    }
346
347    /**
348     * Get url.
349     *
350     * @throws File_Exception
351     * @return string|null
352     */
353    public function get_url() {
354        $this->url = $this->get_cache_attribute( 'location' );
355
356        if ( ! $this->url ) {
357            throw new File_Exception( 'File not found.' );
358        }
359
360        return $this->url;
361    }
362
363    /**
364     * Set checksum algorithm.
365     *
366     * @param string $algorithm
367     *
368     * @throws InvalidArgumentException
369     * @return Tus_Client
370     */
371    public function set_checksum_algorithm( $algorithm ) {
372        if ( ! is_string( $algorithm ) ) {
373            throw new InvalidArgumentException( '$algorithm needs to be a string' );
374        }
375        $this->checksum_algorithm = $algorithm;
376
377        return $this;
378    }
379
380    /**
381     * Get checksum algorithm.
382     *
383     * @return string
384     */
385    public function get_checksum_algorithm() {
386        return $this->checksum_algorithm;
387    }
388
389    /**
390     * Check if current upload is expired.
391     *
392     * @return bool
393     */
394    public function is_expired() {
395        $expires_at = $this->get_cache_attribute( 'expires_at' );
396
397        return empty( $expires_at ) || time() > strtotime( $expires_at );
398    }
399
400    /**
401     * Check if this is a partial upload request.
402     *
403     * @return bool
404     */
405    public function is_partial() {
406        return $this->partial;
407    }
408
409    /**
410     * Get partial offset.
411     *
412     * @return int
413     */
414    public function get_partial_offset() {
415        return $this->partial_offset;
416    }
417
418    /**
419     * Set offset and force this to be a partial upload request.
420     *
421     * @param int $offset
422     *
423     * @throws InvalidArgumentException
424     * @return self
425     */
426    public function seek( $offset ) {
427        if ( ! is_int( $offset ) ) {
428            throw new InvalidArgumentException( '$offset needs to be an integer' );
429        }
430        $this->partial_offset = $offset;
431
432        $this->partial();
433
434        return $this;
435    }
436
437    /**
438     * Upload file.
439     *
440     * @param int $bytes Bytes to upload.
441     *
442     * @throws Tus_Exception
443     * @throws InvalidArgumentException
444     *
445     * @return int
446     */
447    public function upload( $bytes = -1 ) {
448        if ( ! is_int( $bytes ) ) {
449            throw new InvalidArgumentException( '$bytes needs to be an integer' );
450        }
451        $bytes          = $bytes < 0 ? $this->get_file_size() : $bytes;
452        $partial_offset = $this->partial_offset < 0 ? 0 : $this->partial_offset;
453
454        $offset = $this->get_offset();
455        if ( is_wp_error( $offset ) ) {
456            throw new Tus_Exception( "Couldn't connect to server." );
457        }
458
459        if ( false === $offset ) {
460            $this->url = $this->create( $this->get_key() );
461            $offset    = $partial_offset;
462        }
463
464        // Verify that upload is not yet expired.
465        if ( $this->is_expired() ) {
466            throw new Tus_Exception( 'Upload expired.' );
467        }
468
469        // Now, resume upload with PATCH request.
470        return $this->send_patch_request( $bytes, $offset );
471    }
472
473    /**
474     * Create resource with POST request.
475     *
476     * @param string $key
477     *
478     * @throws InvalidArgumentException
479     *
480     * @return string
481     */
482    public function create( $key ) {
483        if ( ! is_string( $key ) ) {
484            throw new InvalidArgumentException( '$key needs to be a string' );
485        }
486        return $this->create_with_upload( $key, 0 )['location'];
487    }
488
489    /**
490     * Create resource with POST request and upload data using the creation-with-upload extension.
491     *
492     * @see https://tus.io/protocols/resumable-upload.html#creation-with-upload
493     *
494     * @param string $key
495     * @param int    $bytes -1 => all data; 0 => no data.
496     *
497     * @throws InvalidArgumentException
498     * @throws Tus_Exception
499     *
500     * @return array [
501     *   'location' => string,
502     *   'offset' => int
503     * ]
504     */
505    public function create_with_upload( $key, $bytes = -1 ) {
506        if ( ! is_string( $key ) ) {
507            throw new InvalidArgumentException( '$key needs to be a string' );
508        }
509        $bytes = $bytes < 0 ? $this->file_size : $bytes;
510
511        $headers = $this->headers + array(
512            'Upload-Length'   => $this->file_size,
513            'Upload-Key'      => $key,
514            'Upload-Checksum' => $this->get_upload_checksum_header(),
515            'Upload-Metadata' => $this->get_upload_metadata_header(),
516        );
517
518        $data = '';
519        if ( $bytes > 0 ) {
520            $data = $this->get_data( 0, $bytes );
521
522            $headers += array(
523                'Content-Type'   => self::HEADER_CONTENT_TYPE,
524                'Content-Length' => \strlen( $data ),
525            );
526        }
527
528        if ( $this->is_partial() ) {
529            $headers += array( 'Upload-Concat' => 'partial' );
530        }
531
532        $response = $this->do_post_request(
533            $this->api_url,
534            array(
535                'body'    => $data,
536                'headers' => $headers,
537            )
538        );
539
540        if ( is_wp_error( $response ) ) {
541            throw new Tus_Exception( 'Error reaching the server.' );
542        }
543
544        $status_code = wp_remote_retrieve_response_code( $response );
545
546        if ( WP_Http::CREATED !== $status_code ) {
547            $body          = json_decode( wp_remote_retrieve_body( $response ) );
548            $error_message = __( 'Unable to create resource.', 'jetpack-videopress-pkg' );
549            if ( ! empty( $body->message ) ) {
550                $error_message = $body->message;
551                if ( ! empty( $body->error ) ) {
552                    $error_message = $body->error . ': ' . $error_message;
553                }
554            }
555            // server can respond in a few different ways.
556            if ( isset( $body->success ) && false === $body->success ) {
557                if ( ! empty( $body->data ) && ! empty( $body->data->message ) ) {
558                    $error_message = $body->data->message;
559                }
560            }
561            throw new Tus_Exception( $error_message, $status_code );
562        }
563
564        $upload_offset   = $bytes > 0 ? wp_remote_retrieve_header( $response, 'upload-offset' ) : 0;
565        $upload_location = wp_remote_retrieve_header( $response, 'location' );
566
567        $cache = $this->get_cache();
568
569        $cache->set(
570            $this->get_key(),
571            array(
572                'location'      => $upload_location,
573                'expires_at'    => gmdate( $cache::RFC_7231, time() + $cache->get_ttl() ),
574                'token_for_key' => wp_remote_retrieve_header( $response, 'x-videopress-upload-key-token' ),
575            )
576        );
577
578        return array(
579            'location' => $upload_location,
580            'offset'   => $upload_offset,
581        );
582    }
583
584    /**
585     * Set as partial request.
586     *
587     * @param bool $state
588     *
589     * @throws InvalidArgumentException
590     * @return void
591     */
592    protected function partial( $state = true ) {
593        if ( ! is_bool( $state ) ) {
594            throw new InvalidArgumentException( '$state needs to be a boolean' );
595        }
596        $this->partial = $state;
597
598        if ( ! $this->partial ) {
599            return;
600        }
601
602        $key = $this->get_key();
603
604        if ( str_contains( $key, self::PARTIAL_UPLOAD_NAME_SEPARATOR ) ) {
605            list($key, /* $partialKey */) = explode( self::PARTIAL_UPLOAD_NAME_SEPARATOR, $key );
606        }
607
608        $this->key = $key . self::PARTIAL_UPLOAD_NAME_SEPARATOR . wp_generate_uuid4();
609    }
610
611    /**
612     * Send HEAD request and retrieves offset.
613     *
614     * @return bool|int|WP_Error integer with the offset uploaded if file exists. False if file does not exist. WP_Error on connection error.
615     */
616    public function get_offset() {
617        $headers = $this->headers + array(
618            'X-HTTP-Method-Override' => 'HEAD',
619        );
620
621        try {
622            $response = $this->do_get_request(
623                $this->get_url(),
624                array( 'headers' => $headers )
625            );
626        } catch ( File_Exception $e ) {
627            return false;
628        }
629
630        if ( is_wp_error( $response ) ) {
631            return $response;
632        }
633
634        $response_code = wp_remote_retrieve_response_code( $response );
635
636        if ( WP_Http::OK !== $response_code ) {
637            return false;
638        }
639
640        return (int) wp_remote_retrieve_header( $response, 'upload-offset' );
641    }
642
643    /**
644     * Send PATCH request.
645     *
646     * @param int $bytes
647     * @param int $offset
648     *
649     * @throws InvalidArgumentException
650     *
651     * @return int
652     */
653    protected function send_patch_request( $bytes, $offset ) {
654        if ( ! is_int( $bytes ) || ! is_int( $offset ) ) {
655            throw new InvalidArgumentException( '$bytes and $offset need to be integers' );
656        }
657        $data    = $this->get_data( $offset, $bytes );
658        $headers = $this->headers + array(
659            'Content-Type'           => self::HEADER_CONTENT_TYPE,
660            'Content-Length'         => \strlen( $data ),
661            'Upload-Checksum'        => $this->get_upload_checksum_header(),
662            'X-HTTP-Method-Override' => 'PATCH',
663
664        );
665
666        $token                                = $this->get_cache_attribute( 'token_for_key' );
667        $headers['x-videopress-upload-token'] = $token;
668
669        if ( $this->is_partial() ) {
670            $headers += array( 'Upload-Concat' => self::UPLOAD_TYPE_PARTIAL );
671        } else {
672            $headers += array( 'Upload-Offset' => $offset );
673        }
674
675        $response = $this->do_post_request(
676            $this->get_url(),
677            array(
678                'body'    => $data,
679                'headers' => $headers,
680            )
681        );
682
683        $response_code = wp_remote_retrieve_response_code( $response );
684        if ( WP_Http::NO_CONTENT !== $response_code ) {
685            throw $this->handle_patch_exception( $response );
686        }
687
688        $guid       = wp_remote_retrieve_header( $response, 'x-videopress-upload-guid' );
689        $media_id   = (int) wp_remote_retrieve_header( $response, 'x-videopress-upload-media-id' );
690        $upload_src = wp_remote_retrieve_header( $response, 'x-videopress-upload-src-url' );
691
692        if ( $guid && $media_id && $upload_src ) {
693            $this->set_uploaded_video_details( $guid, $media_id, $upload_src );
694        }
695
696        return (int) wp_remote_retrieve_header( $response, 'upload-offset' );
697    }
698
699    /**
700     * Handle client exception during patch request.
701     *
702     * @param array $response The response from the PATCH request.
703     *
704     * @return \Exception
705     */
706    protected function handle_patch_exception( $response ) {
707
708        if ( is_wp_error( $response ) ) {
709            return new Tus_Exception( $response->get_error_message() );
710        }
711
712        $response_code = wp_remote_retrieve_response_code( $response );
713
714        if ( WP_Http::REQUESTED_RANGE_NOT_SATISFIABLE === $response_code ) {
715            return new Tus_Exception( 'The uploaded file is corrupt.' );
716        }
717
718        if ( WP_Http::HTTP_CONTINUE === $response_code ) {
719            return new Tus_Exception( 'Connection aborted by user.' );
720        }
721
722        if ( WP_Http::UNSUPPORTED_MEDIA_TYPE === $response_code ) {
723            return new Tus_Exception( 'Unsupported media types.' );
724        }
725
726        return new Tus_Exception( (string) wp_remote_retrieve_body( $response ), $response_code );
727    }
728
729    /**
730     * Get X bytes of data from file.
731     *
732     * @param int $offset
733     * @param int $bytes
734     *
735     * @throws InvalidArgumentException
736     *
737     * @return string
738     */
739    protected function get_data( $offset, $bytes ) {
740        if ( ! is_int( $bytes ) || ! is_int( $offset ) ) {
741            throw new InvalidArgumentException( '$bytes and $offset need to be integers' );
742        }
743        $file   = new Tus_File();
744        $handle = $file->open( $this->get_file_path(), $file::READ_BINARY );
745
746        $file->seek( $handle, $offset );
747
748        $data = $file->read( $handle, $bytes );
749
750        $file->close( $handle );
751
752        return $data;
753    }
754
755    /**
756     * Get upload checksum header.
757     *
758     * @return string
759     */
760    protected function get_upload_checksum_header() {
761        return $this->get_checksum_algorithm() . ' ' . base64_encode( $this->get_checksum() ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
762    }
763
764    /**
765     * Do HTTP request
766     *
767     * @param string $url The URL to make the request to.
768     * @param array  $args Request arguments.
769     * @return array|WP_Error WordPress Http response
770     */
771    protected function do_request( $url, $args ) {
772        $args = wp_parse_args(
773            $args,
774            array(
775                'timeout' => 25,
776            )
777        );
778        return wp_remote_request( $url, $args );
779    }
780
781    /**
782     * Do a GET HTTP request
783     *
784     * @param string $url The URL to make the request to.
785     * @param array  $args Request arguments.
786     * @return array|WP_Error WordPress Http response
787     */
788    protected function do_get_request( $url, $args ) {
789        $args = wp_parse_args(
790            $args,
791            array(
792                'method' => 'GET',
793            )
794        );
795        return $this->do_request( $url, $args );
796    }
797
798    /**
799     * Do a POST HTTP request
800     *
801     * @param string $url The URL to make the request to.
802     * @param array  $args Request arguments.
803     * @return array|WP_Error WordPress Http response
804     */
805    protected function do_post_request( $url, $args ) {
806        $args = wp_parse_args(
807            $args,
808            array(
809                'method' => 'POST',
810            )
811        );
812        return $this->do_request( $url, $args );
813    }
814}