diff --git a/src/composer.lock b/src/composer.lock index 2914eafd..cd9b5b92 100644 --- a/src/composer.lock +++ b/src/composer.lock @@ -488,16 +488,16 @@ }, { "name": "phpcompatibility/phpcompatibility-paragonie", - "version": "1.3.3", + "version": "1.3.4", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac" + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/293975b465e0e709b571cbf0c957c6c0a7b9a2ac", - "reference": "293975b465e0e709b571cbf0c957c6c0a7b9a2ac", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", + "reference": "244d7b04fc4bc2117c15f5abe23eb933b5f02bbf", "shasum": "" }, "require": { @@ -554,22 +554,26 @@ { "url": "https://opencollective.com/php_codesniffer", "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" } ], - "time": "2024-04-24T21:30:46+00:00" + "time": "2025-09-19T17:43:28+00:00" }, { "name": "phpcompatibility/phpcompatibility-wp", - "version": "2.1.7", + "version": "2.1.8", "source": { "type": "git", "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", - "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c" + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/5bfbbfbabb3df2b9a83e601de9153e4a7111962c", - "reference": "5bfbbfbabb3df2b9a83e601de9153e4a7111962c", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/7c8d18b4d90dac9e86b0869a608fa09158e168fa", + "reference": "7c8d18b4d90dac9e86b0869a608fa09158e168fa", "shasum": "" }, "require": { @@ -631,26 +635,26 @@ "type": "thanks_dev" } ], - "time": "2025-05-12T16:38:37+00:00" + "time": "2025-10-18T00:05:59+00:00" }, { "name": "phpcsstandards/phpcsextra", - "version": "1.4.0", + "version": "1.4.2", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca" + "reference": "8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/fa4b8d051e278072928e32d817456a7fdb57b6ca", - "reference": "fa4b8d051e278072928e32d817456a7fdb57b6ca", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e", + "reference": "8e89a01c7b8fed84a12a2a7f5a23a44cdbe4f62e", "shasum": "" }, "require": { "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.1.0", - "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" + "phpcsstandards/phpcsutils": "^1.1.2", + "squizlabs/php_codesniffer": "^3.13.4 || ^4.0" }, "require-dev": { "php-parallel-lint/php-console-highlighter": "^1.0", @@ -713,26 +717,26 @@ "type": "thanks_dev" } ], - "time": "2025-06-14T07:40:39+00:00" + "time": "2025-10-28T17:00:02+00:00" }, { "name": "phpcsstandards/phpcsutils", - "version": "1.1.1", + "version": "1.1.3", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd" + "reference": "8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/f7eb16f2fa4237d5db9e8fed8050239bee17a9bd", - "reference": "f7eb16f2fa4237d5db9e8fed8050239bee17a9bd", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9", + "reference": "8b8e17615d04f2fc2cd46fc1d2fd888fa21b3cf9", "shasum": "" }, "require": { "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.13.0 || ^4.0" + "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" }, "require-dev": { "ext-filter": "*", @@ -806,20 +810,20 @@ "type": "thanks_dev" } ], - "time": "2025-08-10T01:04:45+00:00" + "time": "2025-10-16T16:39:32+00:00" }, { "name": "squizlabs/php_codesniffer", - "version": "3.13.2", + "version": "3.13.5", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c" + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/5b5e3821314f947dd040c70f7992a64eac89025c", - "reference": "5b5e3821314f947dd040c70f7992a64eac89025c", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", "shasum": "" }, "require": { @@ -836,11 +840,6 @@ "bin/phpcs" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "notification-url": "https://packagist.org/downloads/", "license": [ "BSD-3-Clause" @@ -890,7 +889,7 @@ "type": "thanks_dev" } ], - "time": "2025-06-17T22:17:01+00:00" + "time": "2025-11-04T16:30:35+00:00" }, { "name": "wp-coding-standards/wpcs", diff --git a/src/css/common/_badges.scss b/src/css/common/_badges.scss index 6ebb3cde..4cfbe15f 100644 --- a/src/css/common/_badges.scss +++ b/src/css/common/_badges.scss @@ -20,6 +20,13 @@ gap: 5px; line-height: 1; + @at-root .row-actions & { + color: #8c8c8c; + padding-inline: 0px; + text-transform: capitalize; + font-weight: 500; + } + .dashicons { font-size: 18px; inline-size: 18px; @@ -27,6 +34,13 @@ } } +.network-shared { + color: #2271b1; + font-size: 22px; + width: 100%; + cursor: help; +} + .small-badge { block-size: auto; inline-size: auto; @@ -73,6 +87,33 @@ .inverted-badges .badge { color: #fff; background-color: #a7aaad; + border-color: #fff !important; + + .dashicons { + color: #fff; + } +} + +.nav-tab-inactive { + $colors: map.get(theme.$badges, 'pro'); + $text-color: list.nth($colors, 2); + $background-color: list.nth($colors, 1); + + .badge.pro-badge { + color: $text-color; + background-color: $background-color; + } + + &:hover { + &.button, .dashicons-external { + color: #3c434a; + } + + .badge.pro-badge { + color: $background-color; + background-color: $text-color; + } + } } .nav-tab-inactive { diff --git a/src/css/manage.scss b/src/css/manage.scss index d71063f7..0509be08 100644 --- a/src/css/manage.scss +++ b/src/css/manage.scss @@ -24,7 +24,7 @@ } } -.active-snippet .column-name > a { +.active-snippet .column-name > .snippet-name { font-weight: 600; } diff --git a/src/js/components/EditorSidebar/controls/MultisiteSharingSettings.tsx b/src/js/components/EditorSidebar/controls/MultisiteSharingSettings.tsx index 27d6c57d..cefc8e81 100644 --- a/src/js/components/EditorSidebar/controls/MultisiteSharingSettings.tsx +++ b/src/js/components/EditorSidebar/controls/MultisiteSharingSettings.tsx @@ -1,39 +1,41 @@ import React from 'react' import { __ } from '@wordpress/i18n' import { useSnippetForm } from '../../../hooks/useSnippetForm' +import { Tooltip } from '../../common/Tooltip' export const MultisiteSharingSettings: React.FC = () => { const { snippet, setSnippet, isReadOnly } = useSnippetForm() return ( -
+

- + {__('Share with Subsites', 'code-snippets')}

-
- - { - __('Instead of running on every site, allow this snippet to be activated on individual sites on the network.', 'code-snippets') - } -
+ + {__('Instead of running on every site, allow this snippet to be activated on individual sites on the network.', 'code-snippets')} + - - setSnippet(previous => ({ - ...previous, - active: false, - shared_network: event.target.checked - }))} - /> +
) } diff --git a/src/js/utils/snippets/api.ts b/src/js/utils/snippets/api.ts index e9072fd9..64bfb155 100644 --- a/src/js/utils/snippets/api.ts +++ b/src/js/utils/snippets/api.ts @@ -35,6 +35,7 @@ const mapToSchema = ({ priority, active, network, + shared_network, conditionId }: Partial): WritableSnippetSchema => ({ name, @@ -45,6 +46,7 @@ const mapToSchema = ({ priority, active, network, + shared_network, condition_id: conditionId }) diff --git a/src/php/class-admin.php b/src/php/class-admin.php index 70c293a9..dfdf093d 100644 --- a/src/php/class-admin.php +++ b/src/php/class-admin.php @@ -63,6 +63,7 @@ public function run() { add_action( 'init', array( $this, 'load_classes' ), 11 ); add_filter( 'mu_menu_items', array( $this, 'mu_menu_items' ) ); + add_filter( 'manage_sites_action_links', array( $this, 'add_sites_row_action' ), 10, 2 ); add_filter( 'plugin_action_links_' . plugin_basename( PLUGIN_FILE ), array( $this, 'plugin_action_links' ), 10, 2 ); add_filter( 'plugin_row_meta', array( $this, 'plugin_row_meta' ), 10, 2 ); add_filter( 'debug_information', array( $this, 'debug_information' ) ); @@ -89,6 +90,29 @@ public function mu_menu_items( array $menu_items ): array { return $menu_items; } + /** + * Add a "Snippets" row action to the Network Sites table. + * + * @param array $actions Existing row actions. + * @param int $site_id Current site ID. + * + * @return array + */ + public function add_sites_row_action( array $actions, int $site_id ): array { + if ( ! is_multisite() || ! current_user_can( code_snippets()->get_network_cap_name() ) ) { + return $actions; + } + + $menu_slug = code_snippets()->get_menu_slug(); + $actions['code_snippets'] = sprintf( + '%s', + esc_url( get_admin_url( $site_id, 'admin.php?page=' . $menu_slug ) ), + esc_html__( 'Snippets', 'code-snippets' ) + ); + + return $actions; + } + /** * Modify the action links for this plugin. * diff --git a/src/php/class-list-table.php b/src/php/class-list-table.php index 703765cf..41ed1379 100644 --- a/src/php/class-list-table.php +++ b/src/php/class-list-table.php @@ -37,7 +37,7 @@ class List_Table extends WP_List_Table { * * @var array */ - public array $statuses = [ 'all', 'active', 'inactive', 'recently_activated', 'trashed' ]; + public array $statuses = [ 'all', 'active', 'inactive', 'recently_activated', 'shared_network', 'trashed' ]; /** * Column name to use when ordering the snippets list. @@ -246,6 +246,23 @@ public function get_action_link( string $action, Snippet $snippet ): string { private function get_snippet_action_links( Snippet $snippet ): array { $actions = array(); + if ( $snippet->shared_network && ! $this->is_network ) { + $actions['network_shared'] = sprintf( + '%s', + esc_html__( 'Network Snippet', 'code-snippets' ) + ); + + if ( is_multisite() && is_super_admin() ) { + $actions['edit'] = sprintf( + '%s', + esc_url( $this->get_action_link( 'edit', $snippet ) ), + esc_html__( 'Edit', 'code-snippets' ) + ); + } + + return apply_filters( 'code_snippets/list_table/row_actions', $actions, $snippet ); + } + if ( $snippet->is_trashed() ) { $actions['restore'] = sprintf( '%s', @@ -307,7 +324,14 @@ protected function column_activate( Snippet $snippet ): string { return ''; } - if ( $this->is_network && ( $snippet->shared_network || ( ! $this->is_network && $snippet->network && ! $snippet->shared_network ) ) ) { + // Show icon for shared network snippets on network admin. + if ( $snippet->shared_network && $this->is_network ) { + return ''; + } + + if ( ! $this->is_network && $snippet->network && ! $snippet->shared_network ) { return ''; } @@ -367,18 +391,17 @@ protected function column_name( Snippet $snippet ): string { ); $out = esc_html( $snippet->display_name ); + $user_can_manage_network = current_user_can( code_snippets()->get_network_cap_name() ); // Add a link to the snippet if it isn't an unreadable network-only snippet and isn't trashed. - if ( ! $snippet->is_trashed() && ( $this->is_network || ! $snippet->network || current_user_can( code_snippets()->get_network_cap_name() ) ) ) { + if ( ! $snippet->is_trashed() && ( $this->is_network || ! $snippet->network || $user_can_manage_network ) ) { $out = sprintf( '%s', esc_attr( code_snippets()->get_snippet_edit_url( $snippet->id, $snippet->network ? 'network' : 'admin' ) ), $out ); - } - - if ( $snippet->shared_network ) { - $out .= ' ' . esc_html__( 'Shared on Network', 'code-snippets' ) . ''; + } else { + $out = sprintf( '%s', $out ); } $out = apply_filters( 'code_snippets/list_table/column_name', $out, $snippet ); @@ -545,60 +568,88 @@ public function get_views(): array { // Loop through the view counts. foreach ( $totals as $type => $count ) { - $labels = []; - if ( ! $count ) { continue; } - // translators: %s: total number of snippets. - $labels['all'] = _n( - 'All (%s)', - 'All (%s)', - $count, - 'code-snippets' - ); - - // translators: %s: total number of active snippets. - $labels['active'] = _n( - 'Active (%s)', - 'Active (%s)', - $count, - 'code-snippets' - ); + switch ( $type ) { + case 'all': + // translators: %s: total number of snippets. + $template = _n( + 'All (%s)', + 'All (%s)', + $count, + 'code-snippets' + ); + break; + + case 'active': + // translators: %s: total number of active snippets. + $template = _n( + 'Active (%s)', + 'Active (%s)', + $count, + 'code-snippets' + ); + break; + + case 'inactive': + // translators: %s: total number of inactive snippets. + $template = _n( + 'Inactive (%s)', + 'Inactive (%s)', + $count, + 'code-snippets' + ); + break; + + case 'recently_activated': + // translators: %s: total number of recently activated snippets. + $template = _n( + 'Recently Active (%s)', + 'Recently Active (%s)', + $count, + 'code-snippets' + ); + break; - // translators: %s: total number of inactive snippets. - $labels['inactive'] = _n( - 'Inactive (%s)', - 'Inactive (%s)', - $count, - 'code-snippets' - ); + case 'shared_network': + if ( ! is_multisite() ) { + continue 2; + } - // translators: %s: total number of recently activated snippets. - $labels['recently_activated'] = _n( - 'Recently Active (%s)', - 'Recently Active (%s)', - $count, - 'code-snippets' - ); + $shared_label_template = $this->is_network + ? _n_noop( + 'Shared with Subsites (%s)', + 'Shared with Subsites (%s)', + 'code-snippets' + ) + : _n_noop( + 'Network Snippets (%s)', + 'Network Snippets (%s)', + 'code-snippets' + ); + + $template = translate_nooped_plural( $shared_label_template, $count, 'code-snippets' ); + break; + + case 'trashed': + // translators: %s: total number of trashed snippets. + $template = _n( + 'Trashed (%s)', + 'Trashed (%s)', + $count, + 'code-snippets' + ); + break; - // translators: %s: total number of trashed snippets. - $labels['trashed'] = _n( - 'Trashed (%s)', - 'Trashed (%s)', - $count, - 'code-snippets' - ); + default: + continue 2; + } - // The page URL with the status parameter. $url = esc_url( add_query_arg( 'status', $type ) ); - - // Add a class if this view is currently being viewed. $class = $type === $status ? ' class="current"' : ''; - - // Add the view count to the label. - $text = sprintf( $labels[ $type ], number_format_i18n( $count ) ); + $text = sprintf( $template, number_format_i18n( $count ) ); $status_links[ $type ] = sprintf( '%s', $url, $class, $text ); } @@ -986,46 +1037,43 @@ public function no_items() { /** * Fetch all shared network snippets for the current site. * - * @return void + * @param array $all_snippets List of snippets to merge with. + * + * @return array Updated list of snippets. */ - private function fetch_shared_network_snippets() { - /** - * Table data. - * - * @var $snippets array - */ - global $snippets; + private function fetch_shared_network_snippets( array $all_snippets ): array { + if ( ! is_multisite() ) { + return $all_snippets; + } - $ids = get_site_option( 'shared_network_snippets' ); + $shared_ids = get_site_option( 'shared_network_snippets' ); - if ( ! is_multisite() || ! $ids ) { - return; + if ( ! $shared_ids || ! is_array( $shared_ids ) ) { + return $all_snippets; } if ( $this->is_network ) { - $limit = count( $snippets['all'] ); - - for ( $i = 0; $i < $limit; $i++ ) { - $snippet = &$snippets['all'][ $i ]; - - if ( in_array( $snippet->id, $ids, true ) ) { + // Mark shared network snippets on the network admin page. + foreach ( $all_snippets as $snippet ) { + if ( in_array( $snippet->id, $shared_ids, true ) ) { $snippet->shared_network = true; - $snippet->tags = array_merge( $snippet->tags, array( 'shared on network' ) ); $snippet->active = false; } } } else { + // Fetch shared network snippets for subsites. $active_shared_snippets = get_option( 'active_shared_network_snippets', array() ); - $shared_snippets = get_snippets( $ids, true ); + $shared_snippets = get_snippets( $shared_ids, true ); foreach ( $shared_snippets as $snippet ) { $snippet->shared_network = true; - $snippet->tags = array_merge( $snippet->tags, array( 'shared on network' ) ); $snippet->active = in_array( $snippet->id, $active_shared_snippets, true ); } - $snippets['all'] = array_merge( $snippets['all'], $shared_snippets ); + $all_snippets = array_merge( $all_snippets, $shared_snippets ); } + + return $all_snippets; } /** @@ -1061,8 +1109,7 @@ public function prepare_items() { $this->process_requested_actions(); $snippets = array_fill_keys( $this->statuses, array() ); - $all_snippets = apply_filters( 'code_snippets/list_table/get_snippets', get_snippets() ); - $this->fetch_shared_network_snippets(); + $all_snippets = apply_filters( 'code_snippets/list_table/get_snippets', $this->fetch_shared_network_snippets( get_snippets() ) ); // Separate trashed snippets from the main collection $snippets['trashed'] = array_filter( $all_snippets, function( $snippet ) { @@ -1125,6 +1172,19 @@ function ( Snippet $snippet ) use ( $type ) { $snippets['trashed'] = array_filter( $snippets['trashed'], array( $this, 'search_by_line_callback' ) ); } + if ( is_multisite() ) { + $snippets['shared_network'] = array_values( + array_filter( + $snippets['all'], + static function ( Snippet $snippet ) { + return $snippet->shared_network; + } + ) + ); + } else { + $snippets['shared_network'] = array(); + } + // Clear recently activated snippets older than a week. $recently_activated = $this->is_network ? get_site_option( 'recently_activated_snippets', array() ) : diff --git a/src/php/class-plugin.php b/src/php/class-plugin.php index a38e216e..c05613e2 100644 --- a/src/php/class-plugin.php +++ b/src/php/class-plugin.php @@ -309,25 +309,52 @@ public function get_network_cap_name(): string { return apply_filters( 'code_snippets_network_cap', 'manage_network_options' ); } + /** + * Determine if a subsite user menu is enabled via *Network Settings > Enable administration menus*. + * + * @return bool + */ + public function is_subsite_menu_enabled(): bool { + if ( ! is_multisite() ) { + return true; + } + + $menu_perms = get_site_option( 'menu_items', array() ); + return ! empty( $menu_perms['snippets'] ); + } + + /** + * Determine if the current user should have the network snippets capability. + * + * @return bool + */ + public function user_can_manage_network_snippets(): bool { + return is_super_admin() || current_user_can( $this->get_network_cap_name() ); + } + + /** + * Determine whether the current request originates in the network admin. + * + * @return bool + */ + public function is_network_context(): bool { + return is_network_admin(); + } + /** * Get the required capability to perform a certain action on snippets. * Does not check if the user has this capability or not. * - * If multisite, checks if *Enable Administration Menus: Snippets* is active - * under the *Settings > Network Settings* network admin menu + * If multisite, adjusts the capability based on whether the user is viewing + * the network dashboard or a subsite and whether the menu is enabled for subsites. * * @return string The capability required to manage snippets. * * @since 2.0 */ public function get_cap(): string { - if ( is_multisite() ) { - $menu_perms = get_site_option( 'menu_items', array() ); - - // If multisite is enabled and the snippet menu is not activated, restrict snippet operations to super admins only. - if ( empty( $menu_perms['snippets'] ) ) { - return $this->get_network_cap_name(); - } + if ( is_multisite() && $this->is_network_context() ) { + return $this->get_network_cap_name(); } return $this->get_cap_name(); diff --git a/src/php/rest-api/class-snippets-rest-controller.php b/src/php/rest-api/class-snippets-rest-controller.php index 4027d46b..2cfec385 100644 --- a/src/php/rest-api/class-snippets-rest-controller.php +++ b/src/php/rest-api/class-snippets-rest-controller.php @@ -198,6 +198,7 @@ public function register_routes() { public function get_items( $request ): WP_REST_Response { $network = $request->get_param( 'network' ); $all_snippets = get_snippets( [], $network ); + $all_snippets = $this->get_network_items( $all_snippets, $network ); // Get collection params (page, per_page). $collection_params = $this->get_collection_params(); @@ -229,6 +230,36 @@ public function get_items( $request ): WP_REST_Response { return $response; } + /** + * Retrieve and merge shared network snippets. + * + * @param array $all_snippets List of snippets to merge with. + * @param bool|null $network Whether fetching network snippets. + * + * @return array Modified list of snippets. + */ + private function get_network_items( array $all_snippets, $network ): array { + if ( ! is_multisite() || $network ) { + return $all_snippets; + } + + $shared_ids = get_site_option( 'shared_network_snippets' ); + + if ( ! $shared_ids || ! is_array( $shared_ids ) ) { + return $all_snippets; + } + + $active_shared_snippets = get_option( 'active_shared_network_snippets', array() ); + $shared_snippets = get_snippets( $shared_ids, true ); + + foreach ( $shared_snippets as $snippet ) { + $snippet->shared_network = true; + $snippet->active = in_array( $snippet->id, $active_shared_snippets, true ); + } + + return array_merge( $all_snippets, $shared_snippets ); + } + /** * Retrieves one item from the collection. *