Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
15.83% covered (danger)
15.83%
19 / 120
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
Note
15.83% covered (danger)
15.83%
19 / 120
11.11% covered (danger)
11.11%
1 / 9
566.62
0.00% covered (danger)
0.00%
0 / 1
 enabled
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
9
 init
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 admin_init_actions
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 add_filters_and_actions_for_screen
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 set_empty_title
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 register_cpt
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 restrict_blocks_for_social_note
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 maybe_flush_rewrite_rules
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 override_empty_title
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2/**
3 * Register the Social note custom post type.
4 *
5 * @package automattic/jetpack-social-plugin
6 */
7
8namespace Automattic\Jetpack\Social;
9
10use Automattic\Jetpack\Constants;
11
12/**
13 * Register the Jetpack Social Note custom post type.
14 */
15class Note {
16    const JETPACK_SOCIAL_NOTE_CPT     = 'jetpack-social-note';
17    const JETPACK_SOCIAL_NOTES_CONFIG = 'jetpack_social_notes_config';
18    const FLUSH_REWRITE_RULES_FLUSHED = 'jetpack_social_rewrite_rules_flushed';
19
20    /**
21     * Check if the feature is enabled.
22     */
23    public function enabled() {
24        $missing = new \stdClass();
25        $enabled = get_option( self::JETPACK_SOCIAL_NOTE_CPT, $missing );
26        if (
27            $missing === $enabled
28            && ! wp_using_ext_object_cache()
29            && ( is_admin() || wp_doing_cron() || ( defined( 'WP_CLI' ) && WP_CLI ) )
30        ) {
31            global $wpdb;
32            // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- add_option() can overwrite a concurrent enable.
33            $inserted = $wpdb->query(
34                $wpdb->prepare(
35                    "INSERT IGNORE INTO $wpdb->options ( option_name, option_value, autoload ) VALUES ( %s, %s, %s )",
36                    self::JETPACK_SOCIAL_NOTE_CPT,
37                    '',
38                    'yes'
39                )
40            );
41            if ( false !== $inserted ) {
42                wp_cache_delete( self::JETPACK_SOCIAL_NOTE_CPT, 'options' );
43                wp_cache_delete( 'notoptions', 'options' );
44                wp_cache_delete( 'alloptions', 'options' );
45                $enabled = get_option( self::JETPACK_SOCIAL_NOTE_CPT );
46            }
47        }
48
49        return is_scalar( $enabled ) ? (bool) $enabled : false;
50    }
51
52    /**
53     * Initialize the Jetpack Social Note custom post type.
54     */
55    public function init() {
56        if ( ! self::enabled() ) {
57            return;
58        }
59        add_filter( 'allowed_block_types_all', array( $this, 'restrict_blocks_for_social_note' ), 10, 2 );
60
61        /*
62         * The ActivityPub plugin has a block to set a Fediverse post that a new post is in reply to. This is perfect for Social Notes.
63         */
64        if ( Constants::get_constant( 'ACTIVITYPUB_PLUGIN_VERSION' ) ) {
65            add_filter(
66                'jetpack_social_allowed_blocks',
67                function ( $allowed_blocks ) {
68                    $allowed_blocks[] = 'activitypub/reply';
69                    return $allowed_blocks;
70                }
71            );
72        }
73
74        self::register_cpt();
75        add_action( 'wp_insert_post_data', array( $this, 'set_empty_title' ), 10, 2 );
76        add_action( 'admin_init', array( $this, 'admin_init_actions' ) );
77
78        if (
79            /**
80             * Filters whether to override the empty title for Social Notes on the frontend.
81             *
82             * @since 7.1.0
83             *
84             * @param bool $override_empty_title Whether to override the empty title for Social Notes on the frontend.
85             */
86            apply_filters( 'jetpack_social_notes_override_empty_title', false )
87        ) {
88            add_filter( 'the_title', array( $this, 'override_empty_title' ), 10, 2 );
89        }
90    }
91
92    /**
93     * Things to do on admin_init.
94     */
95    public function admin_init_actions() {
96        \Automattic\Jetpack\Post_List\Post_List::setup();
97        add_action( 'current_screen', array( $this, 'add_filters_and_actions_for_screen' ), 5 );
98    }
99
100    /**
101     * If the current_screen has 'edit' as the base, add filter to change the post list tables.
102     *
103     * @param object $current_screen The current screen.
104     */
105    public function add_filters_and_actions_for_screen( $current_screen ) {
106        if ( 'edit' !== $current_screen->base ) {
107            return;
108        }
109
110        add_filter( 'the_title', array( $this, 'override_empty_title' ), 10, 2 );
111    }
112
113    /**
114     * Set the title to empty string.
115     *
116     * @param array $data The Post Data.
117     * @param array $post The Post.
118     */
119    public function set_empty_title( $data, $post ) {
120        if ( self::JETPACK_SOCIAL_NOTE_CPT === $post['post_type'] && 'auto-draft' === $post['post_status'] ) {
121            $data['post_title'] = '';
122        }
123        return $data;
124    }
125
126    /**
127     * Register the Jetpack Social Note custom post type.
128     */
129    public function register_cpt() {
130        $args = array(
131            'public'       => true,
132            'labels'       => array(
133                'name'                  => esc_html__( 'Social Notes', 'jetpack-social' ),
134                'singular_name'         => esc_html__( 'Social Note', 'jetpack-social' ),
135                'menu_name'             => esc_html__( 'Social Notes', 'jetpack-social' ),
136                'name_admin_bar'        => esc_html__( 'Social Note', 'jetpack-social' ),
137                'add_new'               => esc_html__( 'Add New', 'jetpack-social' ),
138                'add_new_item'          => esc_html__( 'Add New Note', 'jetpack-social' ),
139                'new_item'              => esc_html__( 'New Note', 'jetpack-social' ),
140                'edit_item'             => esc_html__( 'Edit Note', 'jetpack-social' ),
141                'view_item'             => esc_html__( 'View Note', 'jetpack-social' ),
142                'all_items'             => esc_html__( 'All Notes', 'jetpack-social' ),
143                'search_items'          => esc_html__( 'Search Notes', 'jetpack-social' ),
144                'parent_item_colon'     => esc_html__( 'Parent Notes:', 'jetpack-social' ),
145                'not_found'             => esc_html__( 'No Notes found.', 'jetpack-social' ),
146                'not_found_in_trash'    => esc_html__( 'No Notes found in Trash.', 'jetpack-social' ),
147                'archives'              => esc_html__( 'Notes archives', 'jetpack-social' ),
148                'insert_into_item'      => esc_html__( 'Insert into Note', 'jetpack-social' ),
149                'uploaded_to_this_item' => esc_html__( 'Uploaded to this Note', 'jetpack-social' ),
150                'filter_items_list'     => esc_html__( 'Filter Notes list', 'jetpack-social' ),
151                'items_list_navigation' => esc_html__( 'Notes list navigation', 'jetpack-social' ),
152                'items_list'            => esc_html__( 'Notes list', 'jetpack-social' ),
153            ),
154            'show_in_rest' => true,
155            'has_archive'  => true,
156            'supports'     => array( 'editor', 'comments', 'thumbnail', 'publicize', 'enhanced_post_list', 'activitypub' ),
157            'menu_icon'    => 'dashicons-welcome-write-blog',
158            'rewrite'      => array( 'slug' => 'sn' ),
159            'template'     => array(
160                array(
161                    'core/paragraph',
162                    array(
163                        'placeholder' => __( "What's on your mind?", 'jetpack-social' ),
164                    ),
165                ),
166                // We should add this back when the double featured image issue is fixed.
167                // array(
168                // 'core/post-featured-image',
169                // ),
170            ),
171        );
172        register_post_type( self::JETPACK_SOCIAL_NOTE_CPT, $args );
173        self::maybe_flush_rewrite_rules();
174    }
175
176    /**
177     * Restrict the blocks for the Social Note CPT.
178     *
179     * @param array                    $allowed_blocks The allowed blocks.
180     * @param \WP_Block_Editor_Context $block_editor_context The current block editor context.
181     * @return array The allowed blocks.
182     */
183    public function restrict_blocks_for_social_note( $allowed_blocks, $block_editor_context ) {
184        if (
185            ! isset( $block_editor_context->post )
186            || ! $block_editor_context->post instanceof \WP_Post
187            || 'jetpack-social-note' !== $block_editor_context->post->post_type
188        ) {
189            return $allowed_blocks;
190        }
191
192        // Only allow the paragraph block and the featured image block.
193        $allowed_blocks = array(
194            'core/paragraph',
195            'core/post-featured-image',
196        );
197
198        /**
199         * Filters the blocks available to the Social Notes CPT.
200         *
201         * Default is ['core/paragraph', 'core/post-featured-image']
202         *
203         * @since 5.5.0
204         *
205         * @param array $allowed_blocks A linear array of blocks allowed by the CPT.
206         */
207        return apply_filters( 'jetpack_social_allowed_blocks', $allowed_blocks );
208    }
209
210    /**
211     * Flush rewrite rules so the post permalink works correctly for the Social Note CPT. Flushing is an expensive operation, so do only when necessary.
212     *
213     * @param boolean $force Force flush the rewrite rules.
214     */
215    public function maybe_flush_rewrite_rules( $force = false ) {
216        if ( empty( get_option( self::FLUSH_REWRITE_RULES_FLUSHED ) ) || $force ) {
217            flush_rewrite_rules( false );
218            update_option( self::FLUSH_REWRITE_RULES_FLUSHED, true );
219        }
220    }
221
222    /**
223     * Use the_title hook so we show the social note's exceprt in the post list view.
224     *
225     * @param array $title The title of the post, which we have set to be an empty string for Social Notes.
226     * @param array $post_id The Post ID.
227     */
228    public function override_empty_title( $title, $post_id ) {
229        $post = get_post( $post_id );
230
231        if (
232            $post instanceof \WP_Post &&
233            self::JETPACK_SOCIAL_NOTE_CPT === $post->post_type
234        ) {
235            $publishing_date = new \DateTimeImmutable(
236                $post->post_date,
237                wp_timezone()
238            );
239
240            $datetime_format = sprintf(
241                /* Translators: %1$s is a formatted date, e.g. June 18, 2025. %2$s is a formatted time, e.g. 8:24 am. All other words/letters need to be escaped. */
242                __( '%1$s \a\t %2$s', 'jetpack-social' ),
243                get_option( 'date_format' ),
244                get_option( 'time_format' )
245            );
246
247            $title = sprintf(
248                /* Translators: placeholder is a fully-formatted date. */
249                __( 'Social note, %1$s', 'jetpack-social' ),
250                wp_date(
251                    $datetime_format,
252                    $publishing_date->getTimestamp()
253                )
254            );
255
256            /**
257             * Filters the default title for a Social Note.
258             *
259             * @since 7.1.0
260             *
261             * @param string $title The default title.
262             * @param \WP_Post $post The post.
263             */
264            $title = apply_filters( 'jetpack_social_notes_default_title', $title, $post );
265        }
266
267        return $title;
268    }
269}