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: = $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 ) );
+ }
+}