diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml index 316382c4e..004879c27 100644 --- a/.github/workflows/php.yml +++ b/.github/workflows/php.yml @@ -42,7 +42,7 @@ jobs: - php: 7.4 wordpress: trunk - php: 8.0.14 - wordpress: 5.9-RC2 + wordpress: 6.1-RC3 # There are test errors in CI that aren't happening locally. What's different? # - php: 8.1 # wordpress: 5.9-RC2 diff --git a/admin/package-lock.json b/admin/package-lock.json index 0e3e47e92..59a879b89 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -1,12 +1,12 @@ { "name": "font-awesome-admin", - "version": "4.3.1", + "version": "4.3.2-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "font-awesome-admin", - "version": "4.3.1", + "version": "4.3.2-1", "dependencies": { "@fortawesome/fa-icon-chooser-react": "0.4.1", "@fortawesome/fontawesome-svg-core": "^6.2.0", diff --git a/admin/package.json b/admin/package.json index 796085fdc..514efa727 100644 --- a/admin/package.json +++ b/admin/package.json @@ -1,6 +1,6 @@ { "name": "font-awesome-admin", - "version": "4.3.1", + "version": "4.3.2-1", "private": true, "dependencies": { "@fortawesome/fa-icon-chooser-react": "0.4.1", diff --git a/compat-js/package-lock.json b/compat-js/package-lock.json index 7d97ff64f..2003fc53a 100644 --- a/compat-js/package-lock.json +++ b/compat-js/package-lock.json @@ -1,12 +1,12 @@ { "name": "fa-compat-js", - "version": "4.3.1", + "version": "4.3.2-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "fa-compat-js", - "version": "4.3.1", + "version": "4.3.2-1", "dependencies": { "@wordpress/api-fetch": "^5.1.1", "@wordpress/components": "14.1.10", diff --git a/compat-js/package.json b/compat-js/package.json index 2a65bdce8..75823cdb7 100644 --- a/compat-js/package.json +++ b/compat-js/package.json @@ -1,6 +1,6 @@ { "name": "fa-compat-js", - "version": "4.3.1", + "version": "4.3.2-1", "private": true, "dependencies": { "@wordpress/api-fetch": "^5.1.1", diff --git a/includes/class-fontawesome-deactivator.php b/includes/class-fontawesome-deactivator.php index 90f517cfb..b479efd9f 100644 --- a/includes/class-fontawesome-deactivator.php +++ b/includes/class-fontawesome-deactivator.php @@ -46,11 +46,13 @@ function( $blog_id ) { } else { self::delete_options(); } + + // This one handles its own multisite considerations. + FontAwesome_Release_Provider::delete_option(); } private static function delete_options() { delete_option( FontAwesome::OPTIONS_KEY ); - FontAwesome_Release_Provider::delete_option(); delete_option( FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY ); delete_option( FontAwesome_API_Settings::OPTIONS_KEY ); } diff --git a/includes/class-fontawesome-exception.php b/includes/class-fontawesome-exception.php index effb2b139..c1b72b060 100644 --- a/includes/class-fontawesome-exception.php +++ b/includes/class-fontawesome-exception.php @@ -591,4 +591,19 @@ public static function main_option_delete() { ) ); } + + /** + * Internal use only. + * + * @internal + * @ignore + */ + public static function multisite_network_option_update() { + return new static( + esc_html__( + 'Failed updating release metadata on a main network option when trying to upgrade the Font Awesome plugin in multisite mode.', + 'font-awesome' + ) + ); + } } diff --git a/includes/class-fontawesome-release-provider.php b/includes/class-fontawesome-release-provider.php index 27bdb9315..35d4ffbae 100644 --- a/includes/class-fontawesome-release-provider.php +++ b/includes/class-fontawesome-release-provider.php @@ -466,7 +466,7 @@ public function latest_version_6() { */ public static function update_option( $option_value ) { if ( is_multisite() ) { - $network_id = get_current_network_id(); + $network_id = get_main_network_id(); return update_network_option( $network_id, self::OPTIONS_KEY, $option_value ); } else { return update_option( self::OPTIONS_KEY, $option_value, false ); @@ -484,7 +484,7 @@ public static function update_option( $option_value ) { */ public static function get_option() { if ( is_multisite() ) { - $network_id = get_current_network_id(); + $network_id = get_main_network_id(); return get_network_option( $network_id, self::OPTIONS_KEY ); } else { return get_option( self::OPTIONS_KEY ); @@ -499,8 +499,22 @@ public static function get_option() { */ public static function delete_option() { if ( is_multisite() ) { - $network_id = get_current_network_id(); - return delete_network_option( $network_id, self::OPTIONS_KEY ); + $result_accumulator = true; + + /** + * Delete the network option for all networks. + * In 4.3.1, it's possible that this option could have been created in + * any network, which ever one was the current network at the time the plugin + * refreshed releases metadata. + * + * Starting in 4.3.2, we only store the releases metadata on an option associated with the main network. + */ + foreach ( get_networks() as $network ) { + $current_result = delete_network_option( $network->id, self::OPTIONS_KEY ); + $result_accumulator = $result_accumulator && $current_result; + } + + return $result_accumulator; } else { return delete_option( self::OPTIONS_KEY ); } diff --git a/includes/class-fontawesome.php b/includes/class-fontawesome.php index 9f3348e9e..cd04ffa73 100644 --- a/includes/class-fontawesome.php +++ b/includes/class-fontawesome.php @@ -126,7 +126,7 @@ class FontAwesome { * * @since 4.0.0 */ - const PLUGIN_VERSION = '4.3.1'; + const PLUGIN_VERSION = '4.3.2-1'; /** * The namespace for this plugin's REST API. * @@ -489,12 +489,28 @@ public function try_upgrade() { $should_upgrade = true; } + if ( is_multisite() && ! boolval( get_network_option( get_main_network_id(), FontAwesome_Release_Provider::OPTIONS_KEY ) ) ) { + /** + * Handle possible multisite upgrade from 4.3.1. + * In 4.3.1 the release metadata might have been stored in a network + * option associated with a non-main network. As of 4.3.2, it's + * always stored on a network option associated with the main network. + * So if we're in multisite mode and we don't find the release metadata + * on a network option associated with the main network, we need to fix it up. + */ + $should_upgrade = true; + } + if ( $should_upgrade ) { $this->validate_options( $options ); $this->maybe_update_last_used_release_schema_for_upgrade(); - $this->maybe_move_release_metadata_for_upgrade(); + if ( is_multisite() ) { + $this->maybe_move_release_metadata_for_upgrade_multisite(); + } else { + $this->maybe_move_release_metadata_for_upgrade_single_site(); + } /** * Delete the main option to make sure it's removed entirely, including @@ -529,7 +545,7 @@ public function try_upgrade() { * @ignore * @internal */ - private function maybe_move_release_metadata_for_upgrade() { + private function maybe_move_release_metadata_for_upgrade_single_site() { if ( boolval( get_option( FontAwesome_Release_Provider::OPTIONS_KEY ) ) ) { // If this option is set, then we're all caught up. return; @@ -568,6 +584,69 @@ private function maybe_move_release_metadata_for_upgrade() { FontAwesome_Release_Provider::reset(); } + /** + * If upgrading from 4.3.1 to 4.3.2 or beyond, and we don't have the release + * metadata stored on a network option associated with the main network, + * we need to find it on a non-main network and move it to main network. + * + * If we can't find it on a non-main network, either, then that's an + * exception. We intentionally will not query the API server, since + * issuing a blocking request on upgrade is known to cause load problems + * and request timeouts. + * + * Internal use only. + * + * @throws ReleaseMetadataMissingException + * @throws UpgradeException + * @ignore + * @internal + */ + private function maybe_move_release_metadata_for_upgrade_multisite() { + if ( ! is_multisite() ) { + return; + } + + if ( boolval( get_network_option( get_main_network_id(), FontAwesome_Release_Provider::OPTIONS_KEY ) ) ) { + // If there's already release metadata on a network option for the main network, we're done. + return; + } + + foreach ( get_networks() as $network ) { + $option_value = get_network_option( $network->id, FontAwesome_Release_Provider::OPTIONS_KEY ); + + if ( is_array( $option_value ) ) { + $result = update_network_option( get_main_network_id(), FontAwesome_Release_Provider::OPTIONS_KEY, $option_value ); + + if ( ! $result ) { + throw UpgradeException::multisite_network_option_update(); + } + + delete_network_option( $network->id, FontAwesome_Release_Provider::OPTIONS_KEY ); + + /** + * Return early, once we've found what we're looking for. + * While it's possible that there are additional non-main networks that also + * have options with release metadata, it's unlikely. Regardless, + * since this upgrade process happens on a normal front-end page load, + * we don't want to do any unnecessary processing here. The only reason + * to go searching through other network options would be to clean up + * any additional obsolete data. We'll leave that clean up to the plugin's + * uninstall logic. + */ + return; + } + } + + /** + * Now we'll reset the release provider. + * + * If we've fallen through to this point, and we haven't found the release + * metadata stored in one of the previous locations, then this will throw an + * exception. + */ + FontAwesome_Release_Provider::reset(); + } + /** * With 4.1.0, the name of one of the keys in the LAST_USED_RELEASE_TRANSIENT changed. * We can fix it up. diff --git a/index.php b/index.php index 1cee02f37..1da2a2167 100644 --- a/index.php +++ b/index.php @@ -3,7 +3,7 @@ * Plugin Name: Font Awesome * Plugin URI: https://fontawesome.com/how-to-use/on-the-web/using-with/wordpress * Description: The official way to use Font Awesome Free or Pro icons on your site, brought to you by the Font Awesome team. - * Version: 4.3.1 + * Version: 4.3.2-1 * Author: Font Awesome * Author URI: https://fontawesome.com/ * License: GPLv2 (or later) diff --git a/integrations/plugins/font-awesome-cleanup/index.php b/integrations/plugins/font-awesome-cleanup/index.php index 5ab9fbb5b..1fbac5691 100644 --- a/integrations/plugins/font-awesome-cleanup/index.php +++ b/integrations/plugins/font-awesome-cleanup/index.php @@ -72,6 +72,10 @@ function( ) { cleanup_site(); } ); + + foreach ( get_options() as $option ) { + delete_network_option( get_current_network_id(), $option ); + } } else { cleanup_site(); } @@ -204,6 +208,8 @@ function display_cleanup_scope_multisite() { if ( $is_cleanup_network_active ) { $network_id = get_current_network_id(); + $networks = get_networks(); + for_each_blog( function( $site ) use (&$sites) { array_push( $sites, $site ); @@ -212,6 +218,10 @@ function( $site ) use (&$sites) { ?>

Cleaning ALL sites in network with network_id: .

To clean up only one site, activate this cleanup plugin only on that one site instead of activating it network-wide.

+ 0 ) { ?> +

There are other networks that will not be affected by this cleanup.

+

To clean up multiple networks, network activate this cleanup plugin for each network, and run this cleanup for each network.

+ ./tests/ ./tests/loader/ ./tests/test-multisite-activation.php + ./tests/test-multisite-deactivation.php + ./tests/test-multisite-upgrade.php diff --git a/readme.txt b/readme.txt index 4cb9c3ee1..bc4cb60cb 100644 --- a/readme.txt +++ b/readme.txt @@ -3,7 +3,7 @@ Contributors: fontawesome, mlwilkerson, robmadole, frrrances, deathnfudge Stable tag: 4.3.1 Tags: font, awesome, fontawesome, font-awesome, icon, svg, webfont Requires at least: 4.7 -Tested up to: 6.0.2 +Tested up to: 6.1 Requires PHP: 5.6 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html diff --git a/tests/_support/font-awesome-phpunit-util.php b/tests/_support/font-awesome-phpunit-util.php index 698adb132..1682ddba8 100644 --- a/tests/_support/font-awesome-phpunit-util.php +++ b/tests/_support/font-awesome-phpunit-util.php @@ -131,3 +131,41 @@ function create_subsites($domains = ['alpha.example.com', 'beta.example.com']) { return $results; } + +if ( is_multisite() ) : + require_once dirname( __FILE__ ) . '/wp-multi-network-functions.php'; + + function add_network() { + $sub_domain = dechex( wp_rand( -2147483648, 2147483647 ) ); + $domain = "$sub_domain.example.com"; + $path = '/'; + + $admin_user = get_users( array( 'role' => 'administrator' ) )[0]; + $result = \add_network( + array( + 'domain' => $domain, + 'path' => '/', + 'site_name' => $domain, + 'network_name' => $domain, + 'user_id' => $admin_user->ID, + 'network_admin_id' => $admin_user->ID, + ) + ); + + if ( is_wp_error( $result ) ) { + throw new \Exception( 'failed creating network' ); + } + + return $result; + } + + function curry_add_network_handler( &$network_ids ) { + return function ( $network_id, $params ) use ( &$network_ids ) { + if ( ! is_array( $network_ids ) ) { + return null; + } + + array_push( $network_ids, $network_id ); + }; + } +endif; diff --git a/tests/_support/wp-multi-network-functions.php b/tests/_support/wp-multi-network-functions.php new file mode 100644 index 000000000..cb59165a6 --- /dev/null +++ b/tests/_support/wp-multi-network-functions.php @@ -0,0 +1,1018 @@ +ID; + $user_login = $user_info->user_login; + } else { + $user_id = (int) $user_id; + $user_info = get_userdata( $user_id ); + $user_login = $user_info->user_login; + } + + /** + * Filters the networks a user is the administrator of, to short-circuit the process. + * + * @since 2.0.0 + * + * @param array|bool|null List of network IDs or false. Anything but null will short-circuit + * the process. + * @param int User ID for which the networks should be returned. + */ + $my_networks = apply_filters( 'networks_pre_user_is_network_admin', null, $user_id ); + if ( null !== $my_networks ) { + if ( empty( $my_networks ) ) { + $my_networks = false; + } + + /** + * Filters the networks a user is the administrator of. + * + * @since 2.0.0 + * + * @param array|bool List of network IDs or false if no networks for the user. + * @param int User ID for which the networks should be returned. + */ + return apply_filters( 'networks_user_is_network_admin', $my_networks, $user_id ); + } + + $my_networks = array(); + + if ( is_multisite() ) { + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery,WordPress.VIP.DirectDatabaseQuery.NoCaching + $my_networks = array_map( 'intval', $wpdb->get_col( $wpdb->prepare( "SELECT site_id FROM {$wpdb->sitemeta} WHERE meta_key = %s AND meta_value LIKE %s", 'site_admins', '%"' . $user_login . '"%' ) ) ); + } + + // If there are no networks, return false. + if ( empty( $my_networks ) ) { + $my_networks = false; + } + + /** This filter is documented in wp-multi-network/includes/functions.php */ + return apply_filters( 'networks_user_is_network_admin', $my_networks, $user_id ); + } +endif; + +if ( ! function_exists( 'get_main_site_for_network' ) ) : + /** + * Gets the main site for a network. + * + * @since 1.3.0 + * + * @param int|WP_Network $network Optional. Network ID or object. Default is the current network. + * @return int Main site ID for the network. + */ + function get_main_site_for_network( $network = null ) { + $network = get_network( $network ); + + // Bail if network not found. + if ( empty( $network ) ) { + return false; + } + + if ( ! empty( $network->blog_id ) ) { + $primary_id = $network->blog_id; + } else { + $primary_id = get_network_option( $network->id, 'main_site' ); + + if ( false === $primary_id ) { + $sites = get_sites( array( + 'network_id' => $network->id, + 'domain' => $network->domain, + 'path' => $network->path, + 'fields' => 'ids', + 'number' => 1, + ) ); + + $primary_id = ! empty( $sites ) ? reset( $sites ) : 0; + + if ( ! empty( $primary_id ) ) { + update_network_option( $network->id, 'main_site', $primary_id ); + } + } + } + + return (int) $primary_id; + } +endif; + +if ( ! function_exists( 'is_main_site_for_network' ) ) : + /** + * Checks whether a main site is a given site for a network. + * + * @since 1.7.0 + * + * @param int $site_id Site ID to check if it's the main site. + * @return bool True if it is the main site, false otherwise. + */ + function is_main_site_for_network( $site_id ) { + $site = get_site( $site_id ); + $main = get_main_site_id( $site->network_id ); + + // Bail if no site or network was found. + if ( empty( $main ) ) { + return false; + } + + return (int) $main === (int) $site_id; + } +endif; + +if ( ! function_exists( 'get_network_name' ) ) : + /** + * Gets the name of the current network. + * + * @since 1.7.0 + * + * @global WP_Network $current_site Current network object. + * + * @return string Name of the current network. + */ + function get_network_name() { + global $current_site; + + $site_name = get_site_option( 'site_name' ); + if ( ! $site_name ) { + $site_name = ucfirst( $current_site->domain ); + } + + return $site_name; + } +endif; + +if ( ! function_exists( 'switch_to_network' ) ) : + /** + * Switches the current context to the given network. + * + * @since 1.3.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * @global bool $switched_network Whether the network context is switched. + * @global array $switched_network_stack Stack of switched network objects. + * @global WP_Network $current_site Current network object. + * + * @param int $new_network Optional. ID of the network to switch to. Default is the current network ID. + * @param bool $validate Optional. Whether to validate that the given network exists. Default false. + * @return bool True on successful switch, false on failure. + */ + function switch_to_network( $new_network = 0, $validate = false ) { + global $wpdb, $switched_network, $switched_network_stack, $current_site; + + if ( empty( $new_network ) ) { + $new_network = $current_site->id; + } + + // Bail if network does not exist. + if ( ( true === (bool) $validate ) && ! get_network( $new_network ) ) { + return false; + } + + if ( empty( $switched_network_stack ) ) { + $switched_network_stack = array(); + } + + array_push( $switched_network_stack, $current_site ); + + // If the same network, fire the hook and bail. + if ( $current_site->id === $new_network ) { + + /** + * Fires when the current network context is switched. + * + * @since 1.3.0 + * + * @param int $new_network_id ID of the network that is being switched to. + * @param int $old_network_id ID of the previously current network. + */ + do_action( 'switch_network', $current_site->id, $current_site->id ); + $switched_network = true; + return true; + } + + $prev_site_id = $current_site->id; + $current_site = get_network( $new_network ); // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited + + // Populate extra properties if not set already. + if ( ! isset( $current_site->blog_id ) ) { + $current_site->blog_id = get_main_site_id( $current_site->id ); + } + if ( ! isset( $current_site->site_name ) ) { + $current_site->site_name = get_network_name(); + } + + // Update network globals. + $wpdb->siteid = $current_site->id; + $GLOBALS['site_id'] = $current_site->id; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited + $GLOBALS['domain'] = $current_site->domain; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited + + /** This action is documented in wp-multi-network/includes/functions.php */ + do_action( 'switch_network', $current_site->id, $prev_site_id ); + + $switched_network = true; + + return true; + } +endif; + +if ( ! function_exists( 'restore_current_network' ) ) : + /** + * Restores the current context to the previous network. + * + * @since 1.3.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * @global bool $switched_network Whether the network context is switched. + * @global array $switched_network_stack Stack of switched network objects. + * @global WP_Network $current_site Current network object. + * + * @return bool True on successful restore, false on failure. + */ + function restore_current_network() { + global $wpdb, $switched_network, $switched_network_stack, $current_site; + + // Bail if not switched. + if ( true !== $switched_network ) { + return false; + } + + // Bail if no stack. + if ( ! is_array( $switched_network_stack ) ) { + return false; + } + + $new_network = array_pop( $switched_network_stack ); + + // If the same network, fire the hook and bail. + if ( (int) $new_network->id === (int) $current_site->id ) { + + /** This action is documented in wp-multi-network/includes/functions.php */ + do_action( 'switch_network', $current_site->id, $current_site->id ); + $switched_network = ( ! empty( $switched_network_stack ) ); + return true; + } + + $prev_network_id = $current_site->id; + + // Update network globals. + $current_site = $new_network; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited + $wpdb->siteid = $new_network->id; + $GLOBALS['site_id'] = $new_network->id; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited + $GLOBALS['domain'] = $new_network->domain; // phpcs:ignore WordPress.Variables.GlobalVariables.OverrideProhibited + + /** This action is documented in wp-multi-network/includes/functions.php */ + do_action( 'switch_network', $new_network->id, $prev_network_id ); + + $switched_network = ! empty( $switched_network_stack ); + + return true; + } +endif; + +if ( ! function_exists( 'insert_network' ) ) : + /** + * Stores basic network info in the sites table. + * + * This function creates a row in the wp_site table and returns + * the new network ID. It is the first step in creating a new network. + * + * @since 2.2.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param string $domain The domain of the new network. + * @param string $path The path of the new network. + * @return int|bool|WP_Error The ID of the new network, or false on failure. + */ + function insert_network( $domain = '', $path = '/' ) { + global $wpdb; + + // Bail if no domain or path. + if ( empty( $domain ) ) { + return new WP_Error( + 'network_empty', + esc_html__( 'Domain and path cannot be empty.', 'wp-multi-network' ) + ); + } + + // Always end path with a slash. + $path = trailingslashit( $path ); + + // Query for networks. + $networks = get_networks( + array( + 'domain' => $domain, + 'path' => $path, + 'number' => '1', + ) + ); + + // Bail if network already exists. + if ( ! empty( $networks ) ) { + return new WP_Error( + 'network_exists', + esc_html__( 'Network already exists.', 'wp-multi-network' ) + ); + } + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $result = $wpdb->insert( + $wpdb->site, + array( + 'domain' => $domain, + 'path' => $path, + ) + ); + + // Bail if database error. + if ( is_wp_error( $result ) ) { + return $result; + } + + // Bail if no result. + if ( empty( $result ) ) { + return false; + } + + // Cast return value as int. + $network_id = (int) $wpdb->insert_id; + + // Clean the network cache. + clean_network_cache( $network_id ); + + // Return network ID. + return $network_id; + } +endif; + +if ( ! function_exists( 'add_network' ) ) : + /** + * Adds a new network. + * + * @since 1.3.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param array $args { + * Array of network arguments. + * + * @type string $domain Domain name for new network - for VHOST=no, + * this should be FQDN, otherwise domain only. + * @type string $path Path to root of network hierarchy - should + * be '/' unless WP is cohabiting with another + * product on a domain. + * @type string $site_name Name of the root blog to be created on + * the new network. + * @type string $network_name Name of the new network. + * @type integer $user_id ID of the user to add as the site owner. + * Defaults to current user ID. + * @type integer $network_admin_id ID of the user to add as the network administrator. + * Defaults to current user ID. + * @type array $meta Array of metadata to save to this network. + * Defaults to array( 'public' => false ). + * @type integer $clone_network ID of network whose networkmeta values are + * to be copied - default NULL. + * @type array $options_to_clone Override default network meta options to copy + * when cloning - default NULL. + * } + * @return int|WP_Error ID of newly created network, or WP_Error on failure. + */ + function add_network( $args = array() ) { + global $wpdb, $wp_version, $wp_db_version; + + $func_args = func_get_args(); + + // Backward compatibility with old method of passing arguments. + if ( ! is_array( $args ) || count( $func_args ) > 1 ) { + + // Log the deprecated arguments. + _deprecated_argument( + __METHOD__, + '1.7.0', + sprintf( + /* translators: 1: method name, 2: file name */ + esc_html__( 'Arguments passed to %1$s should be in an associative array. See the inline documentation at %2$s for more details.', 'wp-multi-network' ), + __METHOD__, + __FILE__ + ) + ); + + // Juggle function parameters. + $old_args_keys = array( + 0 => 'domain', + 1 => 'path', + 2 => 'site_name', + 3 => 'clone_network', + 4 => 'options_to_clone', + ); + + // Reset arguments to empty array. + $args = array(); + + // Loop through deprecated keys and add items to array. + foreach ( $old_args_keys as $arg_num => $arg_key ) { + if ( isset( $func_args[ $arg_num ] ) ) { + $args[ $arg_key ] = $func_args[ $arg_num ]; + } + } + } + + // Get the current user ID. + $current_user_id = get_current_user_id(); + + // Default site meta. + $default_site_meta = array( + 'public' => get_option( 'blog_public', false ), + ); + + // Default network meta. + $default_network_meta = array( + 'wpmu_upgrade_site' => $wp_db_version, + 'initial_db_version' => $wp_db_version, + ); + + // Parse all of the arguments. + $r = wp_parse_args( $args, array( + + // Site & network arguments. + 'domain' => '', + 'path' => '/', + + // Site arguments. + 'site_name' => esc_attr__( 'New Site', 'wp-multi-network' ), + 'user_id' => $current_user_id, + 'meta' => $default_site_meta, + + // Network arguments. + 'network_name' => esc_attr__( 'New Network', 'wp-multi-network' ), + 'network_admin_id' => $current_user_id, + 'network_meta' => $default_network_meta, + 'clone_network' => false, + 'options_to_clone' => array_keys( network_options_to_copy() ), + ) ); + + // Bail if no user with the given ID for the site exists. + if ( empty( $r['user_id'] ) || ! get_userdata( $r['user_id'] ) ) { + return new WP_Error( + 'network_user', + esc_html__( 'User does not exist.', 'wp-multi-network' ), + array( + 'status' => 403, + ) + ); + } + + // Bail if no user with the given ID for the network exists. + if ( empty( $r['network_admin_id'] ) || ! get_userdata( $r['network_admin_id'] ) ) { + return new WP_Error( + 'network_super_admin', + esc_html__( 'User does not exist.', 'wp-multi-network' ), + array( + 'status' => 403, + ) + ); + } + + // Strip spaces out of domain & path. + $r['domain'] = str_replace( ' ', '', strtolower( $r['domain'] ) ); + $r['path'] = str_replace( ' ', '', strtolower( $r['path'] ) ); + + // Insert the new network. + $new_network_id = insert_network( $r['domain'], $r['path'] ); + + // Bail if insert returned an error. + if ( empty( $new_network_id ) || is_wp_error( $new_network_id ) ) { + return $new_network_id; + } + + // Set the installation constant to true. + if ( ! defined( 'WP_INSTALLING' ) ) { + define( 'WP_INSTALLING', true ); + } + + // Switch to new network. + switch_to_network( $new_network_id ); + + // Make sure upload constants are defined. + ms_upload_constants(); + + // Attempt to create the site. + $new_blog_id = wpmu_create_blog( + $r['domain'], + $r['path'], + $r['site_name'], + $r['user_id'], + $r['meta'], + $new_network_id + ); + + // Grant super admin priviledges. + grant_super_admin( $r['network_admin_id'] ); + + // Restore current network. + restore_current_network(); + + // Bail if main site could not be created. + if ( is_wp_error( $new_blog_id ) ) { + return $new_blog_id; + } + + $r['network_meta']['main_site'] = $new_blog_id; + + if ( empty( $r['network_meta']['site_name'] ) ) { + $r['network_meta']['site_name'] = ! empty( $r['network_name'] ) + ? $r['network_name'] + : $r['site_name']; + } + + foreach ( $r['network_meta'] as $key => $value ) { + update_network_option( $new_network_id, $key, $value ); + } + + // Fix upload path and URLs in WP < 3.7. + $use_files_rewriting = defined( 'SITE_ID_CURRENT_SITE' ) && get_network( SITE_ID_CURRENT_SITE ) + ? get_network_option( SITE_ID_CURRENT_SITE, 'ms_files_rewriting' ) + : get_site_option( 'ms_files_rewriting' ); + + // Not using rewriting, and using a newer version of WordPress than 3.7. + if ( empty( $use_files_rewriting ) && version_compare( $wp_version, '3.7', '>' ) ) { + + // WP_CONTENT_URL is locked to the current site and can't be overridden, + // so we have to replace the hostname the hard way. + $current_siteurl = get_option( 'siteurl' ); + $new_siteurl = untrailingslashit( get_blogaddress_by_id( $new_blog_id ) ); + $upload_url = str_replace( $current_siteurl, $new_siteurl, WP_CONTENT_URL ); + $upload_url = $upload_url . '/uploads'; + + $upload_dir = WP_CONTENT_DIR; + if ( 0 === strpos( $upload_dir, ABSPATH ) ) { + $upload_dir = substr( $upload_dir, strlen( ABSPATH ) ); + } + $upload_dir .= '/uploads'; + + if ( defined( 'MULTISITE' ) ) { + $ms_dir = '/sites/' . $new_blog_id; + } else { + $ms_dir = '/' . $new_blog_id; + } + + $upload_dir .= $ms_dir; + $upload_url .= $ms_dir; + + update_blog_option( $new_blog_id, 'upload_path', $upload_dir ); + update_blog_option( $new_blog_id, 'upload_url_path', $upload_url ); + } + + // Clone network meta from existing network. + if ( ! empty( $r['clone_network'] ) && get_network( $r['clone_network'] ) ) { + + // Define empty options cache array. + $options_cache = array(); + + // Get the values of options to clone. + foreach ( $r['options_to_clone'] as $option ) { + $options_cache[ $option ] = get_network_option( $r['clone_network'], $option ); + } + + // Clone options. + foreach ( $r['options_to_clone'] as $option ) { + + // Skip if not set. + if ( ! isset( $options_cache[ $option ] ) ) { + continue; + } + + // Fix for bug that prevents writing the ms_files_rewriting value for new networks. + if ( 'ms_files_rewriting' === $option ) { + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->insert( $wpdb->sitemeta, array( + 'site_id' => $new_network_id, + // phpcs:ignore WordPress.VIP.SlowDBQuery + 'meta_key' => $option, + // phpcs:ignore WordPress.VIP.SlowDBQuery + 'meta_value' => $options_cache[ $option ], + ) ); + } else { + update_network_option( $new_network_id, $option, $options_cache[ $option ] ); + } + } + } + + // Update network counts. + wp_update_network_counts( $new_network_id ); + + // Clean the network cache. + clean_network_cache( $new_network_id ); + + /** + * Fires after a new network has been added. + * + * @since 1.3.0 + * + * @param int $new_network_id ID of the added network. + * @param array $r Full associative array of network arguments. + */ + do_action( 'add_network', $new_network_id, $r ); + + // Return new network ID. + return $new_network_id; + } +endif; + +if ( ! function_exists( 'update_network' ) ) : + /** + * Modifies the domain/path of a network, and updates all of its sites. + * + * @since 1.3.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int $id ID of network to modify. + * @param string $domain New domain for network. + * @param string $path New path for network. + * @return bool|WP_Error True on success, or WP_Error on failure. + */ + function update_network( $id, $domain, $path = '' ) { + global $wpdb; + + $network = get_network( $id ); + + // Bail if network not found. + if ( empty( $network ) ) { + return new WP_Error( 'network_not_exist', __( 'Network does not exist.', 'wp-multi-network' ) ); + } + + $site_id = get_main_site_id( $id ); + $path = wp_sanitize_site_path( $path ); + + // Bail if site URL is invalid. + if ( ! wp_validate_site_url( $domain, $path, $site_id ) ) { + /* translators: %s: site domain and path */ + return new WP_Error( 'blog_bad', sprintf( __( 'The site "%s" is invalid, not available, or already exists.', 'wp-multi-network' ), $domain . $path ) ); + } + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $update_result = $wpdb->update( + $wpdb->site, + array( + 'domain' => $domain, + 'path' => $path, + ), + array( + 'id' => $network->id, + ) + ); + if ( is_wp_error( $update_result ) ) { + return new WP_Error( 'network_not_updated', __( 'Network could not be updated.', 'wp-multi-network' ) ); + } + + $path = ! empty( $path ) ? $path : $network->path; + $full_path = untrailingslashit( $domain . $path ); + $old_path = untrailingslashit( $network->domain . $network->path ); + + $sites = get_sites( array( + 'network_id' => $network->id, + ) ); + + // Update network site domains and paths as necessary. + if ( ! empty( $sites ) ) { + foreach ( $sites as $site ) { + $update = array(); + + if ( $network->domain !== $domain ) { + $update['domain'] = str_replace( $network->domain, $domain, $site->domain ); + } + + if ( $network->path !== $path ) { + $search = sprintf( '|^%s|', preg_quote( $network->path, '|' ) ); + $update['path'] = preg_replace( $search, $path, $site->path, 1 ); + } + + if ( empty( $update ) ) { + continue; + } + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->update( $wpdb->blogs, $update, array( + 'blog_id' => (int) $site->id, + ) ); + + $option_table = $wpdb->get_blog_prefix( $site->id ) . 'options'; + + // Loop through URL-dependent options and correct them. + foreach ( network_options_list() as $option_name ) { + $value = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM {$option_table} WHERE option_name = %s", $option_name ) ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + + if ( ! empty( $value ) && ( false !== strpos( $value->option_value, $old_path ) ) ) { + $new_value = str_replace( $old_path, $full_path, $value->option_value ); + update_blog_option( $site->id, $option_name, $new_value ); + } + } + + // Clean the blog cache. + clean_blog_cache( $site->id ); + } + } + + // Update network counts. + wp_update_network_counts( $network->id ); + + // Clean the network cache. + clean_network_cache( $network->id ); + + /** + * Fires after an existing network has been updated. + * + * @since 1.3.0 + * + * @param int $network_id ID of the added network. + * @param array $args Associative array of network arguments. + */ + do_action( 'update_network', $network->id, array( + 'domain' => $network->domain, + 'path' => $network->path, + ) ); + + return true; + } +endif; + +if ( ! function_exists( 'delete_network' ) ) : + /** + * Deletes a network and all its sites. + * + * @since 1.3.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int $network_id ID of network to delete. + * @param bool $delete_blogs Flag to permit site deletion - default setting + * of false will prevent deletion of occupied networks. + * @return bool|WP_Error True on success, or WP_Error on failure. + */ + function delete_network( $network_id, $delete_blogs = false ) { + global $wpdb; + + $network = get_network( $network_id ); + + // Bail if network does not exist. + if ( empty( $network ) ) { + return new WP_Error( 'network_not_exist', __( 'Network does not exist.', 'wp-multi-network' ) ); + } + + $sites = get_sites( array( + 'network_id' => $network->id, + ) ); + if ( ! empty( $sites ) ) { + + // Bail if site deletion is off. + if ( empty( $delete_blogs ) ) { + return new WP_Error( 'network_not_empty', __( 'Cannot delete network with sites.', 'wp-multi-network' ) ); + } + + if ( true === $delete_blogs ) { + foreach ( $sites as $site ) { + if ( wp_should_rescue_orphaned_sites() ) { + move_site( $site->id, 0 ); + continue; + } + + wpmu_delete_blog( $site->id, true ); + } + } + } + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->site} WHERE id = %d", $network->id ) ); + + // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->sitemeta} WHERE site_id = %d", $network->id ) ); + + // Clean the network cache. + clean_network_cache( $network->id ); + + /** + * Fires after a network has been deleted. + * + * @since 1.3.0 + * + * @param WP_Network $network The deleted network object. + */ + do_action( 'delete_network', $network ); + + return true; + } +endif; + +if ( ! function_exists( 'move_site' ) ) : + /** + * Moves a site to a new network. + * + * @since 1.3.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int $site_id ID of site to move. + * @param int $new_network_id ID of destination network. + * @return int|bool|WP_Error New network ID on success, true if site cannot be moved, + * or WP_Error on failure. + */ + function move_site( $site_id = 0, $new_network_id = 0 ) { + global $wpdb; + + $site = get_site( $site_id ); + + // Cast network IDs to ints. + $old_network_id = (int) $site->network_id; + $new_network_id = (int) $new_network_id; + + // Bail if site does not exist. + if ( empty( $site ) ) { + return new WP_Error( 'blog_not_exist', __( 'Site does not exist.', 'wp-multi-network' ) ); + } + + // Bail if site is the main site. + if ( is_main_site( $site->id, $old_network_id ) ) { + return true; + } + + // Bail if no change. + if ( $new_network_id === $old_network_id ) { + return true; + } + + // Update the database entry. + $result = $wpdb->update( // phpcs:ignore WordPress.VIP.DirectDatabaseQuery.DirectQuery + $wpdb->blogs, + array( + 'site_id' => $new_network_id, + ), + array( + 'blog_id' => $site->id, + ) + ); + + // Bail if error. + if ( empty( $result ) ) { + return new WP_Error( 'blog_not_moved', __( 'Site could not be moved.', 'wp-multi-network' ) ); + } + + // Update old network counts. + if ( 0 !== $old_network_id ) { + wp_update_network_counts( $old_network_id ); + } + + // Update new network counts. + if ( 0 !== $new_network_id ) { + wp_update_network_counts( $new_network_id ); + } + + // Clean the blog cache. + clean_blog_cache( $site_id ); + + // Clean the network caches. + clean_network_cache( + array_filter( + array( + $site->network_id, + $new_network_id, + ) + ) + ); + + /** + * Fires after a site has been moved to a new network. + * + * @since 1.3.0 + * + * @param int $site_id ID of the site that has been moved. + * @param int $old_network_id ID of the original network for the site. + * @param int $new_network_id ID of the network the site has been moved to. + */ + do_action( 'move_site', $site_id, $site->network_id, $new_network_id ); + + return $new_network_id; + } +endif; + +if ( ! function_exists( 'network_options_list' ) ) : + /** + * Lists the URL-dependent options. + * + * @since 1.3.0 + * + * @return array List of network option names. + */ + function network_options_list() { + $network_options = array( + 'siteurl', + 'home', + ); + + /** + * Filters the list of network options that depend on the domain and path of a network. + * + * @since 1.3.0 + * + * @param array $network_options List of network option names. + */ + return apply_filters( 'network_options_list', $network_options ); + } +endif; + +if ( ! function_exists( 'network_options_to_copy' ) ) : + /** + * Lists the default network options to copy. + * + * @since 1.3.0 + * + * @return array List of network $option_name => $option_label pairs. + */ + function network_options_to_copy() { + $network_options = array( + 'admin_email' => __( 'Network admin email', 'wp-multi-network' ), + 'admin_user_id' => __( 'Admin user ID - deprecated', 'wp-multi-network' ), + 'allowed_themes' => __( 'OLD List of allowed themes - deprecated', 'wp-multi-network' ), + 'allowedthemes' => __( 'List of allowed themes', 'wp-multi-network' ), + 'banned_email_domains' => __( 'Banned email domains', 'wp-multi-network' ), + 'first_post' => __( 'Content of first post on a new blog', 'wp-multi-network' ), + 'limited_email_domains' => __( 'Permitted email domains', 'wp-multi-network' ), + 'ms_files_rewriting' => __( 'Uploaded file handling', 'wp-multi-network' ), + 'site_admins' => __( 'List of network admin usernames', 'wp-multi-network' ), + 'upload_filetypes' => __( 'List of allowed file types for uploads', 'wp-multi-network' ), + 'welcome_email' => __( 'Content of welcome email', 'wp-multi-network' ), + ); + + /** + * Filters the default network options to copy. + * + * @since 1.3.0 + * + * @return array List of network $option_name => $option_label pairs. + */ + return apply_filters( 'network_options_to_copy', $network_options ); + } +endif; diff --git a/tests/test-multisite-activation.php b/tests/test-multisite-activation.php index a1b53d7e2..3bf53682e 100644 --- a/tests/test-multisite-activation.php +++ b/tests/test-multisite-activation.php @@ -11,8 +11,10 @@ * Class MultisiteActivationTest */ class MultisiteActivationTest extends TestCase { - protected $sub_sites = array(); - protected $original_blog_id = null; + protected $sub_sites = array(); + protected $original_blog_id = null; + protected $original_network_id = null; + protected $added_network_ids = array(); public function set_up() { parent::set_up(); @@ -28,11 +30,13 @@ public function set_up() { throw new \Exception(); } - $this->original_blog_id = get_current_blog_id(); + $this->original_blog_id = get_current_blog_id(); + $this->original_network_id = get_current_network_id(); reset_db(); remove_all_actions( 'font_awesome_preferences' ); remove_all_filters( 'wp_is_large_network' ); + add_action( 'add_network', curry_add_network_handler( $this->added_network_ids ), 99, 2 ); FontAwesome::reset(); ( new Mock_FontAwesome_Metadata_Provider() )->mock( array( @@ -64,8 +68,26 @@ public function set_up() { public function tear_down() { parent::tear_down(); + remove_all_actions( 'add_network' ); + switch_to_blog( $this->original_blog_id ); + // Delete all sites on the non-original network. + foreach ( $this->added_network_ids as $network_id ) { + \switch_to_network( $network_id ); + $sites = get_sites( + array( + 'network_id' => $network_id, + ) + ); + + foreach ( $sites as $site ) { + wp_delete_site( $site->ID ); + } + } + + \switch_to_network( $this->original_network_id ); + foreach ( $this->sub_sites as $blog_id ) { wp_delete_site( $blog_id ); } @@ -246,4 +268,37 @@ function( $blog_id ) use ( &$visited_blog_ids ) { $this->assertEquals( $all_site_blog_ids, $visited_blog_ids ); } + + public function test_add_network_after_activation() { + if ( ! $this->is_wp_version_compatible() ) { + $this->assertTrue( true ); + return; + } + + if ( ! is_network_admin() ) { + // Do nothing when we're not in network admin mode. + $this->assertTrue( true ); + return; + } + + $test_obj = $this; + + // This activates network wide, for all sites that exist at the time. + FontAwesome_Activator::initialize(); + + // Now create a new network. + $new_network_id = add_network(); + + // Switch to it. + \switch_to_network( $new_network_id ); + + FontAwesome_Release_Provider::reset(); + + // This should not throw an exception, despite switching networks. + $ver = fa()->latest_version_6(); + $this->assertEquals( $ver, '6.1.1' ); + + $expected_options = array_merge( FontAwesome::DEFAULT_USER_OPTIONS, array( 'version' => fa()->latest_version_6() ) ); + $this->assertEquals( $expected_options, fa()->options() ); + } } diff --git a/tests/test-multisite-deactivation.php b/tests/test-multisite-deactivation.php new file mode 100644 index 000000000..7f980e4d7 --- /dev/null +++ b/tests/test-multisite-deactivation.php @@ -0,0 +1,106 @@ +mock( + array( + wp_json_encode( + array( + 'data' => graphql_releases_query_fixture(), + ) + ), + wp_json_encode( + array( + 'data' => graphql_releases_query_fixture(), + ) + ) + ) + ); + + // This activates network wide, for all sites that exist at the time. + FontAwesome_Activator::initialize(); + + add_action( 'add_network', curry_add_network_handler( $this->added_network_ids ), 99, 2 ); + } + + public function tear_down() { + FontAwesome_Metadata_Provider::reset(); + remove_all_actions( 'add_network' ); + } + + public function test_uninstall_on_main_network() { + $this->run_multisite_uninstall_test( true ); + } + + public function test_uninstall_on_non_main_network() { + $this->run_multisite_uninstall_test( false ); + } + + public function run_multisite_uninstall_test( $run_on_main_network = true ) { + if ( ! is_multisite() ) { + throw new \Exception(); + } + + /** + * As of 4.3.2, the initialize() that will have run in the set_up() above will have put the release metadata + * in a network option associated with the main network. + * In 4.3.1, it would have been stored in a network option associated with the *current* network + * at the time of retrieval and storage. + * + * So to simulate the scenario that would have been possible in 4.3.1, we'll add it also to a non-main network. + * + * We want to ensure that any possible network option is cleaned up at the time of uninstall. + */ + + // Create a new, non-main network. + $new_network_id = add_network(); + $main_network_id = get_main_network_id(); + + // Get the metadata that would have been stored on a main network option. + $opt = get_network_option( $main_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ); + + // Put it on an option associated with the new network. + update_network_option( $new_network_id, FontAwesome_Release_Provider::OPTIONS_KEY, $opt ); + + // Make sure they're both there. + $this->assertArrayHasKey( 'refreshed_at', get_network_option( $new_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ) ); + $this->assertArrayHasKey( 'refreshed_at', get_network_option( $main_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ) ); + + if ( ! $run_on_main_network ) { + // Now switch to that new network. + \switch_to_network( $new_network_id ); + } + + FontAwesome_Deactivator::uninstall(); + + $this->assertFalse( get_network_option( $new_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ) ); + $this->assertFalse( get_network_option( $main_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ) ); + } +} diff --git a/tests/test-multisite-upgrade.php b/tests/test-multisite-upgrade.php new file mode 100644 index 000000000..0c767c8d9 --- /dev/null +++ b/tests/test-multisite-upgrade.php @@ -0,0 +1,115 @@ +mock( + array( + wp_json_encode( + array( + 'data' => graphql_releases_query_fixture(), + ) + ), + wp_json_encode( + array( + 'data' => graphql_releases_query_fixture(), + ) + ) + ) + ); + + FontAwesome_Release_Provider::load_releases(); + + add_action( 'add_network', curry_add_network_handler( $this->added_network_ids ), 99, 2 ); + } + + public function tear_down() { + FontAwesome_Metadata_Provider::reset(); + remove_all_actions( 'add_network' ); + } + + public function test_try_upgrade_on_main_network_when_release_metatdata_stored_in_non_main_network() { + $this->run_multisite_upgrade_test( true ); + } + + public function test_try_upgrade_on_non_main_network_when_release_metatdata_stored_in_non_main_network() { + $this->run_multisite_upgrade_test( false ); + } + + public function run_multisite_upgrade_test($run_on_main_network = true) { + if ( ! is_multisite() ) { + throw new \Exception(); + } + + /** + * As of 4.3.2, the initialize() that will have run in the set_up() above will have put the release metadata + * in a network option associated with the main network. + * In 4.3.1, it would have been stored in a network option associated with the *current* network + * at the time of retrieval and storage. + * + * So to simulate the scenario that would have been possible in 4.3.1, we'll move it to a non-main network, + * such that the release metadata are stored on an option associated with the *current* network at the time + * of retrieval and storage. + */ + + // Create a new, non-main network. + $new_network_id = add_network(); + $main_network_id = get_main_network_id(); + + // Get the metadata that would have been stored on a main network option. + $opt = get_network_option( $main_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ); + + // Put it on an option associated with the new network. + update_network_option( $new_network_id, FontAwesome_Release_Provider::OPTIONS_KEY, $opt ); + + // And get rid of the original one on the main network. + delete_network_option( $main_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ); + + $this->assertFalse( get_network_option( $main_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ) ); + $this->assertArrayHasKey( 'refreshed_at', get_network_option( $new_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ) ); + + // Clear options cache. + wp_cache_delete ( 'alloptions', 'options' ); + + if ( ! $run_on_main_network ) { + // Now switch to that new network. + \switch_to_network( $new_network_id ); + } + + // This activates network wide, for all sites that exist at the time on the current network. + $expected_options = array_merge( FontAwesome::DEFAULT_USER_OPTIONS, array( 'version' => fa()->latest_version_6() ) ); + update_option( FontAwesome::OPTIONS_KEY, $expected_options ); + + // Expecting no exception to be thrown. + $this->assertNull( fa()->try_upgrade() ); + + $this->assertFalse( get_network_option( $new_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ) ); + $this->assertArrayHasKey( 'refreshed_at', get_network_option( $main_network_id, FontAwesome_Release_Provider::OPTIONS_KEY ) ); + } +}