['none', 'all', 'active', 'selected', 'except'], 'plugins' => ['none', 'all', 'active', 'selected', 'except'], 'mu-plugins' => ['none', 'all', 'selected', 'except'], 'uploads' => ['none', 'all', 'changed', 'since'], 'other' => ['none', 'all', 'selected', 'except'], ]; private const MAX_ITEMS = 500; private const MAX_RULES = 100; /** @param mixed $input */ public static function normalize($input): array { $in = is_array($input) ? $input : []; $out = []; foreach (self::MODES as $group => $modes) { $g = is_array($in[$group] ?? null) ? $in[$group] : []; $mode = is_string($g['mode'] ?? null) ? $g['mode'] : 'none'; if (!in_array($mode, $modes, true)) { /* translators: 1: choice, 2: file group, e.g. "plugins" */ throw self::invalid(esc_html(sprintf(__('Unknown choice "%1$s" for %2$s.', 'siterelay'), substr($mode, 0, 40), $group))); } if ($group === 'uploads') { $since = null; if ($mode === 'since') { $since = is_string($g['since'] ?? null) ? $g['since'] : ''; if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $since) || !checkdate((int) substr($since, 5, 2), (int) substr($since, 8, 2), (int) substr($since, 0, 4))) { throw self::invalid(esc_html__('Choose the date from which uploads are copied.', 'siterelay')); } } $out[$group] = ['mode' => $mode, 'since' => $since]; continue; } $items = []; if ($mode === 'selected' || $mode === 'except') { foreach ((array) ($g['items'] ?? []) as $item) { if (!is_string($item) || !RelPath::valid($item) || strpos($item, '/') !== false) { /* translators: %s: name of a theme, plugin or folder */ throw self::invalid(esc_html(sprintf(__('"%s" is not a valid name.', 'siterelay'), is_string($item) ? substr($item, 0, 80) : '?'))); } $items[] = $item; } $items = array_values(array_unique($items)); if (count($items) > self::MAX_ITEMS) { throw self::invalid(esc_html__('Too many items were chosen.', 'siterelay')); } if ($mode === 'selected' && $items === []) { /* translators: %s: file group, e.g. "plugins" */ throw self::invalid(esc_html(sprintf(__('Choose at least one item for %s, or copy none.', 'siterelay'), $group))); } } $out[$group] = ['mode' => $mode, 'items' => $items]; } $rules = $in['excludes'] ?? []; if (is_string($rules)) { $rules = preg_split('/\r\n|\r|\n/', $rules) ?: []; } $out['excludes'] = []; foreach (is_array($rules) ? $rules : [] as $rule) { $rule = is_string($rule) ? trim($rule) : ''; if ($rule === '') { continue; } if (strlen($rule) > 255 || strpos($rule, "\0") !== false) { /* translators: %s: an exclude rule */ throw self::invalid(esc_html(sprintf(__('This exclude rule is not valid: %s', 'siterelay'), substr($rule, 0, 80)))); } $out['excludes'][] = $rule; } if (count($out['excludes']) > self::MAX_RULES) { throw self::invalid(esc_html__('Too many exclude rules.', 'siterelay')); } // Low-disk mode only concerns uploads; mirror needs the restore point low-disk mode skips (spec §5.1, §5.4). $out['low_disk'] = !empty($in['low_disk']) && $out['uploads']['mode'] !== 'none'; $out['mirror'] = !empty($in['mirror']) && !$out['low_disk']; return $out; } public static function any(array $parts): bool { return self::groups($parts) !== []; } /** @return string[] the groups that are copied, in a fixed order */ public static function groups(array $parts): array { $out = []; foreach (array_keys(self::MODES) as $group) { if (($parts[$group]['mode'] ?? 'none') !== 'none') { $out[] = $group; } } return $out; } /** * Which of a site's top-level items in $group are copied. * * @param string[] $available the site's top-level items in the group * @param string[] $active the active ones (themes: current theme and its parent; plugins: active plugins) * @return string[] */ public static function items(array $parts, string $group, array $available, array $active): array { $part = $parts[$group] ?? ['mode' => 'none']; switch ($part['mode']) { case 'none': return []; case 'active': return array_values(array_intersect($available, $active)); case 'selected': return array_values(array_intersect($available, $part['items'])); case 'except': return array_values(array_diff($available, $part['items'])); default: return array_values($available); } } /** May a file list entry of $group/$rel belong to these parts? (Checks what can be checked from the other side.) */ public static function allows(array $parts, string $group, string $rel): bool { $part = $parts[$group] ?? ['mode' => 'none']; $top = RelPath::top($rel); switch ($part['mode']) { case 'none': return false; case 'selected': return in_array($top, $part['items'], true); case 'except': return !in_array($top, $part['items'], true); default: return true; } } private static function invalid(string $message): RelayError { return new RelayError('files.invalid', esc_html($message)); } }