Plugin Directory

source: code-snippets/trunk/php/class-list-table.php

Last change on this file was 3392896, checked in by codesnippetspro, 2 months ago

Version v3.9.0-beta.2

File size: 43.8 KB
Line 
1<?php
2/**
3 * Contains the class for handling the snippets table
4 *
5 * @package Code_Snippets
6 *
7 * phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
8 */
9
10namespace Code_Snippets;
11
12use WP_List_Table;
13use function Code_Snippets\Settings\get_setting;
14
15// The WP_List_Table base class is not included by default, so we need to load it.
16if ( ! class_exists( 'WP_List_Table' ) ) {
17        require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
18}
19
20/**
21 * This class handles the table for the manage snippets menu
22 *
23 * @since   1.5
24 * @package Code_Snippets
25 */
26class List_Table extends WP_List_Table {
27
28        /**
29         * Whether the current screen is in the network admin
30         *
31         * @var bool
32         */
33        public bool $is_network;
34
35        /**
36         * A list of statuses (views)
37         *
38         * @var array<string>
39         */
40        public array $statuses = [ 'all', 'active', 'inactive', 'recently_activated', 'shared_network', 'trashed' ];
41
42        /**
43         * Column name to use when ordering the snippets list.
44         *
45         * @var string
46         */
47        protected string $order_by;
48
49        /**
50         * Direction to use when ordering the snippets list. Either 'asc' or 'desc'.
51         *
52         * @var string
53         */
54        protected string $order_dir;
55
56        /**
57         * List of active snippets indexed by attached condition ID.
58         *
59         * @var array <int, Snippet[]>
60         */
61        protected array $active_by_condition = [];
62
63        /**
64         * The constructor function for our class.
65         * Registers hooks, initializes variables, setups class.
66         *
67         * @phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
68         */
69        public function __construct() {
70                global $status, $page;
71                $this->is_network = is_network_admin();
72
73                // Determine the status.
74                $status = apply_filters( 'code_snippets/list_table/default_view', 'all' );
75                if ( isset( $_REQUEST['status'] ) && in_array( sanitize_key( $_REQUEST['status'] ), $this->statuses, true ) ) {
76                        $status = sanitize_key( $_REQUEST['status'] );
77                }
78
79                // Add the search query to the URL.
80                if ( isset( $_REQUEST['s'] ) ) {
81                        $_SERVER['REQUEST_URI'] = add_query_arg( 's', sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) );
82                }
83
84                // Add a snippets per page screen option.
85                $page = $this->get_pagenum();
86
87                add_screen_option(
88                        'per_page',
89                        array(
90                                'label'   => __( 'Snippets per page', 'code-snippets' ),
91                                'default' => 999,
92                                'option'  => 'snippets_per_page',
93                        )
94                );
95
96                add_filter( 'default_hidden_columns', array( $this, 'default_hidden_columns' ) );
97
98                // Strip the result query arg from the URL.
99                $_SERVER['REQUEST_URI'] = remove_query_arg( 'result' );
100
101                // Add filters to format the snippet description in the same way the post content is formatted.
102                $filters = [ 'wptexturize', 'convert_smilies', 'convert_chars', 'wpautop', 'shortcode_unautop', 'capital_P_dangit', [ $this, 'wp_kses_desc' ] ];
103                foreach ( $filters as $filter ) {
104                        add_filter( 'code_snippets/list_table/column_description', $filter );
105                }
106
107                // Set up the class.
108                parent::__construct(
109                        array(
110                                'ajax'     => true,
111                                'plural'   => 'snippets',
112                                'singular' => 'snippet',
113                        )
114                );
115        }
116
117        /**
118         * Determine if a condition is considered 'active' by checking if it is attached to any active snippets.
119         *
120         * @param Snippet $condition Condition snippet to check.
121         *
122         * @return bool
123         */
124        protected function is_condition_active( Snippet $condition ): bool {
125                return $condition->is_condition()
126                       && isset( $this->active_by_condition[ $condition->id ] )
127                       && count( $this->active_by_condition[ $condition->id ] ) > 0;
128        }
129
130        /**
131         * Apply a more permissive version of wp_kses_post() to the snippet description.
132         *
133         * @param string $data Description content to filter.
134         *
135         * @return string Filtered description content with allowed HTML tags and attributes intact.
136         */
137        public function wp_kses_desc( string $data ): string {
138                $safe_style_filter = function ( $styles ) {
139                        $styles[] = 'display';
140                        return $styles;
141                };
142
143                add_filter( 'safe_style_css', $safe_style_filter );
144                $data = wp_kses_post( $data );
145                remove_filter( 'safe_style_css', $safe_style_filter );
146
147                return $data;
148        }
149
150        /**
151         * Set the 'id' column as hidden by default.
152         *
153         * @param array<string> $hidden List of hidden columns.
154         *
155         * @return array<string> Modified list of hidden columns.
156         */
157        public function default_hidden_columns( array $hidden ): array {
158                array_push( $hidden, 'id', 'code', 'cloud_id', 'revision' );
159                return $hidden;
160        }
161
162        /**
163         * Set the 'name' column as the primary column.
164         *
165         * @return string
166         */
167        protected function get_default_primary_column_name(): string {
168                return 'name';
169        }
170
171        /**
172         * Define the output of all columns that have no callback function
173         *
174         * @param Snippet $item        The snippet used for the current row.
175         * @param string  $column_name The name of the column being printed.
176         *
177         * @return string The content of the column to output.
178         */
179        protected function column_default( $item, $column_name ): string {
180                switch ( $column_name ) {
181                        case 'id':
182                                return $item->id;
183
184                        case 'description':
185                                return apply_filters( 'code_snippets/list_table/column_description', $item->desc );
186
187                        case 'type':
188                                $type = $item->type;
189                                $url = add_query_arg( 'type', $type );
190
191                                return sprintf(
192                                        '<a class="badge %s-badge" href="%s">%s</a>',
193                                        esc_attr( $type ),
194                                        esc_url( $url ),
195                                        'cond' === $type ? '<span class="dashicons dashicons-randomize"></span>' : esc_html( $type )
196                                );
197
198                        case 'date':
199                                return $item->modified ? $item->format_modified() : '&#8212;';
200
201                        default:
202                                return apply_filters( "code_snippets/list_table/column_$column_name", '&#8212;', $item );
203                }
204        }
205
206        /**
207         * Retrieve a URL to perform an action on a snippet
208         *
209         * @param string  $action  Name of action to produce a link for.
210         * @param Snippet $snippet Snippet object to produce link for.
211         *
212         * @return string URL to perform action.
213         */
214        public function get_action_link( string $action, Snippet $snippet ): string {
215
216                // Redirect actions to the network dashboard for shared network snippets.
217                $local_actions = array( 'activate', 'activate-shared', 'run-once', 'run-once-shared' );
218                $network_redirect = $snippet->shared_network && ! $this->is_network && ! in_array( $action, $local_actions, true );
219
220                // Edit links go to a different menu.
221                if ( 'edit' === $action ) {
222                        return code_snippets()->get_snippet_edit_url( $snippet->id, $network_redirect ? 'network' : 'self' );
223                }
224
225                $query_args = array(
226                        'action' => $action,
227                        'id'     => $snippet->id,
228                        'scope'  => $snippet->scope,
229                );
230
231                $url = $network_redirect ?
232                        add_query_arg( $query_args, code_snippets()->get_menu_url( 'manage', 'network' ) ) :
233                        add_query_arg( $query_args );
234
235                // Add a nonce to the URL for security purposes.
236                return wp_nonce_url( $url, 'code_snippets_manage_snippet_' . $snippet->id );
237        }
238
239        /**
240         * Build a list of action links for individual snippets
241         *
242         * @param Snippet $snippet The current snippet.
243         *
244         * @return array<string, string> The action links HTML.
245         */
246        private function get_snippet_action_links( Snippet $snippet ): array {
247                $actions = array();
248
249                if ( $snippet->shared_network && ! $this->is_network ) {
250               $actions['network_shared'] = sprintf(
251                                '<span class="badge">%s</span>',
252                                esc_html__( 'Network Snippet', 'code-snippets' )
253               );
254
255                        if ( is_multisite() && is_super_admin() ) {
256                                $actions['edit'] = sprintf(
257                                        '<a href="%s">%s</a>',
258                                        esc_url( $this->get_action_link( 'edit', $snippet ) ),
259                                        esc_html__( 'Edit', 'code-snippets' )
260                                );
261                        }
262
263                        return apply_filters( 'code_snippets/list_table/row_actions', $actions, $snippet );
264                }
265
266                if ( $snippet->is_trashed() ) {
267                        $actions['restore'] = sprintf(
268                                '<a href="%s">%s</a>',
269                                esc_url( $this->get_action_link( 'restore', $snippet ) ),
270                                esc_html__( 'Restore', 'code-snippets' )
271                        );
272
273                        $actions['delete_permanently'] = sprintf(
274                                '<a href="%2$s" class="delete" onclick="%3$s">%1$s</a>',
275                                esc_html__( 'Delete Permanently', 'code-snippets' ),
276                                esc_url( $this->get_action_link( 'delete_permanently', $snippet ) ),
277                                esc_js(
278                                        sprintf(
279                                                'return confirm("%s");',
280                                                esc_html__( 'You are about to permanently delete the selected item.', 'code-snippets' ) . "\n" .
281                                                esc_html__( "'Cancel' to stop, 'OK' to delete.", 'code-snippets' )
282                                        )
283                                )
284                        );
285                } elseif ( ! $this->is_network && $snippet->network && ! $snippet->shared_network ) {
286                        // Display special links if on a subsite and dealing with a network-active snippet.
287                        if ( $snippet->active ) {
288                                $actions['network_active'] = esc_html__( 'Network Active', 'code-snippets' );
289                        } else {
290                                $actions['network_only'] = esc_html__( 'Network Only', 'code-snippets' );
291                        }
292                } elseif ( ! $snippet->shared_network || current_user_can( code_snippets()->get_network_cap_name() ) ) {
293
294                        // If the snippet is a shared network snippet, only display extra actions if the user has network permissions.
295                        $simple_actions = array(
296                                'edit'   => esc_html__( 'Edit', 'code-snippets' ),
297                                'clone'  => esc_html__( 'Clone', 'code-snippets' ),
298                                'export' => esc_html__( 'Export', 'code-snippets' ),
299                        );
300
301                        foreach ( $simple_actions as $action => $label ) {
302                                $actions[ $action ] = sprintf( '<a href="%s">%s</a>', esc_url( $this->get_action_link( $action, $snippet ) ), $label );
303                        }
304
305                        $actions['delete'] = sprintf(
306                                '<a href="%2$s" class="delete">%1$s</a>',
307                                esc_html__( 'Trash', 'code-snippets' ),
308                                esc_url( $this->get_action_link( 'delete', $snippet ) )
309                        );
310                }
311
312                return apply_filters( 'code_snippets/list_table/row_actions', $actions, $snippet );
313        }
314
315        /**
316         * Retrieve the code for a snippet activation switch
317         *
318         * @param Snippet $snippet Snippet object.
319         *
320         * @return string Output for activation switch.
321         */
322        protected function column_activate( Snippet $snippet ): string {
323                if ( $snippet->is_trashed() ) {
324                        return '';
325                }
326
327                // Show icon for shared network snippets on network admin.
328                if ( $snippet->shared_network && $this->is_network ) {
329                        return '<span class="dashicons dashicons-networking network-shared" title="' . 
330                                esc_attr__( 'Shared with Subsites', 'code-snippets' ) . 
331                                '"></span>';
332                }
333
334                if ( ! $this->is_network && $snippet->network && ! $snippet->shared_network ) {
335                        return '';
336                }
337
338                switch ( $snippet->scope ) {
339                        case 'single-use':
340                                $class = 'snippet-execution-button';
341                                $action = 'run-once';
342                                $label = esc_html__( 'Run Once', 'code-snippets' );
343                                break;
344
345                        case 'condition':
346                                $edit_url = code_snippets()->get_snippet_edit_url( $snippet->id, $snippet->network ? 'network' : 'admin' );
347
348                                return sprintf(
349                                        '<a href="%s" class="snippet-condition-count">%s</a>',
350                                        esc_url( $edit_url ),
351                                        isset( $this->active_by_condition[ $snippet->id ] )
352                                                ? esc_html( count( $this->active_by_condition[ $snippet->id ] ) )
353                                                : 0
354                                );
355
356                        default:
357                                $class = 'snippet-activation-switch';
358                                $action = $snippet->active ? 'deactivate' : 'activate';
359                                $label = $snippet->network && ! $snippet->shared_network ?
360                                        ( $snippet->active ? __( 'Network Deactivate', 'code-snippets' ) : __( 'Network Activate', 'code-snippets' ) ) :
361                                        ( $snippet->active ? __( 'Deactivate', 'code-snippets' ) : __( 'Activate', 'code-snippets' ) );
362                                break;
363                }
364
365                if ( $snippet->shared_network ) {
366                        $action .= '-shared';
367                }
368
369                return $action && $label
370                        ? sprintf(
371                                '<a class="%1$s" href="%2$s" title="%3$s" aria-label="%3$s">&nbsp;</a> ',
372                                esc_attr( $class ),
373                                esc_url( $this->get_action_link( $action, $snippet ) ),
374                                esc_attr( $label )
375                        )
376                        : '';
377        }
378
379        /**
380         * Build the content of the snippet name column
381         *
382         * @param Snippet $snippet The snippet being used for the current row.
383         *
384         * @return string The content of the column to output.
385         */
386        protected function column_name( Snippet $snippet ): string {
387
388                $row_actions = $this->row_actions(
389                        $this->get_snippet_action_links( $snippet ),
390                        apply_filters( 'code_snippets/list_table/row_actions_always_visible', true )
391                );
392
393                $out = esc_html( $snippet->display_name );
394                $user_can_manage_network = current_user_can( code_snippets()->get_network_cap_name() );
395
396                // Add a link to the snippet if it isn't an unreadable network-only snippet and isn't trashed.
397                if ( ! $snippet->is_trashed() && ( $this->is_network || ! $snippet->network || $user_can_manage_network ) ) {
398                        $out = sprintf(
399                                '<a href="%s" class="snippet-name">%s</a>',
400                                esc_attr( code_snippets()->get_snippet_edit_url( $snippet->id, $snippet->network ? 'network' : 'admin' ) ),
401                                $out
402                        );
403                } else {
404                        $out = sprintf( '<span class="snippet-name">%s</span>', $out );
405                }
406
407                $out = apply_filters( 'code_snippets/list_table/column_name', $out, $snippet );
408                return $out . $row_actions;
409        }
410
411        /**
412         * Handles the checkbox column output.
413         *
414         * @param Snippet $item The snippet being used for the current row.
415         *
416         * @return string The column content to be printed.
417         */
418        protected function column_cb( $item ): string {
419                $out = sprintf(
420                        '<input type="checkbox" name="%s[]" value="%s">',
421                        $item->shared_network ? 'shared_ids' : 'ids',
422                        $item->id
423                );
424
425                return apply_filters( 'code_snippets/list_table/column_cb', $out, $item );
426        }
427
428        /**
429         * Handles the tags column output.
430         *
431         * @param Snippet $snippet The snippet being used for the current row.
432         *
433         * @return string The column output.
434         */
435        protected function column_tags( Snippet $snippet ): string {
436
437                // Return now if there are no tags.
438                if ( empty( $snippet->tags ) ) {
439                        return '';
440                }
441
442                $out = array();
443
444                // Loop through the tags and create a link for each one.
445                foreach ( $snippet->tags as $tag ) {
446                        $out[] = sprintf(
447                                '<a href="%s">%s</a>',
448                                esc_url( add_query_arg( 'tag', esc_attr( $tag ) ) ),
449                                esc_html( $tag )
450                        );
451                }
452
453                return join( ', ', $out );
454        }
455
456        /**
457         * Handles the priority column output.
458         *
459         * @param Snippet $snippet The snippet being used for the current row.
460         *
461         * @return string The column output.
462         */
463        protected function column_priority( Snippet $snippet ): string {
464                return sprintf( '<input type="number" class="snippet-priority" value="%d" step="1" disabled>', $snippet->priority );
465        }
466
467        /**
468         * Define the column headers for the table
469         *
470         * @return array<string, string> The column headers, ID paired with label
471         */
472        public function get_columns(): array {
473                $columns = array(
474                        'cb'          => '<input type="checkbox">',
475                        'activate'    => '',
476                        'name'        => __( 'Name', 'code-snippets' ),
477                        'type'        => __( 'Type', 'code-snippets' ),
478                        'description' => __( 'Description', 'code-snippets' ),
479                        'tags'        => __( 'Tags', 'code-snippets' ),
480                        'date'        => __( 'Modified', 'code-snippets' ),
481                        'priority'    => __( 'Priority', 'code-snippets' ),
482                        'id'          => __( 'ID', 'code-snippets' ),
483                );
484
485                if ( ! get_setting( 'general', 'enable_description' ) ) {
486                        unset( $columns['description'] );
487                }
488
489                if ( ! get_setting( 'general', 'enable_tags' ) ) {
490                        unset( $columns['tags'] );
491                }
492
493                return apply_filters( 'code_snippets/list_table/columns', $columns );
494        }
495
496        /**
497         * Define the columns that can be sorted. The format is:
498         * 'internal-name' => 'orderby'
499         * or
500         * 'internal-name' => array( 'orderby', true )
501         *
502         * The second format will make the initial sorting order be descending.
503         *
504         * @return array<string, string|array<string|bool>> The IDs of the columns that can be sorted
505         */
506        public function get_sortable_columns(): array {
507                $sortable_columns = [
508                        'id'       => [ 'id', true ],
509                        'name'     => 'name',
510                        'type'     => [ 'type', true ],
511                        'date'     => [ 'modified', true ],
512                        'priority' => [ 'priority', true ],
513                ];
514
515                return apply_filters( 'code_snippets/list_table/sortable_columns', $sortable_columns );
516        }
517
518        /**
519         * Define the bulk actions to include in the drop-down menus
520         *
521         * @return array<string, string> An array of menu items with the ID paired to the label
522         */
523        public function get_bulk_actions(): array {
524                global $status;
525
526                if ( 'trashed' === $status ) {
527                        $actions = [
528                                'restore-selected'           => __( 'Restore', 'code-snippets' ),
529                                'delete-permanently-selected' => __( 'Delete Permanently', 'code-snippets' ),
530                        ];
531                } else {
532                        $actions = [
533                                'activate-selected'   => $this->is_network ? __( 'Network Activate', 'code-snippets' ) : __( 'Activate', 'code-snippets' ),
534                                'deactivate-selected' => $this->is_network ? __( 'Network Deactivate', 'code-snippets' ) : __( 'Deactivate', 'code-snippets' ),
535                                'clone-selected'      => __( 'Clone', 'code-snippets' ),
536                                'download-selected'   => __( 'Export Code', 'code-snippets' ),
537                                'export-selected'     => __( 'Export', 'code-snippets' ),
538                                'delete-selected'     => __( 'Move to Trash', 'code-snippets' ),
539                        ];
540                }
541
542                return apply_filters( 'code_snippets/list_table/bulk_actions', $actions );
543        }
544
545        /**
546         * Retrieve the classes for the table
547         *
548         * We override this in order to add 'snippets' as a class for custom styling
549         *
550         * @return array<string> The classes to include on the table element
551         */
552        public function get_table_classes(): array {
553                $classes = array( 'widefat', $this->_args['plural'] );
554
555                return apply_filters( 'code_snippets/list_table/table_classes', $classes );
556        }
557
558        /**
559         * Retrieve the 'views' of the table
560         *
561         * Example: active, inactive, recently active
562         *
563         * @return array<string, string> A list of the view labels linked to the view
564         */
565        public function get_views(): array {
566                global $totals, $status;
567                $status_links = parent::get_views();
568
569                // Loop through the view counts.
570                foreach ( $totals as $type => $count ) {
571                        if ( ! $count ) {
572                                continue;
573                        }
574
575                        switch ( $type ) {
576                                case 'all':
577                                        // translators: %s: total number of snippets.
578                                        $template = _n(
579                                                'All <span class="count">(%s)</span>',
580                                                'All <span class="count">(%s)</span>',
581                                                $count,
582                                                'code-snippets'
583                                        );
584                                        break;
585
586                                case 'active':
587                                        // translators: %s: total number of active snippets.
588                                        $template = _n(
589                                                'Active <span class="count">(%s)</span>',
590                                                'Active <span class="count">(%s)</span>',
591                                                $count,
592                                                'code-snippets'
593                                        );
594                                        break;
595
596                                case 'inactive':
597                                        // translators: %s: total number of inactive snippets.
598                                        $template = _n(
599                                                'Inactive <span class="count">(%s)</span>',
600                                                'Inactive <span class="count">(%s)</span>',
601                                                $count,
602                                                'code-snippets'
603                                        );
604                                        break;
605
606                                case 'recently_activated':
607                                        // translators: %s: total number of recently activated snippets.
608                                        $template = _n(
609                                                'Recently Active <span class="count">(%s)</span>',
610                                                'Recently Active <span class="count">(%s)</span>',
611                                                $count,
612                                                'code-snippets'
613                                        );
614                                        break;
615
616                                case 'shared_network':
617                                        if ( ! is_multisite() ) {
618                                                continue 2;
619                                        }
620
621                                        $shared_label_template = $this->is_network
622                                                ? _n_noop(
623                                                        'Shared with Subsites <span class="count">(%s)</span>',
624                                                        'Shared with Subsites <span class="count">(%s)</span>',
625                                                        'code-snippets'
626                                                )
627                                                : _n_noop(
628                                                        'Network Snippets <span class="count">(%s)</span>',
629                                                        'Network Snippets <span class="count">(%s)</span>',
630                                                        'code-snippets'
631                                                );
632
633                                        $template = translate_nooped_plural( $shared_label_template, $count, 'code-snippets' );
634                                        break;
635
636                                case 'trashed':
637                                        // translators: %s: total number of trashed snippets.
638                                        $template = _n(
639                                                'Trashed <span class="count">(%s)</span>',
640                                                'Trashed <span class="count">(%s)</span>',
641                                                $count,
642                                                'code-snippets'
643                                        );
644                                        break;
645
646                                default:
647                                        continue 2;
648                        }
649
650                        $url = esc_url( add_query_arg( 'status', $type ) );
651                        $class = $type === $status ? ' class="current"' : '';
652                        $text = sprintf( $template, number_format_i18n( $count ) );
653
654                        $status_links[ $type ] = sprintf( '<a href="%s"%s>%s</a>', $url, $class, $text );
655                }
656
657                return apply_filters( 'code_snippets/list_table/views', $status_links );
658        }
659
660        /**
661         * Gets the tags of the snippets currently being viewed in the table
662         *
663         * @since 2.0
664         */
665        public function get_current_tags() {
666                global $snippets, $status;
667
668                // If we're not viewing a snippets table, get all used tags instead.
669                if ( ! isset( $snippets, $status ) ) {
670                        $tags = get_all_snippet_tags();
671                } else {
672                        $tags = array();
673
674                        // Merge all tags into a single array.
675                        foreach ( $snippets[ $status ] as $snippet ) {
676                                $tags = array_merge( $snippet->tags, $tags );
677                        }
678
679                        // Remove duplicate tags.
680                        $tags = array_unique( $tags );
681                }
682
683                sort( $tags );
684
685                return $tags;
686        }
687
688        /**
689         * Add filters and extra actions above and below the table
690         *
691         * @param string $which Whether the actions are displayed on the before (true) or after (false) the table.
692         */
693        public function extra_tablenav( $which ) {
694                /**
695                 * Status global.
696                 *
697                 * @var string $status
698                 */
699                global $status;
700
701                if ( 'top' === $which ) {
702
703                        // Tags dropdown filter.
704                        $tags = $this->get_current_tags();
705
706                        if ( count( $tags ) ) {
707                                $query = isset( $_GET['tag'] ) ? sanitize_text_field( wp_unslash( $_GET['tag'] ) ) : '';
708
709                                echo '<div class="alignleft actions">';
710                                echo '<select name="tag">';
711
712                                printf(
713                                        "<option %s value=''>%s</option>\n",
714                                        selected( $query, '', false ),
715                                        esc_html__( 'Show all tags', 'code-snippets' )
716                                );
717
718                                foreach ( $tags as $tag ) {
719
720                                        printf(
721                                                "<option %s value='%s'>%s</option>\n",
722                                                selected( $query, $tag, false ),
723                                                esc_attr( $tag ),
724                                                esc_html( $tag )
725                                        );
726                                }
727
728                                echo '</select>';
729
730                                submit_button( __( 'Filter', 'code-snippets' ), 'button', 'filter_action', false );
731                                echo '</div>';
732                        }
733                }
734
735                echo '<div class="alignleft actions">';
736
737                if ( 'recently_activated' === $status ) {
738                        submit_button( __( 'Clear List', 'code-snippets' ), 'secondary', 'clear-recent-list', false );
739                }
740
741                do_action( 'code_snippets/list_table/actions', $which );
742
743                echo '</div>';
744        }
745
746        /**
747         * Output form fields needed to preserve important
748         * query vars over form submissions
749         *
750         * @param string $context The context in which the fields are being outputted.
751         */
752        public static function required_form_fields( string $context = 'main' ) {
753                $vars = apply_filters(
754                        'code_snippets/list_table/required_form_fields',
755                        array( 'page', 's', 'status', 'paged', 'tag' ),
756                        $context
757                );
758
759                if ( 'search_box' === $context ) {
760                        // Remove the 's' var if we're doing this for the search box.
761                        $vars = array_diff( $vars, array( 's' ) );
762                }
763
764                foreach ( $vars as $var ) {
765                        if ( ! empty( $_REQUEST[ $var ] ) ) {
766                                $value = sanitize_text_field( wp_unslash( $_REQUEST[ $var ] ) );
767                                printf( '<input type="hidden" name="%s" value="%s" />', esc_attr( $var ), esc_attr( $value ) );
768                                echo "\n";
769                        }
770                }
771
772                do_action( 'code_snippets/list_table/print_required_form_fields', $context );
773        }
774
775        /**
776         * Perform an action on a single snippet.
777         *
778         * @param int    $id     Snippet ID.
779         * @param string $action Action to perform.
780         *
781         * @return bool|string Result of performing action
782         */
783        private function perform_action( int $id, string $action ) {
784                switch ( $action ) {
785
786                        case 'activate':
787                                activate_snippet( $id, $this->is_network );
788                                return 'activated';
789
790                        case 'deactivate':
791                                deactivate_snippet( $id, $this->is_network );
792                                return 'deactivated';
793
794                        case 'run-once':
795                                $this->perform_action( $id, 'activate' );
796                                return 'executed';
797
798                        case 'run-once-shared':
799                                $this->perform_action( $id, 'activate-shared' );
800                                return 'executed';
801
802                        case 'activate-shared':
803                                $active_shared_snippets = get_option( 'active_shared_network_snippets', array() );
804
805                                if ( ! in_array( $id, $active_shared_snippets, true ) ) {
806                                        $active_shared_snippets[] = $id;
807                                        update_option( 'active_shared_network_snippets', $active_shared_snippets );
808                                        clean_active_snippets_cache( code_snippets()->db->ms_table );
809                                }
810
811                                return 'activated';
812
813                        case 'deactivate-shared':
814                                $active_shared_snippets = get_option( 'active_shared_network_snippets', array() );
815                                update_option( 'active_shared_network_snippets', array_diff( $active_shared_snippets, array( $id ) ) );
816                                clean_active_snippets_cache( code_snippets()->db->ms_table );
817                                return 'deactivated';
818
819                        case 'clone':
820                                $this->clone_snippets( [ $id ] );
821                                return 'cloned';
822
823                        case 'delete':
824                                trash_snippet( $id, $this->is_network );
825                                return 'deleted';
826
827                        case 'restore':
828                                restore_snippet( $id, $this->is_network );
829                                return 'restored';
830
831                        case 'delete_permanently':
832                                delete_snippet( $id, $this->is_network );
833                                return 'deleted_permanently';
834
835                        case 'export':
836                                $export = new Export_Attachment( [ $id ], $this->is_network );
837                                $export->download_snippets_json();
838                                break;
839
840                        case 'download':
841                                $export = new Export_Attachment( [ $id ], $this->is_network );
842                                $export->download_snippets_code();
843                                break;
844                }
845
846                return false;
847        }
848
849        /**
850         * Processes actions requested by the user.
851         *
852         * @return void
853         */
854        public function process_requested_actions() {
855
856                // Clear the recent snippets list if requested to do so.
857                if ( isset( $_POST['clear-recent-list'] ) ) {
858                        check_admin_referer( 'bulk-' . $this->_args['plural'] );
859
860                        if ( $this->is_network ) {
861                                update_site_option( 'recently_activated_snippets', array() );
862                        } else {
863                                update_option( 'recently_activated_snippets', array() );
864                        }
865                }
866
867                // Check if there are any single snippet actions to perform.
868                if ( isset( $_GET['action'], $_GET['id'] ) ) {
869                        $id = absint( $_GET['id'] );
870                        $scope = isset( $_GET['scope'] ) ? sanitize_key( wp_unslash( $_GET['scope'] ) ) : '';
871
872                        // Verify they were sent from a trusted source.
873                        $nonce_action = 'code_snippets_manage_snippet_' . $id;
874                        if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_GET['_wpnonce'] ) ), $nonce_action ) ) {
875                                wp_nonce_ays( $nonce_action );
876                        }
877
878                        $_SERVER['REQUEST_URI'] = remove_query_arg( array( 'action', 'id', 'scope', '_wpnonce' ) );
879
880                        // If so, then perform the requested action and inform the user of the result.
881                        $result = $this->perform_action( $id, sanitize_key( $_GET['action'] ) );
882
883                        if ( $result ) {
884                                $redirect_args = array( 'result' => $result );
885
886                                if ( 'deleted' === $result ) {
887                                        $redirect_args['ids'] = $id;
888                                }
889
890                                wp_safe_redirect( esc_url_raw( add_query_arg( $redirect_args ) ) );
891                                exit;
892                        }
893                }
894
895                if ( isset( $_GET['action'] ) && 'restore' === $_GET['action'] && isset( $_GET['ids'] ) ) {
896                        $ids = array_map( 'intval', explode( ',', sanitize_text_field( $_GET['ids'] ) ) );
897
898                        if ( ! empty( $ids ) ) {
899                                check_admin_referer( 'bulk-' . $this->_args['plural'] );
900
901                                foreach ( $ids as $id ) {
902                                        restore_snippet( $id, $this->is_network );
903                                }
904
905                                wp_safe_redirect( esc_url_raw( add_query_arg( 'result', 'restored' ) ) );
906                                exit;
907                        }
908                }
909
910                // Only continue from this point if there are bulk actions to process.
911                if ( ! isset( $_POST['ids'] ) && ! isset( $_POST['shared_ids'] ) ) {
912                        return;
913                }
914
915                check_admin_referer( 'bulk-' . $this->_args['plural'] );
916
917                $ids = isset( $_POST['ids'] ) ? array_map( 'intval', $_POST['ids'] ) : array();
918                $_SERVER['REQUEST_URI'] = remove_query_arg( 'action' );
919
920                switch ( $this->current_action() ) {
921
922                        case 'activate-selected':
923                                activate_snippets( $ids );
924
925                                // Process the shared network snippets.
926                                if ( isset( $_POST['shared_ids'] ) && is_multisite() && ! $this->is_network ) {
927                                        $active_shared_snippets = get_option( 'active_shared_network_snippets', array() );
928
929                                        foreach ( array_map( 'intval', $_POST['shared_ids'] ) as $id ) {
930                                                if ( ! in_array( $id, $active_shared_snippets, true ) ) {
931                                                        $active_shared_snippets[] = $id;
932                                                }
933                                        }
934
935                                        update_option( 'active_shared_network_snippets', $active_shared_snippets );
936                                        clean_active_snippets_cache( code_snippets()->db->ms_table );
937                                }
938
939                                $result = 'activated-multi';
940                                break;
941
942                        case 'deactivate-selected':
943                                foreach ( $ids as $id ) {
944                                        deactivate_snippet( $id, $this->is_network );
945                                }
946
947                                // Process the shared network snippets.
948                                if ( isset( $_POST['shared_ids'] ) && is_multisite() && ! $this->is_network ) {
949                                        $active_shared_snippets = get_option( 'active_shared_network_snippets', array() );
950                                        $active_shared_snippets = ( '' === $active_shared_snippets ) ? array() : $active_shared_snippets;
951                                        $active_shared_snippets = array_diff( $active_shared_snippets, array_map( 'intval', $_POST['shared_ids'] ) );
952                                        update_option( 'active_shared_network_snippets', $active_shared_snippets );
953                                        clean_active_snippets_cache( code_snippets()->db->ms_table );
954                                }
955
956                                $result = 'deactivated-multi';
957                                break;
958
959                        case 'export-selected':
960                                $export = new Export_Attachment( $ids, $this->is_network );
961                                $export->download_snippets_json();
962                                break;
963
964                        case 'download-selected':
965                                $export = new Export_Attachment( $ids, $this->is_network );
966                                $export->download_snippets_code();
967                                break;
968
969                        case 'clone-selected':
970                                $this->clone_snippets( $ids );
971                                $result = 'cloned-multi';
972                                break;
973
974                        case 'delete-selected':
975                                foreach ( $ids as $id ) {
976                                        trash_snippet( $id, $this->is_network );
977                                }
978                                $result = 'deleted-multi';
979                                break;
980
981                        case 'restore-selected':
982                                foreach ( $ids as $id ) {
983                                        restore_snippet( $id, $this->is_network );
984                                }
985                                $result = 'restored-multi';
986                                break;
987
988                        case 'delete-permanently-selected':
989                                foreach ( $ids as $id ) {
990                                        delete_snippet( $id, $this->is_network );
991                                }
992                                $result = 'deleted-permanently-multi';
993                                break;
994                }
995
996                if ( isset( $result ) ) {
997                        $redirect_args = array( 'result' => $result );
998
999                        // Add snippet IDs for undo functionality on bulk delete
1000                        if ( 'deleted-multi' === $result && ! empty( $ids ) ) {
1001                                $redirect_args['ids'] = implode( ',', $ids );
1002                        }
1003
1004                        wp_safe_redirect( esc_url_raw( add_query_arg( $redirect_args ) ) );
1005                        exit;
1006                }
1007        }
1008
1009        /**
1010         * Message to display if no snippets are found.
1011         *
1012         * @return void
1013         */
1014        public function no_items() {
1015
1016                if ( ! empty( $GLOBALS['s'] ) || ! empty( $_GET['tag'] ) ) {
1017                        esc_html_e( 'No snippets were found matching the current search query. Please enter a new query or use the "Clear Filters" button above.', 'code-snippets' );
1018
1019                } else {
1020                        $add_url = code_snippets()->get_menu_url( 'add' );
1021
1022                        if ( empty( $_GET['type'] ) ) {
1023                                esc_html_e( "It looks like you don't have any snippets.", 'code-snippets' );
1024                        } else {
1025                                esc_html_e( "It looks like you don't have any snippets of this type.", 'code-snippets' );
1026                                $add_url = add_query_arg( 'type', sanitize_key( wp_unslash( $_GET['type'] ) ), $add_url );
1027                        }
1028
1029                        printf(
1030                                ' <a href="%s">%s</a>',
1031                                esc_url( $add_url ),
1032                                esc_html__( 'Perhaps you would like to add a new one?', 'code-snippets' )
1033                        );
1034                }
1035        }
1036
1037        /**
1038         * Fetch all shared network snippets for the current site.
1039         *
1040         * @param array<Snippet> $all_snippets List of snippets to merge with.
1041         *
1042         * @return array<Snippet> Updated list of snippets.
1043         */
1044        private function fetch_shared_network_snippets( array $all_snippets ): array {
1045                if ( ! is_multisite() ) {
1046                        return $all_snippets;
1047                }
1048
1049                $shared_ids = get_site_option( 'shared_network_snippets' );
1050
1051                if ( ! $shared_ids || ! is_array( $shared_ids ) ) {
1052                        return $all_snippets;
1053                }
1054
1055                if ( $this->is_network ) {
1056                        // Mark shared network snippets on the network admin page.
1057                        foreach ( $all_snippets as $snippet ) {
1058                                if ( in_array( $snippet->id, $shared_ids, true ) ) {
1059                                        $snippet->shared_network = true;
1060                                        $snippet->active = false;
1061                                }
1062                        }
1063                } else {
1064                        // Fetch shared network snippets for subsites.
1065                        $active_shared_snippets = get_option( 'active_shared_network_snippets', array() );
1066                        $shared_snippets = get_snippets( $shared_ids, true );
1067
1068                        foreach ( $shared_snippets as $snippet ) {
1069                                $snippet->shared_network = true;
1070                                $snippet->active = in_array( $snippet->id, $active_shared_snippets, true );
1071                        }
1072
1073                        $all_snippets = array_merge( $all_snippets, $shared_snippets );
1074                }
1075
1076                return $all_snippets;
1077        }
1078
1079        /**
1080         * Prepares the items to later display in the table.
1081         * Should run before any headers are sent.
1082         *
1083         * @phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
1084         *
1085         * @return void
1086         */
1087        public function prepare_items() {
1088                /**
1089                 * Global variables.
1090                 *
1091                 * @var string                   $status   Current status view.
1092                 * @var array<string, Snippet[]> $snippets List of snippets for views.
1093                 * @var array<string, integer>   $totals   List of total items for views.
1094                 * @var string                   $s        Current search term.
1095                 */
1096                global $status, $snippets, $totals, $s;
1097
1098                wp_reset_vars( array( 'orderby', 'order', 's' ) );
1099
1100                // Redirect tag filter from POST to GET.
1101                if ( isset( $_POST['filter_action'] ) ) {
1102                        $location = empty( $_POST['tag'] ) ?
1103                                remove_query_arg( 'tag' ) :
1104                                add_query_arg( 'tag', sanitize_text_field( wp_unslash( $_POST['tag'] ) ) );
1105                        wp_safe_redirect( esc_url_raw( $location ) );
1106                        exit;
1107                }
1108
1109                $this->process_requested_actions();
1110                $snippets = array_fill_keys( $this->statuses, array() );
1111
1112                $all_snippets = apply_filters( 'code_snippets/list_table/get_snippets', $this->fetch_shared_network_snippets( get_snippets() ) );
1113
1114                // Separate trashed snippets from the main collection
1115                $snippets['trashed'] = array_filter( $all_snippets, function( $snippet ) {
1116                        return $snippet->is_trashed();
1117                });
1118
1119                // Filter out trashed snippets from the 'all' collection
1120                $snippets['all'] = array_filter( $all_snippets, function( $snippet ) {
1121                        return ! $snippet->is_trashed();
1122                });
1123
1124                foreach ( $snippets['all'] as $snippet ) {
1125                        if ( $snippet->active ) {
1126                                $this->active_by_condition[ $snippet->condition_id ][] = $snippet;
1127                        }
1128                }
1129
1130                // Filter snippets by type.
1131                $type = sanitize_key( wp_unslash( $_GET['type'] ?? '' ) );
1132
1133                if ( $type && 'all' !== $type ) {
1134                        $snippets['all'] = array_filter(
1135                                $snippets['all'],
1136                                function ( Snippet $snippet ) use ( $type ) {
1137                                        return $type === $snippet->type;
1138                                }
1139                        );
1140
1141                        // Filter trashed snippets by type
1142                        $snippets['trashed'] = array_filter(
1143                                $snippets['trashed'],
1144                                function ( Snippet $snippet ) use ( $type ) {
1145                                        return $type === $snippet->type;
1146                                }
1147                        );
1148                }
1149
1150                // Add scope tags to all snippets (including trashed).
1151                foreach ( $snippets['all'] as $snippet ) {
1152                        if ( 'global' !== $snippet->scope ) {
1153                                $snippet->add_tag( $snippet->scope );
1154                        }
1155                }
1156               
1157                foreach ( $snippets['trashed'] as $snippet ) {
1158                        if ( 'global' !== $snippet->scope ) {
1159                                $snippet->add_tag( $snippet->scope );
1160                        }
1161                }
1162
1163                // Filter snippets by tag.
1164                if ( ! empty( $_GET['tag'] ) ) {
1165                        $snippets['all'] = array_filter( $snippets['all'], array( $this, 'tags_filter_callback' ) );
1166                        $snippets['trashed'] = array_filter( $snippets['trashed'], array( $this, 'tags_filter_callback' ) );
1167                }
1168
1169                // Filter snippets based on search query.
1170                if ( $s ) {
1171                        $snippets['all'] = array_filter( $snippets['all'], array( $this, 'search_by_line_callback' ) );
1172                        $snippets['trashed'] = array_filter( $snippets['trashed'], array( $this, 'search_by_line_callback' ) );
1173                }
1174
1175                if ( is_multisite() ) {
1176                        $snippets['shared_network'] = array_values(
1177                                array_filter(
1178                                        $snippets['all'],
1179                                        static function ( Snippet $snippet ) {
1180                                                return $snippet->shared_network;
1181                                        }
1182                                )
1183                        );
1184                } else {
1185                        $snippets['shared_network'] = array();
1186                }
1187
1188                // Clear recently activated snippets older than a week.
1189                $recently_activated = $this->is_network ?
1190                        get_site_option( 'recently_activated_snippets', array() ) :
1191                        get_option( 'recently_activated_snippets', array() );
1192
1193                foreach ( $recently_activated as $key => $time ) {
1194                        if ( $time + WEEK_IN_SECONDS < time() ) {
1195                                unset( $recently_activated[ $key ] );
1196                        }
1197                }
1198
1199                $this->is_network ?
1200                        update_site_option( 'recently_activated_snippets', $recently_activated ) :
1201                        update_option( 'recently_activated_snippets', $recently_activated );
1202
1203                /**
1204                 * Filter snippets into individual sections
1205                 *
1206                 * @var Snippet $snippet
1207                 */
1208                foreach ( $snippets['all'] as $snippet ) {
1209                        // Skip trashed snippets (they're already in their own section)
1210                        if ( $snippet->is_trashed() ) {
1211                                continue;
1212                        }
1213
1214                        if ( $snippet->active || $this->is_condition_active( $snippet ) ) {
1215                                $snippets['active'][] = $snippet;
1216                        } else {
1217                                $snippets['inactive'][] = $snippet;
1218
1219                                // Was the snippet recently deactivated?
1220                                if ( isset( $recently_activated[ $snippet->id ] ) ) {
1221                                        $snippets['recently_activated'][] = $snippet;
1222                                }
1223                        }
1224                }
1225
1226                // Count the totals for each section.
1227                $totals = array_map(
1228                        function ( $section_snippets ) {
1229                                return count( $section_snippets );
1230                        },
1231                        $snippets
1232                );
1233
1234                // If the current status is empty, default to all.
1235                if ( empty( $snippets[ $status ] ) ) {
1236                        $status = 'all';
1237                }
1238
1239                // Get the current data.
1240                $data = $snippets[ $status ];
1241
1242                // Decide how many records per page to show by getting the user's setting in the Screen Options panel.
1243                $sort_by = $this->screen->get_option( 'per_page', 'option' );
1244                $per_page = get_user_meta( get_current_user_id(), $sort_by, true );
1245
1246                if ( empty( $per_page ) || $per_page < 1 ) {
1247                        $per_page = $this->screen->get_option( 'per_page', 'default' );
1248                }
1249
1250                $per_page = (int) $per_page;
1251
1252                $this->set_order_vars();
1253                usort( $data, array( $this, 'usort_reorder_callback' ) );
1254
1255                // Determine what page the user is currently looking at.
1256                $current_page = $this->get_pagenum();
1257
1258                // Check how many items are in the data array.
1259                $total_items = count( $data );
1260
1261                // The WP_List_Table class does not handle pagination for us, so we need to ensure that the data is trimmed to only the current page.
1262                $data = array_slice( $data, ( ( $current_page - 1 ) * $per_page ), $per_page );
1263
1264                // Now we can add our *sorted* data to the 'items' property, where it can be used by the rest of the class.
1265                $this->items = $data;
1266
1267                // We register our pagination options and calculations.
1268                $this->set_pagination_args(
1269                        [
1270                                'total_items' => $total_items, // Calculate the total number of items.
1271                                'per_page'    => $per_page, // Determine how many items to show on a page.
1272                                'total_pages' => ceil( $total_items / $per_page ), // Calculate the total number of pages.
1273                        ]
1274                );
1275        }
1276
1277        /**
1278         * Determine the sort ordering for two pieces of data.
1279         *
1280         * @param mixed $a_data First piece of data.
1281         * @param mixed $b_data Second piece of data.
1282         *
1283         * @return int Returns -1 if $a_data is less than $b_data; 0 if they are equal; 1 otherwise
1284         * @ignore
1285         */
1286        private function get_sort_direction( $a_data, $b_data ) {
1287
1288                // If the data is numeric, then calculate the ordering directly.
1289                if ( is_numeric( $a_data ) && is_numeric( $b_data ) ) {
1290                        return $a_data - $b_data;
1291                }
1292
1293                // If only one of the data points is empty, then place it before the one which is not.
1294                if ( empty( $a_data ) xor empty( $b_data ) ) {
1295                        return empty( $a_data ) ? 1 : -1;
1296                }
1297
1298                // Sort using the default string sort order if possible.
1299                if ( is_string( $a_data ) && is_string( $b_data ) ) {
1300                        return strcasecmp( $a_data, $b_data );
1301                }
1302
1303                // Otherwise, use basic comparison operators.
1304                return $a_data === $b_data ? 0 : ( $a_data < $b_data ? -1 : 1 );
1305        }
1306
1307        /**
1308         * Set the $order_by and $order_dir class variables.
1309         */
1310        private function set_order_vars() {
1311                $order = Settings\get_setting( 'general', 'list_order' );
1312
1313                // set the order by based on the query variable, if set.
1314                if ( ! empty( $_REQUEST['orderby'] ) ) {
1315                        $this->order_by = sanitize_key( wp_unslash( $_REQUEST['orderby'] ) );
1316                } else {
1317                        // otherwise, fetch the order from the setting, ensuring it is valid.
1318                        $valid_fields = [ 'id', 'name', 'type', 'modified', 'priority' ];
1319                        $order_parts = explode( '-', $order, 2 );
1320
1321                        $this->order_by = in_array( $order_parts[0], $valid_fields, true ) ? $order_parts[0] :
1322                                apply_filters( 'code_snippets/list_table/default_orderby', 'priority' );
1323                }
1324
1325                // set the order dir based on the query variable, if set.
1326                if ( ! empty( $_REQUEST['order'] ) ) {
1327                        $this->order_dir = sanitize_key( wp_unslash( $_REQUEST['order'] ) );
1328                } elseif ( '-desc' === substr( $order, -5 ) ) {
1329                        $this->order_dir = 'desc';
1330                } elseif ( '-asc' === substr( $order, -4 ) ) {
1331                        $this->order_dir = 'asc';
1332                } else {
1333                        $this->order_dir = apply_filters( 'code_snippets/list_table/default_order', 'asc' );
1334                }
1335        }
1336
1337        /**
1338         * Callback for usort() used to sort snippets
1339         *
1340         * @param Snippet $a The first snippet to compare.
1341         * @param Snippet $b The second snippet to compare.
1342         *
1343         * @return int The sort order.
1344         * @ignore
1345         */
1346        private function usort_reorder_callback( Snippet $a, Snippet $b ) {
1347                $orderby = $this->order_by;
1348                $result = $this->get_sort_direction( $a->$orderby, $b->$orderby );
1349
1350                if ( 0 === $result && 'id' !== $orderby ) {
1351                        $result = $this->get_sort_direction( $a->id, $b->id );
1352                }
1353
1354                // Apply the sort direction to the calculated order.
1355                return ( 'asc' === $this->order_dir ) ? $result : -$result;
1356        }
1357
1358        /**
1359         * Callback for search function
1360         *
1361         * @param Snippet $snippet The snippet being filtered.
1362         *
1363         * @return bool The result of the filter
1364         * @ignore
1365         */
1366        private function search_callback( Snippet $snippet ): bool {
1367                global $s;
1368
1369                $query = sanitize_text_field( wp_unslash( $s ) );
1370                $fields = [ 'name', 'desc', 'code', 'tags_list' ];
1371
1372                foreach ( $fields as $field ) {
1373                        if ( false !== stripos( $snippet->$field, $query ) ) {
1374                                return true;
1375                        }
1376                }
1377
1378                return false;
1379        }
1380
1381        /**
1382         * Callback for search function
1383         *
1384         * @param Snippet $snippet The snippet being filtered.
1385         *
1386         * @return bool The result of the filter
1387         * @ignore
1388         */
1389        private function search_by_line_callback( Snippet $snippet ): bool {
1390                global $s;
1391                static $line_num;
1392
1393                if ( is_null( $line_num ) ) {
1394
1395                        if ( preg_match( '/@line:(?P<line>\d+)/', $s, $matches ) ) {
1396                                $s = trim( str_replace( $matches[0], '', $s ) );
1397                                $line_num = (int) $matches['line'] - 1;
1398                        } else {
1399                                $line_num = -1;
1400                        }
1401                }
1402
1403                if ( $line_num < 0 ) {
1404                        return $this->search_callback( $snippet );
1405                }
1406
1407                $code_lines = explode( "\n", $snippet->code );
1408
1409                return isset( $code_lines[ $line_num ] ) && false !== stripos( $code_lines[ $line_num ], $s );
1410        }
1411
1412        /**
1413         * Callback for filtering snippets by tag.
1414         *
1415         * @param Snippet $snippet The snippet being filtered.
1416         *
1417         * @return bool The result of the filter.
1418         * @ignore
1419         */
1420        private function tags_filter_callback( Snippet $snippet ): bool {
1421                $tags = isset( $_GET['tag'] ) ?
1422                        explode( ',', sanitize_text_field( wp_unslash( $_GET['tag'] ) ) ) :
1423                        array();
1424
1425                foreach ( $tags as $tag ) {
1426                        if ( in_array( $tag, $snippet->tags, true ) ) {
1427                                return true;
1428                        }
1429                }
1430
1431                return false;
1432        }
1433
1434        /**
1435         * Display a notice showing the current search terms
1436         *
1437         * @since 1.7
1438         */
1439        public function search_notice() {
1440                if ( ! empty( $_REQUEST['s'] ) || ! empty( $_GET['tag'] ) ) {
1441
1442                        echo '<span class="subtitle">' . esc_html__( 'Search results', 'code-snippets' );
1443
1444                        if ( ! empty( $_REQUEST['s'] ) ) {
1445                                $s = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) );
1446
1447                                if ( preg_match( '/@line:(?P<line>\d+)/', $s, $matches ) ) {
1448
1449                                        // translators: 1: search query, 2: line number.
1450                                        $text = __( ' for &ldquo;%1$s&rdquo; on line %2$d', 'code-snippets' );
1451                                        printf(
1452                                                esc_html( $text ),
1453                                                esc_html( trim( str_replace( $matches[0], '', $s ) ) ),
1454                                                intval( $matches['line'] )
1455                                        );
1456
1457                                } else {
1458                                        // translators: %s: search query.
1459                                        echo esc_html( sprintf( __( ' for &ldquo;%s&rdquo;', 'code-snippets' ), $s ) );
1460                                }
1461                        }
1462
1463                        if ( ! empty( $_GET['tag'] ) ) {
1464                                $tag = sanitize_text_field( wp_unslash( $_GET['tag'] ) );
1465                                // translators: %s: tag name.
1466                                echo esc_html( sprintf( __( ' in tag &ldquo;%s&rdquo;', 'code-snippets' ), $tag ) );
1467                        }
1468
1469                        echo '</span>';
1470
1471                        // translators: 1: link URL, 2: link text.
1472                        printf(
1473                                '&nbsp;<a class="button clear-filters" href="%s">%s</a>',
1474                                esc_url( remove_query_arg( array( 's', 'tag', 'cloud_search' ) ) ),
1475                                esc_html__( 'Clear Filters', 'code-snippets' )
1476                        );
1477                }
1478        }
1479
1480        /**
1481         * Outputs content for a single row of the table
1482         *
1483         * @param Snippet $item The snippet being used for the current row.
1484         */
1485        public function single_row( $item ) {
1486                $status = $item->active || $this->is_condition_active( $item ) ? 'active' : 'inactive';
1487                $row_class = "snippet $status-snippet $item->type-snippet $item->scope-scope";
1488
1489                if ( $item->shared_network ) {
1490                        $row_class .= ' shared-network-snippet';
1491                }
1492
1493                printf( '<tr class="%s" data-snippet-scope="%s">', esc_attr( $row_class ), esc_attr( $item->scope ) );
1494                $this->single_row_columns( $item );
1495                echo '</tr>';
1496        }
1497
1498        /**
1499         * Clone a selection of snippets
1500         *
1501         * @param array<integer> $ids List of snippet IDs.
1502         */
1503        private function clone_snippets( array $ids ) {
1504                $snippets = get_snippets( $ids, $this->is_network );
1505
1506                foreach ( $snippets as $snippet ) {
1507                        $snippet->id = 0;
1508                        $snippet->active = false;
1509                        $snippet->cloud_id = '';
1510
1511                        // translators: %s: snippet title.
1512                        $snippet->name = sprintf( __( '%s [CLONE]', 'code-snippets' ), $snippet->name );
1513                        $snippet = apply_filters( 'code_snippets/list_table/clone_snippet', $snippet );
1514
1515                        save_snippet( $snippet );
1516                }
1517        }
1518}
Note: See TracBrowser for help on using the repository browser.