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