Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.46% covered (success)
97.46%
115 / 118
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Shortlinks_Abilities
99.14% covered (success)
99.14%
115 / 116
80.00% covered (warning)
80.00%
4 / 5
21
0.00% covered (danger)
0.00%
0 / 1
 get_category_slug
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_category_definition
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_abilities
100.00% covered (success)
100.00%
67 / 67
100.00% covered (success)
100.00%
1 / 1
1
 can_view_shortlinks
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_shortlinks
97.67% covered (success)
97.67%
42 / 43
0.00% covered (danger)
0.00%
0 / 1
17
1<?php
2/**
3 * Jetpack Shortlinks Abilities Registration
4 *
5 * Registers Jetpack WP.me Shortlinks abilities with the WordPress Abilities API.
6 *
7 * @package automattic/jetpack
8 */
9
10namespace Automattic\Jetpack\Plugin\Abilities;
11
12use Automattic\Jetpack\Connection\Manager as Connection_Manager;
13use Automattic\Jetpack\WP_Abilities\Registrar;
14
15if ( ! defined( 'ABSPATH' ) ) {
16    exit( 0 );
17}
18
19/**
20 * Registers Jetpack WP.me Shortlinks abilities with the WordPress Abilities API.
21 *
22 * Exposes a single read ability that returns wp.me shortlinks for the
23 * requested posts and (optionally) the site homepage so AI agents can fetch
24 * shareable URLs in bulk through the standard `wp-abilities/v1` REST surface.
25 */
26class Shortlinks_Abilities extends Registrar {
27
28    /**
29     * Maximum number of post IDs accepted per call.
30     */
31    const MAX_POST_IDS = 100;
32
33    /**
34     * Returns the category slug this registrar owns.
35     *
36     * @return string
37     */
38    public static function get_category_slug(): string {
39        return 'jetpack-shortlinks';
40    }
41
42    /**
43     * Returns the category definition passed to wp_register_ability_category().
44     *
45     * @return array
46     */
47    public static function get_category_definition(): array {
48        return array(
49            // "Jetpack" is a product name and should not be translated.
50            'label'       => 'Jetpack WP.me Shortlinks',
51            'description' => __( 'Abilities for retrieving wp.me shortlinks for posts, pages, attachments, and the site homepage.', 'jetpack' ),
52        );
53    }
54
55    /**
56     * Returns the ability specs this registrar owns as a [ slug => spec ] map.
57     *
58     * @return array<string, array>
59     */
60    public static function get_abilities(): array {
61        $item_schema = array(
62            'type'       => 'object',
63            'properties' => array(
64                'post_id'    => array(
65                    'type'        => 'integer',
66                    'description' => __( '0 for the site homepage entry; otherwise the WordPress post ID.', 'jetpack' ),
67                ),
68                'post_title' => array( 'type' => 'string' ),
69                'post_type'  => array(
70                    'type'        => 'string',
71                    'description' => __( '"blog" for the homepage entry; otherwise the WordPress post type slug (post, page, attachment, or a CPT that supports shortlinks).', 'jetpack' ),
72                ),
73                'shortlink'  => array(
74                    'type'        => 'string',
75                    'description' => __( 'The wp.me URL.', 'jetpack' ),
76                ),
77                'target_url' => array(
78                    'type'        => 'string',
79                    'description' => __( 'The canonical permalink the shortlink resolves to.', 'jetpack' ),
80                ),
81            ),
82        );
83
84        return array(
85            'jetpack-shortlinks/get-shortlinks' => array(
86                'label'               => __( 'Get wp.me shortlinks', 'jetpack' ),
87                'description'         => __( 'Return wp.me shortlinks for the given posts and (optionally) the site homepage. Each result is a { post_id, post_title, post_type, shortlink, target_url } object — post_id is 0 for the homepage entry, post_type is "blog". IDs that do not exist, are unreadable to the current user, or belong to a post type without shortlinks support are silently omitted (returns a shorter array, not WP_Error). Caps post_ids at 100 per call. Requires the WP.me Shortlinks Jetpack module (slug: shortlinks) to be active and the site to be connected; an unconnected site returns jetpack_shortlinks_blog_id_unavailable. Use jetpack/get-modules to verify the module is on, jetpack/set-module-status to activate it.', 'jetpack' ),
88                'input_schema'        => array(
89                    'type'                 => 'object',
90                    'default'              => array(),
91                    'properties'           => array(
92                        'post_ids'     => array(
93                            'type'        => 'array',
94                            'description' => __( 'Up to 100 post IDs to fetch shortlinks for. Order is preserved in the response. Defaults to an empty list.', 'jetpack' ),
95                            'maxItems'    => self::MAX_POST_IDS,
96                            'items'       => array(
97                                'type'    => 'integer',
98                                'minimum' => 1,
99                            ),
100                        ),
101                        'include_blog' => array(
102                            'type'        => 'boolean',
103                            'description' => __( 'When true, prepend a single entry for the site homepage (post_id 0, post_type "blog") to the response.', 'jetpack' ),
104                            'default'     => false,
105                        ),
106                    ),
107                    'additionalProperties' => false,
108                ),
109                'output_schema'       => array(
110                    'type'  => 'array',
111                    'items' => $item_schema,
112                ),
113                'execute_callback'    => array( __CLASS__, 'get_shortlinks' ),
114                'permission_callback' => array( __CLASS__, 'can_view_shortlinks' ),
115                'meta'                => array(
116                    'annotations'  => array(
117                        'readonly'    => true,
118                        'destructive' => false,
119                        'idempotent'  => true,
120                    ),
121                    'show_in_rest' => true,
122                    'mcp'          => array(
123                        'public' => true,
124                        'type'   => 'tool', // default is already "tool", but can be explicit.
125                    ),
126                ),
127            ),
128        );
129    }
130
131    /**
132     * Permission check: require edit_posts so only users with content-management
133     * rights may enumerate shortlinks. Per-post read capability is still
134     * enforced inside the execute callback so unreadable IDs are silently
135     * omitted rather than failing the whole call.
136     */
137    public static function can_view_shortlinks(): bool {
138        return current_user_can( 'edit_posts' );
139    }
140
141    /**
142     * Execute: return wp.me shortlinks for the requested posts and (optionally) the homepage.
143     *
144     * @param array|null $input Input matching the ability's input_schema.
145     * @return array|\WP_Error
146     */
147    public static function get_shortlinks( $input = null ) {
148        $input = is_array( $input ) ? $input : array();
149
150        if ( null === Connection_Manager::get_site_id( true ) ) {
151            return new \WP_Error(
152                'jetpack_shortlinks_blog_id_unavailable',
153                __( 'No Jetpack/WordPress.com blog ID is available for this site. Connect Jetpack before requesting wp.me shortlinks.', 'jetpack' )
154            );
155        }
156
157        $post_ids = array();
158        if ( isset( $input['post_ids'] ) && is_array( $input['post_ids'] ) ) {
159            foreach ( $input['post_ids'] as $candidate ) {
160                $candidate_id = absint( $candidate );
161                if ( $candidate_id > 0 ) {
162                    $post_ids[] = $candidate_id;
163                }
164            }
165        }
166        if ( count( $post_ids ) > self::MAX_POST_IDS ) {
167            $post_ids = array_slice( $post_ids, 0, self::MAX_POST_IDS );
168        }
169
170        $include_blog = ! empty( $input['include_blog'] );
171
172        $out = array();
173
174        if ( $include_blog ) {
175            $blog_shortlink = wp_get_shortlink( 0, 'blog' );
176            if ( is_string( $blog_shortlink ) && '' !== $blog_shortlink ) {
177                $out[] = array(
178                    'post_id'    => 0,
179                    'post_title' => (string) get_bloginfo( 'name' ),
180                    'post_type'  => 'blog',
181                    'shortlink'  => $blog_shortlink,
182                    'target_url' => home_url( '/' ),
183                );
184            }
185        }
186
187        if ( ! empty( $post_ids ) ) {
188            _prime_post_caches( $post_ids, false, false );
189        }
190
191        foreach ( $post_ids as $post_id ) {
192            $post = get_post( $post_id );
193            if ( ! $post || ! current_user_can( 'read_post', $post_id ) ) {
194                continue;
195            }
196            $shortlink = wp_get_shortlink( $post_id, 'post' );
197            if ( ! is_string( $shortlink ) || '' === $shortlink ) {
198                continue;
199            }
200            $out[] = array(
201                'post_id'    => (int) $post->ID,
202                'post_title' => (string) $post->post_title,
203                'post_type'  => (string) $post->post_type,
204                'shortlink'  => $shortlink,
205                'target_url' => (string) get_permalink( $post ),
206            );
207        }
208
209        return $out;
210    }
211}