PluginProbe ʕ •ᴥ•ʔ
Code Snippets / 3.9.2
Code Snippets v3.9.2
3.9.6 trunk 2.10.0 2.10.1 2.12.0 2.12.1 2.13.0 2.13.1 2.13.2 2.13.3 2.14.0 2.14.1 2.14.2 2.14.3 2.14.4 2.14.5 2.14.6 3.0.0 3.0.1 3.1.0 3.1.1 3.2.1 3.2.2 3.3.0 3.4.0 3.4.1 3.4.2 3.5.0 3.5.0-beta.1 3.6.0 3.6.1 3.6.2 3.6.3 3.6.4 3.6.5 3.6.5.1 3.6.6 3.6.6.1 3.6.7 3.6.8 3.7.0 3.7.0-beta 3.7.0-beta.5 3.7.0-beta.7 3.7.1-beta.1 3.7.1-beta.2 3.7.1-beta.3 3.8.0 3.8.1 3.8.2 3.9.0 3.9.0-beta.1 3.9.0-beta.2 3.9.1 3.9.2 3.9.3 3.9.4 3.9.5
code-snippets / php / class-list-table.php
code-snippets / php Last commit date
admin-menus 6 months ago cloud 6 months ago evaluation 7 months ago export 9 months ago flat-files 7 months ago front-end 6 months ago rest-api 6 months ago settings 6 months ago views 7 months ago class-admin.php 6 months ago class-contextual-help.php 9 months ago class-data-item.php 9 months ago class-db.php 9 months ago class-licensing.php 1 year ago class-list-table.php 6 months ago class-plugin.php 6 months ago class-snippet.php 7 months ago class-upgrade.php 1 year ago class-validator.php 9 months ago class-welcome-api.php 1 year ago deactivation-notice.php 1 year ago editor.php 7 months ago load.php 6 months ago snippet-ops.php 6 months ago strings.php 9 months ago uninstall.php 7 months ago
class-list-table.php
1519 lines
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
10 namespace Code_Snippets;
11
12 use WP_List_Table;
13 use 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.
16 if ( ! 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 */
26 class 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 }
1519