*/ public static function unchosen(Scope $scope, array $roots): array { if (!$scope->is_subsite()) { return []; } $out = [ 'themes' => array_values(array_diff(Inventory::top_level($roots['themes'], new Excludes(false)), self::active($scope, 'themes'))), 'plugins' => array_values(array_diff(Inventory::top_level($roots['plugins'], new Excludes(false)), self::active($scope, 'plugins'))), ]; return $out + self::untouched($scope); } /** * What "active" means for $scope: on a subsite its theme and the theme's parent, and the plugins it activates * plus those the network activates for every site; on a single site, Inventory::active(). * * @return string[] top-level item names */ public static function active(Scope $scope, string $group): array { if (!$scope->is_subsite()) { return Inventory::active($group); } switch_to_blog($scope->blog_id); try { $items = Inventory::active($group); } finally { restore_current_blog(); } if ($group === 'plugins') { foreach (Network::active_plugins() as $basename) { $items[] = strpos($basename, '/') !== false ? strstr($basename, '/', true) : $basename; } } return array_values(array_unique($items)); } /** * Top-level items of $scope's folders that belong to someone else, never sent or moved aside: the main site's * uploads folder holds the other subsites' (uploads/sites). * * @return array */ public static function untouched(Scope $scope): array { return $scope->is_main_site() ? ['uploads' => ['sites']] : []; } /** * How a group's files are placed on $scope: a single site takes them all; a subsite takes its uploads, only the * themes and plugins the network lacks, and never must-use plugins or other wp-content (they affect every subsite). */ public static function policy(Scope $scope, string $group): string { if (!$scope->is_subsite()) { return self::REPLACE; } if ($group === 'uploads') { return self::REPLACE; } return in_array($group, ['themes', 'plugins'], true) ? self::ADD_MISSING : self::SKIP; } /** @return array group => placement policy for the groups that aren't simply replaced on $scope */ public static function policies(Scope $scope): array { if (!$scope->is_subsite()) { return []; } $out = []; foreach (['themes', 'plugins', 'mu-plugins', 'other'] as $group) { $out[$group] = self::policy($scope, $group); } return $out; } /** * What a subsite accepts of the files sent to it: never must-use plugins or other wp-content (spec 2026-10-16 * §6.2). Returns the parts it takes and the groups it leaves out. * * @return array{0: array, 1: string[]} */ public static function receiving(array $parts): array { $skipped = []; foreach (['mu-plugins', 'other'] as $group) { if (($parts[$group]['mode'] ?? 'none') !== 'none') { $parts[$group] = ['mode' => 'none', 'items' => []]; $skipped[] = $group; } } return [$parts, $skipped]; } /** * A subsite's theme has to be enabled for it to stay its theme: one the network doesn't enable is enabled for * that subsite only. */ public static function enable_theme(Scope $scope): void { if (!$scope->is_subsite()) { return; } $network = (array) get_site_option('allowedthemes', []); $allowed = (array) get_blog_option($scope->blog_id, 'allowedthemes', []); $changed = false; foreach (array_unique([(string) get_blog_option($scope->blog_id, 'stylesheet'), (string) get_blog_option($scope->blog_id, 'template')]) as $theme) { if ($theme !== '' && empty($network[$theme]) && empty($allowed[$theme])) { $allowed[$theme] = true; $changed = true; } } if ($changed) { update_blog_option($scope->blog_id, 'allowedthemes', $allowed); } } }