Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
8.42% covered (danger)
8.42%
8 / 95
25.00% covered (danger)
25.00%
4 / 16
CRAP
0.00% covered (danger)
0.00%
0 / 1
Uploader
8.42% covered (danger)
8.42%
8 / 95
25.00% covered (danger)
25.00%
4 / 16
818.48
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
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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_uploaded_attachment_id
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_client
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 upload
0.00% covered (danger)
0.00%
0 / 30
0.00% covered (danger)
0.00%
0 / 1
20
 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            $this->get_client()->file( $this->get_file_path(), $this->get_file_name() );
213
214            $bytes_uploaded = $this->get_client()->upload( self::CHUNK_SIZE );
215
216            if ( $bytes_uploaded === $this->get_file_size() ) {
217                $this->mark_as_uploaded( $this->get_client()->get_uploaded_video_details()['media_id'] );
218                return array(
219                    'status'           => 'complete',
220                    'bytes_uploaded'   => $bytes_uploaded,
221                    'file_size'        => $this->get_file_size(),
222                    'file_name'        => $this->get_file_name(),
223                    'upload_key'       => $this->get_key(),
224                    'uploaded_details' => $this->get_client()->get_uploaded_video_details(),
225                );
226            }
227
228            return array(
229                'status'         => 'uploading',
230                'bytes_uploaded' => $bytes_uploaded,
231                'file_size'      => $this->get_file_size(),
232                'file_name'      => $this->get_file_name(),
233                'upload_key'     => $this->get_key(),
234            );
235        } catch ( \Exception $e ) {
236            return array(
237                'status'         => 'error',
238                'bytes_uploaded' => -1,
239                'file_size'      => $this->get_file_size(),
240                'file_name'      => $this->get_file_name(),
241                'upload_key'     => $this->get_key(),
242                'error'          => $e->getCode() . ': ' . $e->getMessage(),
243            );
244        }
245    }
246
247    /**
248     * Checks the status of the upload of this attachment
249     *
250     * @return array
251     */
252    public function check_status() {
253
254        if ( $this->is_uploaded() ) {
255            $uploaded_attachment_id = $this->get_uploaded_attachment_id();
256            $uploaded_video_guid    = get_post_meta( $uploaded_attachment_id, 'videopress_guid', true );
257            if ( $uploaded_video_guid ) {
258                return array(
259                    'status'              => 'uploaded',
260                    'upload_key'          => $this->get_key(),
261                    'uploaded_post_id'    => $uploaded_attachment_id,
262                    'uploaded_video_guid' => $uploaded_video_guid,
263                );
264            } else {
265                // VideoPress attachment is gone, allow user to upload it again.
266                $this->unmark_as_uploaded();
267            }
268        }
269
270        try {
271            $offset = $this->get_client()->get_offset();
272            $status = false !== $offset ? 'resume' : 'new';
273            $offset = false === $offset ? 0 : $offset;
274
275            return array(
276                'status'         => $status,
277                'bytes_uploaded' => $offset,
278                'file_size'      => $this->get_file_size(),
279                'file_name'      => $this->get_file_name(),
280                'upload_key'     => $this->get_key(),
281            );
282        } catch ( File_Exception $e ) {
283            return array(
284                'status'         => 'resume',
285                'bytes_uploaded' => 0,
286                'file_size'      => $this->get_file_size(),
287                'file_name'      => $this->get_file_name(),
288                'upload_key'     => $this->get_key(),
289            );
290        } catch ( \Exception $e ) {
291            return array(
292                'status'         => 'error',
293                'bytes_uploaded' => -1,
294                'file_size'      => $this->get_file_size(),
295                'file_name'      => $this->get_file_name(),
296                'message'        => $e->getMessage(),
297            );
298        }
299    }
300}