Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
58.02% covered (warning)
58.02%
76 / 131
47.06% covered (danger)
47.06%
8 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Attachment_Handler
58.02% covered (warning)
58.02%
76 / 131
47.06% covered (danger)
47.06%
8 / 17
260.89
0.00% covered (danger)
0.00%
0 / 1
 init
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
3.00
 maybe_get_attached_url_for_videopress
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 add_video_upload_mimes
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 filter_video_mimes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 delete_video_wpcom
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
42
 delete_video_poster_attachment
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 wp_mime_type_icon
66.67% covered (warning)
66.67%
4 / 6
0.00% covered (danger)
0.00%
0 / 1
4.59
 add_videopress_extenstion
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 prepare_attachment_for_js
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 conversion_failure_field
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 ajax_query_attachments_args
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 media_list_table_query
21.43% covered (danger)
21.43%
3 / 14
0.00% covered (danger)
0.00%
0 / 1
30.77
 disable_delete_if_disconnected
57.14% covered (warning)
57.14%
4 / 7
0.00% covered (danger)
0.00%
0 / 1
8.83
 enqueue_media_library_poll
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 enqueue_media_library_styles
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 heartbeat_settings
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 heartbeat_received
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
7
1<?php
2/**
3 * VideoPress Attachment_Handler
4 *
5 * @package automattic/jetpack-videopress
6 */
7
8namespace Automattic\Jetpack\VideoPress;
9
10use Automattic\Jetpack\Connection\Client;
11use Automattic\Jetpack\Current_Plan;
12use WP_Error;
13use WP_Post;
14
15/**
16 * VideoPress Attachment_Handler class.
17 */
18class Attachment_Handler {
19
20    /**
21     * Initializer
22     *
23     * This method should be called only once by the Initializer class. Do not call this method again.
24     */
25    public static function init() {
26
27        if ( ! Status::is_active() ) {
28            return;
29        }
30
31        add_filter( 'wp_get_attachment_url', array( __CLASS__, 'maybe_get_attached_url_for_videopress' ), 10, 2 );
32        add_filter( 'get_attached_file', array( __CLASS__, 'maybe_get_attached_url_for_videopress' ), 10, 2 );
33
34        if ( Current_Plan::supports( 'videopress' ) ) {
35            add_filter( 'upload_mimes', array( __CLASS__, 'add_video_upload_mimes' ), 999 );
36        }
37
38        add_filter( 'pre_delete_attachment', array( __CLASS__, 'delete_video_wpcom' ), 10, 2 );
39        add_filter( 'wp_mime_type_icon', array( __CLASS__, 'wp_mime_type_icon' ), 10, 3 );
40        add_filter( 'wp_video_extensions', array( __CLASS__, 'add_videopress_extenstion' ) );
41
42        add_filter( 'attachment_fields_to_edit', array( __CLASS__, 'conversion_failure_field' ), 10, 2 );
43        add_filter( 'wp_prepare_attachment_for_js', array( __CLASS__, 'prepare_attachment_for_js' ) );
44        add_filter( 'ajax_query_attachments_args', array( __CLASS__, 'ajax_query_attachments_args' ) );
45        add_action( 'pre_get_posts', array( __CLASS__, 'media_list_table_query' ) );
46
47        add_filter( 'user_has_cap', array( __CLASS__, 'disable_delete_if_disconnected' ), 10, 3 );
48
49        add_action( 'admin_print_scripts-upload.php', array( __CLASS__, 'enqueue_media_library_poll' ) );
50        add_action( 'admin_print_styles-upload.php', array( __CLASS__, 'enqueue_media_library_styles' ) );
51        add_filter( 'heartbeat_received', array( __CLASS__, 'heartbeat_received' ), 10, 2 );
52    }
53
54    /**
55     * Returns the VideoPress URL for the give post id, otherwise returns the provided default.
56     *
57     * This is an attachment-based filter handler.
58     *
59     * @param string $default The default return value if post id is not a VideoPress video.
60     * @param int    $post_id The post id for the current attachment.
61     */
62    public static function maybe_get_attached_url_for_videopress( $default, $post_id ) {
63        $videopress_url = videopress_get_attachment_url( $post_id );
64
65        if ( null !== $videopress_url ) {
66            return $videopress_url;
67        }
68
69        return $default;
70    }
71
72    /**
73     * Makes sure that all video mimes are added in, as multi site installs can remove them.
74     *
75     * @param array $existing_mimes Mime types to extend/filter.
76     * @return array
77     */
78    public static function add_video_upload_mimes( $existing_mimes = array() ) {
79        $mime_types  = wp_get_mime_types();
80        $video_types = array_filter( $mime_types, array( __CLASS__, 'filter_video_mimes' ) );
81
82        foreach ( $video_types as $key => $value ) {
83            $existing_mimes[ $key ] = $value;
84        }
85
86        // Make sure that videopress mimes are considered videos.
87        $existing_mimes['videopress'] = 'video/videopress';
88
89        return $existing_mimes;
90    }
91
92    /**
93     * Filter designed to get rid of non video mime types.
94     *
95     * @param string $value Mime type to filter.
96     * @return int
97     */
98    public static function filter_video_mimes( $value ) {
99        return preg_match( '@^video/@', $value );
100    }
101
102    /**
103     * Attempts to delete a VideoPress video from wp.com.
104     * Will block the deletion from continuing if certain errors return from the wp.com API.
105     *
106     * @param Boolean $delete if the deletion should occur or not (unused).
107     * @param WP_Post $post the post object.
108     *
109     * @return null|WP_Error|Boolean null if deletion should continue.
110     */
111    public static function delete_video_wpcom( $delete, $post ) {
112        if ( ! is_videopress_attachment( $post->ID ) ) {
113            return null;
114        }
115
116        $guid = get_post_meta( $post->ID, 'videopress_guid', true );
117        if ( empty( $guid ) ) {
118            self::delete_video_poster_attachment( $post->ID );
119            return null;
120        }
121
122        // Phone home and have wp.com delete the VideoPress entry and files.
123        $wpcom_response = Client::wpcom_json_api_request_as_blog(
124            sprintf( '/videos/%s/delete', $guid ),
125            '1.1',
126            array( 'method' => 'POST' ),
127            array( 'user_id' => get_current_user_id() )
128        );
129
130        if ( is_wp_error( $wpcom_response ) ) {
131            return $wpcom_response;
132        }
133
134        // Upon success or a 404 (video already deleted on wp.com), return null to allow the deletion to continue.
135        if ( 200 === $wpcom_response['response']['code'] || 404 === $wpcom_response['response']['code'] ) {
136            self::delete_video_poster_attachment( $post->ID );
137            return null;
138        }
139
140        // Otherwise we stop the deletion from proceeding.
141        return false;
142    }
143
144    /**
145     * Deletes a video poster attachment if it exists.
146     *
147     * @param int $attachment_id the WP attachment id.
148     */
149    private static function delete_video_poster_attachment( $attachment_id ) {
150        $thumbnail_id = get_post_meta( $attachment_id, '_thumbnail_id', true );
151        if ( ! empty( $thumbnail_id ) ) {
152            // Let's ensure this is a VP poster image before we delete it.
153            if ( '1' === get_post_meta( $thumbnail_id, 'videopress_poster_image', true ) ) {
154                // This call triggers the `delete_video_wpcom` filter again but it bails early at the is_videopress_attachment() check.
155                wp_delete_attachment( $thumbnail_id );
156            }
157        }
158    }
159
160    /**
161     * Filter the mime type icon.
162     *
163     * @param string $icon Icon path.
164     * @param string $mime Mime type.
165     * @param int    $post_id Post ID.
166     *
167     * @return string
168     */
169    public static function wp_mime_type_icon( $icon, $mime, $post_id ) {
170
171        if ( $mime !== 'video/videopress' ) {
172            return $icon;
173        }
174
175        $status = get_post_meta( $post_id, 'videopress_status', true );
176
177        if ( $status === 'complete' || $status === 'failed' ) {
178            return $icon;
179        }
180
181        return 'https://wordpress.com/wp-content/mu-plugins/videopress/images/media-video-processing-icon.png';
182    }
183
184    /**
185     * Filter the list of supported video formats.
186     *
187     * @param array $extensions Supported video formats.
188     *
189     * @return array
190     */
191    public static function add_videopress_extenstion( $extensions ) {
192        $extensions[] = 'videopress';
193        return $extensions;
194    }
195
196    /**
197     * Make sure that any Video that has a VideoPress GUID passes that data back.
198     *
199     * @param array $post Attachment data array.
200     * @return array
201     */
202    public static function prepare_attachment_for_js( $post ) {
203        if ( 'video' === $post['type'] ) {
204            $guid = get_post_meta( $post['id'], 'videopress_guid', true );
205            if ( $guid ) {
206                $post['videopress_guid'] = $guid;
207            }
208            $status = get_post_meta( $post['id'], 'videopress_status', true );
209            if ( $status ) {
210                $post['videopress_status'] = $status;
211            }
212        }
213        return $post;
214    }
215
216    /**
217     * Explain a terminal conversion failure in the Media Library details.
218     *
219     * @since $$next-version$$
220     *
221     * @param array   $fields Attachment fields.
222     * @param WP_Post $post Attachment.
223     * @return array
224     */
225    public static function conversion_failure_field( $fields, $post ) {
226        if ( 'failed' === get_post_meta( $post->ID, 'videopress_status', true ) ) {
227            $fields['videopress_conversion_error'] = array(
228                'label' => __( 'VideoPress', 'jetpack-videopress-pkg' ),
229                'input' => 'html',
230                'html'  => esc_html__( 'Conversion failed. Delete this video and upload it again.', 'jetpack-videopress-pkg' ),
231            );
232        }
233        return $fields;
234    }
235
236    /**
237     * Media Grid:
238     * Filter out any videopress video posters that we've downloaded,
239     * so that they don't seem to display twice.
240     *
241     * @param array $args Query variables.
242     */
243    public static function ajax_query_attachments_args( $args ) {
244        $meta_query = array(
245            array(
246                'key'     => 'videopress_poster_image',
247                'compare' => 'NOT EXISTS',
248            ),
249        );
250
251        // If there was already a meta query, let's AND it via
252        // nesting it with our new one. No need to specify the
253        // relation, as it defaults to AND.
254        if ( ! empty( $args['meta_query'] ) ) {
255            $meta_query[] = $args['meta_query'];
256        }
257        $args['meta_query'] = $meta_query;
258
259        return $args;
260    }
261
262    /**
263     * Media List:
264     * Do the same as `videopress_ajax_query_attachments_args()` but for the list view.
265     *
266     * @param array $query WP_Query instance.
267     */
268    public static function media_list_table_query( $query ) {
269
270        if (
271            ! function_exists( 'get_current_screen' )
272            || get_current_screen() === null
273        ) {
274            return;
275        }
276
277        if ( is_admin() && $query->is_main_query() && ( 'upload' === get_current_screen()->id ) ) {
278            $meta_query = array(
279                array(
280                    'key'     => 'videopress_poster_image',
281                    'compare' => 'NOT EXISTS',
282                ),
283            );
284
285            $old_meta_query = $query->get( 'meta_query' );
286            if ( $old_meta_query ) {
287                $meta_query[] = $old_meta_query;
288            }
289
290            $query->set( 'meta_query', $meta_query );
291        }
292    }
293
294    /**
295     * Filter to disable the `delete_post` capability
296     * for VideoPress attachments if the current user is
297     * not connected.
298     *
299     * @param array $allcaps All the capabilities of the user.
300     * @param array $cap     [0] Required capability.
301     * @param array $args    [0] Requested capability.
302     *                       [1] User ID.
303     *                       [2] Associated object ID.
304     * @return array the filtered array of capabilities.
305     */
306    public static function disable_delete_if_disconnected( $allcaps, $cap, $args ) {
307
308        // Only apply this filter to `delete_post` checks
309        if ( ! isset( $args[0] ) || 'delete_post' !== $args[0] ) {
310            return $allcaps;
311        }
312
313        // Only apply this filter to VideoPress attachments
314        if ( ! isset( $args[2] ) || ! is_videopress_attachment( $args[2] ) ) {
315            return $allcaps;
316        }
317
318        // Set the capability to false if the user can't perform the actions
319        if ( ! Data::can_perform_action() ) {
320            $allcaps[ $cap[0] ] = false;
321        }
322
323        return $allcaps;
324    }
325
326    /**
327     * Enqueues the script that hooks into WP Heartbeat to refresh
328     * processing VideoPress video data in the media library grid.
329     *
330     * @since 0.35.4
331     *
332     * @return void
333     */
334    public static function enqueue_media_library_poll() {
335        wp_enqueue_script(
336            'videopress-media-library-poll',
337            plugins_url( 'js/media-library-poll.js', __FILE__ ),
338            array( 'jquery', 'heartbeat', 'media-grid' ),
339            Package_Version::PACKAGE_VERSION,
340            true
341        );
342
343        add_filter( 'heartbeat_settings', array( __CLASS__, 'heartbeat_settings' ) );
344    }
345
346    /**
347     * Adds inline styles for the media library grid on the upload page.
348     *
349     * @since 0.35.4
350     *
351     * @return void
352     */
353    public static function enqueue_media_library_styles() {
354        // Constrain poster images so they fit within the media library grid cell.
355        wp_add_inline_style(
356            'media-views',
357            '.attachment-preview.type-video .thumbnail .centered img.thumbnail { max-width: 100%; max-height: 100%; }'
358        );
359    }
360
361    /**
362     * Lowers the Heartbeat minimum interval on the media library page
363     * so the polling script can request a faster tick rate.
364     *
365     * @since 0.35.4
366     *
367     * @param array $settings Heartbeat settings.
368     * @return array
369     */
370    public static function heartbeat_settings( $settings ) {
371        $settings['minimalInterval'] = 5;
372
373        return $settings;
374    }
375
376    /**
377     * Heartbeat API handler that checks the processing status of VideoPress videos.
378     *
379     * @since 0.35.4
380     *
381     * @param array $response The Heartbeat response.
382     * @param array $data     The data sent with the Heartbeat request.
383     * @return array
384     */
385    public static function heartbeat_received( $response, $data ) {
386        if ( empty( $data['videopress_processing_ids'] ) || ! is_array( $data['videopress_processing_ids'] ) ) {
387            return $response;
388        }
389
390        $statuses = array();
391        foreach ( $data['videopress_processing_ids'] as $id ) {
392            $id = (int) $id;
393
394            if ( ! current_user_can( 'edit_post', $id ) ) {
395                continue;
396            }
397
398            $status = get_post_meta( $id, 'videopress_status', true );
399            if ( $status ) {
400                $statuses[ $id ] = $status;
401            }
402        }
403
404        if ( ! empty( $statuses ) ) {
405            $response['videopress_processing_status'] = $statuses;
406        }
407
408        return $response;
409    }
410}