prepare_items();
$add_new_url = admin_url( 'post-new.php?post_type=' . Custom_Code::POST_TYPE );
$rebuild_nonce = wp_create_nonce( 'ao_cc_rebuild_cache' );
// phpcs:ignore WordPress.Security.NonceVerification -- read-only display flags
$just_rebuilt = isset( $_GET['ao-cc-rebuilt'] );
// phpcs:ignore WordPress.Security.NonceVerification
$just_toggled = isset( $_GET['ao-cc-toggled'] );
?>
is_pro_active();
$presets = Custom_Code_Presets::get_presets();
$show_create_form = true;
require Custom_Code::MODULE_PATH . 'settings/views/preset-modal.php';
}
/**
* Process list table bulk actions before the admin page outputs headers.
*
* @return void
*/
public function maybe_handle_bulk_actions(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce is verified before any state change in handle_bulk_actions().
$page = isset( $_POST['page'] ) ? sanitize_key( wp_unslash( $_POST['page'] ) ) : '';
if ( 'ao-custom-code' !== $page ) {
return;
}
if ( ! current_user_can( Custom_Code::CAPABILITY ) ) {
wp_die( esc_html__( 'You do not have permission to access this page.', 'admin-optimizer' ) );
}
$this->handle_bulk_actions();
}
/**
* Process bulk actions submitted from the list table.
*
* @return void
*/
private function handle_bulk_actions(): void {
$action = isset( $_POST['action'] ) && '-1' !== $_POST['action']
? sanitize_key( wp_unslash( $_POST['action'] ) )
: ( isset( $_POST['action2'] ) && '-1' !== $_POST['action2'] ? sanitize_key( wp_unslash( $_POST['action2'] ) ) : '' );
if ( empty( $action ) || ! in_array( $action, [ 'trash', 'restore', 'delete', 'activate', 'deactivate' ], true ) ) {
return;
}
check_admin_referer( 'ao_cc_bulk_action', 'ao_cc_bulk_nonce' );
$post_ids = isset( $_POST['post'] ) ? array_map( 'absint', (array) $_POST['post'] ) : [];
foreach ( $post_ids as $post_id ) {
if ( 'trash' === $action && current_user_can( 'delete_post', $post_id ) ) {
wp_trash_post( $post_id );
} elseif ( 'restore' === $action && current_user_can( 'delete_post', $post_id ) ) {
wp_untrash_post( $post_id );
} elseif ( 'delete' === $action && current_user_can( 'delete_post', $post_id ) ) {
wp_delete_post( $post_id, true );
} elseif ( 'activate' === $action && current_user_can( 'edit_post', $post_id ) ) {
wp_update_post(
[
'ID' => $post_id,
'post_status' => 'publish',
]
);
} elseif ( 'deactivate' === $action && current_user_can( 'edit_post', $post_id ) ) {
wp_update_post(
[
'ID' => $post_id,
'post_status' => 'draft',
]
);
}
}
// Status mutations bypass the edit-form save path, so rebuild all affected
// option caches. The Pro engine also refreshes currently-published targets.
$this->get_cache()->rebuild_all();
$redirect_args = [];
if ( 'restore' === $action ) {
$redirect_args['restored'] = count( $post_ids );
} elseif ( 'delete' === $action ) {
$redirect_args['deleted'] = count( $post_ids );
} elseif ( 'activate' === $action ) {
$redirect_args['activated'] = count( $post_ids );
} elseif ( 'deactivate' === $action ) {
$redirect_args['deactivated'] = count( $post_ids );
} else {
$redirect_args['trashed'] = count( $post_ids );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce was verified above.
if ( ! empty( $_REQUEST['post_status'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce was verified above.
$redirect_args['post_status'] = sanitize_key( wp_unslash( $_REQUEST['post_status'] ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce was verified above.
if ( ! empty( $_REQUEST['ao_cc_type_filter'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce was verified above.
$redirect_args['ao_cc_type_filter'] = sanitize_key( wp_unslash( $_REQUEST['ao_cc_type_filter'] ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce was verified above.
if ( ! empty( $_REQUEST['s'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- nonce was verified above.
$redirect_args['s'] = sanitize_text_field( wp_unslash( $_REQUEST['s'] ) );
}
wp_safe_redirect( Custom_Code::get_module_admin_url( $redirect_args ) );
exit;
}
/**
* Permanently delete a single custom code item from the Trash.
*
* @return void
*/
public function handle_force_delete_post(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checked below with the post-specific action.
$post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;
if ( ! $post_id || ! current_user_can( 'delete_post', $post_id ) ) {
wp_die( esc_html__( 'You do not have permission to delete this item.', 'admin-optimizer' ) );
}
check_admin_referer( 'ao_cc_delete_code_' . $post_id );
$post = get_post( $post_id );
if ( ! $post || Custom_Code::POST_TYPE !== $post->post_type ) {
wp_die( esc_html__( 'Invalid custom code item.', 'admin-optimizer' ) );
}
wp_delete_post( $post_id, true );
wp_safe_redirect(
Custom_Code::get_module_admin_url(
[
'post_status' => 'trash',
'deleted' => 1,
]
)
);
exit;
}
/**
* Restore a single custom code item from the Trash.
*
* @return void
*/
public function handle_restore_post(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checked below with the post-specific action.
$post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;
if ( ! $post_id || ! current_user_can( 'delete_post', $post_id ) ) {
wp_die( esc_html__( 'You do not have permission to restore this item.', 'admin-optimizer' ) );
}
check_admin_referer( 'ao_cc_restore_code_' . $post_id );
$post = get_post( $post_id );
if ( ! $post || Custom_Code::POST_TYPE !== $post->post_type ) {
wp_die( esc_html__( 'Invalid custom code item.', 'admin-optimizer' ) );
}
wp_untrash_post( $post_id );
wp_safe_redirect(
Custom_Code::get_module_admin_url(
[
'post_status' => 'trash',
'restored' => 1,
]
)
);
exit;
}
/**
* Register metaboxes on the ao_custom_code edit screen.
*
* @return void
*/
public function register_metaboxes(): void {
add_meta_box(
'ao-cc-content',
__( 'Code', 'admin-optimizer' ),
[ $this, 'render_content_metabox' ],
Custom_Code::POST_TYPE,
'normal',
'high'
);
add_meta_box(
'ao-cc-notes',
__( 'Internal Notes', 'admin-optimizer' ),
[ $this, 'render_notes_metabox' ],
Custom_Code::POST_TYPE,
'normal'
);
add_meta_box(
'ao-cc-rules',
__( 'Display Rules', 'admin-optimizer' ),
[ $this, 'render_display_rules_metabox' ],
Custom_Code::POST_TYPE,
'side'
);
add_meta_box(
'ao-cc-display-settings',
__( 'Display Settings', 'admin-optimizer' ),
[ $this, 'render_display_settings_metabox' ],
Custom_Code::POST_TYPE,
'side'
);
remove_meta_box( 'slugdiv', Custom_Code::POST_TYPE, 'normal' );
}
/**
* Render the code content metabox.
* Type selector shown on new posts; locked to read-only on existing posts.
*
* @param \WP_Post $post The post being edited.
* @return void
*/
public function render_content_metabox( $post ): void {
wp_nonce_field( Custom_Code::NONCE_ACTION, Custom_Code::NONCE_FIELD );
$type_meta = get_post_meta( $post->ID, '_ao_cc_type', true );
$type = $type_meta ? $type_meta : 'css';
$is_pro = $this->is_pro_active();
// JS and HTML snippets require unfiltered_html; save_post() rejects them otherwise.
$can_unfiltered = current_user_can( 'unfiltered_html' );
$shortcode_label = $is_pro
? __( 'Shortcode', 'admin-optimizer' )
: __( 'Shortcode (Pro)', 'admin-optimizer' );
?>
<script> tag. Example: <script src="https://example.com/script.js" defer></script>', 'admin-optimizer' ), [ 'code' => [] ] ); ?>
<script> tag. Example: <script src="https://example.com/script.js" defer></script>', 'admin-optimizer' ), [ 'code' => [] ] ); ?>
is_pro_active() ) {
/**
* Lets Pro render its own built-in shortcode reference panel
* instead of Free's default.
*/
do_action( 'ao_cc_render_builtin_shortcodes_panel' );
} else {
$this->render_builtin_shortcodes_panel();
}
?>
is_pro_active();
$presets = Custom_Code_Presets::get_presets();
$show_create_form = false;
require Custom_Code::MODULE_PATH . 'settings/views/preset-modal.php';
}
/**
* Render the collapsible "Built-in shortcodes" reference panel.
*
* @return void
*/
private function render_builtin_shortcodes_panel(): void {
$lib = [
[ 'ao_copyright', __( 'Copyright symbol', 'admin-optimizer' ) ],
[ 'ao_year', __( 'Current year', 'admin-optimizer' ) ],
[ 'ao_sitename', __( 'Site name', 'admin-optimizer' ) ],
[ 'ao_tagline', __( 'Site tagline', 'admin-optimizer' ) ],
[ 'ao_home_url', __( 'Site home URL', 'admin-optimizer' ) ],
[ 'ao_post_title', __( 'Current post/page title', 'admin-optimizer' ) ],
[ 'ao_post_date', __( 'Post published date', 'admin-optimizer' ) ],
[ 'ao_post_modified', __( 'Post last modified date', 'admin-optimizer' ) ],
[ 'ao_user_name', __( "Logged-in user's display name", 'admin-optimizer' ) ],
[ 'ao_user_email', __( "Logged-in user's email address", 'admin-optimizer' ) ],
[ 'ao_login_url', __( 'Login page URL', 'admin-optimizer' ) ],
[ 'ao_logout_url', __( 'Logout URL', 'admin-optimizer' ) ],
[ 'ao_privacy_page', __( 'Privacy Policy page URL', 'admin-optimizer' ) ],
];
?>
|
|
[%s] | %s | ',
esc_html( $tag ),
esc_html( $desc )
);
}
?>
ID ) ? (string) get_post_meta( $post->ID, '_ao_cc_notes', true ) : '';
?>
ID, '_ao_cc_type', true );
$type = $type_meta ? $type_meta : 'css';
$display_meta = get_post_meta( $post->ID, '_ao_cc_display', true );
$display = $display_meta ? $display_meta : 'inline';
$position_meta = get_post_meta( $post->ID, '_ao_cc_position', true );
$position = $position_meta ? $position_meta : 'wp_head';
$locations = get_post_meta( $post->ID, '_ao_cc_locations', true );
$locations = is_array( $locations ) ? $locations : [];
if ( 'shortcode' === $type ) {
echo '' . esc_html__( 'Shortcodes have no location targeting — use the shortcode tag wherever you need it.', 'admin-optimizer' ) . '
';
return;
}
?>
$label ) : ?>
is_pro_active() ) : ?>
',
''
);
?>
render_load_priority( $post );
$this->render_specific_posts_fields( $post );
$this->render_post_types_fields( $post );
$this->render_tax_slugs_fields( $post );
}
/**
* Render the load priority radio buttons.
* Free: disabled placeholder. Pro: fires ao_cc_render_load_priority.
*
* @param \WP_Post $post The post being edited.
* @return void
*/
private function render_load_priority( $post ) {
if ( $this->is_pro_active() ) {
/**
* Fires the Load Priority section's real radios.
*
* @param \WP_Post $post The post being edited.
*/
do_action( 'ao_cc_render_load_priority', $post );
return;
}
?>
is_pro_active() ) {
/**
* Fires the Custom Post Types targeting section's real checkboxes.
*
* @param \WP_Post $post The post being edited.
*/
do_action( 'ao_cc_render_post_types_fields', $post );
return;
}
$wrap_style = 'color:#aaa;pointer-events:none;';
$public_types = get_post_types(
[
'public' => true,
'_builtin' => false,
],
'objects'
);
unset( $public_types[ Custom_Code::POST_TYPE ] );
?>
is_pro_active() ) {
/**
* Fires the Custom Taxonomy Archives targeting section's real checkboxes.
*
* @param \WP_Post $post The post being edited.
*/
do_action( 'ao_cc_render_tax_slugs_fields', $post );
return;
}
$wrap_style = 'color:#aaa;pointer-events:none;';
$public_taxes = array_filter(
get_taxonomies(
[
'public' => true,
'_builtin' => false,
],
'objects'
),
static fn( $tax ) => ! empty( $tax->rewrite )
);
?>
is_pro_active() ) {
/**
* Fires the Specific Posts targeting section's real fields.
*
* @param \WP_Post $post The post being edited.
*/
do_action( 'ao_cc_render_specific_posts_fields', $post );
return;
}
$wrap_style = 'color:#aaa;pointer-events:none;';
?>
is_pro_active() ) {
/**
* Fires the Display Settings metabox's real controls (minify
* checkbox, user-condition radios, role/user pickers).
*
* @param \WP_Post $post The post being edited.
*/
do_action( 'ao_cc_render_display_settings', $post );
return;
}
$wrap_style = 'color: #aaa;pointer-events:none;';
?>
',
''
);
?>
id, 'ao-custom-code' ) ) {
return;
}
add_screen_option(
'per_page',
[
'label' => __( 'Snippets per page', 'admin-optimizer' ),
'default' => 20,
'option' => 'ao_cc_per_page',
]
);
}
/**
* Save the per-page Screen Option value.
*
* @param mixed $status Existing status (false to skip saving).
* @param string $option Option name.
* @param mixed $value Submitted value.
* @return int
*/
public function save_screen_option( $_status, string $_option, $value ): int {
return (int) $value;
}
/**
* Handle the "Rebuild Cache" admin-post action.
*
* @return void
*/
/**
* Resolve the cache engine to use for a rebuild.
*
* Free always gets its own Custom_Code_Cache. When Pro is active, the
* `ao_cc_cache_engine` filter lets Custom_Code_Pro swap in
* Custom_Code_Cache_Pro instead — mirrors DB_Cleaner_Pro's
* `adminoptim_db_cleaner_query_engine` filter for the same purpose.
*
* @return Custom_Code_Cache
*/
private function get_cache(): Custom_Code_Cache {
$cache = new Custom_Code_Cache();
if ( ! $this->is_pro_active() ) {
return $cache;
}
/**
* Filters the cache engine used to rebuild Custom Code's frontend output.
*
* @param Custom_Code_Cache $cache Free's default cache engine.
*/
return apply_filters( 'ao_cc_cache_engine', $cache );
}
/**
* Handle the admin-page "Rebuild Cache" request (nonce-verified form submission, not AJAX).
*
* @return void
*/
public function handle_rebuild_cache(): void {
if ( ! current_user_can( Custom_Code::CAPABILITY ) ) {
wp_die( -1 );
}
check_admin_referer( 'ao_cc_rebuild_cache' );
$this->get_cache()->rebuild_all();
wp_safe_redirect(
Custom_Code::get_module_admin_url(
[ 'ao-cc-rebuilt' => '1' ]
)
);
exit;
}
/**
* Handle the AJAX "Rebuild Cache" request.
*
* @return void
*/
public function ajax_rebuild_cache(): void {
if ( ! current_user_can( Custom_Code::CAPABILITY ) ) {
wp_send_json_error( 'forbidden', 403 );
}
check_ajax_referer( 'ao_cc_rebuild_cache' );
$this->get_cache()->rebuild_all();
wp_send_json_success( [ 'message' => __( 'Cache rebuilt successfully.', 'admin-optimizer' ) ] );
}
/**
* Toggle a snippet between published and draft.
*
* @return void
*/
public function handle_toggle_status(): void {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checked below.
$post_id = isset( $_GET['post'] ) ? absint( $_GET['post'] ) : 0;
if ( ! $post_id ) {
wp_die( -1 );
}
check_admin_referer( 'ao_cc_toggle_status_' . $post_id );
if ( ! current_user_can( Custom_Code::CAPABILITY ) || ! current_user_can( 'edit_post', $post_id ) ) {
wp_die( -1 );
}
$post = get_post( $post_id );
if ( ! $post || Custom_Code::POST_TYPE !== $post->post_type ) {
wp_die( -1 );
}
$new_status = 'publish' === $post->post_status ? 'draft' : 'publish';
remove_action( 'save_post_' . Custom_Code::POST_TYPE, [ $this, 'save_post' ] );
wp_update_post(
[
'ID' => $post_id,
'post_status' => $new_status,
]
);
add_action( 'save_post_' . Custom_Code::POST_TYPE, [ $this, 'save_post' ], 10, 3 );
// Rebuild so the change is immediately reflected on the frontend.
$old_locations = get_post_meta( $post_id, '_ao_cc_locations', true );
$old_display_raw = (string) get_post_meta( $post_id, '_ao_cc_display', true );
$old_display = $old_display_raw ? $old_display_raw : 'inline';
$old_position_raw = (string) get_post_meta( $post_id, '_ao_cc_position', true );
$old_position = $old_position_raw ? $old_position_raw : '';
$old_priority_raw = (string) get_post_meta( $post_id, '_ao_cc_load_priority', true );
$old_priority = in_array( $old_priority_raw, Custom_Code::VALID_PRIORITIES, true ) ? $old_priority_raw : 'default';
$cache = $this->get_cache();
$cache->rebuild( $post_id, is_array( $old_locations ) ? $old_locations : [], $old_display, $old_position, $old_priority );
if ( $this->is_pro_active() ) {
$post_ids = get_post_meta( $post_id, '_ao_cc_post_ids', true );
$post_ids = is_array( $post_ids ) ? $post_ids : [];
$cache->rebuild_post_meta( $post_ids, $post_ids );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce was verified above.
$redirect_args = [
'ao-cc-toggled' => '1',
];
if ( ! empty( $_REQUEST['post_status'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$redirect_args['post_status'] = sanitize_key( wp_unslash( $_REQUEST['post_status'] ) );
}
wp_safe_redirect( Custom_Code::get_module_admin_url( $redirect_args ) );
exit;
}
/**
* Rebuild the cache when a post is unpublished or trashed without going through save_post.
*
* @param string $new_status New status.
* @param string $old_status Old status.
* @param \WP_Post $post Post object.
* @return void
*/
public function on_status_transition( string $new_status, string $old_status, \WP_Post $post ): void {
if ( Custom_Code::POST_TYPE !== $post->post_type ) {
return;
}
// Only activation/deactivation can change frontend output.
if ( $old_status === $new_status || ( 'publish' !== $old_status && 'publish' !== $new_status ) ) {
return;
}
// The save_post handler (with nonce) will rebuild as well when this transition
// comes from an edit-screen form submission, so avoid a redundant double rebuild.
if ( isset( $_POST[ Custom_Code::NONCE_FIELD ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
return;
}
$old_locations = get_post_meta( $post->ID, '_ao_cc_locations', true );
$old_locations = is_array( $old_locations ) ? $old_locations : [];
$old_display_raw = (string) get_post_meta( $post->ID, '_ao_cc_display', true );
$old_display = $old_display_raw !== '' ? $old_display_raw : 'inline';
$old_position = (string) get_post_meta( $post->ID, '_ao_cc_position', true );
$old_priority_raw = (string) get_post_meta( $post->ID, '_ao_cc_load_priority', true );
$old_priority = in_array( $old_priority_raw, Custom_Code::VALID_PRIORITIES, true ) ? $old_priority_raw : 'default';
$cache = $this->get_cache();
$cache->rebuild( $post->ID, $old_locations, $old_display, $old_position, $old_priority );
if ( $this->is_pro_active() ) {
$post_ids = get_post_meta( $post->ID, '_ao_cc_post_ids', true );
$post_ids = is_array( $post_ids ) ? $post_ids : [];
$cache->rebuild_post_meta( $post_ids, $post_ids );
}
}
/**
* Save post data and rebuild the frontend cache.
*
* @param int $post_id The ID of the post being saved.
* @param \WP_Post $post The post object being saved. Unused directly,
* but required in the signature so $update is
* reachable — save_post_{post_type} always
* passes all three positionally.
* @param bool $update Whether this is an update to an existing
* post (true) or a genuinely new post's first
* save (false), as WordPress itself resolves it.
* @return void
*/
public function save_post( int $post_id, \WP_Post $post, bool $update = true ): void {
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
return;
}
if ( ! current_user_can( Custom_Code::CAPABILITY ) || ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
// No nonce field present — not a form submission (e.g. programmatic save).
if ( ! isset( $_POST[ Custom_Code::NONCE_FIELD ] ) ) {
return;
}
$nonce = sanitize_key( wp_unslash( $_POST[ Custom_Code::NONCE_FIELD ] ) );
if ( ! wp_verify_nonce( $nonce, Custom_Code::NONCE_ACTION ) ) {
wp_die( -1 );
}
// Read pre-save meta for cache diff.
$old_loc_meta = get_post_meta( $post_id, '_ao_cc_locations', true );
$old_locations = $old_loc_meta ? $old_loc_meta : [];
$old_disp_meta = get_post_meta( $post_id, '_ao_cc_display', true );
$old_display = $old_disp_meta ? $old_disp_meta : 'inline';
$old_pos_meta = get_post_meta( $post_id, '_ao_cc_position', true );
$old_position = $old_pos_meta ? $old_pos_meta : '';
$old_priority_meta = get_post_meta( $post_id, '_ao_cc_load_priority', true );
$old_priority = in_array( $old_priority_meta, Custom_Code::VALID_PRIORITIES, true ) ? $old_priority_meta : 'default';
$old_post_ids_meta = get_post_meta( $post_id, '_ao_cc_post_ids', true );
$old_post_ids = is_array( $old_post_ids_meta ) ? $old_post_ids_meta : [];
// --- Type ---
$existing_type = get_post_meta( $post_id, '_ao_cc_type', true );
if ( isset( $_POST['_ao_cc_type'] ) ) {
$raw_type = sanitize_key( wp_unslash( $_POST['_ao_cc_type'] ) );
if ( 'shortcode' === $raw_type && ! $this->is_pro_active() ) {
return;
}
$type = in_array( $raw_type, Custom_Code::TYPES, true ) ? $raw_type : 'css';
} else {
$type = $existing_type ? $existing_type : 'css';
}
if ( 'shortcode' === $type && ! $this->is_pro_active() ) {
return;
}
$type_changed = '' !== $existing_type && $existing_type !== $type;
if ( 'shortcode' === $type && $this->is_pro_active() ) {
$shortcode_error = apply_filters( 'ao_cc_shortcode_validation_error', '', $post_id );
if ( is_string( $shortcode_error ) && '' !== $shortcode_error ) {
set_transient( 'ao_cc_error_' . get_current_user_id() . '_' . $post_id, $shortcode_error, 45 );
return;
}
}
// --- Validate and save post content ---
$post_data = [ 'ID' => $post_id ];
$validation_error = null;
if ( isset( $_POST['_ao_cc_content'] ) ) {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below based on capability
$raw_content = wp_unslash( $_POST['_ao_cc_content'] );
$raw_content = $this->normalize_line_endings( $raw_content );
// Strip accidental wrapping \s*$#i', '', $raw_content );
}
if ( 'shortcode' === $type ) {
// Free stores shortcode content as text. Pro may safely widen this through its gated filter.
$shortcode_content = sanitize_textarea_field( $raw_content );
if ( $this->is_pro_active() ) {
$shortcode_content = apply_filters( 'ao_cc_shortcode_content', $shortcode_content, $raw_content, $post_id );
}
$post_data['post_content'] = is_string( $shortcode_content ) ? $shortcode_content : '';
} else {
// JS and HTML snippets require unfiltered_html — wp_kses_post strips