Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
45.71% covered (danger)
45.71%
48 / 105
56.25% covered (warning)
56.25%
9 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Uploader
45.71% covered (danger)
45.71%
48 / 105
56.25% covered (warning)
56.25%
9 / 16
243.33
0.00% covered (danger)
0.00%
0 / 1
 is_valid_attachment_id
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
20
 __construct
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
5.39
 get_file_path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_file_mime_type
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_file_name
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_file_size
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 file_has_supported_mime_type
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_upload_token
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_key
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 mark_as_uploaded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unmark_as_uploaded
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 is_uploaded
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_uploaded_attachment_id
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_client
50.00% covered (danger)
50.00%
2 / 4
0.00% covered (danger)
0.00%
0 / 1
2.50
 upload
53.85% covered (warning)
53.85%
21 / 39
0.00% covered (danger)
0.00%
0 / 1
11.82
 check_status
27.03% covered (danger)
27.03%
10 / 37
0.00% covered (danger)
0.00%
0 / 1
26.04
1<?php
2/**
3 * VideoPress Uploader
4 *
5 * @package automattic/jetpack-videopress
6 */
7
8namespace Automattic\Jetpack\VideoPress;
9
10use Jetpack_Options;
11use VideoPressUploader\File_Exception;
12use VideoPressUploader\Tus_Client;
13
14/**
15 * VideoPress Uploader class
16 *
17 * Handles the upload from the Media Library to VideoPress servers
18 */
19class Uploader {
20
21    /**
22     * The key of the post meta that holds the ID of the attachment that holds the VideoPress video, in case this attachment was uploaded before.
23     *
24     * @var string
25     */
26    const UPLOADED_KEY = '_videopress_uploaded_id';
27
28    /**
29     * The chunk size of each upload step
30     *
31     * @var int
32     */
33    const CHUNK_SIZE = 5000000;
34
35    /**
36     * The Tus Client instance
37     *
38     * @var Tus_Client
39     */
40    protected $client = null;
41
42    /**
43     * The attachment ID
44     *
45     * @var int
46     */
47    protected $attachment_id;
48
49    /**
50     * Explicit upload entry surface, when known.
51     *
52     * @var string|null
53     */
54    protected $source;
55
56    /**
57     * Checks whether this feature is supported by the server
58     *
59     * @param int $attachment_id The ID of the video attachment we want to upload to VideoPress.
60     * @return boolean
61     */
62    public static function is_valid_attachment_id( $attachment_id ) {
63        $file_path = get_attached_file( $attachment_id );
64        if ( ! $file_path || ! is_readable( $file_path ) ) {
65            return false;
66        }
67        if ( ! str_starts_with( get_post_mime_type( $attachment_id ), 'video/' ) ) {
68            return false;
69        }
70        return true;
71    }
72
73    /**
74     * Constructs the object
75     *
76     * @throws Upload_Exception If attachment is invalid or server does not support it.
77     * @param int         $attachment_id The ID of the video attachment we want to upload to VideoPress.
78     * @param string|null $source        Explicit upload entry surface, when known.
79     */
80    public function __construct( $attachment_id, $source = null ) {
81        $this->attachment_id = $attachment_id;
82        $this->source        = in_array( $source, array( 'dashboard_promote', 'block_editor' ), true ) ? $source : null;
83        if ( ! $this->get_file_path() ) {
84            throw new Upload_Exception( __( 'Invalid attachment ID', 'jetpack-videopress-pkg' ), Upload_Exception::ERROR_INVALID_ATTACHMENT_ID );
85        }
86        if ( ! is_readable( $this->get_file_path() ) ) {
87            throw new Upload_Exception( __( 'File not found', 'jetpack-videopress-pkg' ), Upload_Exception::ERROR_FILE_NOT_FOUND );
88        }
89        if ( ! $this->file_has_supported_mime_type() ) {
90            throw new Upload_Exception( __( 'Mime type not supported', 'jetpack-videopress-pkg' ), Upload_Exception::ERROR_MIME_TYPE_NOT_SUPPORTED );
91        }
92    }
93
94    /**
95     * Gets the path of the video file
96     *
97     * @return string
98     */
99    public function get_file_path() {
100        return get_attached_file( $this->attachment_id );
101    }
102
103    /**
104     * Gets the mime type of the attachment
105     *
106     * @return string
107     */
108    public function get_file_mime_type() {
109        return get_post_mime_type( $this->attachment_id );
110    }
111
112    /**
113     * Gets the name of the video file
114     *
115     * @return string
116     */
117    public function get_file_name() {
118        return basename( $this->get_file_path() );
119    }
120
121    /**
122     * Gets the size of the video file
123     *
124     * @return int
125     */
126    public function get_file_size() {
127        return filesize( $this->get_file_path() );
128    }
129
130    /**
131     * Checks if the mime type of the attachment is supported to be uploaded
132     *
133     * @return boolean
134     */
135    public function file_has_supported_mime_type() {
136        return str_starts_with( $this->get_file_mime_type(), 'video/' );
137    }
138
139    /**
140     * Gets the VideoPress upload token
141     *
142     * @throws Upload_Exception If it fails to fetch the token.
143     *
144     * @return string
145     */
146    public function get_upload_token() {
147        return VideoPressToken::videopress_upload_jwt();
148    }
149
150    /**
151     * Gets a unique upload key for this attachment
152     *
153     * @return string
154     */
155    public function get_key() {
156        return sprintf( 's-%d-v-%d', Jetpack_Options::get_option( 'id' ), $this->attachment_id );
157    }
158
159    /**
160     * Sets the current attachment as uploaded and stores the ID of the VideoPress video attachment ID
161     *
162     * @param int $new_attachment_id The ID of the new attachment created to hold the VideoPress video.
163     * @return void
164     */
165    protected function mark_as_uploaded( $new_attachment_id ) {
166        update_post_meta( $this->attachment_id, self::UPLOADED_KEY, $new_attachment_id );
167    }
168
169    /**
170     * Sets the current attachment as not being uploaded before. Deletes the reference to the videopress attachment
171     *
172     * @return void
173     */
174    protected function unmark_as_uploaded() {
175        delete_post_meta( $this->attachment_id, self::UPLOADED_KEY );
176    }
177
178    /**
179     * Checks whether this attachment was uploaded before
180     *
181     * @return boolean
182     */
183    public function is_uploaded() {
184        return ! empty( $this->get_uploaded_attachment_id() );
185    }
186
187    /**
188     * Gets the ID of the VideoPress video attachment in case this attachment was uploaded before
189     *
190     * @return boolean|string False if value is absent. Post ID on success.
191     */
192    public function get_uploaded_attachment_id() {
193        return get_post_meta( $this->attachment_id, self::UPLOADED_KEY, true );
194    }
195
196    /**
197     * Retrieves the instance of the Tus_Client
198     *
199     * @return Tus_Client
200     */
201    public function get_client() {
202        if ( $this->client !== null ) {
203            return $this->client;
204        }
205
206        $this->client = new Tus_Client( $this->get_key(), $this->get_upload_token(), Jetpack_Options::get_option( 'id' ) );
207
208        return $this->client;
209    }
210
211    /**
212     * Uploads a chunk of the file
213     *
214     * @return array With the status of the upload
215     */
216    public function upload() {
217        if ( $this->is_uploaded() ) {
218            return $this->check_status();
219        }
220        try {
221            $attachment = get_post( $this->attachment_id );
222            $client     = $this->get_client();
223            $client->file( $this->get_file_path(), $this->get_file_name() );
224            $client->add_metadata( 'title', $attachment->post_title );
225            if ( null !== $this->source ) {
226                $client->add_metadata( 'source', $this->source );
227            }
228            if ( '' !== $attachment->post_content ) {
229                $client->add_metadata( 'description', $attachment->post_content );
230            }
231            if ( '' !== $attachment->post_excerpt ) {
232                $client->add_metadata( 'caption', $attachment->post_excerpt );
233            }
234
235            $bytes_uploaded = $client->upload( self::CHUNK_SIZE );
236
237            if ( $bytes_uploaded === $this->get_file_size() ) {
238                $this->mark_as_uploaded( $this->get_client()->get_uploaded_video_details()['media_id'] );
239                return array(
240                    'status'           => 'complete',
241                    'bytes_uploaded'   => $bytes_uploaded,
242                    'file_size'        => $this->get_file_size(),
243                    'file_name'        => $this->get_file_name(),
244                    'upload_key'       => $this->get_key(),
245                    'uploaded_details' => $this->get_client()->get_uploaded_video_details(),
246                );
247            }
248
249            return array(
250                'status'         => 'uploading',
251                'bytes_uploaded' => $bytes_uploaded,
252                'file_size'      => $this->get_file_size(),
253                'file_name'      => $this->get_file_name(),
254                'upload_key'     => $this->get_key(),
255            );
256        } catch ( \Exception $e ) {
257            return array(
258                'status'         => 'error',
259                'bytes_uploaded' => -1,
260                'file_size'      => $this->get_file_size(),
261                'file_name'      => $this->get_file_name(),
262                'upload_key'     => $this->get_key(),
263                'error'          => $e->getCode() . ': ' . $e->getMessage(),
264            );
265        }
266    }
267
268    /**
269     * Checks the status of the upload of this attachment
270     *
271     * @return array
272     */
273    public function check_status() {
274
275        if ( $this->is_uploaded() ) {
276            $uploaded_attachment_id = $this->get_uploaded_attachment_id();
277            $uploaded_video_guid    = get_post_meta( $uploaded_attachment_id, 'videopress_guid', true );
278            if ( $uploaded_video_guid ) {
279                return array(
280                    'status'              => 'uploaded',
281                    'upload_key'          => $this->get_key(),
282                    'uploaded_post_id'    => $uploaded_attachment_id,
283                    'uploaded_video_guid' => $uploaded_video_guid,
284                );
285            } else {
286                // VideoPress attachment is gone, allow user to upload it again.
287                $this->unmark_as_uploaded();
288            }
289        }
290
291        try {
292            $offset = $this->get_client()->get_offset();
293            $status = false !== $offset ? 'resume' : 'new';
294            $offset = false === $offset ? 0 : $offset;
295
296            return array(
297                'status'         => $status,
298                'bytes_uploaded' => $offset,
299                'file_size'      => $this->get_file_size(),
300                'file_name'      => $this->get_file_name(),
301                'upload_key'     => $this->get_key(),
302            );
303        } catch ( File_Exception $e ) {
304            return array(
305                'status'         => 'resume',
306                'bytes_uploaded' => 0,
307                'file_size'      => $this->get_file_size(),
308                'file_name'      => $this->get_file_name(),
309                'upload_key'     => $this->get_key(),
310            );
311        } catch ( \Exception $e ) {
312            return array(
313                'status'         => 'error',
314                'bytes_uploaded' => -1,
315                'file_size'      => $this->get_file_size(),
316                'file_name'      => $this->get_file_name(),
317                'message'        => $e->getMessage(),
318            );
319        }
320    }
321}