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