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