['nullable', 'string'], 'logoLight' => ['nullable', 'string'], 'logoPermalinkMode' => ['nullable', 'string'], 'logoPermalinkCustom' => ['nullable', 'string'], 'portalAccess' => ['nullable', 'string'], 'portalFilters' => ['nullable', 'array'], 'promo' => ['nullable', 'array'], 'sidebarFollowing' => ['nullable', 'boolean'], ]; } public function toSettingsData(): array { $portalFilters = \is_array($this->portalFilters) ? $this->portalFilters : []; $showSort = self::isFilterVisible($portalFilters, 'sort'); return [ 'communityTitle' => sanitize_text_field($this->communityTitle ?? ''), 'logoLight' => esc_url_raw($this->logoLight ?? ''), 'logoPermalinkMode' => \in_array($this->logoPermalinkMode, ['default', 'custom'], true) ? $this->logoPermalinkMode : 'default', 'logoPermalinkCustom' => esc_url_raw($this->logoPermalinkCustom ?? ''), 'portalAccess' => \in_array($this->portalAccess, ['everyone', 'logged_in'], true) ? $this->portalAccess : 'everyone', 'portalFilters' => [ 'sort' => $showSort, 'tags' => self::isFilterVisible($portalFilters, 'tags'), // A form that predates the type switch posts none: the chips // keep following Sort, as they did — see GeneralSettings::portalFilters(). 'types' => isset($portalFilters['types']) ? self::isFilterVisible($portalFilters, 'types') : $showSort, ], 'promo' => $this->promo(), 'sidebarFollowing' => $this->sidebarFollowing(), ]; } /** * The sidebar's Following row. As with promo(), a payload without the key * leaves the stored switch as it is. */ private function sidebarFollowing(): bool { if (!$this->has('sidebarFollowing')) { return GeneralSettings::sidebarFollowing( Config::getOption(GeneralSettings::OPTION_NAME->value) ); } return filter_var($this->sidebarFollowing, FILTER_VALIDATE_BOOLEAN); } /** * The credit card in the portal sidebar: only its switch is stored — the * copy is the plugin's own, resolved by GeneralSettings::promo(). Anything * else posted under `promo` is dropped. * * A key missing from the payload means "leave it as it is", not "reset it": * the onboarding form posts only the branding fields, and it must not undo * an admin's deliberate opt-in. * * @return array{enabled: bool} */ private function promo(): array { $stored = GeneralSettings::promo( Config::getOption(GeneralSettings::OPTION_NAME->value) ); $posted = \is_array($this->promo) ? $this->promo : []; return [ // JSON sends a real boolean, a form post sends "true"/"1"/"on". 'enabled' => $this->has('promo') && \array_key_exists('enabled', $posted) ? filter_var($posted['enabled'], FILTER_VALIDATE_BOOLEAN) : $stored['enabled'], ]; } /** * A portal filter is visible unless it was explicitly switched off. A key * missing from the payload — an older admin build, or the onboarding form * that posts only the branding fields — must not silently hide a control. */ private static function isFilterVisible(array $filters, string $key): bool { if (!isset($filters[$key])) { return true; } return filter_var($filters[$key], FILTER_VALIDATE_BOOLEAN); } }