Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 786
0.00% covered (danger)
0.00%
0 / 48
CRAP
0.00% covered (danger)
0.00%
0 / 1
Nova_Restaurant
0.00% covered (danger)
0.00%
0 / 785
0.00% covered (danger)
0.00%
0 / 48
30102
0.00% covered (danger)
0.00%
0 / 1
 init
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 __construct
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
6
 site_supports_nova
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 register_taxonomies
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
12
 register_post_types
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
6
 updated_messages
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
6
 enqueue_nova_styles
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 change_default_title
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 add_to_dashboard
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
6
 is_menu_item_query
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
30
 sort_menu_item_queries_by_menu_order
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 sort_menu_item_queries_by_menu_taxonomy
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
110
 add_admin_menus
0.00% covered (danger)
0.00%
0 / 25
0.00% covered (danger)
0.00%
0 / 1
6
 set_custom_font_icon
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 current_screen_load
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 admin_notices
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 no_title_sorting
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 setup_menu_item_columns
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 menu_item_columns
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 menu_item_column_callback
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
42
 get_menu_by_post_id
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
 maybe_reorder_menu_items
0.00% covered (danger)
0.00%
0 / 36
0.00% covered (danger)
0.00%
0 / 1
182
 edit_menu_items_page_load
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
6
 handle_menu_item_actions
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 1
812
 show_menu_titles_in_menu_item_list
0.00% covered (danger)
0.00%
0 / 50
0.00% covered (danger)
0.00%
0 / 1
72
 add_many_new_items_page_load
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 enqueue_many_items_scripts
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 process_form_request
0.00% covered (danger)
0.00%
0 / 46
0.00% covered (danger)
0.00%
0 / 1
240
 add_many_new_items_page
0.00% covered (danger)
0.00%
0 / 44
0.00% covered (danger)
0.00%
0 / 1
2
 register_menu_item_meta_boxes
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
2
 menu_item_price_meta_box
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 add_post_meta
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 get_menus
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
56
 get_menu_item_menu_leaf
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
 list_labels
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 list_admin_labels
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
12
 set_price
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_price
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 display_price
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 get_menu_item_loop_markup
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setup_menu_item_loop_markup__in_filter
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 start_menu_item_loop
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 menu_item_loop_each_post
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
72
 stop_menu_item_loop
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 menu_item_loop_header
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 menu_item_loop_open_element
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 menu_item_loop_close_element
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
2
 menu_item_loop_class
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * Manage restaurant menus from your WordPress site,
4 * via a new "nova" CPT.
5 *
6 * Put the following code in your theme's Food Menu Page Template to customize the markup of the menu.
7 *
8 * if ( class_exists( 'Nova_Restaurant' ) ) {
9 *  Nova_Restaurant::init( array(
10 *      'menu_tag'               => 'section',
11 *      'menu_class'             => 'menu-items',
12 *      'menu_header_tag'        => 'header',
13 *      'menu_header_class'      => 'menu-group-header',
14 *      'menu_title_tag'         => 'h1',
15 *      'menu_title_class'       => 'menu-group-title',
16 *      'menu_description_tag'   => 'div',
17 *      'menu_description_class' => 'menu-group-description',
18 *  ) );
19 * }
20 *
21 * @todo
22 * - Bulk/Quick edit response of Menu Item rows is broken.
23 * - Drag and Drop reordering.
24 *
25 * @package automattic/jetpack-classic-theme-helper
26 */
27
28namespace Automattic\Jetpack\Classic_Theme_Helper;
29
30use Automattic\Jetpack\Assets;
31use Automattic\Jetpack\Roles;
32use WP_Post;
33use WP_Query;
34
35if ( ! class_exists( __NAMESPACE__ . '\Nova_Restaurant' ) ) {
36
37    /**
38     * Create the new Nova CPT.
39     */
40    class Nova_Restaurant {
41        const MENU_ITEM_POST_TYPE = 'nova_menu_item';
42        const MENU_ITEM_LABEL_TAX = 'nova_menu_item_label';
43        const MENU_TAX            = 'nova_menu';
44
45        /**
46         * Version number used when enqueuing all resources (css and js).
47         *
48         * @var string
49         */
50        public $version = '20210303';
51
52        /**
53         * Default markup for the menu items.
54         *
55         * @var array
56         */
57        protected $default_menu_item_loop_markup = array(
58            'menu_tag'               => 'section',
59            'menu_class'             => 'menu-items',
60            'menu_header_tag'        => 'header',
61            'menu_header_class'      => 'menu-group-header',
62            'menu_title_tag'         => 'h1',
63            'menu_title_class'       => 'menu-group-title',
64            'menu_description_tag'   => 'div',
65            'menu_description_class' => 'menu-group-description',
66        );
67
68        /**
69         * Array of markup for the menu items.
70         *
71         * @var array
72         */
73        protected $menu_item_loop_markup = array();
74
75        /**
76         * Last term ID of a loop of menu items.
77         *
78         * @var bool|int
79         */
80        protected $menu_item_loop_last_term_id = false;
81
82        /**
83         * Current term ID of a loop of menu items.
84         *
85         * @var bool|int|\WP_Term
86         */
87        protected $menu_item_loop_current_term = false;
88
89        /**
90         * Initialize class.
91         *
92         * @param array $menu_item_loop_markup Array of markup for the menu items.
93         *
94         * @return self
95         */
96        public static function init( $menu_item_loop_markup = array() ) {
97            static $instance = false;
98
99            if ( ! $instance ) {
100                $instance = new Nova_Restaurant();
101            }
102
103            if ( $menu_item_loop_markup ) {
104                $instance->menu_item_loop_markup = wp_parse_args( $menu_item_loop_markup, $instance->default_menu_item_loop_markup );
105            }
106
107            return $instance;
108        }
109
110        /**
111         * Constructor.
112         * Hook into WordPress to create CPT and utilities if needed.
113         */
114        public function __construct() {
115            if ( ! $this->site_supports_nova() ) {
116                return;
117            }
118
119            $this->register_taxonomies();
120            $this->register_post_types();
121            add_action( 'admin_menu', array( $this, 'add_admin_menus' ) );
122            add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_nova_styles' ) );
123            add_action( 'admin_head', array( $this, 'set_custom_font_icon' ) );
124
125            // Always sort menu items correctly
126            add_action( 'parse_query', array( $this, 'sort_menu_item_queries_by_menu_order' ) );
127            add_filter( 'posts_results', array( $this, 'sort_menu_item_queries_by_menu_taxonomy' ), 10, 2 );
128
129            add_action( 'wp_insert_post', array( $this, 'add_post_meta' ) );
130
131            $this->menu_item_loop_markup = $this->default_menu_item_loop_markup;
132
133            // Only output our Menu Item Loop Markup on a real blog view.  Not feeds, XML-RPC, admin, etc.
134            add_filter( 'template_include', array( $this, 'setup_menu_item_loop_markup__in_filter' ) );
135
136            add_filter( 'enter_title_here', array( $this, 'change_default_title' ) );
137            add_filter( 'post_updated_messages', array( $this, 'updated_messages' ) );
138            add_filter( 'dashboard_glance_items', array( $this, 'add_to_dashboard' ) );
139        }
140
141        /**
142         * Should this Custom Post Type be made available?
143         *
144         * @return bool
145         */
146        public function site_supports_nova() {
147            // If we're on WordPress.com, and it has the menu site vertical.
148            if ( function_exists( 'site_vertical' ) && 'nova_menu' === site_vertical() ) { // @phan-suppress-current-line PhanUndeclaredFunction -- only calling if it exists.
149                return true;
150            }
151
152            // Else, if the current theme requests it.
153            if ( current_theme_supports( self::MENU_ITEM_POST_TYPE ) ) {
154                return true;
155            }
156
157            // Otherwise, say no unless something wants to filter us to say yes.
158            /**
159             * Allow something else to hook in and enable this CPT.
160             *
161             * @module custom-content-types
162             *
163             * @since 2.6.0
164             *
165             * @param bool false Whether or not to enable this CPT.
166             * @param string $var The slug for this CPT.
167             */
168            return (bool) apply_filters( 'jetpack_enable_cpt', false, self::MENU_ITEM_POST_TYPE );
169        }
170
171        /* Setup */
172
173        /**
174         * Register Taxonomies and Post Type
175         */
176        public function register_taxonomies() {
177            if ( ! taxonomy_exists( self::MENU_ITEM_LABEL_TAX ) ) {
178                register_taxonomy(
179                    self::MENU_ITEM_LABEL_TAX,
180                    self::MENU_ITEM_POST_TYPE,
181                    array(
182                        'labels'       => array(
183                            /* translators: this is about a food menu */
184                            'name'                       => __( 'Menu Item Labels', 'jetpack-classic-theme-helper' ),
185                            /* translators: this is about a food menu */
186                            'singular_name'              => __( 'Menu Item Label', 'jetpack-classic-theme-helper' ),
187                            /* translators: this is about a food menu */
188                            'search_items'               => __( 'Search Menu Item Labels', 'jetpack-classic-theme-helper' ),
189                            'popular_items'              => __( 'Popular Labels', 'jetpack-classic-theme-helper' ),
190                            /* translators: this is about a food menu */
191                            'all_items'                  => __( 'All Menu Item Labels', 'jetpack-classic-theme-helper' ),
192                            /* translators: this is about a food menu */
193                            'edit_item'                  => __( 'Edit Menu Item Label', 'jetpack-classic-theme-helper' ),
194                            /* translators: this is about a food menu */
195                            'view_item'                  => __( 'View Menu Item Label', 'jetpack-classic-theme-helper' ),
196                            /* translators: this is about a food menu */
197                            'update_item'                => __( 'Update Menu Item Label', 'jetpack-classic-theme-helper' ),
198                            /* translators: this is about a food menu */
199                            'add_new_item'               => __( 'Add New Menu Item Label', 'jetpack-classic-theme-helper' ),
200                            /* translators: this is about a food menu */
201                            'new_item_name'              => __( 'New Menu Item Label Name', 'jetpack-classic-theme-helper' ),
202                            'separate_items_with_commas' => __( 'For example, spicy, favorite, etc. <br /> Separate Labels with commas', 'jetpack-classic-theme-helper' ),
203                            'add_or_remove_items'        => __( 'Add or remove Labels', 'jetpack-classic-theme-helper' ),
204                            'choose_from_most_used'      => __( 'Choose from the most used Labels', 'jetpack-classic-theme-helper' ),
205                            'items_list_navigation'      => __( 'Menu item label list navigation', 'jetpack-classic-theme-helper' ),
206                            'items_list'                 => __( 'Menu item labels list', 'jetpack-classic-theme-helper' ),
207                        ),
208                        'no_tagcloud'  => __( 'No Labels found', 'jetpack-classic-theme-helper' ),
209                        'hierarchical' => false,
210                    )
211                );
212            }
213
214            if ( ! taxonomy_exists( self::MENU_TAX ) ) {
215                register_taxonomy(
216                    self::MENU_TAX,
217                    self::MENU_ITEM_POST_TYPE,
218                    array(
219                        'labels'        => array(
220                            /* translators: this is about a food menu */
221                            'name'                  => __( 'Menu Sections', 'jetpack-classic-theme-helper' ),
222                            /* translators: this is about a food menu */
223                            'singular_name'         => __( 'Menu Section', 'jetpack-classic-theme-helper' ),
224                            /* translators: this is about a food menu */
225                            'search_items'          => __( 'Search Menu Sections', 'jetpack-classic-theme-helper' ),
226                            /* translators: this is about a food menu */
227                            'all_items'             => __( 'All Menu Sections', 'jetpack-classic-theme-helper' ),
228                            /* translators: this is about a food menu */
229                            'parent_item'           => __( 'Parent Menu Section', 'jetpack-classic-theme-helper' ),
230                            /* translators: this is about a food menu */
231                            'parent_item_colon'     => __( 'Parent Menu Section:', 'jetpack-classic-theme-helper' ),
232                            /* translators: this is about a food menu */
233                            'edit_item'             => __( 'Edit Menu Section', 'jetpack-classic-theme-helper' ),
234                            /* translators: this is about a food menu */
235                            'view_item'             => __( 'View Menu Section', 'jetpack-classic-theme-helper' ),
236                            /* translators: this is about a food menu */
237                            'update_item'           => __( 'Update Menu Section', 'jetpack-classic-theme-helper' ),
238                            /* translators: this is about a food menu */
239                            'add_new_item'          => __( 'Add New Menu Section', 'jetpack-classic-theme-helper' ),
240                            /* translators: this is about a food menu */
241                            'new_item_name'         => __( 'New Menu Sections Name', 'jetpack-classic-theme-helper' ),
242                            'items_list_navigation' => __( 'Menu section list navigation', 'jetpack-classic-theme-helper' ),
243                            'items_list'            => __( 'Menu section list', 'jetpack-classic-theme-helper' ),
244                        ),
245                        'rewrite'       => array(
246                            'slug'         => 'menu',
247                            'with_front'   => false,
248                            'hierarchical' => true,
249                        ),
250                        'hierarchical'  => true,
251                        'show_tagcloud' => false,
252                        'query_var'     => 'menu',
253                    )
254                );
255            }
256        }
257
258        /**
259         * Register our Post Type.
260         */
261        public function register_post_types() {
262            if ( post_type_exists( self::MENU_ITEM_POST_TYPE ) ) {
263                return;
264            }
265
266            register_post_type(
267                self::MENU_ITEM_POST_TYPE,
268                array(
269                    'description'          => __( "Items on your restaurant's menu", 'jetpack-classic-theme-helper' ),
270
271                    'labels'               => array(
272                        /* translators: this is about a food menu */
273                        'name'                  => __( 'Menu Items', 'jetpack-classic-theme-helper' ),
274                        /* translators: this is about a food menu */
275                        'singular_name'         => __( 'Menu Item', 'jetpack-classic-theme-helper' ),
276                        /* translators: this is about a food menu */
277                        'menu_name'             => __( 'Food Menus', 'jetpack-classic-theme-helper' ),
278                        /* translators: this is about a food menu */
279                        'all_items'             => __( 'Menu Items', 'jetpack-classic-theme-helper' ),
280                        /* translators: this is about a food menu */
281                        'add_new'               => __( 'Add One Item', 'jetpack-classic-theme-helper' ),
282                        /* translators: this is about a food menu */
283                        'add_new_item'          => __( 'Add Menu Item', 'jetpack-classic-theme-helper' ),
284                        /* translators: this is about a food menu */
285                        'edit_item'             => __( 'Edit Menu Item', 'jetpack-classic-theme-helper' ),
286                        /* translators: this is about a food menu */
287                        'new_item'              => __( 'New Menu Item', 'jetpack-classic-theme-helper' ),
288                        /* translators: this is about a food menu */
289                        'view_item'             => __( 'View Menu Item', 'jetpack-classic-theme-helper' ),
290                        /* translators: this is about a food menu */
291                        'search_items'          => __( 'Search Menu Items', 'jetpack-classic-theme-helper' ),
292                        /* translators: this is about a food menu */
293                        'not_found'             => __( 'No Menu Items found', 'jetpack-classic-theme-helper' ),
294                        /* translators: this is about a food menu */
295                        'not_found_in_trash'    => __( 'No Menu Items found in Trash', 'jetpack-classic-theme-helper' ),
296                        'filter_items_list'     => __( 'Filter menu items list', 'jetpack-classic-theme-helper' ),
297                        'items_list_navigation' => __( 'Menu item list navigation', 'jetpack-classic-theme-helper' ),
298                        'items_list'            => __( 'Menu items list', 'jetpack-classic-theme-helper' ),
299                    ),
300                    'supports'             => array(
301                        'title',
302                        'editor',
303                        'thumbnail',
304                        'excerpt',
305                    ),
306                    'rewrite'              => array(
307                        'slug'       => 'item',
308                        'with_front' => false,
309                        'feeds'      => false,
310                        'pages'      => false,
311                    ),
312                    'register_meta_box_cb' => array( $this, 'register_menu_item_meta_boxes' ),
313
314                    'public'               => true,
315                    'show_ui'              => true, // set to false to replace with custom UI
316                    'menu_position'        => 20, // below Pages
317                    'capability_type'      => 'page',
318                    'map_meta_cap'         => true,
319                    'has_archive'          => false,
320                    'query_var'            => 'item',
321                )
322            );
323        }
324
325        /**
326         * Update messages for the Menu Item admin.
327         *
328         * @param array $messages Existing post update messages.
329         *
330         * @return array $messages Updated post update messages.
331         */
332        public function updated_messages( $messages ) {
333            global $post;
334
335            $messages[ self::MENU_ITEM_POST_TYPE ] = array(
336                0  => '', // Unused. Messages start at index 1.
337                1  => sprintf(
338                    /* translators: this is about a food menu. Placeholder is a link to the food menu. */
339                    __( 'Menu item updated. <a href="%s">View item</a>', 'jetpack-classic-theme-helper' ),
340                    esc_url( get_permalink( $post->ID ) )
341                ),
342                2  => esc_html__( 'Custom field updated.', 'jetpack-classic-theme-helper' ),
343                3  => esc_html__( 'Custom field deleted.', 'jetpack-classic-theme-helper' ),
344                /* translators: this is about a food menu */
345                4  => esc_html__( 'Menu item updated.', 'jetpack-classic-theme-helper' ),
346                5  => isset( $_GET['revision'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Copying core message handling.
347                    ? sprintf(
348                        /* translators: %s: date and time of the revision */
349                        esc_html__( 'Menu item restored to revision from %s', 'jetpack-classic-theme-helper' ),
350                        wp_post_revision_title( (int) $_GET['revision'], false ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Copying core message handling.
351                    )
352                    : false,
353                6  => sprintf(
354                    /* translators: this is about a food menu. Placeholder is a link to the food menu. */
355                    __( 'Menu item published. <a href="%s">View item</a>', 'jetpack-classic-theme-helper' ),
356                    esc_url( get_permalink( $post->ID ) )
357                ),
358                /* translators: this is about a food menu */
359                7  => esc_html__( 'Menu item saved.', 'jetpack-classic-theme-helper' ),
360                8  => sprintf(
361                    /* translators: this is about a food menu */
362                    __( 'Menu item submitted. <a target="_blank" href="%s">Preview item</a>', 'jetpack-classic-theme-helper' ),
363                    esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) )
364                ),
365                9  => sprintf(
366                    /* translators: this is about a food menu. 1. Publish box date format, see https://php.net/date 2. link to the food menu. */
367                    __( 'Menu item scheduled for: <strong>%1$s</strong>. <a target="_blank" href="%2$s">Preview item</a>', 'jetpack-classic-theme-helper' ),
368                    /* translators: Publish box date format, see https://php.net/date */
369                    date_i18n( __( 'M j, Y @ G:i', 'jetpack-classic-theme-helper' ), strtotime( $post->post_date ) ),
370                    esc_url( get_permalink( $post->ID ) )
371                ),
372                10 => sprintf(
373                    /* translators: this is about a food menu. Placeholder is a link to the food menu. */
374                    __( 'Menu item draft updated. <a target="_blank" href="%s">Preview item</a>', 'jetpack-classic-theme-helper' ),
375                    esc_url( add_query_arg( 'preview', 'true', get_permalink( $post->ID ) ) )
376                ),
377            );
378
379            return $messages;
380        }
381
382        /**
383         * Nova styles and scripts.
384         *
385         * @param string $hook Page hook.
386         *
387         * @return void
388         */
389        public function enqueue_nova_styles( $hook ) {
390            global $post_type;
391            $pages = array( 'edit.php', 'post.php', 'post-new.php' );
392
393            if ( in_array( $hook, $pages, true ) && $post_type === self::MENU_ITEM_POST_TYPE ) {
394                wp_enqueue_style( 'nova-style', plugins_url( 'css/nova.css', __FILE__ ), array(), $this->version );
395            }
396
397            wp_enqueue_style( 'nova-font', plugins_url( 'css/nova-font.css', __FILE__ ), array(), $this->version );
398        }
399
400        /**
401         * Change â€˜Enter Title Here’ text for the Menu Item.
402         *
403         * @param string $title Default title placeholder text.
404         *
405         * @return string
406         */
407        public function change_default_title( $title ) {
408            if ( self::MENU_ITEM_POST_TYPE === get_post_type() ) {
409                /* translators: this is about a food menu */
410                $title = esc_html__( "Enter the menu item's name here", 'jetpack-classic-theme-helper' );
411            }
412
413            return $title;
414        }
415
416        /**
417         * Add to Dashboard At A Glance
418         *
419         * @return void
420         */
421        public function add_to_dashboard() {
422            $number_menu_items = wp_count_posts( self::MENU_ITEM_POST_TYPE );
423
424            $roles = new Roles(); // @phan-suppress-current-line PhanUndeclaredClassMethod -- declared at top of file.
425            if ( current_user_can( $roles->translate_role_to_cap( 'administrator' ) ) ) { // @phan-suppress-current-line PhanUndeclaredClassMethod
426                $number_menu_items_published = sprintf(
427                    '<a href="%1$s">%2$s</a>',
428                    esc_url(
429                        get_admin_url(
430                            get_current_blog_id(),
431                            'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE
432                        )
433                    ),
434                    sprintf(
435                        /* translators: Placehoder is a number of items. */
436                        _n(
437                            '%1$d Food Menu Item',
438                            '%1$d Food Menu Items',
439                            (int) $number_menu_items->publish,
440                            'jetpack-classic-theme-helper'
441                        ),
442                        number_format_i18n( $number_menu_items->publish )
443                    )
444                );
445            } else {
446                $number_menu_items_published = sprintf(
447                    '<span>%1$s</span>',
448                    sprintf(
449                        /* translators: Placehoder is a number of items. */
450                        _n(
451                            '%1$d Food Menu Item',
452                            '%1$d Food Menu Items',
453                            (int) $number_menu_items->publish,
454                            'jetpack-classic-theme-helper'
455                        ),
456                        number_format_i18n( $number_menu_items->publish )
457                    )
458                );
459            }
460
461            echo '<li class="nova-menu-count">' . $number_menu_items_published . '</li>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we escape things above.
462        }
463
464        /**
465         * If the WP query for our menu items.
466         *
467         * @param WP_Query $query WP Query.
468         *
469         * @return bool
470         */
471        public function is_menu_item_query( $query ) {
472            if (
473                ( isset( $query->query_vars['taxonomy'] ) && self::MENU_TAX === $query->query_vars['taxonomy'] )
474            ||
475                ( isset( $query->query_vars['post_type'] ) && self::MENU_ITEM_POST_TYPE === $query->query_vars['post_type'] )
476            ) {
477                return true;
478            }
479
480            return false;
481        }
482
483        /**
484         * Custom sort the menu item queries by menu order.
485         *
486         * @param WP_Query $query WP Query.
487         *
488         * @return void
489         */
490        public function sort_menu_item_queries_by_menu_order( $query ) {
491            if ( ! $this->is_menu_item_query( $query ) ) {
492                return;
493            }
494
495            $query->query_vars['orderby'] = 'menu_order';
496            $query->query_vars['order']   = 'ASC';
497
498            // For now, just turn off paging so we can sort by taxonmy later
499            // If we want paging in the future, we'll need to add the taxonomy sort here (or at least before the DB query is made)
500            $query->query_vars['posts_per_page'] = -1;
501        }
502
503        /**
504         * Custom sort the menu item queries by menu taxonomies.
505         *
506         * @param WP_Post[] $posts Array of post objects.
507         * @param WP_Query  $query The WP_Query instance.
508         *
509         * @return WP_Post[]
510         */
511        public function sort_menu_item_queries_by_menu_taxonomy( $posts, $query ) {
512            if ( ! $posts ) {
513                return $posts;
514            }
515
516            if ( ! $this->is_menu_item_query( $query ) ) {
517                return $posts;
518            }
519
520            $grouped_by_term = array();
521
522            foreach ( $posts as $post ) {
523                $term = $this->get_menu_item_menu_leaf( $post->ID );
524                if ( ! $term || is_wp_error( $term ) ) {
525                    $term_id = 0;
526                } else {
527                    $term_id = $term->term_id;
528                }
529
530                if ( ! isset( $grouped_by_term[ "$term_id" ] ) ) {
531                    $grouped_by_term[ "$term_id" ] = array();
532                }
533
534                $grouped_by_term[ "$term_id" ][] = $post;
535            }
536
537            $term_order = get_option( 'nova_menu_order', array() );
538
539            $return = array();
540            foreach ( $term_order as $term_id ) {
541                if ( isset( $grouped_by_term[ "$term_id" ] ) ) {
542                    $return = array_merge( $return, $grouped_by_term[ "$term_id" ] );
543                    unset( $grouped_by_term[ "$term_id" ] );
544                }
545            }
546
547            foreach ( $grouped_by_term as $term_id => $posts ) {
548                $return = array_merge( $return, $posts );
549            }
550
551            return $return;
552        }
553
554        /**
555         * Add new "Add many items" submenu, custom colunmns, and custom bulk actions.
556         *
557         * @return void
558         */
559        public function add_admin_menus() {
560            $hook = add_submenu_page(
561                'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE,
562                __( 'Add Many Items', 'jetpack-classic-theme-helper' ),
563                __( 'Add Many Items', 'jetpack-classic-theme-helper' ),
564                'edit_pages',
565                'add_many_nova_items',
566                array( $this, 'add_many_new_items_page' )
567            );
568
569            add_action( "load-$hook", array( $this, 'add_many_new_items_page_load' ) );
570
571            add_action( 'current_screen', array( $this, 'current_screen_load' ) );
572
573            /*
574            * Adjust 'Add Many Items' submenu position
575            * We're making changes to the menu global, but no other choice unfortunately.
576            * phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
577            */
578            if ( isset( $GLOBALS['submenu'][ 'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE ] ) ) {
579                $submenu_item = array_pop( $GLOBALS['submenu'][ 'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE ] );
580                $GLOBALS['submenu'][ 'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE ][11] = $submenu_item;
581                ksort( $GLOBALS['submenu'][ 'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE ] );
582            }
583            // phpcs:enable WordPress.WP.GlobalVariablesOverride.Prohibited
584
585            $this->setup_menu_item_columns();
586
587            Assets::register_script(
588                'nova-menu-checkboxes',
589                '../../dist/custom-post-types/js/menu-checkboxes.js',
590                __FILE__,
591                array(
592                    'in_footer'  => true,
593                    'enqueue'    => false,
594                    'textdomain' => 'jetpack-classic-theme-helper',
595                )
596            );
597        }
598
599        /**
600         * Custom Nova Icon CSS
601         *
602         * @return void
603         */
604        public function set_custom_font_icon() {
605            ?>
606        <style type="text/css">
607        #menu-posts-nova_menu_item .wp-menu-image::before {
608            font-family: 'nova-font' !important;
609            content: '\e603' !important;
610        }
611        </style>
612            <?php
613        }
614
615        /**
616         * Load Nova menu management tools on the CPT admin screen.
617         *
618         * @return void
619         */
620        public function current_screen_load() {
621            $screen = get_current_screen();
622            if ( 'edit-nova_menu_item' !== $screen->id ) {
623                return;
624            }
625
626            $this->edit_menu_items_page_load();
627            add_filter( 'admin_notices', array( $this, 'admin_notices' ) );
628        }
629
630        /* Edit Items List */
631
632        /**
633         * Display a notice in wp-admin after items have been changed.
634         *
635         * @return void
636         */
637        public function admin_notices() {
638            if ( isset( $_GET['nova_reordered'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- this is only displaying a message with no dynamic values.
639                printf(
640                    '<div class="updated"><p>%s</p></div>',
641                    /* translators: this is about a food menu */
642                    esc_html__( 'Menu Items re-ordered.', 'jetpack-classic-theme-helper' )
643                );
644            }
645        }
646
647        /**
648         * Do not allow sorting by title.
649         *
650         * @param array $columns An array of sortable columns.
651         *
652         * @return array $columns.
653         */
654        public function no_title_sorting( $columns ) {
655            if ( isset( $columns['title'] ) ) {
656                unset( $columns['title'] );
657            }
658            return $columns;
659        }
660
661        /**
662         * Set up custom columns for our Nova menu.
663         *
664         * @return void
665         */
666        public function setup_menu_item_columns() {
667            add_filter( sprintf( 'manage_edit-%s_sortable_columns', self::MENU_ITEM_POST_TYPE ), array( $this, 'no_title_sorting' ) );
668            add_filter( sprintf( 'manage_%s_posts_columns', self::MENU_ITEM_POST_TYPE ), array( $this, 'menu_item_columns' ) );
669
670            add_action( sprintf( 'manage_%s_posts_custom_column', self::MENU_ITEM_POST_TYPE ), array( $this, 'menu_item_column_callback' ), 10, 2 );
671        }
672
673        /**
674         * Add custom columns to the Nova menu item list.
675         *
676         * @param array $columns An array of columns.
677         *
678         * @return array $columns.
679         */
680        public function menu_item_columns( $columns ) {
681            unset( $columns['date'], $columns['likes'] );
682
683            $columns['thumbnail'] = __( 'Thumbnail', 'jetpack-classic-theme-helper' );
684            $columns['labels']    = __( 'Labels', 'jetpack-classic-theme-helper' );
685            $columns['price']     = __( 'Price', 'jetpack-classic-theme-helper' );
686            $columns['order']     = __( 'Order', 'jetpack-classic-theme-helper' );
687
688            return $columns;
689        }
690
691        /**
692         * Display custom data in each new custom column we created.
693         *
694         * @param string $column  The name of the column to display.
695         * @param int    $post_id The current post ID.
696         *
697         * @return void
698         */
699        public function menu_item_column_callback( $column, $post_id ) {
700            $screen = get_current_screen();
701
702            switch ( $column ) {
703                case 'thumbnail':
704                    echo get_the_post_thumbnail( $post_id, array( 50, 50 ) );
705                    break;
706                case 'labels':
707                    $this->list_admin_labels( $post_id );
708                    break;
709                case 'price':
710                    $this->display_price( $post_id );
711                    break;
712                case 'order':
713                    $url = admin_url( $screen->parent_file );
714
715                    $up_url = add_query_arg(
716                        array(
717                            'action'  => 'move-item-up',
718                            'post_id' => (int) $post_id,
719                        ),
720                        wp_nonce_url( $url, 'nova_move_item_up_' . $post_id )
721                    );
722
723                    $down_url  = add_query_arg(
724                        array(
725                            'action'  => 'move-item-down',
726                            'post_id' => (int) $post_id,
727                        ),
728                        wp_nonce_url( $url, 'nova_move_item_down_' . $post_id )
729                    );
730                    $menu_item = get_post( $post_id );
731                    $menu      = $this->get_menu_by_post_id( $post_id );
732                    $term_id   = is_object( $menu ) ? $menu->term_id : '';
733                    ?>
734                    <input type="hidden" class="menu-order-value" name="nova_order[<?php echo (int) $post_id; ?>]" value="<?php echo esc_attr( (string) $menu_item->menu_order ); ?>" />
735                    <input type="hidden" class='nova-menu-term' name="nova_menu_term[<?php echo (int) $post_id; ?>]" value="<?php echo esc_attr( (string) $term_id ); ?>">
736
737                    <span class="hide-if-js">
738                    &nbsp; &nbsp; &mdash; <a class="nova-move-item-up" data-post-id="<?php echo (int) $post_id; ?>" href="<?php echo esc_url( $up_url ); ?>">up</a>
739                    <br />
740                    &nbsp; &nbsp; &mdash; <a class="nova-move-item-down" data-post-id="<?php echo (int) $post_id; ?>" href="<?php echo esc_url( $down_url ); ?>">down</a>
741                    </span>
742                    <?php
743                    break;
744            }
745        }
746
747        /**
748         * Get menu item by post ID.
749         *
750         * @param int $post_id Post ID.
751         *
752         * @return bool|\WP_Term
753         */
754        public function get_menu_by_post_id( $post_id = null ) {
755            if ( ! $post_id ) {
756                return false;
757            }
758
759            $terms = get_the_terms( $post_id, self::MENU_TAX );
760
761            if ( ! is_array( $terms ) ) {
762                return false;
763            }
764
765            return array_pop( $terms );
766        }
767
768        /**
769         * Fires on a menu edit page. We might have drag-n-drop reordered
770         */
771        public function maybe_reorder_menu_items() {
772            // make sure we clicked our button.
773            if (
774                empty( $_REQUEST['menu_reorder_submit'] )
775                || __( 'Save New Order', 'jetpack-classic-theme-helper' ) !== $_REQUEST['menu_reorder_submit']
776            ) {
777                return;
778            }
779
780            // make sure we have the nonce.
781            if (
782                empty( $_REQUEST['drag-drop-reorder'] )
783                || ! wp_verify_nonce( sanitize_key( $_REQUEST['drag-drop-reorder'] ), 'drag-drop-reorder' )
784            ) {
785                return;
786            }
787
788            // make sure we have data to work with.
789            if ( empty( $_REQUEST['nova_menu_term'] ) || empty( $_REQUEST['nova_order'] ) ) {
790                return;
791            }
792
793            $term_pairs  = array_map( 'absint', $_REQUEST['nova_menu_term'] );
794            $order_pairs = array_map( 'absint', $_REQUEST['nova_order'] );
795
796            foreach ( $order_pairs as $id => $menu_order ) {
797                $id = absint( $id );
798                unset( $order_pairs[ $id ] );
799                if ( $id < 0 ) {
800                    continue;
801                }
802
803                $post = get_post( $id );
804                if ( ! $post ) {
805                    continue;
806                }
807
808                // save a write if the order hasn't changed
809                if ( (int) $menu_order !== $post->menu_order ) {
810                    $args = array(
811                        'ID'         => $id,
812                        'menu_order' => $menu_order,
813                    );
814                    wp_update_post( $args );
815                }
816
817                // save a write if the term hasn't changed
818                if (
819                    is_object( $this->get_menu_by_post_id( $id ) ) &&
820                    (int) $term_pairs[ $id ] !== $this->get_menu_by_post_id( $id )->term_id
821                ) {
822                    wp_set_object_terms( $id, $term_pairs[ $id ], self::MENU_TAX );
823                }
824            }
825
826            $redirect = add_query_arg(
827                array(
828                    'post_type'      => self::MENU_ITEM_POST_TYPE,
829                    'nova_reordered' => '1',
830                ),
831                admin_url( 'edit.php' )
832            );
833            wp_safe_redirect( $redirect );
834            exit( 0 );
835        }
836
837        /**
838         * Handle changes to menu items.
839         * (process actions, update data, enqueue necessary scripts).
840         *
841         * @return void
842         */
843        public function edit_menu_items_page_load() {
844            if ( isset( $_GET['action'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we process the form and check nonces in handle_menu_item_actions.
845                $this->handle_menu_item_actions();
846            }
847
848            $this->maybe_reorder_menu_items();
849
850            Assets::register_script(
851                'nova-drag-drop',
852                '../../dist/custom-post-types/js/nova-drag-drop.js',
853                __FILE__,
854                array(
855                    'dependencies' => array(
856                        'jquery',
857                        'jquery-ui-sortable',
858                    ),
859                    'in_footer'    => true,
860                    'enqueue'      => true,
861                    'textdomain'   => 'jetpack-classic-theme-helper',
862                )
863            );
864
865            wp_localize_script(
866                'nova-drag-drop',
867                '_novaDragDrop',
868                array(
869                    'nonce'       => wp_create_nonce( 'drag-drop-reorder' ),
870                    'nonceName'   => 'drag-drop-reorder',
871                    'reorder'     => __( 'Save New Order', 'jetpack-classic-theme-helper' ),
872                    'reorderName' => 'menu_reorder_submit',
873                )
874            );
875            add_action( 'the_post', array( $this, 'show_menu_titles_in_menu_item_list' ) );
876        }
877
878        /**
879         * Process actions to move menu items around.
880         *
881         * @return void
882         */
883        public function handle_menu_item_actions() {
884            if ( isset( $_GET['action'] ) ) {
885                $action = (string) wp_unslash( $_GET['action'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- we check for nonces below, and check against specific strings in switch statement.
886            } else {
887                return;
888            }
889
890            switch ( $action ) {
891                case 'move-item-up':
892                case 'move-item-down':
893                    $reorder = false;
894
895                    if ( empty( $_GET['post_id'] ) ) {
896                        break;
897                    }
898
899                    $post_id = (int) $_GET['post_id'];
900
901                    $term = $this->get_menu_item_menu_leaf( $post_id );
902
903                    // Get all posts in that term.
904                    $query = new WP_Query(
905                        array(
906                            'taxonomy' => self::MENU_TAX,
907                            'term'     => $term->slug,
908                        )
909                    );
910
911                    $order = array();
912                    foreach ( $query->posts as $post ) {
913                        $order[] = $post->ID;
914                    }
915
916                    if ( 'move-item-up' === $action ) {
917                        check_admin_referer( 'nova_move_item_up_' . $post_id );
918
919                        $first_post_id = $order[0];
920                        if ( $post_id === $first_post_id ) {
921                            break;
922                        }
923
924                        foreach ( $order as $menu_order => $order_post_id ) {
925                            if ( $post_id !== $order_post_id ) {
926                                continue;
927                            }
928
929                            $swap_post_id             = $order[ $menu_order - 1 ];
930                            $order[ $menu_order - 1 ] = $post_id;
931                            $order[ $menu_order ]     = $swap_post_id;
932
933                            $reorder = true;
934                            break;
935                        }
936                    } else {
937                        check_admin_referer( 'nova_move_item_down_' . $post_id );
938
939                        $last_post_id = end( $order );
940                        if ( $post_id === $last_post_id ) {
941                            break;
942                        }
943
944                        foreach ( $order as $menu_order => $order_post_id ) {
945                            if ( $post_id !== $order_post_id ) {
946                                continue;
947                            }
948
949                            $swap_post_id             = $order[ $menu_order + 1 ];
950                            $order[ $menu_order + 1 ] = $post_id;
951                            $order[ $menu_order ]     = $swap_post_id;
952
953                            $reorder = true;
954                        }
955                    }
956
957                    if ( $reorder ) {
958                        foreach ( $order as $menu_order => $id ) {
959                            wp_update_post( compact( 'id', 'menu_order' ) );
960                        }
961                    }
962
963                    break;
964                case 'move-menu-up':
965                case 'move-menu-down':
966                    $reorder = false;
967
968                    if ( empty( $_GET['term_id'] ) ) {
969                        break;
970                    }
971
972                    $term_id = (int) $_GET['term_id'];
973
974                    $terms = $this->get_menus();
975
976                    $order = array();
977                    foreach ( $terms as $term ) {
978                        $order[] = $term->term_id;
979                    }
980
981                    if ( 'move-menu-up' === $action ) {
982                        check_admin_referer( 'nova_move_menu_up_' . $term_id );
983
984                        $first_term_id = $order[0];
985                        if ( $term_id === $first_term_id ) {
986                            break;
987                        }
988
989                        foreach ( $order as $menu_order => $order_term_id ) {
990                            if ( $term_id !== $order_term_id ) {
991                                continue;
992                            }
993
994                            $swap_term_id             = $order[ $menu_order - 1 ];
995                            $order[ $menu_order - 1 ] = $term_id;
996                            $order[ $menu_order ]     = $swap_term_id;
997
998                            $reorder = true;
999                            break;
1000                        }
1001                    } else {
1002                        check_admin_referer( 'nova_move_menu_down_' . $term_id );
1003
1004                        $last_term_id = end( $order );
1005                        if ( $term_id === $last_term_id ) {
1006                            break;
1007                        }
1008
1009                        foreach ( $order as $menu_order => $order_term_id ) {
1010                            if ( $term_id !== $order_term_id ) {
1011                                continue;
1012                            }
1013
1014                            $swap_term_id             = $order[ $menu_order + 1 ];
1015                            $order[ $menu_order + 1 ] = $term_id;
1016                            $order[ $menu_order ]     = $swap_term_id;
1017
1018                            $reorder = true;
1019                        }
1020                    }
1021
1022                    if ( $reorder ) {
1023                        update_option( 'nova_menu_order', $order );
1024                    }
1025
1026                    break;
1027                default:
1028                    return;
1029            }
1030
1031            $redirect = add_query_arg(
1032                array(
1033                    'post_type'      => self::MENU_ITEM_POST_TYPE,
1034                    'nova_reordered' => '1',
1035                ),
1036                admin_url( 'edit.php' )
1037            );
1038            wp_safe_redirect( $redirect );
1039            exit( 0 );
1040        }
1041
1042        /**
1043         * Add menu title rows to the list table
1044         *
1045         * @param \WP_Post $post The Post object.
1046         *
1047         * @return void
1048         */
1049        public function show_menu_titles_in_menu_item_list( $post ) {
1050            global $wp_list_table;
1051
1052            static $last_term_id = false;
1053
1054            $term = $this->get_menu_item_menu_leaf( $post->ID );
1055
1056            $term_id = $term instanceof \WP_Term ? $term->term_id : null;
1057
1058            if ( false !== $last_term_id && $last_term_id === $term_id ) {
1059                return;
1060            }
1061
1062            if ( $term_id === null ) {
1063                $last_term_id = null;
1064                $term_name    = '';
1065                $parent_count = 0;
1066            } else {
1067                $last_term_id = $term->term_id;
1068                $term_name    = $term->name;
1069                $parent_count = 0;
1070                $current_term = $term;
1071                while ( $current_term->parent ) {
1072                    ++$parent_count;
1073                    $current_term = get_term( $current_term->parent, self::MENU_TAX );
1074                }
1075            }
1076
1077            $non_order_column_count = $wp_list_table->get_column_count() - 1;
1078
1079            $screen = get_current_screen();
1080
1081            $url = admin_url( $screen->parent_file );
1082
1083            $up_url = add_query_arg(
1084                array(
1085                    'action'  => 'move-menu-up',
1086                    'term_id' => (int) $term_id,
1087                ),
1088                wp_nonce_url( $url, 'nova_move_menu_up_' . $term_id )
1089            );
1090
1091            $down_url = add_query_arg(
1092                array(
1093                    'action'  => 'move-menu-down',
1094                    'term_id' => (int) $term_id,
1095                ),
1096                wp_nonce_url( $url, 'nova_move_menu_down_' . $term_id )
1097            );
1098
1099            ?>
1100            <tr class="no-items menu-label-row" data-term_id="<?php echo esc_attr( (string) $term_id ); ?>">
1101                <td class="colspanchange" colspan="<?php echo (int) $non_order_column_count; ?>">
1102                    <h3>
1103                    <?php
1104                        echo str_repeat( ' &mdash; ', (int) $parent_count ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- nothing to escape here.
1105
1106                    if ( $term instanceof \WP_Term ) {
1107                        echo esc_html( sanitize_term_field( 'name', $term_name, (int) $term_id, self::MENU_TAX, 'display' ) );
1108                        edit_term_link( __( 'edit', 'jetpack-classic-theme-helper' ), '<span class="edit-nova-section"><span class="dashicon dashicon-edit"></span>', '</span>', $term );
1109
1110                    } else {
1111                        esc_html_e( 'Uncategorized', 'jetpack-classic-theme-helper' );
1112                    }
1113                    ?>
1114                    </h3>
1115                </td>
1116                <td>
1117                    <?php if ( $term instanceof \WP_Term ) { ?>
1118                    <a class="nova-move-menu-up" title="<?php esc_attr_e( 'Move menu section up', 'jetpack-classic-theme-helper' ); ?>" href="<?php echo esc_url( $up_url ); ?>"><?php echo esc_html_x( 'UP', 'indicates movement (up or down)', 'jetpack-classic-theme-helper' ); ?></a>
1119                    <br />
1120                    <a class="nova-move-menu-down" title="<?php esc_attr_e( 'Move menu section down', 'jetpack-classic-theme-helper' ); ?>" href="<?php echo esc_url( $down_url ); ?>"><?php echo esc_html_x( 'DOWN', 'indicates movement (up or down)', 'jetpack-classic-theme-helper' ); ?></a>
1121                    <?php } ?>
1122                </td>
1123            </tr>
1124            <?php
1125        }
1126
1127        /* Edit Many Items */
1128
1129        /**
1130         * Handle form submissions that aim to add many menu items at once.
1131         * (process posted data and enqueue necessary script).
1132         *
1133         * @return void
1134         */
1135        public function add_many_new_items_page_load() {
1136            if (
1137                isset( $_SERVER['REQUEST_METHOD'] )
1138                && 'POST' === strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) )
1139            ) {
1140                $this->process_form_request();
1141                exit( 0 );
1142            }
1143
1144            $this->enqueue_many_items_scripts();
1145        }
1146
1147        /**
1148         * Enqueue script to create many items at once.
1149         *
1150         * @return void
1151         */
1152        public function enqueue_many_items_scripts() {
1153
1154            Assets::register_script(
1155                'nova-many-items',
1156                '../../dist/custom-post-types/js/many-items.js',
1157                __FILE__,
1158                array(
1159                    'in_footer'  => true,
1160                    'enqueue'    => true,
1161                    'textdomain' => 'jetpack-classic-theme-helper',
1162                )
1163            );
1164        }
1165
1166        /**
1167         * Process form request to create many items at once.
1168         *
1169         * @return void
1170         */
1171        public function process_form_request() {
1172            if ( ! isset( $_POST['nova_title'] ) || ! is_array( $_POST['nova_title'] ) ) {
1173                return;
1174            }
1175
1176            $is_ajax = ! empty( $_POST['ajax'] );
1177
1178            if ( $is_ajax ) {
1179                check_ajax_referer( 'nova_many_items' );
1180            } else {
1181                check_admin_referer( 'nova_many_items' );
1182            }
1183
1184            /*
1185            * $_POST is already slashed
1186            * phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1187            */
1188            foreach ( array_keys( $_POST['nova_title'] ) as $key ) : // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- we sanitize below.
1189                $post_details = array(
1190                    'post_status'  => 'publish',
1191                    'post_type'    => self::MENU_ITEM_POST_TYPE,
1192                    'post_content' => ! empty( $_POST['nova_content'] ) && ! empty( $_POST['nova_content'][ $key ] )
1193                        ? sanitize_text_field( $_POST['nova_content'][ $key ] )
1194                        : '',
1195                    'post_title'   => isset( $_POST['nova_title'][ $key ] )
1196                        ? sanitize_title( $_POST['nova_title'][ $key ] )
1197                        : '',
1198                    'tax_input'    => array(
1199                        self::MENU_ITEM_LABEL_TAX => isset( $_POST['nova_labels'][ $key ] )
1200                            ? sanitize_meta( self::MENU_ITEM_LABEL_TAX, $_POST['nova_labels'][ $key ], 'term' )
1201                            : null,
1202                        self::MENU_TAX            => isset( $_POST['nova_menu_tax'] )
1203                            ? sanitize_meta( self::MENU_TAX, $_POST['nova_menu_tax'], 'term' )
1204                            : null,
1205                    ),
1206                );
1207
1208                $post_id = wp_insert_post( $post_details );
1209                if ( ! $post_id || is_wp_error( $post_id ) ) {
1210                    continue;
1211                }
1212
1213                $this->set_price(
1214                    $post_id,
1215                    isset( $_POST['nova_price'][ $key ] )
1216                        ? sanitize_meta( 'nova_price', $_POST['nova_price'][ $key ], 'post' )
1217                        : ''
1218                );
1219                // phpcs:enable WordPress.Security.ValidatedSanitizedInput.MissingUnslash
1220
1221                if ( $is_ajax ) :
1222                    $post            = get_post( $post_id );
1223                    $GLOBALS['post'] = $post; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1224                    setup_postdata( $post );
1225
1226                    ?>
1227                <td><?php the_title(); ?></td>
1228                <td class="nova-price"><?php $this->display_price(); ?></td>
1229                <td><?php $this->list_labels( $post_id ); ?></td>
1230                <td><?php the_content(); ?></td>
1231                    <?php
1232                endif;
1233
1234            endforeach;
1235
1236            if ( $is_ajax ) {
1237                exit( 0 );
1238            }
1239
1240            wp_safe_redirect( admin_url( 'edit.php?post_type=' . self::MENU_ITEM_POST_TYPE ) );
1241            exit( 0 );
1242        }
1243
1244        /**
1245         * Admin page contents for adding many menu items at once.
1246         *
1247         * @return void
1248         */
1249        public function add_many_new_items_page() {
1250            ?>
1251        <div class="wrap">
1252            <h2><?php esc_html_e( 'Add Many Items', 'jetpack-classic-theme-helper' ); ?></h2>
1253
1254            <p>
1255            <?php
1256            echo wp_kses(
1257                __( 'Use the <kbd>TAB</kbd> key on your keyboard to move between columns and the <kbd>ENTER</kbd> or <kbd>RETURN</kbd> key to save each row and move on to the next.', 'jetpack-classic-theme-helper' ),
1258                array(
1259                    'kbd' => array(),
1260                )
1261            );
1262            ?>
1263            </p>
1264
1265            <form method="post" action="" enctype="multipart/form-data">
1266                <p>
1267                    <h3><?php esc_html_e( 'Add to section:', 'jetpack-classic-theme-helper' ); ?>
1268                    <?php
1269                    wp_dropdown_categories(
1270                        array(
1271                            'id'           => 'nova-menu-tax',
1272                            'name'         => 'nova_menu_tax',
1273                            'taxonomy'     => self::MENU_TAX,
1274                            'hide_empty'   => false,
1275                            'hierarchical' => true,
1276                        )
1277                    );
1278                    ?>
1279                </h3></p>
1280
1281                <table class="many-items-table wp-list-table widefat">
1282                    <thead>
1283                        <tr>
1284                            <th scope="col"><?php esc_html_e( 'Name', 'jetpack-classic-theme-helper' ); ?></th>
1285                            <th scope="col" class="nova-price"><?php esc_html_e( 'Price', 'jetpack-classic-theme-helper' ); ?></th>
1286                            <th scope="col">
1287                                <?php
1288                                echo wp_kses(
1289                                    __( 'Labels: <small>spicy, favorite, etc. <em>Separate Labels with commas</em></small>', 'jetpack-classic-theme-helper' ),
1290                                    array(
1291                                        'small' => array(),
1292                                        'em'    => array(),
1293                                    )
1294                                );
1295                                ?>
1296                            </th>
1297                            <th scope="col"><?php esc_html_e( 'Description', 'jetpack-classic-theme-helper' ); ?></th>
1298                        </tr>
1299                    </thead>
1300                    <tbody>
1301                        <tr>
1302                            <td><input type="text" name="nova_title[]" aria-required="true" /></td>
1303                            <td class="nova-price"><input type="text" name="nova_price[]" /></td>
1304                            <td><input type="text" name="nova_labels[]" /></td>
1305                            <td><textarea name="nova_content[]" cols="20" rows="1"></textarea>
1306                        </tr>
1307                    </tbody>
1308                    <tbody>
1309                        <tr>
1310                            <td><input type="text" name="nova_title[]" aria-required="true" /></td>
1311                            <td class="nova-price"><input type="text" name="nova_price[]" /></td>
1312                            <td><input type="text" name="nova_labels[]" /></td>
1313                            <td><textarea name="nova_content[]" cols="20" rows="1"></textarea>
1314                        </tr>
1315                    </tbody>
1316                    <tfoot>
1317                        <tr>
1318                            <th><a class="button button-secondary nova-new-row"><span class="dashicon dashicon-plus"></span> <?php esc_html_e( 'New Row', 'jetpack-classic-theme-helper' ); ?></a></th>
1319                            <th class="nova-price"></th>
1320                            <th></th>
1321                            <th></th>
1322                        </tr>
1323                    </tfoot>
1324                </table>
1325
1326                <p class="submit">
1327                    <input type="submit" class="button-primary" value="<?php esc_attr_e( 'Add These New Menu Items', 'jetpack-classic-theme-helper' ); ?>" />
1328                    <?php wp_nonce_field( 'nova_many_items' ); ?>
1329                </p>
1330            </form>
1331        </div>
1332            <?php
1333        }
1334
1335        /* Edit One Item */
1336
1337        /**
1338         * Create admin meta box to save price for a menu item,
1339         * and add script to add extra checkboxes to the UI.
1340         *
1341         * @return void
1342         */
1343        public function register_menu_item_meta_boxes() {
1344            wp_enqueue_script( 'nova-menu-checkboxes' );
1345
1346            add_meta_box(
1347                'menu_item_price',
1348                __( 'Price', 'jetpack-classic-theme-helper' ),
1349                array( $this, 'menu_item_price_meta_box' ),
1350                array(),
1351                'side',
1352                'high'
1353            );
1354        }
1355
1356        /**
1357         * Meta box to edit the price of a menu item.
1358         *
1359         * @param WP_Post $post The post object.
1360         *
1361         * @return void
1362         */
1363        public function menu_item_price_meta_box( $post ) {
1364            printf(
1365                '<label for="nova-price-%1$s" class="screen-reader-text">%2$s</label><input type="text" id="nova-price-%1$s" class="widefat" name="nova_price[%1$s]" value="%3$s" />',
1366                (int) $post->ID,
1367                esc_html__( 'Price', 'jetpack-classic-theme-helper' ),
1368                esc_attr( $this->get_price( (int) $post->ID ) )
1369            );
1370        }
1371
1372        /**
1373         * Save the price of a menu item.
1374         *
1375         * @param int $post_id Post ID.
1376         */
1377        public function add_post_meta( $post_id ) {
1378            if ( ! isset( $_POST['nova_price'][ $post_id ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce handling happens via core, since we hook into wp_insert_post.
1379                return;
1380            }
1381
1382            $this->set_price(
1383                $post_id,
1384                sanitize_meta( 'nova_price', wp_unslash( $_POST['nova_price'][ $post_id ] ), 'post' ) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce handling happens via core, since we hook into wp_insert_post.
1385            );
1386        }
1387
1388        /* Data */
1389
1390        /**
1391         * Get ordered array of menu items.
1392         *
1393         * @param array $args Optional argumments.
1394         *
1395         * @return array
1396         */
1397        public function get_menus( $args = array() ) {
1398            $args             = wp_parse_args(
1399                $args,
1400                array(
1401                    'hide_empty' => false,
1402                )
1403            );
1404            $args['taxonomy'] = self::MENU_TAX;
1405
1406            $terms = get_terms( $args ); // @phan-suppress-current-line PhanAccessMethodInternal @phan-suppress-current-line UnusedSuppression -- Fixed in WP 6.9, but then we need a suppression for the WP 6.8 compat run. @todo Remove this suppression when we drop WP <6.9.
1407            if ( ! $terms || is_wp_error( $terms ) ) {
1408                return array();
1409            }
1410
1411            $terms_by_id = array();
1412            foreach ( $terms as $term ) {
1413                $terms_by_id[ "{$term->term_id}" ] = $term;
1414            }
1415
1416            $term_order = get_option( 'nova_menu_order', array() );
1417
1418            $return = array();
1419            foreach ( $term_order as $term_id ) {
1420                if ( isset( $terms_by_id[ "$term_id" ] ) ) {
1421                    $return[] = $terms_by_id[ "$term_id" ];
1422                    unset( $terms_by_id[ "$term_id" ] );
1423                }
1424            }
1425
1426            foreach ( $terms_by_id as $term_id => $term ) {
1427                $return[] = $term;
1428            }
1429
1430            return $return;
1431        }
1432
1433        /**
1434         * Get first menu taxonomy "leaf".
1435         *
1436         * @param int $post_id Post ID.
1437         *
1438         * @return bool|\WP_Term|\WP_Error|null
1439         */
1440        public function get_menu_item_menu_leaf( $post_id ) {
1441            // Get first menu taxonomy "leaf".
1442            $term_ids = wp_get_object_terms( $post_id, self::MENU_TAX, array( 'fields' => 'ids' ) );
1443
1444            foreach ( $term_ids as $term_id ) { // possibly ignore PhanTypeSuspiciousNonTraversableForeach
1445                $children = get_term_children( $term_id, self::MENU_TAX );
1446                if ( ! $children ) {
1447                    break;
1448                }
1449            }
1450
1451            if ( ! isset( $term_id ) ) {
1452                return false;
1453            }
1454
1455            return get_term( $term_id, self::MENU_TAX );
1456        }
1457
1458        /**
1459         * Get a list of the labels linked to a menu item.
1460         *
1461         * @param int $post_id Post ID.
1462         *
1463         * @return void
1464         */
1465        public function list_labels( $post_id = 0 ) {
1466            $post = get_post( $post_id );
1467            echo get_the_term_list( $post->ID, self::MENU_ITEM_LABEL_TAX, '', _x( ', ', 'Nova label separator', 'jetpack-classic-theme-helper' ), '' );
1468        }
1469
1470        /**
1471         * Get a list of the labels linked to a menu item, with links to manage them.
1472         *
1473         * @param int $post_id Post ID.
1474         *
1475         * @return void
1476         */
1477        public function list_admin_labels( $post_id = 0 ) {
1478            $post   = get_post( $post_id );
1479            $labels = get_the_terms( $post->ID, self::MENU_ITEM_LABEL_TAX );
1480            if ( ! empty( $labels ) ) {
1481                $out = array();
1482                foreach ( $labels as $label ) { // possibly ignore PhanTypeSuspiciousNonTraversableForeach
1483                    $out[] = sprintf(
1484                        '<a href="%s">%s</a>',
1485                        esc_url(
1486                            add_query_arg(
1487                                array(
1488                                    'post_type' => self::MENU_ITEM_POST_TYPE,
1489                                    'taxonomy'  => self::MENU_ITEM_LABEL_TAX,
1490                                    'term'      => $label->slug,
1491                                ),
1492                                'edit.php'
1493                            )
1494                        ),
1495                        esc_html(
1496                            sanitize_term_field( 'name', $label->name, $label->term_id, self::MENU_ITEM_LABEL_TAX, 'display' )
1497                        )
1498                    );
1499                }
1500
1501                echo implode( _x( ', ', 'Nova label separator', 'jetpack-classic-theme-helper' ), $out ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- we build $out ourselves and escape things there.
1502            } else {
1503                esc_html_e( 'No Labels', 'jetpack-classic-theme-helper' );
1504            }
1505        }
1506
1507        /**
1508         * Update post meta with the price defined in meta box.
1509         *
1510         * @param int    $post_id Post ID.
1511         * @param string $price   Price.
1512         *
1513         * @return int|bool
1514         */
1515        public function set_price( $post_id = 0, $price = '' ) {
1516            return update_post_meta( $post_id, 'nova_price', $price );
1517        }
1518
1519        /**
1520         * Get the price of a menu item.
1521         *
1522         * @param int $post_id Post ID.
1523         *
1524         * @return bool|string
1525         */
1526        public function get_price( $post_id = 0 ) {
1527            return get_post_meta( $post_id, 'nova_price', true );
1528        }
1529
1530        /**
1531         * Echo the price of a menu item.
1532         *
1533         * @param int $post_id Post ID.
1534         *
1535         * @return void
1536         */
1537        public function display_price( $post_id = 0 ) {
1538            echo esc_html( $this->get_price( $post_id ) );
1539        }
1540
1541        /* Menu Item Loop Markup */
1542
1543        /**
1544         * Get markup for a menu item.
1545         * Note: Does not support nested loops.
1546         *
1547         * @param null|string $field The field to get the value for.
1548         *
1549         * @return array
1550         */
1551        public function get_menu_item_loop_markup( $field = null ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1552            return $this->menu_item_loop_markup;
1553        }
1554
1555        /**
1556         * Sets up the loop markup.
1557         * Attached to the 'template_include' *filter*,
1558         * which fires only during a real blog view (not in admin, feeds, etc.)
1559         *
1560         * @param string $template Template File.
1561         *
1562         * @return string Template File.  VERY Important.
1563         */
1564        public function setup_menu_item_loop_markup__in_filter( $template ) {
1565            add_action( 'loop_start', array( $this, 'start_menu_item_loop' ) );
1566
1567            return $template;
1568        }
1569
1570        /**
1571         * If the Query is a Menu Item Query, start outputing the Menu Item Loop Marku
1572         * Attached to the 'loop_start' action.
1573         *
1574         * @param WP_Query $query Post query.
1575         *
1576         * @return void
1577         */
1578        public function start_menu_item_loop( $query ) {
1579            if ( ! $this->is_menu_item_query( $query ) ) {
1580                return;
1581            }
1582
1583            $this->menu_item_loop_last_term_id = false;
1584            $this->menu_item_loop_current_term = false;
1585
1586            add_action( 'the_post', array( $this, 'menu_item_loop_each_post' ) );
1587            add_action( 'loop_end', array( $this, 'stop_menu_item_loop' ) );
1588        }
1589
1590        /**
1591         * Outputs the Menu Item Loop Markup
1592         * Attached to the 'the_post' action.
1593         *
1594         * @param WP_Post $post Post object.
1595         *
1596         * @return void
1597         */
1598        public function menu_item_loop_each_post( $post ) {
1599            $this->menu_item_loop_current_term = $this->get_menu_item_menu_leaf( $post->ID );
1600
1601            if (
1602                false === $this->menu_item_loop_current_term
1603                || null === $this->menu_item_loop_current_term
1604                || is_wp_error( $this->menu_item_loop_current_term )
1605            ) {
1606                return;
1607            }
1608
1609            if ( false === $this->menu_item_loop_last_term_id ) {
1610                // We're at the very beginning of the loop
1611
1612                $this->menu_item_loop_open_element( 'menu' ); // Start a new menu section
1613                $this->menu_item_loop_header(); // Output the menu's header
1614            } elseif (
1615                is_object( $this->menu_item_loop_current_term ) &&
1616                $this->menu_item_loop_last_term_id !== $this->menu_item_loop_current_term->term_id
1617            ) {
1618                // We're not at the very beginning but still need to start a new menu section.  End the previous menu section first.
1619
1620                $this->menu_item_loop_close_element( 'menu' ); // End the previous menu section
1621                $this->menu_item_loop_open_element( 'menu' ); // Start a new menu section
1622                $this->menu_item_loop_header(); // Output the menu's header
1623            }
1624            if ( is_object( $this->menu_item_loop_current_term ) ) {
1625
1626                $this->menu_item_loop_last_term_id = $this->menu_item_loop_current_term->term_id;
1627            }
1628        }
1629
1630        /**
1631         * If the Query is a Menu Item Query, stop outputing the Menu Item Loop Marku
1632         * Attached to the 'loop_end' action.
1633         *
1634         * @param WP_Query $query Post query.
1635         *
1636         * @return void
1637         */
1638        public function stop_menu_item_loop( $query ) {
1639            if ( ! $this->is_menu_item_query( $query ) ) {
1640                return;
1641            }
1642
1643            remove_action( 'the_post', array( $this, 'menu_item_loop_each_post' ) );
1644            remove_action( 'loop_start', array( $this, 'start_menu_item_loop' ) );
1645            remove_action( 'loop_end', array( $this, 'stop_menu_item_loop' ) );
1646
1647            $this->menu_item_loop_close_element( 'menu' ); // End the last menu section
1648        }
1649
1650        /**
1651         * Outputs the Menu Group Header
1652         *
1653         * @return void
1654         */
1655        public function menu_item_loop_header() {
1656            $this->menu_item_loop_open_element( 'menu_header' );
1657                $this->menu_item_loop_open_element( 'menu_title' );
1658                    echo esc_html( $this->menu_item_loop_current_term->name ); // @todo tax filter
1659                $this->menu_item_loop_close_element( 'menu_title' );
1660            if ( $this->menu_item_loop_current_term->description ) :
1661                $this->menu_item_loop_open_element( 'menu_description' );
1662                    echo esc_html( $this->menu_item_loop_current_term->description ); // @todo kses, tax filter
1663                $this->menu_item_loop_close_element( 'menu_description' );
1664            endif;
1665            $this->menu_item_loop_close_element( 'menu_header' );
1666        }
1667
1668        /**
1669         * Outputs a Menu Item Markup element opening tag
1670         *
1671         * @param string $field - Menu Item Markup settings field.
1672         *
1673         * @return void
1674         */
1675        public function menu_item_loop_open_element( $field ) {
1676            $markup = $this->get_menu_item_loop_markup();
1677            /**
1678             * Filter a menu item's element opening tag.
1679             *
1680             * @module custom-content-types
1681             *
1682             * @since 4.4.0
1683             *
1684             * @param string       $tag    Menu item's element opening tag.
1685             * @param string       $field  Menu Item Markup settings field.
1686             * @param array        $markup Array of markup elements for the menu item.
1687             * @param false|object $term   Taxonomy term for current menu item.
1688             */
1689            echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- it's escaped in menu_item_loop_class.
1690                'jetpack_nova_menu_item_loop_open_element',
1691                '<' . tag_escape( $markup[ "{$field}_tag" ] ) . $this->menu_item_loop_class( $markup[ "{$field}_class" ] ) . ">\n",
1692                $field,
1693                $markup,
1694                $this->menu_item_loop_current_term
1695            );
1696        }
1697
1698        /**
1699         * Outputs a Menu Item Markup element closing tag
1700         *
1701         * @param string $field - Menu Item Markup settings field.
1702         *
1703         * @return void
1704         */
1705        public function menu_item_loop_close_element( $field ) {
1706            $markup = $this->get_menu_item_loop_markup();
1707            /**
1708             * Filter a menu item's element closing tag.
1709             *
1710             * @module custom-content-types
1711             *
1712             * @since 4.4.0
1713             *
1714             * @param string       $tag    Menu item's element closing tag.
1715             * @param string       $field  Menu Item Markup settings field.
1716             * @param array        $markup Array of markup elements for the menu item.
1717             * @param false|object $term   Taxonomy term for current menu item.
1718             */
1719            echo apply_filters( // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- tag_escape is used.
1720                'jetpack_nova_menu_item_loop_close_element',
1721                '</' . tag_escape( $markup[ "{$field}_tag" ] ) . ">\n",
1722                $field,
1723                $markup,
1724                $this->menu_item_loop_current_term
1725            );
1726        }
1727
1728        /**
1729         * Returns a Menu Item Markup element's class attribute.
1730         *
1731         * @param  string $class Class name.
1732         *
1733         * @return string HTML class attribute with leading whitespace.
1734         */
1735        public function menu_item_loop_class( $class ) {
1736            if ( ! $class ) {
1737                return '';
1738            }
1739
1740            /**
1741             * Filter a menu Item Markup element's class attribute.
1742             *
1743             * @module custom-content-types
1744             *
1745             * @since 4.4.0
1746             *
1747             * @param string       $tag    Menu Item Markup element's class attribute.
1748             * @param string       $class  Menu Item Class name.
1749             * @param false|object $term   Taxonomy term for current menu item.
1750             */
1751            return apply_filters(
1752                'jetpack_nova_menu_item_loop_class',
1753                ' class="' . esc_attr( $class ) . '"',
1754                $class,
1755                $this->menu_item_loop_current_term
1756            );
1757        }
1758    }
1759
1760}