function ( $post ) { $id = (int) $post['id']; $image_id = (int) get_post_meta( $id, '_streamsera_logo_id', true ); if ( ! $image_id ) { $image_id = (int) get_post_thumbnail_id( $id ); } $url = $image_id ? wp_get_attachment_image_url( $image_id, 'thumbnail' ) : ''; /** * Filter the listing thumbnail for a post. * * @param string $url Image URL ('' when none). * @param int $id Post ID. */ return (string) apply_filters( 'streamsera_list_image', $url ? $url : '', $id ); }, 'schema' => array( 'type' => 'string', 'context' => array( 'view', 'edit' ), ), ) ); } } /** * Supported object types → post types. * * @return array */ public static function types() { /** * Filter the object types the bulk endpoint accepts (slug ⇒ post type). * * @param array $types Types. */ return apply_filters( 'streamsera_bulk_types', array( 'station' => 'streamsera_station', 'player' => 'streamsera_player', ) ); } /** * Routes. */ public function routes() { register_rest_route( 'streamsera/v1', '/bulk', array( 'methods' => 'POST', 'callback' => array( $this, 'handle' ), // The lists are open to everyone who can edit posts; every item is checked below // (edit_post / delete_post, and the post type's publish capability for "publish"). 'permission_callback' => array( $this, 'can_bulk' ), 'args' => array( 'type' => array( 'required' => true, 'type' => 'string', 'sanitize_callback' => 'sanitize_key', 'validate_callback' => 'rest_validate_request_arg', ), 'action' => array( 'required' => true, 'type' => 'string', 'enum' => array( 'delete', 'publish', 'draft', 'duplicate' ), ), 'ids' => array( 'required' => true, 'type' => 'array', 'maxItems' => 200, 'items' => array( 'type' => 'integer', 'minimum' => 1, ), ), ), ) ); } /** * REST permission: use the list bulk actions (anyone who can edit posts; every item is then * checked against edit_post / delete_post and the post type's publish capability). * * @return bool */ public function can_bulk() { /** This filter is documented in includes/Admin/Menu.php */ return current_user_can( apply_filters( 'streamsera_required_cap', 'edit_posts', 'bulk' ) ); } /** * Copy a post with all its meta (and taxonomy terms) as a new draft. * * @param \WP_Post $post Source. * @return int New post ID or 0. */ public static function duplicate( $post ) { if ( ! current_user_can( 'edit_post', $post->ID ) ) { return 0; } // The post API unslashes its input: slash the copied values so backslashes survive. $new_id = wp_insert_post( wp_slash( array( 'post_type' => $post->post_type, 'post_status' => 'draft', /* translators: %s: original title */ 'post_title' => sprintf( __( '%s (copy)', 'streamsera' ), $post->post_title ), 'post_content' => $post->post_content, 'post_excerpt' => $post->post_excerpt, 'post_author' => get_current_user_id(), ) ), true ); if ( is_wp_error( $new_id ) || ! $new_id ) { return 0; } foreach ( array_keys( (array) get_post_meta( $post->ID ) ) as $key ) { if ( in_array( $key, array( '_edit_lock', '_edit_last' ), true ) ) { continue; } // get_post_meta() returns the stored values already unserialized. foreach ( (array) get_post_meta( $post->ID, $key, false ) as $value ) { add_post_meta( $new_id, $key, wp_slash( $value ) ); } } foreach ( get_object_taxonomies( $post->post_type ) as $tax ) { $terms = wp_get_object_terms( $post->ID, $tax, array( 'fields' => 'ids' ) ); if ( ! is_wp_error( $terms ) && $terms ) { wp_set_object_terms( $new_id, $terms, $tax ); } } /** * Fires after a post was duplicated from the admin lists. * * @param int $new_id New post. * @param \WP_Post $post Source. */ do_action( 'streamsera_post_duplicated', $new_id, $post ); return (int) $new_id; } /** * Apply the action. * * @param WP_REST_Request $request Request. * @return WP_REST_Response|WP_Error */ public function handle( WP_REST_Request $request ) { $types = self::types(); $type = sanitize_key( $request['type'] ); $action = sanitize_key( $request['action'] ); $ids = array_filter( array_map( 'absint', (array) $request['ids'] ) ); if ( empty( $types[ $type ] ) ) { return new WP_Error( 'streamsera_bulk_type', __( 'Unknown object type.', 'streamsera' ), array( 'status' => 400 ) ); } if ( ! in_array( $action, array( 'delete', 'publish', 'draft', 'duplicate' ), true ) ) { return new WP_Error( 'streamsera_bulk_action', __( 'Unknown bulk action.', 'streamsera' ), array( 'status' => 400 ) ); } if ( empty( $ids ) || count( $ids ) > 200 ) { return new WP_Error( 'streamsera_bulk_ids', __( 'Select between 1 and 200 items.', 'streamsera' ), array( 'status' => 400 ) ); } $post_type = $types[ $type ]; $done = array(); $skipped = array(); foreach ( $ids as $id ) { $post = get_post( $id ); if ( ! $post || $post->post_type !== $post_type ) { $skipped[] = $id; continue; } $cap = 'delete' === $action ? 'delete_post' : 'edit_post'; if ( ! current_user_can( $cap, $id ) ) { $skipped[] = $id; continue; } $type_object = get_post_type_object( $post_type ); if ( 'publish' === $action && ( ! $type_object || ! current_user_can( $type_object->cap->publish_posts ) ) ) { // Contributors may edit their drafts but not publish them. $skipped[] = $id; continue; } if ( 'duplicate' === $action && ( ! $type_object || ! current_user_can( $type_object->cap->create_posts ) ) ) { $skipped[] = $id; continue; } if ( 'delete' === $action ) { $ok = (bool) wp_delete_post( $id, true ); } elseif ( 'duplicate' === $action ) { $ok = (bool) self::duplicate( $post ); } elseif ( 'publish' === $action && \StreamSera\PostTypes\Station::POST_TYPE === $post->post_type && ! \StreamSera\PostTypes\Station::has_streams( $id ) ) { // Same rule as the editor: a station can't go live without a stream URL. $ok = false; } else { $ok = ! is_wp_error( wp_update_post( array( 'ID' => $id, 'post_status' => $action, ), true ) ); } if ( $ok ) { $done[] = $id; } else { $skipped[] = $id; } } /** * Fires after a bulk action ran. * * @param string $type Object type slug. * @param string $action delete|publish|draft. * @param int[] $done IDs changed. */ do_action( 'streamsera_bulk_applied', $type, $action, $done ); return new WP_REST_Response( array( 'ok' => true, 'done' => $done, 'skipped' => $skipped, ) ); } }