Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.25% covered (success)
98.25%
56 / 57
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
Posts_To_Podcast_Helper
98.25% covered (success)
98.25%
56 / 57
66.67% covered (warning)
66.67%
2 / 3
13
0.00% covered (danger)
0.00%
0 / 1
 is_enabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_status_permission_check
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
 get_episodes
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
1 / 1
10
1<?php
2/**
3 * Helper for the Posts to Podcast feature.
4 *
5 * @package automattic/jetpack-podcast
6 */
7
8namespace Automattic\Jetpack\Podcast;
9
10/**
11 * Gating, permission, and query helpers for the Posts to Podcast feature.
12 */
13class Posts_To_Podcast_Helper {
14
15    /**
16     * Post meta key the generation pipeline writes on each post it creates.
17     */
18    const EPISODE_META_KEY = 'posts_to_podcast_metadata';
19
20    /**
21     * Whether the Posts to Podcast feature is active for the current request.
22     *
23     * @return bool
24     */
25    public static function is_enabled() {
26        /**
27         * Filter to allow disabling the Posts to Podcast feature on a per-site basis.
28         * Defaults to true wherever the podcast package is active; flip this to false
29         * to hide the section during a staged rollout without disabling the package.
30         *
31         * @since 0.1.0
32         *
33         * @param bool $enabled Whether the feature is enabled. Default true.
34         */
35        return (bool) apply_filters( 'jetpack_posts_to_podcast_is_enabled', true );
36    }
37
38    /**
39     * Permission callback for the local proxy REST endpoint.
40     *
41     * @param \WP_REST_Request $request Full details about the request.
42     *
43     * @return true|\WP_Error
44     */
45    public static function get_status_permission_check( $request ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
46        if ( ! current_user_can( 'edit_posts' ) ) {
47            return new \WP_Error(
48                'rest_forbidden',
49                __( 'Sorry, you are not allowed to use this feature on this site.', 'jetpack-podcast' ),
50                array( 'status' => rest_authorization_required_code() )
51            );
52        }
53
54        return true;
55    }
56
57    /**
58     * List the posts this feature generated, newest first, as the paginated
59     * envelope both the Create AI podcast page and its REST route render.
60     *
61     * `perm => editable` makes the list follow the wp-admin Posts screen: users
62     * without `edit_others_posts` only see the episodes they authored.
63     *
64     * @param int $page     1-based page number.
65     * @param int $per_page Items per page, clamped to 1..50.
66     *
67     * @return array{items: array, total: int, page: int, perPage: int, totalPages: int}
68     */
69    public static function get_episodes( $page, $per_page ) {
70        $page     = max( 1, (int) $page );
71        $per_page = max( 1, min( 50, (int) $per_page ) );
72
73        $query = new \WP_Query(
74            array(
75                'post_type'              => 'post',
76                'post_status'            => array( 'draft', 'publish' ),
77                'perm'                   => 'editable',
78                'posts_per_page'         => $per_page,
79                'paged'                  => $page,
80                'orderby'                => 'date',
81                'order'                  => 'DESC',
82                'update_post_term_cache' => false,
83                'meta_query'             => array(
84                    array(
85                        'key'     => self::EPISODE_META_KEY,
86                        'compare' => 'EXISTS',
87                    ),
88                ),
89            )
90        );
91
92        $items = array();
93        foreach ( $query->posts as $post ) {
94            $raw_meta = get_post_meta( $post->ID, self::EPISODE_META_KEY, true );
95            $meta     = is_string( $raw_meta ) ? json_decode( $raw_meta, true ) : null;
96            $audio    = ( is_array( $meta ) && isset( $meta['audio'] ) && is_array( $meta['audio'] ) ) ? $meta['audio'] : array();
97            $title    = wp_strip_all_tags(
98                html_entity_decode( (string) get_the_title( $post ), ENT_QUOTES | ENT_HTML5, 'UTF-8' )
99            );
100            if ( '' === trim( $title ) ) {
101                // translators: Fallback shown in the Generated podcasts list when a draft has an empty title.
102                $title = __( '(no title)', 'jetpack-podcast' );
103            }
104
105            $items[] = array(
106                'id'        => $post->ID,
107                'title'     => $title,
108                'status'    => $post->post_status,
109                'date'      => mysql2date( 'c', $post->post_date_gmt, false ),
110                'editUrl'   => get_edit_post_link( $post->ID, 'raw' ),
111                'mediaUrl'  => isset( $audio['url'] ) ? esc_url_raw( (string) $audio['url'] ) : '',
112                'mediaType' => 'audio',
113                'mediaMime' => isset( $audio['mimeType'] ) ? (string) $audio['mimeType'] : '',
114                'duration'  => isset( $audio['durationSeconds'] ) ? (int) round( (float) $audio['durationSeconds'] ) : 0,
115            );
116        }
117
118        $total = (int) $query->found_posts;
119
120        return array(
121            'items'      => $items,
122            'total'      => $total,
123            'page'       => $page,
124            'perPage'    => $per_page,
125            'totalPages' => (int) ceil( $total / $per_page ),
126        );
127    }
128}