diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000..65f1ce16 --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,10 @@ +last 2 versions +not dead +Chrome >= 111 +Edge >= 111 +Firefox >= 112 +Safari >= 16.4 +Android >= 111 +ChromeAndroid >= 111 +FirefoxAndroid >= 112 +iOS >= 16.4 \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 58f3c835..e2bcb26b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -55,5 +55,5 @@ script: - find . -maxdepth 1 \( -name '*.php' \) -exec php -lf {} \; - find src/php/ \( -name '*.php' \) -exec php -lf {} \; - # Run tests - - npm run test + # Run linters + - npm run lint diff --git a/CHANGELOG.md b/CHANGELOG.md index 38f67002..aea69e36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## [3.9.0-beta.2] (2025-11-10) + +### Added +* Added 'Snippets' row action to the Network Sites table +* Improved snippet name visibility for network users + +### Changed +* Refined badge styling and hover effects for row actions and badges +* Impoved icon style and color usage for network snippets for clearer differentiation + +### Fixed +* Improved network snippets management with better subsite menu permission checks +* Fixed status labels for shared network snippets +* Corrected network condition checks and improved snippet fetching logic +* Handled fatal errors in file-based snippets to prevent crashes ## [3.9.0-beta.1] (2025-11-03) diff --git a/package-lock.json b/package-lock.json index 99945c97..3fe1f4f6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "code-snippets", - "version": "3.9.0-beta.1", + "version": "3.9.0-beta.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "code-snippets", - "version": "3.9.0-beta.1", + "version": "3.9.0-beta.2", "license": "GPL-2.0-or-later", "dependencies": { "@codemirror/fold": "^0.19.4", diff --git a/package.json b/package.json index 7d34e093..d9c0a9bd 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,12 @@ "name": "code-snippets", "description": "Manage code snippets running on a WordPress-powered site through a graphical interface.", "homepage": "https://codesnippets.pro", - "version": "3.9.0-beta.1", + "version": "3.9.0-beta.2", "main": "src/dist/edit.js", "directories": { "test": "tests" }, "scripts": { - "test": "npm run stylelint && eslint && npm run phpcs", "test:playwright": "playwright test -c tests/playwright/playwright.config.ts", "test:playwright:debug": "npm run test:playwright -- --debug", "test:playwright:ui": "npm run test:playwright -- --ui", @@ -20,9 +19,13 @@ "build": "webpack", "watch": "webpack --watch", "bundle": "ts-node scripts/bundle.ts", - "phpcs": "src/vendor/bin/phpcs -s --colors ./src/phpcs.xml", - "phpcbf": "src/vendor/bin/phpcbf ./src/phpcs.xml", - "stylelint": "stylelint --fix 'src/css/**/*.scss'", + "lint": "npm run lint:styles && npm run lint:js && npm run lint:php", + "lint:styles": "stylelint 'src/css/**/*.scss'", + "lint:styles:fix": "stylelint --fix 'src/css/**/*.scss'", + "lint:js": "eslint", + "lint:js:fix": "eslint --fix", + "lint:php": "src/vendor/bin/phpcs -s --colors ./src/phpcs.xml", + "lint:php:fix": "src/vendor/bin/phpcbf ./src/phpcs.xml", "version": "ts-node scripts/version.ts", "version-dev": "npm version --git-tag-version=false --preid=dev", "version-alpha": "npm version --git-tag-version=false --preid=alpha", diff --git a/src/code-snippets.php b/src/code-snippets.php index 6958ee5b..357911ae 100644 --- a/src/code-snippets.php +++ b/src/code-snippets.php @@ -8,11 +8,11 @@ * License: GPL-2.0-or-later * License URI: license.txt * Text Domain: code-snippets - * Version: 3.9.0-beta.1 + * Version: 3.9.0-beta.2 * Requires PHP: 7.4 * Requires at least: 5.0 * - * @version 3.9.0-beta.1 + * @version 3.9.0-beta.2 * @package Code_Snippets * @author Shea Bunge * @copyright 2012-2024 Code Snippets Pro @@ -37,7 +37,7 @@ * * @const string */ - define( 'CODE_SNIPPETS_VERSION', '3.9.0-beta.1' ); + define( 'CODE_SNIPPETS_VERSION', '3.9.0-beta.2' ); /** * The full path to the main file of this plugin. 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/edit/_editor.scss b/src/css/edit/_editor.scss index 257f1628..8063c8f6 100644 --- a/src/css/edit/_editor.scss +++ b/src/css/edit/_editor.scss @@ -68,7 +68,7 @@ padding-inline-end: 0.5em; } - @media screen and (width >= 512px) { + @media (width >= 512px) { white-space: nowrap; } } diff --git a/src/css/manage.scss b/src/css/manage.scss index 0332fc59..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; } @@ -150,7 +150,7 @@ td.column-description { @include theme.link-colors(#579); } -@media screen and (width <= 782px) { +@media (width <= 782px) { p.search-box { float: inline-start; position: initial; @@ -205,7 +205,7 @@ td.column-description { } } -@media screen and (width <= 1190px) { +@media (width <= 1190px) { .nav-tab { .snippet-label { display: none; diff --git a/src/css/welcome.scss b/src/css/welcome.scss index cfceb7db..81c9872a 100644 --- a/src/css/welcome.scss +++ b/src/css/welcome.scss @@ -138,7 +138,7 @@ $breakpoint: 1060px; grid-template-columns: repeat(4, 1fr); gap: 40px 15px; - @media (max-width: $breakpoint) { + @media (width <= $breakpoint) { grid-template-columns: 1fr !important; } } 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. * diff --git a/src/php/settings/settings.php b/src/php/settings/settings.php index 249bdc24..4d906483 100644 --- a/src/php/settings/settings.php +++ b/src/php/settings/settings.php @@ -11,6 +11,7 @@ use Code_Snippets\Welcome_API; use function Code_Snippets\clean_snippets_cache; use function Code_Snippets\code_snippets; +use const Code_Snippets\CACHE_GROUP; const CACHE_KEY = 'code_snippets_settings'; const OPTION_GROUP = 'code-snippets'; @@ -79,7 +80,7 @@ function are_settings_unified(): bool { * @return array> */ function get_settings_values(): array { - $settings = wp_cache_get( CACHE_KEY ); + $settings = wp_cache_get( CACHE_KEY, CACHE_GROUP ); if ( $settings ) { return $settings; } @@ -93,7 +94,7 @@ function get_settings_values(): array { } } - wp_cache_set( CACHE_KEY, $settings ); + wp_cache_set( CACHE_KEY, $settings, CACHE_GROUP ); return $settings; } @@ -125,7 +126,7 @@ function update_setting( string $section, string $field, $new_value ): bool { $settings[ $section ][ $field ] = $new_value; - wp_cache_set( CACHE_KEY, $settings ); + wp_cache_set( CACHE_KEY, $settings, CACHE_GROUP ); return update_self_option( are_settings_unified(), OPTION_NAME, $settings ); } @@ -306,7 +307,7 @@ function process_settings_actions( array $input ): ?array { * @return array> The validated settings. */ function sanitize_settings( array $input ): array { - wp_cache_delete( CACHE_KEY ); + wp_cache_delete( CACHE_KEY, CACHE_GROUP ); $result = process_settings_actions( $input ); if ( ! is_null( $result ) ) { diff --git a/src/php/snippet-ops.php b/src/php/snippet-ops.php index 9e69c63f..cfd66191 100644 --- a/src/php/snippet-ops.php +++ b/src/php/snippet-ops.php @@ -750,7 +750,22 @@ function execute_snippet_from_flat_file( $code, $file, int $id = 0, bool $force return false; } - require_once $file; + ob_start(); + + try { + require_once $file; + $result = null; + } catch ( ParseError $parse_error ) { + $result = $parse_error; + } catch ( Error $error ) { + $result = $error; + } catch ( Throwable $throwable ) { + $result = $throwable; + } + + ob_end_clean(); do_action( 'code_snippets/after_execute_snippet_from_flat_file', $file, $id ); + + return $result ?? null; }