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