diff --git a/.github/workflows/code-check.yml b/.github/workflows/code-check.yml index d4fd988c9..056b4cfa6 100644 --- a/.github/workflows/code-check.yml +++ b/.github/workflows/code-check.yml @@ -20,8 +20,8 @@ jobs: name: Check the code uses: publishpress/github-workflows/.github/workflows/code-standards.yml@v3 - welcome-tests: - name: Welcome regressions (PHP ${{ matrix.php }}) + php-regression-tests: + name: Regressions (PHP ${{ matrix.php }}) runs-on: ubuntu-latest strategy: fail-fast: false @@ -33,10 +33,6 @@ jobs: with: php-version: ${{ matrix.php }} coverage: none - - name: Check post-type choices and save handler - run: | - php tests/unit/welcome-post-types.php - php tests/unit/welcome-save.php - name: Check Read More fallback behavior run: php tests/unit/read-more-fallback.php - name: Check permission-scoped Trash counts diff --git a/activation.php b/activation.php index 0d097d1e4..59bb1503d 100644 --- a/activation.php +++ b/activation.php @@ -11,14 +11,6 @@ } $db_ver = (is_array($ver) && isset( $ver['db_version'] ) ) ? $ver['db_version'] : ''; - -// Enroll only genuine first installations. Existing sites can still open the -// welcome page manually, but are never redirected or shown onboarding notices. -if (empty($ver)) { - update_option('presspermit_welcome_enrolled', 1, false); - update_option('presspermit_welcome_redirect_pending', 1, false); -} - require_once(__DIR__ . '/classes/PublishPress/Permissions/DB/DatabaseSetup.php'); new \PublishPress\Permissions\DB\DatabaseSetup($db_ver); @@ -31,4 +23,4 @@ update_option('presspermit_activation', true); update_option('presspermit_refresh_role_usage', true); -do_action('presspermit_activate'); +do_action('presspermit_activate'); \ No newline at end of file diff --git a/classes/PublishPress/Permissions/UI/WelcomeCallbackStep.php b/classes/PublishPress/Permissions/UI/WelcomeCallbackStep.php deleted file mode 100644 index c476d67e4..000000000 --- a/classes/PublishPress/Permissions/UI/WelcomeCallbackStep.php +++ /dev/null @@ -1,46 +0,0 @@ -id = sanitize_key($id); - $this->renderer = $renderer; - $this->ownNavigation = (bool) $ownNavigation; - } - - public function getId() - { - return $this->id; - } - - public function hasOwnNavigation() - { - return $this->ownNavigation; - } - - public function render(RenderContext $context) - { - call_user_func($this->renderer, $context); - } -} diff --git a/classes/PublishPress/Permissions/UI/WelcomeExperience.php b/classes/PublishPress/Permissions/UI/WelcomeExperience.php deleted file mode 100644 index 9dbc43704..000000000 --- a/classes/PublishPress/Permissions/UI/WelcomeExperience.php +++ /dev/null @@ -1,652 +0,0 @@ - 'permissions', - 'page_slug' => 'presspermit-welcome', - 'parent_slug' => 'presspermit-groups', - 'page_title' => __('Welcome to PublishPress Permissions', 'press-permit-core'), - 'menu_title' => __('Welcome', 'press-permit-core'), - 'capability' => 'pp_manage_settings', - 'accent_color' => '#655997', - 'mode' => $this->displayMode(), - 'allow_mode_override' => defined('WP_DEBUG') && WP_DEBUG, - 'brand_name' => __('Permissions', 'press-permit-core'), - 'logo' => [ - 'url' => $this->urlPath() . '/common/img/publishpress-logo.png', - 'alt' => 'PublishPress', - 'show_name' => true, - ], - // Optional, skippable email step from the shared library. Override or disable it with the - // "publishpress_welcome_permissions_email_capture" filter. Delivery uses the shared connector endpoint once deployed. - 'email_capture' => [ - 'personas' => [], - 'asset' => $this->asset('penguins.png'), - 'after_form_callback' => [$this, 'renderCapabilities'], - 'consent_statement' => 'permissions-email-v1', - 'consent_prechecked' => true, - 'privacy_url' => 'https://publishpress.com/privacy-policy/', - 'copy' => [ - 'lede' => __('Get Permissions tips and product news in your inbox. This is optional, and everything works if you skip it.', 'press-permit-core'), - 'disclosure' => __('Your email address is sent to %s when you subscribe. Nothing is sent if you skip.', 'press-permit-core'), - 'confirmation_note' => '', - 'consent_label' => __('Yes, send me tips, product news and offers tailored to how I use PublishPress Permissions. I can unsubscribe at any time.', 'press-permit-core'), - ], - ], - // Sample release highlights, shown once per version after an update. Replace with real release notes. - 'whats_new' => [ - 'version' => PRESSPERMIT_VERSION, - 'title' => sprintf(__('What is new in Permissions %s', 'press-permit-core'), PRESSPERMIT_VERSION), - 'items' => [ - ['title' => __('Guided tour', 'press-permit-core'), 'body' => __('A short walkthrough of roles, exceptions and the Teaser.', 'press-permit-core')], - ['title' => __('Setup checklist', 'press-permit-core'), 'body' => __('See at a glance what is left to configure.', 'press-permit-core')], - ], - 'url' => 'https://publishpress.com/blog/', - ], - 'notice_icon_url' => $this->urlPath() . '/common/img/publishpress-logo-icon.png', - 'styles' => [ - [ - 'handle' => 'presspermit-welcome', - 'url' => $this->urlPath() . '/common/css/welcome-permissions.css', - 'version' => PRESSPERMIT_VERSION, - ], - ], - 'enrollment_option' => 'presspermit_welcome_enrolled', - 'redirect_option' => 'presspermit_welcome_redirect_pending', - 'user_option_prefix' => 'presspermit_welcome', - 'notice_title' => __('Permissions is active. Want a two minute tour?', 'press-permit-core'), - 'notice_body' => __('Choose your content, explore the Teaser, and set up access for your users.', 'press-permit-core'), - 'checklist_pages' => ['presspermit-groups'], - 'notice_pages' => ['presspermit-groups', 'presspermit-settings'], - 'checklist_title' => __('Finish setting up Permissions', 'press-permit-core'), - 'checklist_complete_title' => __('Permissions is set up', 'press-permit-core'), - 'exit_url' => admin_url('admin.php?page=presspermit-settings'), - 'labels' => [ - 'back' => __('Back', 'press-permit-core'), - 'next' => __('Next', 'press-permit-core'), - 'skip' => __('Skip', 'press-permit-core'), - 'start' => __('Start the tour', 'press-permit-core'), - 'continue' => __('Continue the tour', 'press-permit-core'), - 'dismiss' => __('Dismiss', 'press-permit-core'), - 'hide' => __('Hide', 'press-permit-core'), - 'do_it' => __('Do it', 'press-permit-core'), - 'view' => __('View', 'press-permit-core'), - 'replay' => __('Replay the guide', 'press-permit-core'), - 'step_format' => __('Step %1$s of %2$s', 'press-permit-core'), - 'reassurance' => __('Your settings change only when you choose Save and continue.', 'press-permit-core'), - ], - ] - ); - - $this->controller = new WelcomeController($config, $this->steps(), $this->tasks()); - $this->controller->register(); - - add_action('admin_post_' . self::POST_TYPES_ACTION, [$this, 'savePostTypes']); - } - - /** - * "fullscreen" covers the admin chrome like a setup wizard; "screen" keeps the guide inside it. - * - * @return string - */ - private function displayMode() - { - $mode = apply_filters('presspermit_welcome_mode', 'fullscreen'); - - return in_array($mode, ['screen', 'fullscreen'], true) ? $mode : 'fullscreen'; - } - - private function steps() - { - return [ - new HeroStep( - 'welcome', - [ - 'eyebrow' => __('Welcome to PublishPress Permissions', 'press-permit-core'), - 'title' => __('Control who can read and edit your content.', 'press-permit-core'), - 'lede' => __('Choose the content to manage, set up the Teaser for restricted content, and decide who can read or edit what. It takes about two minutes.', 'press-permit-core'), - 'asset' => $this->asset('penguins.png'), - 'secondary_action' => [ - 'label' => __('Skip, take me to Settings', 'press-permit-core'), - 'url' => admin_url('admin.php?page=presspermit-settings'), - ], - ] - ), - new ConceptsStep( - 'concepts', - [ - 'title' => __('Two ideas to know', 'press-permit-core'), - 'lede' => __('Get these right and the rest of the plugin makes sense.', 'press-permit-core'), - 'concepts' => [ - [ - 'name' => __('Roles', 'press-permit-core'), - 'body' => __('What a person can do everywhere on the site. Roles come from WordPress.', 'press-permit-core'), - 'slug' => 'concept-roles', - 'asset' => $this->asset('concept-roles.svg'), - 'art_callback' => [$this, 'renderRolesArt'], - ], - [ - 'name' => __('Exceptions', 'press-permit-core'), - 'slug' => 'concept-exceptions', - 'body' => __('An override for one post, page or category. An exception beats the role.', 'press-permit-core'), - 'asset' => $this->asset('concept-exceptions.svg'), - 'art_callback' => [$this, 'renderExceptionsArt'], - ], - ], - ] - ), - new WelcomeCallbackStep('content', [$this, 'renderPostTypes'], true), - new WelcomeCallbackStep('teaser', [$this, 'renderTeaser']), - new FinishStep( - 'finish', - [ - 'title' => __('You are ready to get started', 'press-permit-core'), - 'lede' => __('Keep setting up Permissions with these next steps.', 'press-permit-core'), - 'next_steps_title' => __('What to do next', 'press-permit-core'), - 'next_steps' => [ - ['label' => __('Choose the content you want to control.', 'press-permit-core'), 'url' => $this->postTypesSettingsUrl()], - ['label' => __('Configure the Teaser for restricted content.', 'press-permit-core'), 'url' => $this->teaserUrl()], - ['label' => __('Set permissions on one post or page.', 'press-permit-core'), 'url' => $this->postPermissionsUrl()], - ], - 'primary_action' => [ - 'label' => __('Go to Permissions', 'press-permit-core'), - 'url' => admin_url('admin.php?page=' . ($this->canManageGroups() ? 'presspermit-groups' : 'presspermit-settings')), - ], - // Mirrors the Pro-only modules listed in Admin::getModuleInfo(). Editing Permissions, Teaser and - // Content Visibility are bundled with the free plugin, so they are not promoted here. - 'promo' => presspermit()->isPro() ? [] : [ - 'title' => __('If you need more control', 'press-permit-core'), - 'features' => [ - __('File Access: protect direct file URLs', 'press-permit-core'), - __('Access Circles: limit access by author group', 'press-permit-core'), - __('Membership: date-limited group access', 'press-permit-core'), - __('User Posts: auto-create a page per user', 'press-permit-core'), - __('Compatibility: ACF, WPML and more', 'press-permit-core'), - __('Priority support', 'press-permit-core'), - ], - 'url' => 'https://publishpress.com/permissions/', - 'label' => __('See Pro features', 'press-permit-core'), - ], - ] - ), - ]; - } - - private function tasks() - { - return [ - new ChecklistTask( - 'post-types', - __('Choose the content you want to control', 'press-permit-core'), - $this->postTypesSettingsUrl(), - function () { - return $this->enabledPostTypeCount() > 0; - }, - function () { - $count = $this->enabledPostTypeCount(); - return sprintf( - _n('%s type on', '%s types on', $count, 'press-permit-core'), - number_format_i18n($count) - ); - } - ), - new ChecklistTask( - 'teaser', - __('Configure the Teaser for restricted content', 'press-permit-core'), - $this->teaserUrl(), - function () { - return (bool) array_filter((array) presspermit()->getOption('tease_post_types')); - }, - function () { - return __('on', 'press-permit-core'); - } - ), - new ChecklistTask( - 'exception', - __('Open a post or page and use its Permissions panel', 'press-permit-core'), - $this->postPermissionsUrl(), - function () { - return $this->exceptionCount() > 0; - }, - function () { - return __('done', 'press-permit-core'); - } - ), - ]; - } - - /* ------------------------------------------------------------------ concepts */ - - public function renderRolesArt(RenderContext $context) - { - $people = [ - ['label' => __('Editor', 'press-permit-core'), 'face' => 'long', 'bg' => '#ece8f8', 'skin' => '#e0a779', 'shade' => '#cc9161', 'hair' => '#2b2033', 'shirt' => '#655997'], - ['label' => __('Author', 'press-permit-core'), 'face' => 'short', 'bg' => '#e0eff2', 'skin' => '#f2c9a0', 'shade' => '#e2b387', 'hair' => '#7b4a2a', 'shirt' => '#3f8f9c'], - ['label' => __('Subscriber', 'press-permit-core'), 'face' => 'curly', 'bg' => '#fbebdd', 'skin' => '#8d5a3b', 'shade' => '#7a4a2f', 'hair' => '#1f1a24', 'shirt' => '#e59a4b'], - ]; - ?> -
- -
- renderAvatar($person); ?> - -
- -
- '; - $hairFront = ''; - } elseif ('curly' === $person['face']) { - $hairBack = ''; - $hairFront = ''; - } else { - $hairFront = ''; - } - ?> - - __('Editors', 'press-permit-core'), 'access' => 'on'], - ['label' => __('Contributors', 'press-permit-core'), 'access' => 'on'], - ['label' => __('Guests', 'press-permit-core'), 'access' => 'off'], - ]; - ?> -
-
- - - - - -
- -
- - - -
- -
- stepUrlFor('teaser', $context); - ?> -
-

-

-
- -
-

- -
- -
- - -
- -
- $type) : ?> - postTypeHint($name); - ?> - - -
-
-

-
- - -
-
- - 405]); - } - - check_admin_referer(self::POST_TYPES_NONCE); - - if (! current_user_can('pp_manage_settings')) { - wp_die(esc_html__('You are not allowed to access this page.', 'press-permit-core'), '', ['response' => 403]); - } - - // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput -- nonce checked above; each name is validated against the offered post types. - $posted = isset($_POST['ppw_post_types']) && is_array($_POST['ppw_post_types']) ? wp_unslash($_POST['ppw_post_types']) : []; - - WelcomePostTypes::save($posted); - - wp_safe_redirect($this->stepUrlFor('teaser')); - exit; - } - - private function postTypeIcon($type) - { - $icons = ['post' => 'dashicons-admin-post', 'page' => 'dashicons-admin-page', 'attachment' => 'dashicons-admin-media']; - - if (isset($icons[$type->name])) { - return $icons[$type->name]; - } - - return (is_string($type->menu_icon) && 0 === strpos($type->menu_icon, 'dashicons-')) - ? sanitize_html_class($type->menu_icon) - : 'dashicons-admin-post'; - } - - private function postTypeHint($name) - { - $hints = [ - 'post' => __('Articles and news', 'press-permit-core'), - 'page' => __('Static pages', 'press-permit-core'), - 'attachment' => __('Images and files', 'press-permit-core'), - ]; - - return isset($hints[$name]) ? $hints[$name] : ''; - } - - /* -------------------------------------------------------------------- teaser */ - - public function renderTeaser(RenderContext $context) - { - $canConfigure = current_user_can('pp_manage_teaser') && presspermit()->moduleActive('teaser'); - $openInNewTab = __('opens in a new tab, this guide stays here', 'press-permit-core'); - ?> -
-
-

-

-
    -
  1. -
  2. -
  3. -
- -
-
- -
-
- - - stepUrlForId($id); - } - - if ($this->controller && method_exists($this->controller, 'stepUrlForId')) { - return $this->controller->stepUrlForId($id); - } - - $position = array_search($id, ['welcome', 'concepts', 'content', 'teaser', 'finish'], true); - - return $this->controller->stepUrl(false === $position ? 1 : $position + 1); - } - - private function postTypesSettingsUrl() - { - return admin_url('admin.php?page=presspermit-settings&pp_tab=core'); - } - - private function teaserUrl() - { - if (current_user_can('pp_manage_teaser') && presspermit()->moduleActive('teaser')) { - return admin_url('admin.php?page=presspermit-posts-teaser'); - } - - return admin_url('admin.php?page=presspermit-settings&pp_tab=modules'); - } - - private function urlPath() - { - return defined('PRESSPERMIT_URLPATH') ? PRESSPERMIT_URLPATH : plugins_url('', PRESSPERMIT_FILE); - } - - private function asset($file) - { - return [ - 'path' => PRESSPERMIT_ABSPATH . '/common/img/welcome/' . $file, - 'url' => $this->urlPath() . '/common/img/welcome/' . $file, - ]; - } - - private function canManageGroups() - { - return current_user_can('pp_manage_permissions') - || current_user_can('pp_edit_groups') - || presspermit()->groups()->anyGroupManager(); - } - - private function enabledPostTypeCount() - { - $enabled = presspermit()->getEnabledPostTypes(); - return is_array($enabled) ? count($enabled) : 0; - } - - private function exceptionCount() - { - global $wpdb; - - if (null !== $this->exceptionCount) { - return $this->exceptionCount; - } - - if (empty($wpdb->ppc_exceptions) || empty($wpdb->ppc_exception_items)) { - return 0; - } - - // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom Permissions tables; memoized per request to avoid stale checklist state after permission changes. - $this->exceptionCount = (int) $wpdb->get_var( - "SELECT COUNT(DISTINCT i.item_id) FROM $wpdb->ppc_exception_items AS i" - . " INNER JOIN $wpdb->ppc_exceptions AS e ON e.exception_id = i.exception_id" - . " WHERE e.for_item_source = 'post' AND e.via_item_source = 'post'" - . ' AND i.item_id > 0' - ); - - return $this->exceptionCount; - } - - private function postPermissionsUrl() - { - $enabled = (array) presspermit()->getEnabledPostTypes(); - $postTypes = []; - - foreach ($enabled as $key => $value) { - $postType = is_string($value) ? $value : (is_string($key) ? $key : ''); - - if ($postType && ! in_array($postType, $postTypes, true)) { - $postTypes[] = $postType; - } - } - - foreach (['post', 'page'] as $preferred) { - if (in_array($preferred, $postTypes, true)) { - return 'post' === $preferred - ? admin_url('edit.php') - : add_query_arg('post_type', 'page', admin_url('edit.php')); - } - } - - foreach ($postTypes as $postType) { - if (post_type_exists($postType)) { - return add_query_arg('post_type', sanitize_key($postType), admin_url('edit.php')); - } - } - - return $this->postTypesSettingsUrl(); - } -} diff --git a/classes/PublishPress/Permissions/UI/WelcomePostTypes.php b/classes/PublishPress/Permissions/UI/WelcomePostTypes.php deleted file mode 100644 index c800a84f2..000000000 --- a/classes/PublishPress/Permissions/UI/WelcomePostTypes.php +++ /dev/null @@ -1,140 +0,0 @@ - true, 'show_ui' => true], 'objects'); - - $omit = array_merge( - (array) apply_filters('presspermit_unfiltered_post_types', ['wp_block']), - array_keys((array) apply_filters('presspermit_hidden_post_types', [])) - ); - - $types = array_diff_key($types, array_fill_keys($omit, true)); - $first = ['post' => 0, 'page' => 1, 'attachment' => 2]; - - uasort($types, function ($a, $b) use ($first) { - $rankA = isset($first[$a->name]) ? $first[$a->name] : 99; - $rankB = isset($first[$b->name]) ? $first[$b->name] : 99; - - return $rankA === $rankB ? strcasecmp(self::label($a), self::label($b)) : $rankA - $rankB; - }); - - return $types; - } - - /** @return string */ - public static function label($type) - { - if (isset($type->labels_pp->name)) { - return (string) $type->labels_pp->name; - } - - return isset($type->labels->name) ? (string) $type->labels->name : (string) $type->name; - } - - /** - * Types Permissions always keeps on, keyed by name. - * - * @return array - */ - public static function locked() - { - return array_filter((array) apply_filters('presspermit_locked_post_types', ['nav_menu' => 'nav_menu'])); - } - - /** - * Enabled state as the Settings screen reports it, keyed by post type name. - * - * @return array - */ - public static function enabled() - { - return (array) apply_filters('presspermit_enabled_post_types', presspermit()->getOption(self::OPTION)); - } - - /** - * Whether the current user may change the setting here. A network-wide value belongs to the network admin. - * - * @return bool - */ - public static function canSave() - { - if (! current_user_can('pp_manage_settings')) { - return false; - } - - return ! (is_multisite() && in_array(self::OPTION, (array) presspermit()->netwide_options, true)); - } - - /** - * @param array $posted Submitted checkbox values keyed by post type name. - * @return bool True when the submission was accepted. - */ - public static function save($posted) - { - if (! self::canSave()) { - return false; - } - - $selected = []; - foreach ((array) $posted as $name => $value) { - if (is_string($name) && $value) { - $selected[] = sanitize_key($name); - } - } - - $pp = presspermit(); - $current = $pp->getOption(self::OPTION); - $current = is_array($current) ? $current : []; - - $value = self::applyChoices($current, array_keys(self::choices()), array_keys(self::locked()), $selected); - - if ($value !== $current) { - $pp->updateOption(self::OPTION, $value); - } - - return true; - } - - /** - * Merge a submission into the stored setting. - * - * Only types the guide offered can change, so a forged field name never creates or alters an entry. - * Types left unticked are stored as '0', matching the Settings screen, and locked types stay on. - * - * @param array $current Stored setting. - * @param string[] $allowed Post types offered by the guide. - * @param string[] $locked Post types that must stay enabled. - * @param string[] $selected Post types ticked in the submission. - * @return array - */ - public static function applyChoices(array $current, array $allowed, array $locked, array $selected) - { - $selected = array_flip(array_intersect($selected, $allowed)); - - foreach ($allowed as $name) { - $current[$name] = (isset($selected[$name]) || in_array($name, $locked, true)) ? '1' : '0'; - } - - return $current; - } -} diff --git a/classes/PublishPress/PermissionsHooksAdmin.php b/classes/PublishPress/PermissionsHooksAdmin.php index e1e88d7c2..21404b665 100644 --- a/classes/PublishPress/PermissionsHooksAdmin.php +++ b/classes/PublishPress/PermissionsHooksAdmin.php @@ -71,14 +71,6 @@ function($query) { add_action('presspermit_trigger_cache_flush', [$this, 'wpeCacheFlush']); add_action('presspermit_activate', [$this, 'actPluginSettingsUpdated']); add_action('shutdown', [$this, 'actConfigUpdateFollowup']); - - add_action('init', [$this, 'actRegisterWelcomeExperience'], 20); - } - - public function actRegisterWelcomeExperience() - { - require_once(PRESSPERMIT_CLASSPATH . '/UI/WelcomeExperience.php'); - new Permissions\UI\WelcomeExperience(); } public function init() diff --git a/common/css/welcome-permissions.css b/common/css/welcome-permissions.css deleted file mode 100644 index 635ff5321..000000000 --- a/common/css/welcome-permissions.css +++ /dev/null @@ -1,655 +0,0 @@ -/* - * PublishPress Permissions welcome guide. - * - * Presentation for the shared PublishPress Welcome library, matched to the PublishPress Hub: a quiet - * lavender gradient card, a small uppercase eyebrow, a bold headline, flat controls with 8px corners, - * and the penguin illustration in a white panel. - */ - -/* ------------------------------------------------------------------ tokens */ - -.ppw-root, -.ppw-notice, -.ppw-checklist { - --ppw-line: #e4e1ef; - --ppw-panel: linear-gradient(135deg, #ffffff 0%, #f8f6fd 55%, #f1edfb 100%); -} - -/* ----------------------------------------------------------------- controls */ - -.ppw-root .ppw-btn, -.ppw-notice .ppw-btn, -.ppw-checklist .ppw-btn { - border-radius: 8px; -} - -.ppw-root .ppw-btn.ppw-btn-primary, -.ppw-notice .ppw-btn.ppw-btn-primary { - box-shadow: none; -} - -.ppw-root .ppw-btn-lg { - min-height: 44px; - padding: 0 22px; - font-size: 14px; -} - -/* --------------------------------------------------------------------- hero */ - -.ppw-step-welcome .ppw-hero.has-art { - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 48px; - padding: 44px 48px; - border: 1px solid var(--ppw-line); - border-radius: 16px; - background: var(--ppw-panel); - box-shadow: 0 1px 2px rgba(15, 23, 42, .04), 0 24px 48px -32px rgba(var(--ppw-accent-rgb), .35); -} - -.ppw-step-welcome .ppw-eyebrow { - margin-bottom: 14px; - padding: 0; - border-radius: 0; - background: none; - color: var(--ppw-accent); - font-size: 11px; - font-weight: 700; - letter-spacing: .09em; - text-transform: uppercase; -} - -.ppw-root .ppw-step-welcome .ppw-hero h1 { - font-size: 32px; - font-weight: 700; - line-height: 1.18; - letter-spacing: -.02em; -} - -.ppw-root .ppw-step-welcome .ppw-lede { - margin-top: 12px; - font-size: 15px; - line-height: 1.6; -} - -.ppw-step-welcome .ppw-hero-actions { - margin-top: 26px; -} - -.ppw-step-welcome .ppw-hero-art { - display: block; - min-height: 0; - padding: 0; - overflow: hidden; - border: 1px solid #ebe8f5; - border-radius: 12px; - background: #fff; -} - -.ppw-step-welcome .ppw-hero-art .ppw-figure img { - width: 100%; - height: auto; -} - -/* ----------------------------------------------------------------- concepts */ - -.ppw-step-concepts .ppw-concepts { - grid-template-columns: repeat(2, minmax(0, 1fr)); - max-width: 760px; - margin: 0 auto; -} - -.ppw-step-concepts .ppw-concept { - padding: 14px 14px 26px; -} - -.ppw-step-concepts .ppw-concept-art { - height: 272px; - min-height: 0; - padding: 20px; - border-radius: 12px; - background: var(--ppw-panel); -} - -.ppw-roles-art { - display: flex; - align-items: flex-end; - justify-content: center; - gap: 18px; -} - -.ppw-person { - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; -} - -.ppw-avatar { - display: block; - width: 72px; - height: 72px; - border-radius: 50%; - box-shadow: 0 0 0 3px #fff, 0 8px 16px -8px rgba(var(--ppw-accent-rgb), .55); -} - -.ppw-person-role { - padding: 4px 12px; - border-radius: 999px; - background: #fff; - box-shadow: 0 1px 2px rgba(15, 23, 42, .1); - color: var(--ppw-ink); - font-size: 12px; - font-weight: 650; -} - -.ppw-permissions-card { - width: 100%; - max-width: 252px; - padding: 16px 18px 12px; - border-radius: 14px; - background: #fff; - box-shadow: 0 18px 34px -16px rgba(var(--ppw-accent-rgb), .45), 0 1px 3px rgba(15, 23, 42, .08); -} - -.ppw-permissions-card-head { - display: flex; - align-items: center; - gap: 12px; - padding-bottom: 12px; - border-bottom: 1px solid #eceef5; -} - -.ppw-permissions-lock.dashicons { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 10px; - background: linear-gradient(145deg, var(--ppw-accent), var(--ppw-accent-dark)); - color: #fff; - font-size: 19px; -} - -.ppw-permissions-card-title { - display: flex; - flex-direction: column; - gap: 1px; -} - -.ppw-permissions-card-title strong { - color: var(--ppw-ink); - font-size: 13px; -} - -.ppw-permissions-card-title span { - color: var(--ppw-muted); - font-size: 11px; -} - -.ppw-permissions-row { - display: flex; - align-items: center; - gap: 10px; - padding: 10px 0; - border-bottom: 1px solid #f1f2f7; -} - -.ppw-permissions-row:last-child { - border-bottom: 0; -} - -.ppw-permissions-avatar { - width: 24px; - height: 24px; - border-radius: 50%; - background: linear-gradient(145deg, #e3dff2, #c9c1e4); -} - -.ppw-permissions-name { - flex: 1; - color: #1e293b; - font-size: 12.5px; - font-weight: 600; -} - -.ppw-permissions-switch { - position: relative; - width: 34px; - height: 20px; - border-radius: 999px; - background: #d5d9e4; -} - -.ppw-permissions-switch::after { - position: absolute; - top: 3px; - left: 3px; - width: 14px; - height: 14px; - border-radius: 50%; - background: #fff; - box-shadow: 0 1px 2px rgba(15, 23, 42, .25); - content: ''; -} - -.ppw-permissions-switch.is-on { - background: var(--ppw-accent); -} - -.ppw-permissions-switch.is-on::after { - left: 17px; -} - -/* ------------------------------------------------------------ content step */ - -.ppw-step-content .ppw-heading { - margin-bottom: 32px; -} - -.ppw-post-types { - max-width: 760px; - margin: 0 auto; -} - -.ppw-post-types fieldset { - min-width: 0; - margin: 0; - padding: 0; - border: 0; -} - -.ppw-type-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - gap: 14px; -} - -.ppw-type-choice { - position: relative; - display: flex; - cursor: pointer; -} - -.ppw-root .ppw-type-choice input[type="checkbox"] { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - border: 0; - clip: rect(0, 0, 0, 0); - opacity: 0; -} - -.ppw-type-card { - display: flex; - flex: 1; - align-items: center; - gap: 14px; - min-width: 0; - min-height: 76px; - padding: 14px 16px; - border: 1px solid var(--ppw-line); - border-radius: 12px; - background: #fff; - transition: border-color .15s ease, box-shadow .15s ease, background-color .15s ease; -} - -.ppw-type-choice:hover .ppw-type-card { - border-color: rgba(var(--ppw-accent-rgb), .45); -} - -.ppw-type-icon.dashicons { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 10px; - background: var(--ppw-accent-soft); - color: var(--ppw-accent); - font-size: 20px; -} - -.ppw-type-copy { - display: flex; - flex: 1; - flex-direction: column; - gap: 2px; - min-width: 0; -} - -.ppw-type-copy strong { - color: var(--ppw-ink); - font-size: 14.5px; - font-weight: 650; -} - -.ppw-type-copy small { - color: var(--ppw-muted); - font-size: 12.5px; -} - -.ppw-type-check { - flex: none; - width: 22px; - height: 22px; - border: 2px solid #cdd2de; - border-radius: 50%; - background: #fff center / 14px no-repeat; - transition: border-color .15s ease, background-color .15s ease; -} - -.ppw-type-choice input:checked + .ppw-type-card { - border-color: var(--ppw-accent); - background: #faf9fe; - box-shadow: 0 0 0 3px var(--ppw-accent-soft); -} - -.ppw-type-choice input:checked + .ppw-type-card .ppw-type-check { - border-color: var(--ppw-accent); - background-color: var(--ppw-accent); - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 6.2l2 2 4-4.4' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); -} - -.ppw-type-choice input:disabled + .ppw-type-card { - cursor: not-allowed; - opacity: .75; -} - -.ppw-type-choice input:focus-visible + .ppw-type-card { - outline: 2px solid var(--ppw-accent); - outline-offset: 2px; -} - -.ppw-root .ppw-setup-note { - margin-top: 20px; - color: var(--ppw-muted); - font-size: 13px; - text-align: center; -} - -.ppw-setup-actions { - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: center; - gap: 14px 24px; - margin-top: 24px; -} - -.ppw-setup-actions.is-centered { - flex-direction: column; -} - -/* ------------------------------------------------------------- teaser step */ - -.ppw-step-teaser .ppw-lesson-art { - min-width: 0; -} - -.ppw-teaser-mock { - overflow: hidden; - border: 1px solid var(--ppw-line); - border-radius: 14px; - background: #fff; - box-shadow: var(--ppw-shadow); -} - -.ppw-mock-bar { - display: flex; - align-items: center; - gap: 6px; - height: 34px; - padding: 0 14px; - border-bottom: 1px solid var(--ppw-line); - background: #f8f9fc; -} - -.ppw-mock-bar i { - width: 8px; - height: 8px; - border-radius: 50%; - background: #ff6b6b; -} - -.ppw-mock-bar i:nth-child(2) { - background: #fbbf24; -} - -.ppw-mock-bar i:nth-child(3) { - background: #34d399; -} - -.ppw-mock-bar span { - flex: 1; - max-width: 260px; - margin-inline-start: 14px; - padding: 3px 12px; - overflow: hidden; - border: 1px solid var(--ppw-line); - border-radius: 999px; - background: #fff; - color: var(--ppw-muted); - font-size: 11px; - text-overflow: ellipsis; - white-space: nowrap; -} - -.ppw-mock-page { - display: flex; - flex-direction: column; - gap: 10px; - padding: 28px 30px 30px; -} - -.ppw-mock-tag { - align-self: flex-start; - padding: 3px 10px; - border-radius: 999px; - background: var(--ppw-accent-soft); - color: var(--ppw-accent-dark); - font-size: 11px; - font-weight: 700; -} - -.ppw-mock-title { - color: var(--ppw-ink); - font-size: 20px; - line-height: 1.25; - letter-spacing: -.01em; -} - -.ppw-root .ppw-mock-excerpt { - font-size: 13.5px; - line-height: 1.6; -} - -.ppw-mock-lines { - display: block; -} - -.ppw-mock-lines i { - display: block; - height: 8px; - margin-top: 8px; - border-radius: 4px; - background: linear-gradient(90deg, #e6e8f0, #f3f4f8); -} - -.ppw-mock-lines i:nth-child(2) { - width: 88%; - opacity: .7; -} - -.ppw-mock-lines i:nth-child(3) { - width: 62%; - opacity: .4; -} - -.ppw-mock-gate { - display: flex; - align-items: center; - gap: 14px; - margin-top: 14px; - padding: 14px 16px; - border: 1px solid rgba(var(--ppw-accent-rgb), .25); - border-radius: 12px; - background: #faf9fe; -} - -.ppw-mock-lock.dashicons { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 10px; - background: var(--ppw-accent); - color: #fff; - font-size: 18px; -} - -.ppw-mock-gate-copy { - display: flex; - flex: 1; - flex-direction: column; - gap: 2px; - min-width: 0; - color: var(--ppw-muted); - font-size: 12px; -} - -.ppw-mock-gate-copy strong { - color: var(--ppw-ink); - font-size: 13px; -} - -.ppw-mock-button { - flex: none; - padding: 8px 14px; - border-radius: 8px; - background: var(--ppw-accent); - color: #fff; - font-size: 12px; - font-weight: 650; -} - -/* -------------------------------------------------------------- email step */ - -.ppw-capture-art .ppw-figure { - border-color: #ebe8f5; - border-radius: 14px; - background: #fff; - box-shadow: 0 1px 2px rgba(15, 23, 42, .04), 0 24px 48px -32px rgba(var(--ppw-accent-rgb), .35); -} - -.ppw-capabilities-promo { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 14px 16px; - max-width: 560px; - margin: 28px auto 0; - padding: 16px 18px; - border: 1px solid var(--ppw-line); - border-radius: 14px; - background: var(--ppw-panel); -} - -.ppw-capture-layout.has-art .ppw-capabilities-promo { - margin-inline-start: 0; -} - -.ppw-capabilities-icon.dashicons { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 10px; - background: var(--ppw-accent-soft); - color: var(--ppw-accent); - font-size: 20px; -} - -.ppw-capabilities-copy { - display: flex; - flex: 1; - flex-direction: column; - gap: 2px; - min-width: min(100%, 180px); - color: var(--ppw-muted); - font-size: 13px; - line-height: 1.5; -} - -.ppw-capabilities-copy strong { - color: var(--ppw-ink); - font-size: 14px; -} - -/* ------------------------------------------------------------------ finish */ - -.ppw-root .ppw-promo { - max-width: 880px; - border-radius: 16px; -} - -.ppw-root .ppw-promo ul { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -/* -------------------------------------------------------------- responsive */ - -@media (max-width: 960px) { - .ppw-step-welcome .ppw-hero.has-art { - grid-template-columns: minmax(0, 1fr); - gap: 32px; - padding: 32px; - } -} - -@media (max-width: 782px) { - /* Brand, step count and exit stay on one row. */ - .ppw-root .ppw-topbar { - grid-template-columns: minmax(0, 1fr) auto auto; - gap: 12px; - } - - .ppw-step-welcome .ppw-hero.has-art { - padding: 24px 20px; - } - - .ppw-step-concepts .ppw-concepts { - grid-template-columns: minmax(0, 1fr); - } - - .ppw-mock-page { - padding: 22px 20px 24px; - } - - .ppw-mock-gate { - flex-wrap: wrap; - } -} - -@media (max-width: 480px) { - /* The progress bar already shows where you are; give the brand name the room. */ - .ppw-root .ppw-step-label { - display: none; - } -} - -@media (prefers-reduced-motion: reduce) { - .ppw-type-card, - .ppw-type-check { - transition: none; - } -} diff --git a/common/img/welcome/concept-exceptions.svg b/common/img/welcome/concept-exceptions.svg deleted file mode 100644 index 0e1a1e40f..000000000 --- a/common/img/welcome/concept-exceptions.svg +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/common/img/welcome/concept-roles.svg b/common/img/welcome/concept-roles.svg deleted file mode 100644 index ab2ec010b..000000000 --- a/common/img/welcome/concept-roles.svg +++ /dev/null @@ -1,41 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/common/img/welcome/penguins.png b/common/img/welcome/penguins.png deleted file mode 100644 index c5c5486e6..000000000 Binary files a/common/img/welcome/penguins.png and /dev/null differ diff --git a/lib/composer.json b/lib/composer.json index 37856b510..43896e1ba 100644 --- a/lib/composer.json +++ b/lib/composer.json @@ -9,18 +9,11 @@ "platform-check": false, "autoloader-suffix": "PressPermit" }, - "repositories": [ - { - "type": "vcs", - "url": "https://github.com/publishpress/library-publishpress-welcome" - } - ], "require": { "php": ">=7.2.5", "publishpress/pimple-pimple": "^3.5.0.9", "publishpress/psr-container": "^2.0.1.9", "publishpress/instance-protection": "^2.0", - "publishpress/publishpress-welcome": "dev-codex/welcome-feedback", "publishpress/wordpress-reviews": "^1.1", "publishpress/wordpress-version-notices": "^2.1" } diff --git a/lib/composer.lock b/lib/composer.lock index 8858a0025..bd021450d 100644 --- a/lib/composer.lock +++ b/lib/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "23c70fa6aa3d0741d2a70c35ded34860", + "content-hash": "9e0491b02bfcb557a25294364568f2db", "packages": [ { "name": "publishpress/instance-protection", @@ -231,95 +231,6 @@ }, "time": "2025-12-09T15:18:16+00:00" }, - { - "name": "publishpress/publishpress-welcome", - "version": "dev-codex/welcome-feedback", - "source": { - "type": "git", - "url": "git@github.com:publishpress/library-publishpress-welcome.git", - "reference": "e0898f44a4d40a343459fb77ebc83727014e6b69" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/publishpress/library-publishpress-welcome/zipball/e0898f44a4d40a343459fb77ebc83727014e6b69", - "reference": "e0898f44a4d40a343459fb77ebc83727014e6b69", - "shasum": "" - }, - "require": { - "php": ">=7.2.5" - }, - "require-dev": { - "overtrue/phplint": "^9.5", - "publishpress/version-loader-generator": "^1.3.1" - }, - "type": "library", - "extra": { - "generator": { - "lib-class-test": "class_exists('PublishPress\\Welcome\\WelcomeController')", - "src-dir": "src", - "namespace": "PublishPress\\Welcome", - "action-register-priority": "-200", - "action-initialize-priority": "-190", - "generate-test-file": false - } - }, - "autoload": { - "files": [ - "src/include.php" - ] - }, - "scripts": { - "generate-files": [ - "vendor/bin/version-loader-generator" - ], - "post-install-cmd": [ - "@generate-files" - ], - "post-update-cmd": [ - "@generate-files" - ], - "check:lint": [ - "phplint --no-cache src tests" - ], - "test:unit": [ - "php tests/run.php" - ], - "test:integration": [ - "php tests/integration/version-loader.php" - ], - "test": [ - "@check:lint", - "@test:unit", - "@test:integration" - ], - "build": [ - "bash bin/build.sh" - ], - "check:release": [ - "php bin/check-release.php" - ], - "make:pot": [ - "bash bin/make-pot.sh" - ] - }, - "license": [ - "GPL-3.0-or-later" - ], - "authors": [ - { - "name": "PublishPress", - "email": "help@publishpress.com", - "homepage": "https://publishpress.com/", - "role": "Developer" - } - ], - "description": "Reusable, configurable welcome and onboarding experiences for PublishPress WordPress plugins.", - "support": { - "source": "https://github.com/publishpress/library-publishpress-welcome/tree/codex/welcome-feedback", - "issues": "https://github.com/publishpress/library-publishpress-welcome/issues" - }, - "time": "2026-10-21T10:50:19+00:00" - }, { "name": "publishpress/wordpress-reviews", "version": "1.2.2", @@ -463,9 +374,7 @@ "packages-dev": [], "aliases": [], "minimum-stability": "stable", - "stability-flags": { - "publishpress/publishpress-welcome": 20 - }, + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { diff --git a/lib/vendor/composer/autoload_files.php b/lib/vendor/composer/autoload_files.php index 72c33a27d..736be8d0e 100644 --- a/lib/vendor/composer/autoload_files.php +++ b/lib/vendor/composer/autoload_files.php @@ -8,7 +8,6 @@ return array( '3b1e1688e889525de91ac2456aba9efd' => $vendorDir . '/publishpress/psr-container/lib/include.php', '24b27b1b9a32bf58eda571c3e5ae3480' => $vendorDir . '/publishpress/pimple-pimple/lib/include.php', - 'bc707a56594837f83dcc83b3274ae09e' => $vendorDir . '/publishpress/publishpress-welcome/src/include.php', '41c664bd04a95c2d6a2f2a3e00f06593' => $vendorDir . '/publishpress/wordpress-reviews/ReviewsController.php', 'a61bc28a742b9f9f2fd5ef4d2d1e2037' => $vendorDir . '/publishpress/wordpress-version-notices/src/include.php', ); diff --git a/lib/vendor/composer/autoload_static.php b/lib/vendor/composer/autoload_static.php index d76a522c7..754acf98a 100644 --- a/lib/vendor/composer/autoload_static.php +++ b/lib/vendor/composer/autoload_static.php @@ -9,7 +9,6 @@ class ComposerStaticInitPressPermit public static $files = array ( '3b1e1688e889525de91ac2456aba9efd' => __DIR__ . '/..' . '/publishpress/psr-container/lib/include.php', '24b27b1b9a32bf58eda571c3e5ae3480' => __DIR__ . '/..' . '/publishpress/pimple-pimple/lib/include.php', - 'bc707a56594837f83dcc83b3274ae09e' => __DIR__ . '/..' . '/publishpress/publishpress-welcome/src/include.php', '41c664bd04a95c2d6a2f2a3e00f06593' => __DIR__ . '/..' . '/publishpress/wordpress-reviews/ReviewsController.php', 'a61bc28a742b9f9f2fd5ef4d2d1e2037' => __DIR__ . '/..' . '/publishpress/wordpress-version-notices/src/include.php', ); diff --git a/lib/vendor/composer/installed.json b/lib/vendor/composer/installed.json index 9a77265c9..5c702c0a0 100644 --- a/lib/vendor/composer/installed.json +++ b/lib/vendor/composer/installed.json @@ -234,98 +234,6 @@ }, "install-path": "../publishpress/psr-container" }, - { - "name": "publishpress/publishpress-welcome", - "version": "dev-codex/welcome-feedback", - "version_normalized": "dev-codex/welcome-feedback", - "source": { - "type": "git", - "url": "git@github.com:publishpress/library-publishpress-welcome.git", - "reference": "e0898f44a4d40a343459fb77ebc83727014e6b69" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/publishpress/library-publishpress-welcome/zipball/e0898f44a4d40a343459fb77ebc83727014e6b69", - "reference": "e0898f44a4d40a343459fb77ebc83727014e6b69", - "shasum": "" - }, - "require": { - "php": ">=7.2.5" - }, - "require-dev": { - "overtrue/phplint": "^9.5", - "publishpress/version-loader-generator": "^1.3.1" - }, - "time": "2026-10-21T10:50:19+00:00", - "type": "library", - "extra": { - "generator": { - "lib-class-test": "class_exists('PublishPress\\Welcome\\WelcomeController')", - "src-dir": "src", - "namespace": "PublishPress\\Welcome", - "action-register-priority": "-200", - "action-initialize-priority": "-190", - "generate-test-file": false - } - }, - "installation-source": "dist", - "autoload": { - "files": [ - "src/include.php" - ] - }, - "scripts": { - "generate-files": [ - "vendor/bin/version-loader-generator" - ], - "post-install-cmd": [ - "@generate-files" - ], - "post-update-cmd": [ - "@generate-files" - ], - "check:lint": [ - "phplint --no-cache src tests" - ], - "test:unit": [ - "php tests/run.php" - ], - "test:integration": [ - "php tests/integration/version-loader.php" - ], - "test": [ - "@check:lint", - "@test:unit", - "@test:integration" - ], - "build": [ - "bash bin/build.sh" - ], - "check:release": [ - "php bin/check-release.php" - ], - "make:pot": [ - "bash bin/make-pot.sh" - ] - }, - "license": [ - "GPL-3.0-or-later" - ], - "authors": [ - { - "name": "PublishPress", - "email": "help@publishpress.com", - "homepage": "https://publishpress.com/", - "role": "Developer" - } - ], - "description": "Reusable, configurable welcome and onboarding experiences for PublishPress WordPress plugins.", - "support": { - "source": "https://github.com/publishpress/library-publishpress-welcome/tree/codex/welcome-feedback", - "issues": "https://github.com/publishpress/library-publishpress-welcome/issues" - }, - "install-path": "../publishpress/publishpress-welcome" - }, { "name": "publishpress/wordpress-reviews", "version": "1.2.2", diff --git a/lib/vendor/publishpress/publishpress-welcome/LICENSE b/lib/vendor/publishpress/publishpress-welcome/LICENSE deleted file mode 100644 index 3b4ecb358..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/LICENSE +++ /dev/null @@ -1,8 +0,0 @@ -GNU GENERAL PUBLIC LICENSE -Version 3, 29 June 2007 - -Copyright (C) 2007 Free Software Foundation, Inc. - -This project is distributed under the terms of the GNU General Public -License version 3 or (at your option) any later version. The complete -license text is available at https://www.gnu.org/licenses/gpl-3.0.txt. diff --git a/lib/vendor/publishpress/publishpress-welcome/README.md b/lib/vendor/publishpress/publishpress-welcome/README.md deleted file mode 100644 index 350b4d7d1..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/README.md +++ /dev/null @@ -1,147 +0,0 @@ -# PublishPress Welcome - -Reusable welcome tours, activation notices and setup checklists for PublishPress WordPress plugins. - -The library owns the shared presentation, navigation and per-user state. Each consumer supplies its own translated content, branding, routes, capability checks, product artwork and completion callbacks. - -## Installation - -```bash -composer require publishpress/publishpress-welcome -``` - -Load Composer's autoloader in the plugin's main context. The generated version loader selects the newest bundled copy on `plugins_loaded`. Register consumer experiences at priority `-20` or later. - -```php -add_action('plugins_loaded', function () { - $config = new \PublishPress\Welcome\ExperienceConfig([ - 'id' => 'example-plugin', - 'page_slug' => 'example-welcome', - 'parent_slug' => 'example-settings', - 'page_title' => __('Welcome to Example', 'example-plugin'), - 'capability' => 'manage_options', - 'exit_url' => admin_url('admin.php?page=example-settings'), - 'checklist_pages' => ['example-settings'], - ]); - - $steps = [ - new \PublishPress\Welcome\Step\HeroStep('welcome', [ - 'title' => __('Welcome to Example', 'example-plugin'), - 'lede' => __('Learn the essentials in a few short steps.', 'example-plugin'), - ]), - ]; - - (new \PublishPress\Welcome\WelcomeController($config, $steps))->register(); -}, -20); -``` - -Consumers may implement `PublishPress\Welcome\Contract\StepInterface` for product-specific step markup. Custom renderers are responsible for escaping their own HTML. - -For a small consumer-owned step, use `new CallbackStep('content', $renderer, true)` from `PublishPress\Welcome\Step`. The renderer receives `RenderContext`; `true` means it supplies its own navigation, such as a Save button. Keep nonce checks, capability checks and saving in the consuming plugin. Use `$context->stepUrlForId('next-step')` to navigate without depending on numeric positions that optional or filtered steps can change. - -Concept cards accept an `art_callback` instead of an image. Email capture accepts an `asset` (`url` and optional `path`) and an `after_form_callback` receiving `RenderContext` for an optional resource link outside the subscription form. Set `personas` to an empty array to omit the persona question, and `copy.confirmation_note` to an empty string when confirmation is already explained in the disclosure. Consent is unchecked unless the plugin sets `consent_prechecked`, and it is only ever sent when the user clicks Subscribe. - -## Translations - -All library strings use the `publishpress-welcome` text domain. `languages/publishpress-welcome.pot` is the translation template; regenerate it after changing strings with `composer make:pot` (needs GNU gettext). Place `publishpress-welcome-{locale}.mo` files in `languages/` and they load automatically. Consumers pass their own translated copy for everything plugin-specific. - -## Display modes - -Set `mode` in `ExperienceConfig` to choose the layout: - -| Mode | Look | -| --- | --- | -| `screen` (default) | A card inside the normal WordPress admin, like Yoast or Site Kit. | -| `fullscreen` | Covers the admin menu and toolbar with a wizard-style stage, top bar and sticky footer, like Elementor, WooCommerce or WPForms. | - -Other presentation options: `brand_name`, `logo` (`url`, `alt`, `show_name`, or `dashicon`), `notice_icon_url` and `accent_color` (any hex colour; everything is derived from it). Set `allow_mode_override` to `true` during development to preview either layout with `&ppw_mode=screen|fullscreen` on the welcome URL. - -Built-in steps: `HeroStep`, `FeaturesStep` (icon grid), `ConceptsStep`, `LessonStep` (screenshot with numbered pins) and `FinishStep`. A step may define `hasOwnNavigation()` returning `true` to hide the shared footer's "Next" button when it renders its own call to action. - -## Email capture - -An optional, skippable step asks "what best describes you?" and, with explicit consent, for an email address. It is **enabled by default** and inserted just before the final step. The consent box is unticked unless the plugin sets `consent_prechecked` to `true`, nothing is sent until the user clicks Subscribe, and "No thanks" is always available. - -Configure it with the `email_capture` option, or pass `false` to turn it off: - -```php -'email_capture' => [ - 'position' => 'before_last', // or 'after_first', or a 1-based step number - 'privacy_url' => 'https://example.com/privacy/', - 'consent_prechecked' => false, // true renders the consent box ticked; the user must still click Subscribe - 'personas' => ['blogger' => __('Blogger', 'example'), 'agency' => __('Agency', 'example')], - 'copy' => ['title' => __('Stay in the loop', 'example')], - 'handler' => function (array $payload) { return true; }, // replaces the HTTP request -], -``` - -Consumers can also adjust it at runtime with the `publishpress_welcome_{id}_email_capture` filter, and the outgoing data with `publishpress_welcome_{id}_subscribe_payload`. - -If delivery fails (endpoint down, timeout), the subscription is queued and retried in the background with a growing delay (5 minutes up to a day, five attempts, discarded after 7 days), so the user never sees an error and the lead is not lost. Retries run through WP-Cron and, as a fallback, on admin requests. Queued addresses are included in the personal data export and erasure. - -The subscribe/decline choice is stored per user in plugin-independent options, so a person who already answered in one PublishPress plugin is not asked again by another on the same site (`decline_snooze_days` can re-ask after a decline; `0` never does). Register experiences on `init` so the current user is known. - -Delivery uses `endpoint` (JSON `POST`), defaulting to `https://connect.publishpress.com/v1/subscribe`. Set an empty endpoint or `EmailSubscriber::PLACEHOLDER_ENDPOINT` for an explicit dry run. Do not release a plugin with the live default until the connector is deployed and its real forwarding checks pass. HTTP acceptance is not confirmation: MailerPress owns the confirmation email and the user must click its link before receiving campaigns. - -The payload contains `email`, `consent: true`, `consent_statement`, `plugin`, `persona` and `locale`; no client consent wording or timestamp is sent. Default wording uses `welcome-email-v1`. A `copy.consent_label` override must be paired with a distinct `consent_statement` ID registered with its exact wording in the connector repository, or email capture is disabled. Permanent HTTP 4xx errors (except 408/429) are dropped, while 429/503 `Retry-After` delays are respected. Unknown payload filter keys and license keys are excluded from HTTP and retry storage in this version. Retryable delivery failures remain queued locally; their addresses are included in privacy export/erasure. - -## What's new - -Show release highlights once per version, after an update: - -```php -'whats_new' => [ - 'version' => '4.9.1', - 'title' => __('What is new in Example 4.9.1', 'example'), - 'items' => [['title' => __('Faster lists', 'example'), 'body' => __('Large tables load twice as fast.', 'example')]], - 'url' => 'https://example.com/changelog/', -], -``` - -At most three items are shown, in a dismissible card on the plugin's own screens (`notice_pages`). It stays out of the way: it never appears next to the welcome notice, expires after `notice_days`, and is skipped for anyone who has just finished or dismissed the guide. - -## WordPress.org compliance - -The defaults follow the [plugin guidelines](https://developer.wordpress.org/plugins/wordpress-org/detailed-plugin-guidelines/) for plugins hosted on WordPress.org: - -- **Guideline 11 (no admin hijacking).** The activation notice is dismissible, appears only on the plugin's own screens (`notice_pages`, defaulting to `checklist_pages`; the Dashboard needs `notice_on_dashboard`), hides itself once the guide is finished and stops after `notice_days` (14) days. The only redirect happens once, right after a confirmed first install. -- **Guideline 7 (consent and external services).** Nothing contacts a server without a click on Subscribe with the consent box ticked. The box is unticked by default; a plugin that sets `consent_prechecked` is responsible for confirming that is acceptable for its audience. Declining or skipping sends nothing. The consuming plugin must also document the service in its `readme.txt`, for example: - -``` -== External services == -This plugin can send your email address and the answer to "what best describes you?" to PublishPress -(publishpress.com) if you opt in on the optional welcome screen. Nothing is sent unless you tick the consent -box and click Subscribe. Privacy policy: https://publishpress.com/privacy-policy/ -``` - -## First-install enrollment - -The library never assumes that activation means a first installation. A consumer should set the configured enrollment and redirect options only when its own installation logic confirms a new install: - -```php -update_option('example-plugin_welcome_enrolled', 1, false); -update_option('example-plugin_welcome_redirect_pending', 1, false); -``` - -Existing sites can always open the registered welcome page manually without being enrolled for automatic notices or redirects. - -## Version loading - -`src/include.php` and `src/VersionLoader.php` are generated by `publishpress/version-loader-generator`. Do not edit those files manually. Every bundled copy registers at `plugins_loaded` priority `-200`; the newest initializes at `-190`. - -## CI and releases - -GitHub Actions checks pull requests into `development`, `master` and `release-*`, pushes to those branches, every tag, published releases, and merge queues. The `Required checks` job passes only after lint, metadata checks, PHP 7.2–8.5 runtime tests and the package build all succeed. Configure this status check as required in branch protection to block failing merges. - -For local checks (development tools require PHP 8.4 with DOM, mbstring and ZIP): - -```bash -composer install -composer validate --strict --no-check-version -composer test -composer build -``` - -The build produces `build/publishpress-welcome.zip`, verifies runtime assets are present and development files are excluded, then installs the ZIP in a temporary consumer directory and exercises the generated version loader through Composer's autoloader. CI retains this verified ZIP as a workflow artifact; it does not automatically publish a release. - -Before releasing, update `composer.json`'s version, run `composer update --lock` to refresh the lockfile and regenerate loaders, and commit all changes. Open a PR into the release branch and require green CI before merging. Tag that tested commit with the same version (for example, `0.1.0` or `v0.1.0`). Tag and release checks reject mismatched versions. Run `composer check:release -- v0.1.0` locally to check a tag before pushing. Checks triggered by a published release validate it after publication; they cannot prevent someone manually publishing an unchecked release in GitHub's UI. diff --git a/lib/vendor/publishpress/publishpress-welcome/assets/css/welcome.css b/lib/vendor/publishpress/publishpress-welcome/assets/css/welcome.css deleted file mode 100644 index 77c964a81..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/assets/css/welcome.css +++ /dev/null @@ -1,1652 +0,0 @@ -/* - * PublishPress Welcome - * - * Two layouts share one component set: - * .ppw-mode-screen the guide sits inside the WordPress admin chrome - * .ppw-mode-fullscreen the guide covers the admin chrome (setup-wizard style) - * - * Consumers recolour everything through --ppw-accent / --ppw-accent-rgb / --ppw-accent-dark, - * which the library sets from the "accent_color" option. - */ - -.ppw-root, -.ppw-notice, -.ppw-checklist { - --ppw-accent: #655997; - --ppw-accent-rgb: 101, 89, 151; - --ppw-accent-dark: #4e4575; - --ppw-accent-soft: rgba(var(--ppw-accent-rgb), .10); - --ppw-accent-ring: rgba(var(--ppw-accent-rgb), .28); - --ppw-ink: #0f172a; - --ppw-text: #475569; - --ppw-muted: #64748b; - --ppw-line: #e6e8ef; - --ppw-surface: #ffffff; - --ppw-canvas: #f6f7fb; - --ppw-radius: 16px; - --ppw-shadow: 0 1px 2px rgba(15, 23, 42, .05), 0 12px 32px -12px rgba(15, 23, 42, .14); - --ppw-font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; - - box-sizing: border-box; - font-family: var(--ppw-font); - color: var(--ppw-text); -} - -.ppw-root *, -.ppw-root *::before, -.ppw-root *::after, -.ppw-notice *, -.ppw-checklist * { - box-sizing: border-box; -} - -/* ----------------------------------------------------------- accessibility */ - -.ppw-sr-only { - position: absolute; - width: 1px; - height: 1px; - margin: -1px; - padding: 0; - overflow: hidden; - border: 0; - clip: rect(0, 0, 0, 0); - white-space: nowrap; -} - -.ppw-skip-link { - position: absolute; - z-index: 100001; - inset-block-start: 8px; - inset-inline-start: 8px; - padding: 10px 16px; - border-radius: 8px; - background: var(--ppw-ink); - color: #fff; - font-size: 14px; - font-weight: 600; - text-decoration: none; - transform: translateY(-200%); -} - -.ppw-skip-link:focus { - color: #fff; - outline: 2px solid var(--ppw-accent); - outline-offset: 2px; - transform: none; -} - -.ppw-stage:focus { - outline: none; -} - -/* ---------------------------------------------------------------- buttons */ - -.ppw-btn { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - min-height: 42px; - padding: 0 20px; - border: 1px solid transparent; - border-radius: 999px; - font-family: inherit; - font-size: 14px; - font-weight: 600; - line-height: 1; - text-decoration: none; - white-space: nowrap; - cursor: pointer; - transition: background-color .15s ease, border-color .15s ease, color .15s ease, box-shadow .15s ease, transform .15s ease; -} - -.ppw-btn:focus-visible, -.ppw-text-link:focus-visible, -.ppw-back:focus-visible, -.ppw-skip:focus-visible, -.ppw-exit:focus-visible, -.ppw-step-dot:focus-visible, -.ppw-next-steps a:focus-visible { - outline: 2px solid var(--ppw-accent); - outline-offset: 3px; - box-shadow: none; -} - -.ppw-btn.ppw-btn-primary { - background: var(--ppw-accent); - border-color: var(--ppw-accent); - box-shadow: 0 6px 16px -6px rgba(var(--ppw-accent-rgb), .7); - color: #fff; -} - -.ppw-btn.ppw-btn-primary:hover, -.ppw-btn.ppw-btn-primary:focus { - background: var(--ppw-accent-dark); - border-color: var(--ppw-accent-dark); - color: #fff; - transform: translateY(-1px); -} - -.ppw-btn.ppw-btn-outline { - background: var(--ppw-surface); - border-color: var(--ppw-line); - color: var(--ppw-ink); -} - -.ppw-btn.ppw-btn-outline:hover, -.ppw-btn-outline:focus { - border-color: var(--ppw-accent); - color: var(--ppw-accent); -} - -.ppw-btn.ppw-btn-ghost { - background: transparent; - color: var(--ppw-muted); -} - -.ppw-btn.ppw-btn-ghost:hover, -.ppw-btn-ghost:focus { - background: var(--ppw-accent-soft); - color: var(--ppw-accent); -} - -.ppw-btn.ppw-btn-light { - background: #fff; - color: var(--ppw-accent-dark); -} - -.ppw-btn.ppw-btn-light:hover, -.ppw-btn.ppw-btn-light:focus { - background: rgba(255, 255, 255, .88); - color: var(--ppw-accent-dark); -} - -.ppw-btn-lg { - min-height: 52px; - padding: 0 28px; - font-size: 15px; -} - -.ppw-btn-arrow { - transition: transform .15s ease; -} - -.ppw-btn:hover .ppw-btn-arrow { - transform: translateX(3px); -} - -a.ppw-text-link, -a.ppw-back, -a.ppw-skip, -a.ppw-exit { - color: var(--ppw-muted); - font-size: 13px; - font-weight: 600; - text-decoration: none; - transition: color .15s ease; -} - -a.ppw-text-link:hover, -a.ppw-back:hover, -a.ppw-skip:hover, -a.ppw-exit:hover, -a.ppw-text-link:focus, -a.ppw-back:focus, -a.ppw-skip:focus, -a.ppw-exit:focus { - color: var(--ppw-accent); -} - -.ppw-back::before { - margin-inline-end: 8px; - content: '\2190'; -} - -/* Win against third-party admin link styles inside notices and cards. */ - -.ppw-notice a, -.ppw-checklist a, -.ppw-root a { - text-decoration: none; -} - -.ppw-notice a.ppw-text-link, -.ppw-checklist a.ppw-text-link { - color: var(--ppw-muted); -} - -.ppw-notice a.ppw-text-link:hover, -.ppw-checklist a.ppw-text-link:hover { - color: var(--ppw-accent); -} - -.ppw-notice a.ppw-btn-primary, -.ppw-notice a.ppw-btn-primary:hover { - color: #fff; -} - -.ppw-checklist a.ppw-btn-outline { - color: var(--ppw-ink); -} - -.ppw-checklist a.ppw-btn-outline:hover { - color: var(--ppw-accent); -} - -.ppw-checklist a.ppw-btn-ghost { - color: var(--ppw-muted); -} - -/* ----------------------------------------------------------------- frame */ - -.ppw-root { - color: var(--ppw-text); -} - -.ppw-frame { - display: flex; - flex-direction: column; - background: var(--ppw-surface); -} - -.ppw-root h1, -.ppw-root h2 { - margin: 0; - padding: 0; - color: var(--ppw-ink); - font-family: var(--ppw-font); - letter-spacing: -.02em; -} - -.ppw-root h1 { - font-size: 34px; - font-weight: 750; - line-height: 1.15; -} - -.ppw-root h2 { - font-size: 17px; - font-weight: 650; - line-height: 1.3; -} - -.ppw-root p { - margin: 0; - color: var(--ppw-text); - font-size: 15px; - line-height: 1.65; -} - -.ppw-root .ppw-lede { - margin-top: 14px; - color: var(--ppw-muted); - font-size: 17px; - line-height: 1.6; -} - -/* Top bar */ - -.ppw-topbar { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 20px; - min-height: 68px; - padding: 0 28px; -} - -.ppw-brand { - display: flex; - align-items: center; - gap: 10px; - min-width: 0; - color: var(--ppw-ink); -} - -.ppw-brand-logo { - display: block; - width: auto; - max-width: 180px; - height: 30px; -} - -.ppw-brand-icon.dashicons { - display: inline-flex; - align-items: center; - justify-content: center; - width: 36px; - height: 36px; - border-radius: 10px; - background: var(--ppw-accent); - color: #fff; - font-size: 20px; -} - -.ppw-brand-name { - overflow: hidden; - font-size: 15px; - font-weight: 700; - letter-spacing: -.01em; - text-overflow: ellipsis; - white-space: nowrap; -} - -.ppw-steps { - display: flex; - align-items: center; - gap: 8px; -} - -.ppw-step-dot { - display: block; - width: 10px; - height: 10px; - border-radius: 999px; - background: #d7dbe6; - transition: width .25s ease, background-color .25s ease; -} - -.ppw-step-dot:hover { - background: rgba(var(--ppw-accent-rgb), .5); -} - -.ppw-step-dot.is-done { - background: rgba(var(--ppw-accent-rgb), .55); -} - -.ppw-step-dot.is-current { - width: 28px; - background: var(--ppw-accent); -} - -.ppw-step-label { - display: none; -} - -.ppw-exit { - display: inline-flex; - align-items: center; - justify-self: end; - gap: 10px; -} - -.ppw-exit-icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 34px; - height: 34px; - border-radius: 999px; - background: var(--ppw-canvas); - font-size: 22px; - font-weight: 400; - line-height: 1; - transition: background-color .15s ease; -} - -.ppw-exit:hover .ppw-exit-icon { - background: var(--ppw-accent-soft); -} - -/* Progress bar */ - -.ppw-bar { - display: block; - flex: 1; - height: 4px; - overflow: hidden; - background: #eceef5; -} - -.ppw-bar-fill { - display: block; - height: 100%; - border-radius: inherit; - background: linear-gradient(90deg, rgba(var(--ppw-accent-rgb), .75), var(--ppw-accent)); - transition: width .4s cubic-bezier(.4, 0, .2, 1); -} - -.ppw-root > .ppw-frame > .ppw-bar { - flex: none; -} - -/* Stage */ - -.ppw-stage { - flex: 1; - display: flex; - align-items: center; - justify-content: center; - padding: 56px 40px; -} - -.ppw-body { - width: 100%; - max-width: 1040px; - animation: ppw-rise .45s cubic-bezier(.2, .7, .2, 1) both; -} - -@keyframes ppw-rise { - from { - opacity: 0; - transform: translateY(10px); - } - to { - opacity: 1; - transform: none; - } -} - -.ppw-heading { - max-width: 640px; - margin: 0 auto 40px; - text-align: center; -} - -/* Footer navigation */ - -.ppw-navigation { - display: grid; - grid-template-columns: 1fr auto 1fr; - align-items: center; - gap: 20px; - min-height: 76px; - padding: 0 28px; - border-top: 1px solid var(--ppw-line); - background: rgba(255, 255, 255, .9); -} - -.ppw-nav-start { - display: flex; - align-items: center; -} - -.ppw-nav-end { - display: flex; - align-items: center; - justify-content: flex-end; - gap: 20px; -} - -.ppw-root .ppw-reassurance { - margin: 0; - color: #64748b; - font-size: 12.5px; - text-align: center; -} - -/* ------------------------------------------------------- mode: full screen */ - -body.ppw-is-fullscreen { - overflow: hidden; -} - -body.ppw-is-fullscreen #wpadminbar, -body.ppw-is-fullscreen #adminmenumain, -body.ppw-is-fullscreen #wpfooter { - display: none; -} - -.ppw-mode-fullscreen { - position: fixed; - z-index: 100000; - inset: 0; - overflow-y: auto; - background: - radial-gradient(1000px 520px at 85% -10%, rgba(var(--ppw-accent-rgb), .10), transparent 60%), - radial-gradient(800px 480px at -10% 110%, rgba(var(--ppw-accent-rgb), .07), transparent 60%), - #fbfbfe; -} - -.ppw-mode-fullscreen .ppw-frame { - min-height: 100%; - background: transparent; -} - -.ppw-mode-fullscreen .ppw-topbar { - background: rgba(255, 255, 255, .72); - backdrop-filter: blur(8px); -} - -.ppw-mode-fullscreen .ppw-navigation { - position: sticky; - bottom: 0; -} - -/* --------------------------------------------------------- mode: in screen */ - -.wrap.ppw-mode-screen { - max-width: 1120px; - margin: 28px auto 48px; - padding: 0; - padding-inline-end: 20px; -} - -.ppw-mode-screen .ppw-frame { - overflow: hidden; - border: 1px solid var(--ppw-line); - border-radius: 20px; - box-shadow: var(--ppw-shadow); -} - -.ppw-mode-screen .ppw-topbar { - border-bottom: 1px solid var(--ppw-line); -} - -.ppw-mode-screen .ppw-stage { - padding: 48px 48px 56px; - background: - radial-gradient(700px 320px at 100% 0%, rgba(var(--ppw-accent-rgb), .07), transparent 65%), - var(--ppw-surface); -} - -.ppw-mode-screen .ppw-navigation { - background: #fafbfd; -} - -.ppw-mode-screen .ppw-exit-label { - display: none; -} - -/* ------------------------------------------------------------------ hero */ - -.ppw-hero { - display: grid; - grid-template-columns: minmax(0, 1fr); - align-items: center; - gap: 56px; -} - -.ppw-hero.has-art { - grid-template-columns: minmax(0, 1.05fr) minmax(0, .95fr); -} - -.ppw-hero:not(.has-art) { - max-width: 680px; - margin: 0 auto; - text-align: center; -} - -.ppw-hero:not(.has-art) .ppw-hero-actions, -.ppw-hero:not(.has-art) .ppw-highlights { - justify-content: center; -} - -.ppw-root .ppw-hero h1 { - font-size: 44px; - line-height: 1.08; - letter-spacing: -.03em; -} - -.ppw-eyebrow { - display: inline-block; - margin-bottom: 20px; - padding: 7px 14px; - border-radius: 999px; - background: var(--ppw-accent-soft); - color: var(--ppw-accent-dark); - font-size: 13px; - font-weight: 650; -} - -.ppw-root .ppw-hero-body { - margin-top: 14px; -} - -.ppw-highlights { - display: flex; - flex-wrap: wrap; - gap: 10px 22px; - margin: 24px 0 0; - padding: 0; - list-style: none; -} - -.ppw-highlights li { - display: inline-flex; - align-items: center; - gap: 8px; - margin: 0; - color: var(--ppw-ink); - font-size: 14px; - font-weight: 550; -} - -.ppw-highlights li::before { - display: inline-block; - flex: none; - width: 18px; - height: 18px; - border-radius: 50%; - background: var(--ppw-accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 6.2l2 2 4-4.4' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat; - content: ''; -} - -.ppw-hero-actions, -.ppw-finish { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 14px 24px; - margin-top: 32px; -} - -.ppw-hero-art { - display: flex; - align-items: center; - justify-content: center; - min-height: 400px; - padding: 32px; - overflow: hidden; - border-radius: 28px; - background: - radial-gradient(circle at 20% 15%, rgba(255, 255, 255, .55), transparent 45%), - linear-gradient(145deg, rgba(var(--ppw-accent-rgb), .22), rgba(var(--ppw-accent-rgb), .08) 60%, rgba(var(--ppw-accent-rgb), .18)); -} - -.ppw-hero-art .ppw-figure { - max-width: 100%; - border: 0; - background: transparent; - box-shadow: none; -} - -/* -------------------------------------------------------------- features */ - -.ppw-features { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 32px 40px; - margin: 0; - padding: 0; - list-style: none; -} - -.ppw-features.ppw-cols-2 { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.ppw-feature { - display: flex; - align-items: flex-start; - gap: 16px; - margin: 0; -} - -.ppw-feature-icon.dashicons { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 44px; - height: 44px; - border-radius: 12px; - background: var(--ppw-accent-soft); - color: var(--ppw-accent); - font-size: 22px; -} - -.ppw-feature-copy { - display: flex; - flex-direction: column; - gap: 4px; -} - -.ppw-feature-copy strong { - color: var(--ppw-ink); - font-size: 15px; - font-weight: 650; -} - -.ppw-feature-copy span { - color: var(--ppw-muted); - font-size: 14px; - line-height: 1.55; -} - -/* -------------------------------------------------------------- concepts */ - -.ppw-concepts { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr)); - gap: 24px; -} - -.ppw-concept { - display: flex; - flex-direction: column; - padding: 14px 14px 26px; - border: 1px solid var(--ppw-line); - border-radius: var(--ppw-radius); - background: var(--ppw-surface); - transition: transform .2s ease, box-shadow .2s ease, border-color .2s ease; -} - -.ppw-concept:hover { - border-color: rgba(var(--ppw-accent-rgb), .35); - box-shadow: var(--ppw-shadow); - transform: translateY(-3px); -} - -.ppw-concept .ppw-figure { - margin-bottom: 20px; -} - -.ppw-concept h2, -.ppw-concept p { - padding: 0 12px; -} - -.ppw-concept h2 { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 8px; - font-size: 18px; -} - -.ppw-concept-number { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 50%; - background: var(--ppw-accent-soft); - color: var(--ppw-accent-dark); - font-size: 12px; - font-weight: 700; -} - -.ppw-concept p { - font-size: 14px; -} - -.ppw-root .ppw-aside { - max-width: 640px; - margin: 32px auto 0; - padding: 16px 20px; - border-radius: 12px; - background: var(--ppw-accent-soft); - color: var(--ppw-accent-dark); - font-size: 14px; - text-align: center; -} - -/* ---------------------------------------------------------------- lesson */ - -.ppw-lesson { - display: grid; - grid-template-columns: minmax(0, .9fr) minmax(0, 1.1fr); - align-items: center; - gap: 56px; -} - -.ppw-lesson-copy h1 { - font-size: 32px; -} - -.ppw-callouts { - display: grid; - gap: 14px; - margin: 28px 0 0; - padding: 0; - list-style: none; - counter-reset: ppw-callout; -} - -.ppw-callouts li { - display: flex; - align-items: flex-start; - gap: 14px; - margin: 0; - color: var(--ppw-ink); - font-size: 15px; - font-weight: 500; - line-height: 1.5; - counter-increment: ppw-callout; -} - -.ppw-callouts li::before { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 50%; - background: var(--ppw-accent); - color: #fff; - font-size: 13px; - font-weight: 700; - content: counter(ppw-callout); -} - -.ppw-root .ppw-link-out { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 12px 16px; - margin: 32px 0 0; -} - -.ppw-link-note { - color: #64748b; - font-size: 12.5px; -} - -/* Figures: screenshots get a browser frame, diagrams are drawn edge to edge. */ - -.ppw-figure { - position: relative; - overflow: hidden; - border: 1px solid var(--ppw-line); - border-radius: 14px; - background: var(--ppw-surface); - box-shadow: var(--ppw-shadow); -} - -.ppw-lesson-art .ppw-figure { - padding-top: 30px; -} - -.ppw-lesson-art .ppw-figure::before { - position: absolute; - top: 0; - inset-inline: 0; - height: 30px; - border-bottom: 1px solid var(--ppw-line); - background: - radial-gradient(circle 4px at 16px 15px, #ff6b6b 98%, transparent), - radial-gradient(circle 4px at 30px 15px, #fbbf24 98%, transparent), - radial-gradient(circle 4px at 44px 15px, #34d399 98%, transparent), - #f8f9fc; - content: ''; -} - -.ppw-figure img { - display: block; - width: 100%; - height: auto; -} - -.ppw-figure-placeholder { - display: flex; - align-items: center; - justify-content: center; - min-height: 220px; - padding: 24px; - background: repeating-linear-gradient(135deg, #f8f9fc, #f8f9fc 10px, #f2f4f9 10px, #f2f4f9 20px); - color: #64748b; - font-size: 13px; - text-align: center; -} - -.ppw-pin { - position: absolute; - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border: 2px solid #fff; - border-radius: 50%; - background: var(--ppw-accent); - box-shadow: 0 0 0 5px var(--ppw-accent-ring), 0 4px 10px rgba(15, 23, 42, .2); - color: #fff; - font-size: 13px; - font-weight: 700; -} - -.ppw-pin-1 { top: 22%; left: 16%; } -.ppw-pin-2 { top: 46%; left: 48%; } -.ppw-pin-3 { right: 14%; bottom: 16%; } - -/* ---------------------------------------------------------------- finish */ - -.ppw-finish-head { - max-width: 620px; - margin: 0 auto; - text-align: center; -} - -.ppw-success { - display: inline-flex; - align-items: center; - justify-content: center; - width: 72px; - height: 72px; - margin-bottom: 24px; - border-radius: 50%; - background: #dcfce7; - box-shadow: 0 0 0 10px #f0fdf4; -} - -.ppw-success-mark { - display: block; - width: 28px; - height: 28px; - background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath d='M5 12.5l4.5 4.5L19 7.5' fill='none' stroke='%2316a34a' stroke-width='2.6' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / contain no-repeat; -} - -.ppw-root .ppw-next-steps-title { - max-width: 560px; - margin: 40px auto 14px; - color: var(--ppw-muted); - font-size: 12px; - font-weight: 700; - letter-spacing: .08em; - text-transform: uppercase; -} - -.ppw-next-steps { - display: grid; - gap: 10px; - max-width: 560px; - margin: 0 auto; - padding: 0; - list-style: none; - counter-reset: ppw-next; -} - -.ppw-next-steps li { - margin: 0; - counter-increment: ppw-next; -} - -.ppw-next-steps a { - display: flex; - align-items: center; - gap: 14px; - padding: 14px 18px; - border: 1px solid var(--ppw-line); - border-radius: 14px; - background: var(--ppw-surface); - color: var(--ppw-ink); - font-size: 15px; - font-weight: 550; - text-decoration: none; - transition: border-color .15s ease, box-shadow .15s ease, transform .15s ease; -} - -.ppw-next-steps a::before { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 50%; - background: var(--ppw-accent-soft); - color: var(--ppw-accent-dark); - font-size: 13px; - font-weight: 700; - content: counter(ppw-next); -} - -.ppw-next-steps a:hover { - border-color: rgba(var(--ppw-accent-rgb), .45); - box-shadow: var(--ppw-shadow); - color: var(--ppw-ink); - transform: translateY(-1px); -} - -.ppw-next-label { - flex: 1; -} - -.ppw-next-arrow { - color: var(--ppw-accent); - transition: transform .15s ease; -} - -.ppw-next-steps a:hover .ppw-next-arrow { - transform: translateX(3px); -} - -.ppw-finish { - justify-content: center; - margin-top: 36px; -} - -.ppw-promo { - display: flex; - align-items: center; - justify-content: space-between; - gap: 32px; - max-width: 760px; - margin: 44px auto 0; - padding: 28px 32px; - border-radius: 20px; - background: linear-gradient(135deg, var(--ppw-accent), var(--ppw-accent-dark)); - color: #fff; -} - -.ppw-root .ppw-promo h2 { - margin-bottom: 12px; - color: #fff; - font-size: 18px; -} - -.ppw-promo ul { - display: grid; - grid-template-columns: repeat(2, minmax(0, auto)); - gap: 6px 28px; - margin: 0; - padding: 0; - list-style: none; -} - -.ppw-promo li { - margin: 0; - color: rgba(255, 255, 255, .88); - font-size: 13.5px; -} - -.ppw-promo li::before { - margin-inline-end: 8px; - content: '\2713'; - font-weight: 700; -} - -/* ----------------------------------------------------------- email capture */ - -.ppw-capture { - display: grid; - gap: 26px; - max-width: 560px; - margin: 0 auto; -} - -.ppw-capture .ppw-field { - display: grid; - gap: 10px; - margin: 0; - padding: 0; - border: 0; -} - -.ppw-capture .ppw-field > label, -.ppw-capture legend { - padding: 0; - color: var(--ppw-ink); - font-size: 14px; - font-weight: 650; -} - -.ppw-personas { - display: flex; - flex-wrap: wrap; - gap: 10px; -} - -.ppw-persona { - position: relative; - cursor: pointer; -} - -.ppw-persona input { - position: absolute; - opacity: 0; - pointer-events: none; -} - -.ppw-persona span { - display: inline-block; - padding: 10px 18px; - border: 1px solid var(--ppw-line); - border-radius: 999px; - background: var(--ppw-surface); - color: var(--ppw-ink); - font-size: 14px; - font-weight: 550; - transition: border-color .15s ease, background-color .15s ease, color .15s ease; -} - -.ppw-persona:hover span { - border-color: rgba(var(--ppw-accent-rgb), .5); -} - -.ppw-persona input:checked + span { - border-color: var(--ppw-accent); - background: var(--ppw-accent-soft); - color: var(--ppw-accent-dark); -} - -.ppw-persona input:focus-visible + span { - outline: 2px solid var(--ppw-accent); - outline-offset: 2px; -} - -.ppw-root .ppw-input { - width: 100%; - min-height: 48px; - padding: 0 16px; - border: 1px solid var(--ppw-line); - border-radius: 12px; - background: var(--ppw-surface); - box-shadow: none; - color: var(--ppw-ink); - font-size: 15px; -} - -.ppw-root .ppw-input:focus { - border-color: var(--ppw-accent); - box-shadow: 0 0 0 4px var(--ppw-accent-ring); - outline: 0; -} - -.ppw-consent { - display: flex; - align-items: flex-start; - gap: 12px; - color: var(--ppw-text); - font-size: 14px; - line-height: 1.55; - cursor: pointer; -} - -.ppw-root .ppw-consent input[type="checkbox"] { - flex: none; - width: 20px; - height: 20px; - margin: 1px 0 0; - border-color: #b8bfce; - border-radius: 6px; -} - -.ppw-root .ppw-consent input[type="checkbox"]:checked { - border-color: var(--ppw-accent); - background: var(--ppw-accent); -} - -.ppw-root .ppw-consent input[type="checkbox"]:checked::before { - margin: 0; - width: 18px; - height: 18px; - content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3E%3Cpath d='M5 10.5l3.2 3.2L15 6.8' fill='none' stroke='%23fff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); -} - -.ppw-root .ppw-disclosure a { - color: var(--ppw-accent); -} - -.ppw-root .ppw-disclosure { - margin: -8px 0 0; - color: #64748b; - font-size: 12.5px; - line-height: 1.55; -} - -.ppw-root .ppw-capture-error { - padding: 12px 16px; - border: 1px solid #fecaca; - border-radius: 12px; - background: #fef2f2; - color: #b91c1c; - font-size: 14px; -} - -.ppw-capture-actions { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 14px 24px; -} - -.ppw-root .ppw-link-button { - padding: 4px; - border: 0; - background: none; - box-shadow: none; - color: var(--ppw-muted); - font-family: inherit; - font-size: 13px; - font-weight: 600; - cursor: pointer; - transition: color .15s ease; -} - -.ppw-root .ppw-link-button:hover, -.ppw-root .ppw-link-button:focus-visible { - color: var(--ppw-accent); -} - -/* ---------------------------------------------------------------- notice */ - -.notice.ppw-notice { - margin: 20px 0 8px; - margin-inline-end: 20px; - padding: 0; - border: 1px solid var(--ppw-line); - border-inline-start: 4px solid var(--ppw-accent); - border-radius: 14px; - background: var(--ppw-surface); - box-shadow: var(--ppw-shadow); -} - -.ppw-notice-inner { - display: flex; - align-items: center; - gap: 18px; - padding: 18px 22px; -} - -.ppw-notice-icon { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 46px; - height: 46px; - overflow: hidden; - border-radius: 12px; - background: var(--ppw-accent-soft); - color: var(--ppw-accent); -} - -.ppw-notice-icon .dashicons { - width: 24px; - height: 24px; - font-size: 24px; -} - -.ppw-notice-icon img { - display: block; - width: 28px; - height: 28px; - object-fit: contain; -} - -.ppw-notice-copy { - display: flex; - flex: 1; - flex-direction: column; - gap: 2px; -} - -.ppw-notice-copy strong { - color: var(--ppw-ink); - font-size: 15px; - font-weight: 650; -} - -.ppw-notice-copy span { - color: var(--ppw-muted); - font-size: 13.5px; - line-height: 1.5; -} - -.ppw-notice-actions { - display: flex; - flex: none; - align-items: center; - gap: 18px; -} - -.ppw-whatsnew-list { - display: grid; - gap: 6px; - margin: 8px 0 0; - padding: 0; - list-style: none; -} - -.ppw-whatsnew-list li { - display: block; - margin: 0; - color: var(--ppw-muted); - font-size: 13.5px; - line-height: 1.5; -} - -.ppw-whatsnew-list li strong { - color: var(--ppw-ink); - font-size: 13.5px; - font-weight: 650; -} - -.ppw-whatsnew-list li strong::after { - content: ' \2013 '; - color: var(--ppw-muted); - font-weight: 400; -} - -.ppw-whatsnew-list li strong:last-child::after { - content: ''; -} - -.ppw-notice .ppw-notice-icon .dashicons-star-filled { - color: #f59e0b; -} - -/* ------------------------------------------------------------- checklist */ - -.ppw-checklist { - clear: both; - min-width: 0; - max-width: 100%; - margin: 20px 0 8px; - margin-inline-end: 20px; - overflow: hidden; - border: 1px solid var(--ppw-line); - border-radius: 16px; - background: var(--ppw-surface); - box-shadow: var(--ppw-shadow); -} - -.ppw-checklist-head { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 18px; - padding: 18px 22px; -} - -.ppw-ring { - --ppw-percent: 0; - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 52px; - height: 52px; - border-radius: 50%; - background: conic-gradient(var(--ppw-accent) calc(var(--ppw-percent) * 1%), #eceef5 0); -} - -.ppw-ring-value { - display: inline-flex; - align-items: center; - justify-content: center; - width: 40px; - height: 40px; - border-radius: 50%; - background: var(--ppw-surface); - color: var(--ppw-ink); - font-size: 11.5px; - font-weight: 700; -} - -.ppw-checklist-title { - display: flex; - flex: 1; - min-width: min(100%, 180px); - flex-direction: column; - gap: 10px; -} - -.ppw-checklist h2 { - margin: 0; - color: var(--ppw-ink); - font-size: 16px; - font-weight: 650; -} - -.ppw-checklist .ppw-bar { - flex: none; - height: 6px; - max-width: 320px; - border-radius: 999px; -} - -.ppw-tasks { - margin: 0; - padding: 0; - border-top: 1px solid var(--ppw-line); - list-style: none; -} - -.ppw-tasks li { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 16px; - margin: 0; - padding: 14px 22px; - border-bottom: 1px solid var(--ppw-line); -} - -.ppw-tasks li:last-child { - border-bottom: 0; -} - -.ppw-checkmark { - display: inline-flex; - flex: none; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border: 2px solid #cdd2de; - border-radius: 50%; -} - -.ppw-tasks li.is-complete .ppw-checkmark { - border-color: var(--ppw-accent); - background: var(--ppw-accent) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath d='M3 6.2l2 2 4-4.4' fill='none' stroke='%23fff' stroke-width='1.8' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 14px no-repeat; -} - -.ppw-task-copy { - display: flex; - flex: 1; - min-width: min(100%, 160px); - overflow-wrap: anywhere; - flex-direction: column; - gap: 2px; -} - -.ppw-task-label { - color: var(--ppw-ink); - font-size: 14.5px; - font-weight: 600; -} - -.ppw-tasks li.is-complete .ppw-task-label { - color: var(--ppw-muted); - text-decoration: line-through; - text-decoration-color: #cbd5e1; -} - -.ppw-task-detail { - color: var(--ppw-muted); - font-size: 12.5px; -} - -.ppw-checklist .ppw-btn { - min-height: 36px; - padding: 0 16px; - font-size: 13px; -} - -/* ------------------------------------------------------------ responsive */ - -.ppw-concept-art { - display: flex; - align-items: center; - justify-content: center; - min-height: 180px; - margin-bottom: 20px; -} - -.ppw-capture-layout.has-art { - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - align-items: center; - gap: 40px; -} - -.ppw-capture-layout.has-art .ppw-heading { - text-align: start; - margin-bottom: 24px; -} - -.ppw-capture-content, -.ppw-capture-art { - min-width: 0; -} - -.ppw-capture-art img { - display: block; - max-width: 100%; - height: auto; -} - -@media (max-width: 960px) { - .ppw-capture-layout.has-art { - grid-template-columns: minmax(0, 1fr); - } - - .ppw-capture-art { - display: none; - } - .ppw-hero.has-art, - .ppw-lesson { - grid-template-columns: minmax(0, 1fr); - gap: 36px; - } - - .ppw-hero-art { - min-height: 260px; - } - - .ppw-features, - .ppw-concepts { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 782px) { - .wrap.ppw-mode-screen { - padding-inline-end: 10px; - } - - .ppw-topbar, - .ppw-navigation { - padding-right: 16px; - padding-left: 16px; - } - - .ppw-topbar { - grid-template-columns: minmax(0, 1fr) auto auto; - gap: 10px; - } - - .ppw-brand-logo { - max-width: 110px; - height: auto; - max-height: 28px; - } - - .ppw-brand-name { - font-size: 12px; - } - - .ppw-steps { - display: none; - } - - .ppw-step-label { - display: block; - color: var(--ppw-muted); - font-size: 12px; - font-weight: 600; - } - - .ppw-exit-label { - display: none; - } - - .ppw-stage, - .ppw-mode-screen .ppw-stage { - padding: 32px 20px 40px; - } - - .ppw-root h1, - .ppw-root .ppw-hero h1 { - font-size: 28px; - } - - .ppw-features, - .ppw-features.ppw-cols-2, - .ppw-concepts { - grid-template-columns: minmax(0, 1fr); - } - - .ppw-navigation { - grid-template-columns: auto 1fr; - padding-top: 12px; - padding-bottom: 12px; - } - - .ppw-root .ppw-reassurance { - display: none; - } - - .ppw-promo { - flex-direction: column; - align-items: flex-start; - } - - .ppw-promo ul { - grid-template-columns: minmax(0, 1fr); - } - - .ppw-notice-inner { - flex-wrap: wrap; - } - - .ppw-notice-copy { - flex-basis: calc(100% - 70px); - } - - .ppw-checklist-head { - flex-wrap: wrap; - } - - .ppw-tasks li { - flex-wrap: wrap; - } -} - -@media (prefers-reduced-motion: reduce) { - .ppw-body, - .ppw-bar-fill, - .ppw-btn, - .ppw-btn-arrow, - .ppw-concept, - .ppw-step-dot, - .ppw-next-steps a { - animation: none; - transition: none; - } -} - -/* ------------------------------------------------------------------- RTL */ - -/* Layout uses logical properties, so only directional glyphs and gradients need mirroring. - * Screenshot pins stay physical on purpose: they point at coordinates inside an unmirrored image. */ - -body.rtl .ppw-back::before { - content: '\2192'; -} - -body.rtl .ppw-btn-arrow, -body.rtl .ppw-next-arrow { - display: inline-block; - transform: scaleX(-1); -} - -body.rtl .ppw-btn:hover .ppw-btn-arrow { - transform: scaleX(-1) translateX(3px); -} - -body.rtl .ppw-next-steps a:hover .ppw-next-arrow { - transform: scaleX(-1) translateX(3px); -} - -body.rtl .ppw-bar-fill { - background: linear-gradient(270deg, rgba(var(--ppw-accent-rgb), .75), var(--ppw-accent)); -} diff --git a/lib/vendor/publishpress/publishpress-welcome/composer.json b/lib/vendor/publishpress/publishpress-welcome/composer.json deleted file mode 100644 index 962fdae8a..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/composer.json +++ /dev/null @@ -1,65 +0,0 @@ -{ - "name": "publishpress/publishpress-welcome", - "description": "Reusable, configurable welcome and onboarding experiences for PublishPress WordPress plugins.", - "type": "library", - "version": "0.1.0", - "license": "GPL-3.0-or-later", - "authors": [ - { - "name": "PublishPress", - "email": "help@publishpress.com", - "homepage": "https://publishpress.com/", - "role": "Developer" - } - ], - "minimum-stability": "stable", - "prefer-stable": true, - "require": { - "php": ">=7.2.5" - }, - "require-dev": { - "publishpress/version-loader-generator": "^1.3.1", - "overtrue/phplint": "^9.5" - }, - "config": { - "preferred-install": "dist", - "allow-plugins": { - "dealerdirect/phpcodesniffer-composer-installer": true - } - }, - "autoload": { - "files": [ - "src/include.php" - ] - }, - "scripts": { - "generate-files": "vendor/bin/version-loader-generator", - "post-install-cmd": [ - "@generate-files" - ], - "post-update-cmd": [ - "@generate-files" - ], - "check:lint": "phplint --no-cache src tests", - "test:unit": "php tests/run.php", - "test:integration": "php tests/integration/version-loader.php", - "test": [ - "@check:lint", - "@test:unit", - "@test:integration" - ], - "build": "bash bin/build.sh", - "check:release": "php bin/check-release.php", - "make:pot": "bash bin/make-pot.sh" - }, - "extra": { - "generator": { - "lib-class-test": "class_exists('PublishPress\\Welcome\\WelcomeController')", - "src-dir": "src", - "namespace": "PublishPress\\Welcome", - "action-register-priority": "-200", - "action-initialize-priority": "-190", - "generate-test-file": false - } - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/languages/publishpress-welcome.pot b/lib/vendor/publishpress/publishpress-welcome/languages/publishpress-welcome.pot deleted file mode 100644 index 20e315c3b..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/languages/publishpress-welcome.pot +++ /dev/null @@ -1,229 +0,0 @@ -# PublishPress Welcome translation template. -# Copyright (C) PublishPress -# This file is distributed under the GPL-3.0-or-later license. -# -msgid "" -msgstr "" -"Project-Id-Version: PublishPress Welcome 0.1.0\n" -"Report-Msgid-Bugs-To: https://github.com/publishpress/library-publishpress-" -"welcome/issues\n" -"POT-Creation-Date: 2026-10-19 23:18+0700\n" -"Language: \n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" - -msgid "Welcome! Would you like a quick tour?" -msgstr "" - -msgid "" -"We will show you where the important settings are. Nothing is changed while " -"you read." -msgstr "" - -msgid "Finish setting up the plugin" -msgstr "" - -msgid "Setup is complete" -msgstr "" - -msgid "Site owner" -msgstr "" - -msgid "Writer or editor" -msgstr "" - -msgid "Agency or freelancer" -msgstr "" - -msgid "Developer" -msgstr "" - -msgid "Something else" -msgstr "" - -msgid "Stay in the loop" -msgstr "" - -msgid "" -"Tell us a little about how you use WordPress and we will send tips and " -"offers that fit. This is optional, and everything works if you skip it." -msgstr "" - -msgid "What best describes you?" -msgstr "" - -msgid "Email address" -msgstr "" - -msgid "" -"Yes, send me tips, product news and offers tailored to how I use this " -"plugin. I can unsubscribe at any time." -msgstr "" - -msgid "Privacy policy" -msgstr "" - -#, php-format -msgid "" -"Your email address and your answer are sent to %s when you subscribe. " -"Nothing is sent if you skip." -msgstr "" - -msgid "Subscribe" -msgstr "" - -msgid "No thanks" -msgstr "" - -msgid "Continue" -msgstr "" - -msgid "Check your inbox" -msgstr "" - -msgid "" -"Please confirm your email using the link from PublishPress before receiving " -"tips and offers." -msgstr "" - -msgid "" -"Check your inbox for a confirmation link after subscribing. Confirm your " -"email before receiving tips and offers." -msgstr "" - -msgid "" -"We could not accept this subscription. Please try again later or choose \"No " -"thanks\"." -msgstr "" - -msgid "Please enter a valid email address." -msgstr "" - -msgid "" -"Please tick the box to confirm you want to hear from us, or choose \"No " -"thanks\"." -msgstr "" - -msgid "Back" -msgstr "" - -msgid "Next" -msgstr "" - -msgid "Skip" -msgstr "" - -msgid "Exit guide" -msgstr "" - -msgid "Guide steps" -msgstr "" - -msgid "Guide navigation" -msgstr "" - -msgid "Skip to the guide content" -msgstr "" - -msgid "(opens in a new tab)" -msgstr "" - -msgid "Completed:" -msgstr "" - -msgid "Not completed:" -msgstr "" - -msgid "See the full changelog" -msgstr "" - -msgid "Finish" -msgstr "" - -msgid "Start the tour" -msgstr "" - -msgid "Continue the tour" -msgstr "" - -msgid "Dismiss" -msgstr "" - -msgid "Hide" -msgstr "" - -msgid "Do it" -msgstr "" - -msgid "View" -msgstr "" - -msgid "Replay the guide" -msgstr "" - -#, php-format -msgid "Step %1$s of %2$s" -msgstr "" - -msgid "This guide does not change any setting on your site." -msgstr "" - -msgid "PublishPress welcome guide" -msgstr "" - -msgid "Pending subscription (delivery will be retried)" -msgstr "" - -msgid "Email subscription choice" -msgstr "" - -msgid "Answer to \"what best describes you?\"" -msgstr "" - -msgid "Suggested text for the optional PublishPress welcome guide:" -msgstr "" - -msgid "" -"If you opt in on the welcome guide, we receive your email address, your " -"answer to \"what best describes you?\" and an identifier for the consent " -"statement shown to you. PublishPress records the receive time and asks you " -"to confirm your email before sending tips and offers. Nothing is sent unless " -"you choose Subscribe with the consent box ticked. The guide also stores your " -"progress and your choice on this site; both are included in the personal " -"data export and removed by the erasure tool, and you can unsubscribe at any " -"time." -msgstr "" - -#. translators: 1: step number, 2: step ID -#, php-format -msgid "Step %1$d (%2$s)" -msgstr "" - -#. translators: 1: number of views, 2: number of exits -#, php-format -msgid "%1$d views, %2$d exits" -msgstr "" - -msgid "Completed the guide" -msgstr "" - -msgid "Subscribed by email" -msgstr "" - -msgid "Declined the email step" -msgstr "" - -msgid "A welcome experience must retain at least one step." -msgstr "" - -msgid "Filtered welcome steps must implement StepInterface." -msgstr "" - -msgid "You are not allowed to access this page." -msgstr "" - -#. translators: %s: experience ID -#, php-format -msgid "PublishPress welcome guide: %s" -msgstr "" diff --git a/lib/vendor/publishpress/publishpress-welcome/src/ChecklistTask.php b/lib/vendor/publishpress/publishpress-welcome/src/ChecklistTask.php deleted file mode 100644 index 5460bb70b..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/ChecklistTask.php +++ /dev/null @@ -1,55 +0,0 @@ -id = $id; - $this->label = (string) $label; - $this->url = (string) $url; - $this->completeCallback = $completeCallback; - $this->detailCallback = $detailCallback; - } - - public function getId() - { - return $this->id; - } - - public function getLabel() - { - return $this->label; - } - - public function getUrl() - { - return $this->url; - } - - public function isComplete() - { - return (bool) call_user_func($this->completeCallback); - } - - public function getDetail() - { - return $this->detailCallback ? (string) call_user_func($this->detailCallback) : ''; - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/Contract/ChecklistTaskInterface.php b/lib/vendor/publishpress/publishpress-welcome/src/Contract/ChecklistTaskInterface.php deleted file mode 100644 index efd8c9785..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/Contract/ChecklistTaskInterface.php +++ /dev/null @@ -1,21 +0,0 @@ - */ - private static $senders = []; - - /** @var bool */ - private static $hooked = false; - - /** - * Register how to deliver for an experience. Items of unregistered experiences are left untouched. - */ - public static function registerSender($experience, callable $sender) - { - self::$senders[(string) $experience] = $sender; - - if (self::$hooked) { - return; - } - - self::$hooked = true; - - add_action(self::CRON_HOOK, [__CLASS__, 'process']); - add_action('admin_init', [__CLASS__, 'maybeProcess']); - } - - /** - * Queue a payload whose first delivery attempt has just failed. - */ - public static function enqueue($experience, array $payload, $now = null, $result = null) - { - if (! self::retryable($result)) { - return; - } - $now = null === $now ? time() : (int) $now; - $queue = self::load(); - - $queue[] = [ - 'experience' => (string) $experience, - 'payload' => EmailSubscriber::contractPayload($payload), - 'attempts' => 1, - 'created' => $now, - 'next' => $now + self::retryDelay(1, $result), - ]; - - // Keep the newest entries if the queue ever grows unreasonably. - $queue = array_slice($queue, -self::MAX_ITEMS); - - self::save($queue); - self::schedule($queue); - } - - /** @return int Seconds to wait after the given number of failed attempts. */ - public static function backoffSeconds($attempts) - { - $index = min(max((int) $attempts, 1), count(self::BACKOFF)) - 1; - - return self::BACKOFF[$index]; - } - - /** Permanent client errors cannot be fixed by retrying. */ - public static function retryable($result) - { - $data = is_wp_error($result) ? $result->get_error_data() : null; - $status = is_array($data) && isset($data['http_status']) ? (int) $data['http_status'] : 0; - return $status < 400 || $status >= 500 || in_array($status, [408, 429], true); - } - - private static function retryDelay($attempts, $result) - { - $data = is_wp_error($result) ? $result->get_error_data() : null; - $retry = is_array($data) && isset($data['retry_after']) ? (int) $data['retry_after'] : 0; - return max(self::backoffSeconds($attempts), min(86400, max(0, $retry))); - } - - /** @return array */ - public static function pending() - { - return self::load(); - } - - /** Throttled entry point for admin requests. */ - public static function maybeProcess() - { - if (! self::load() || get_transient(self::LOCK_TRANSIENT)) { - return; - } - - set_transient(self::LOCK_TRANSIENT, 1, 300); - self::process(); - } - - /** - * Attempt every due item once. - * - * @param int|null $now Unix time, injectable for tests. - * @return int Number of items delivered. - */ - public static function process($now = null) - { - $now = null === $now ? time() : (int) $now; - $queue = self::load(); - $kept = []; - $delivered = 0; - - foreach ($queue as $item) { - if (($now - (int) $item['created']) >= self::MAX_AGE || (int) $item['attempts'] >= self::MAX_ATTEMPTS) { - continue; - } - - if (! isset(self::$senders[$item['experience']]) || (int) $item['next'] > $now) { - $kept[] = $item; - continue; - } - - $result = call_user_func(self::$senders[$item['experience']], EmailSubscriber::contractPayload($item['payload'])); - - if (true === $result) { - $delivered++; - continue; - } - - if (! self::retryable($result)) { - continue; - } - - $item['attempts'] = (int) $item['attempts'] + 1; - - if ($item['attempts'] >= self::MAX_ATTEMPTS) { - continue; - } - - $item['payload'] = EmailSubscriber::contractPayload($item['payload']); - $item['next'] = $now + self::retryDelay($item['attempts'], $result); - $kept[] = $item; - } - - self::save($kept); - self::schedule($kept); - - return $delivered; - } - - /** @return array Queued payloads that belong to the address. */ - public static function itemsForEmail($email) - { - return array_values(array_filter(self::load(), function ($item) use ($email) { - return isset($item['payload']['email']) && 0 === strcasecmp((string) $item['payload']['email'], (string) $email); - })); - } - - /** @return bool Whether anything was removed. */ - public static function eraseByEmail($email) - { - $queue = self::load(); - $kept = array_values(array_filter($queue, function ($item) use ($email) { - return ! (isset($item['payload']['email']) && 0 === strcasecmp((string) $item['payload']['email'], (string) $email)); - })); - - if (count($kept) === count($queue)) { - return false; - } - - self::save($kept); - - return true; - } - - private static function schedule(array $queue) - { - if (! $queue) { - return; - } - - $next = min(array_map(function ($item) { - return (int) $item['next']; - }, $queue)); - - if (! wp_next_scheduled(self::CRON_HOOK)) { - wp_schedule_single_event(max($next, time() + 60), self::CRON_HOOK); - } - } - - private static function load() - { - $queue = get_option(self::OPTION, []); - - return is_array($queue) ? $queue : []; - } - - private static function save(array $queue) - { - if (! $queue) { - delete_option(self::OPTION); - return; - } - - update_option(self::OPTION, array_values($queue), false); - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/EmailSubscriber.php b/lib/vendor/publishpress/publishpress-welcome/src/EmailSubscriber.php deleted file mode 100644 index 78a73948c..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/EmailSubscriber.php +++ /dev/null @@ -1,106 +0,0 @@ -settings = $settings; - } - - /** - * @param array $payload - * @return true|\WP_Error - */ - public function send(array $payload) - { - $settings = ExperienceConfig::mergeEmailCapture($this->settings); - if (! empty($settings['configuration_error']) || empty($settings['enabled'])) { - return new \WP_Error('publishpress_welcome_subscribe_failed', 'Email capture configuration is invalid or disabled.', ['http_status' => 400]); - } - if (! isset($payload['email']) || ! is_string($payload['email']) || true !== (isset($payload['consent']) ? $payload['consent'] : null) - || ! isset($payload['consent_statement']) || $settings['consent_statement'] !== $payload['consent_statement']) { - return new \WP_Error('publishpress_welcome_subscribe_failed', 'Subscription payload is missing valid email, consent or statement ID.', ['http_status' => 400]); - } - // Keep legacy client text/time and unknown filter values out of requests and retry storage. - $payload = self::contractPayload($payload); - - if (! empty($this->settings['handler']) && is_callable($this->settings['handler'])) { - $result = call_user_func($this->settings['handler'], $payload); - - return $this->normalize($result); - } - - $endpoint = isset($this->settings['endpoint']) ? (string) $this->settings['endpoint'] : ''; - - if ('' === $endpoint || self::PLACEHOLDER_ENDPOINT === $endpoint) { - if (defined('WP_DEBUG') && WP_DEBUG) { - // The address is deliberately not logged. - error_log('[publishpress-welcome] Dry run: no subscription endpoint is configured, nothing was sent.'); - } - - return true; - } - - $response = wp_remote_post( - $endpoint, - [ - 'timeout' => 5, - 'redirection' => 0, - 'limit_response_size' => 4096, - 'headers' => ['Content-Type' => 'application/json', 'User-Agent' => 'PublishPress-Welcome/1.0'], - 'body' => wp_json_encode($payload), - ] - ); - - if (is_wp_error($response)) { - return $response; - } - - $code = (int) wp_remote_retrieve_response_code($response); - - if ($code < 200 || $code >= 300) { - $retryAfter = wp_remote_retrieve_header($response, 'retry-after'); - $delay = 0; - if (is_string($retryAfter) && strlen($retryAfter) <= 128 && in_array($code, [429, 503], true)) { - $delay = ctype_digit($retryAfter) ? (float) $retryAfter : (float) strtotime($retryAfter) - time(); - $delay = (int) max(0, min(86400, $delay)); - } - return new \WP_Error('publishpress_welcome_subscribe_failed', sprintf('Subscription endpoint returned HTTP %d.', $code), ['http_status' => $code, 'retry_after' => $delay]); - } - - return true; - } - - public static function contractPayload(array $payload) - { - return array_intersect_key($payload, array_flip(['email', 'consent', 'consent_statement', 'plugin', 'persona', 'locale', 'plan_claim'])); - } - - private function normalize($result) - { - if (true === $result) { - return true; - } - - if (is_wp_error($result)) { - return $result; - } - - return new \WP_Error('publishpress_welcome_subscribe_failed', 'The subscription handler reported a failure.'); - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/ExperienceConfig.php b/lib/vendor/publishpress/publishpress-welcome/src/ExperienceConfig.php deleted file mode 100644 index 14f1d1a93..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/ExperienceConfig.php +++ /dev/null @@ -1,242 +0,0 @@ - $values['page_title'], - 'show_in_menu' => false, - 'mode' => self::MODE_SCREEN, - 'allow_mode_override' => false, - 'brand_name' => '', - 'logo' => [], - 'accent_color' => self::DEFAULT_ACCENT, - 'styles' => [], - 'enrollment_option' => $id . '_welcome_enrolled', - 'redirect_option' => $id . '_welcome_redirect_pending', - 'user_option_prefix' => $id . '_welcome', - 'notice_enabled' => true, - 'notice_icon_url' => '', - // Screens (?page= slugs) the notice may appear on. Empty means the checklist pages, i.e. the plugin's own screens. - 'notice_pages' => [], - // Also allow the main Dashboard. Off by default: WordPress.org asks that promotional notices stay limited in scope. - 'notice_on_dashboard' => false, - // The notice stops appearing this many days after it was first shown. 0 keeps it until dismissed. - 'notice_days' => 14, - // Release highlights shown once per version: ['version', 'title', 'items' => [['title', 'body']], 'url', 'url_label']. - 'whats_new' => false, - 'email_capture' => [], - 'notice_title' => __('Welcome! Would you like a quick tour?', 'publishpress-welcome'), - 'notice_body' => __('We will show you where the important settings are. Nothing is changed while you read.', 'publishpress-welcome'), - 'checklist_enabled' => true, - 'checklist_pages' => [], - 'checklist_title' => __('Finish setting up the plugin', 'publishpress-welcome'), - 'checklist_complete_title' => __('Setup is complete', 'publishpress-welcome'), - 'exit_url' => admin_url(), - 'labels' => [], - ]; - - $values['id'] = $id; - $values['page_slug'] = sanitize_key($values['page_slug']); - $values['labels'] = array_merge($this->defaultLabels(), isset($values['labels']) ? $values['labels'] : []); - $values['checklist_pages'] = array_map('sanitize_key', isset($values['checklist_pages']) ? (array) $values['checklist_pages'] : []); - $values['notice_pages'] = array_map('sanitize_key', isset($values['notice_pages']) ? (array) $values['notice_pages'] : []); - if (! $values['notice_pages']) { - $values['notice_pages'] = $values['checklist_pages']; - } - $values['styles'] = isset($values['styles']) ? (array) $values['styles'] : []; - $values['mode'] = self::normalizeMode(isset($values['mode']) ? $values['mode'] : self::MODE_SCREEN); - $values['logo'] = isset($values['logo']) ? (array) $values['logo'] : []; - $values['email_capture'] = self::mergeEmailCapture(isset($values['email_capture']) ? $values['email_capture'] : []); - - $this->values = array_merge($defaults, $values); - } - - /** @return mixed */ - public function get($key) - { - return array_key_exists($key, $this->values) ? $this->values[$key] : null; - } - - /** @return string */ - public function label($key) - { - $labels = $this->values['labels']; - - return isset($labels[$key]) ? (string) $labels[$key] : ''; - } - - /** - * Accepts only the two supported display modes and falls back to the in-screen layout. - * - * @return string - */ - public static function normalizeMode($mode) - { - return self::MODE_FULLSCREEN === $mode ? self::MODE_FULLSCREEN : self::MODE_SCREEN; - } - - /** - * CSS custom properties derived from the accent colour. Invalid colours fall back to the default. - * - * @return string - */ - public function styleVariables() - { - $hex = self::normalizeHex($this->values['accent_color']); - list($r, $g, $b) = self::hexToRgb($hex); - - $dark = sprintf('#%02x%02x%02x', (int) ($r * .78), (int) ($g * .78), (int) ($b * .78)); - - return sprintf('--ppw-accent:%1$s;--ppw-accent-rgb:%2$d,%3$d,%4$d;--ppw-accent-dark:%5$s;', $hex, $r, $g, $b, $dark); - } - - /** - * Defaults for the optional email step. Pass `false` to disable it, or an array of overrides. - * A consumer can also adjust the final array through the "publishpress_welcome_{id}_email_capture" filter. - * - * @param array|bool $overrides - * @return array - */ - public static function mergeEmailCapture($overrides) - { - if (false === $overrides) { - $overrides = ['enabled' => false]; - } - - $defaults = [ - 'enabled' => true, - // "before_last", "after_first", or a 1-based step position. - 'position' => 'before_last', - // Days before asking again after "No thanks". 0 means never ask again (in any bundled plugin). - 'decline_snooze_days' => 0, - // Callable receiving the payload array; returns true, false or WP_Error. Replaces the HTTP request. - 'handler' => null, - 'endpoint' => EmailSubscriber::DEFAULT_ENDPOINT, - 'consent_statement' => 'welcome-email-v1', - // Render the consent checkbox ticked. Off by default; the user still has to click Subscribe to send anything. - 'consent_prechecked' => false, - 'recipient_name' => 'PublishPress', - 'privacy_url' => '', - 'personas' => [ - 'site-owner' => __('Site owner', 'publishpress-welcome'), - 'content-team' => __('Writer or editor', 'publishpress-welcome'), - 'agency' => __('Agency or freelancer', 'publishpress-welcome'), - 'developer' => __('Developer', 'publishpress-welcome'), - 'other' => __('Something else', 'publishpress-welcome'), - ], - 'copy' => [ - 'title' => __('Stay in the loop', 'publishpress-welcome'), - 'lede' => __('Tell us a little about how you use WordPress and we will send tips and offers that fit. This is optional, and everything works if you skip it.', 'publishpress-welcome'), - 'persona_label' => __('What best describes you?', 'publishpress-welcome'), - 'email_label' => __('Email address', 'publishpress-welcome'), - 'consent_label' => __('Yes, send me tips, product news and offers tailored to how I use this plugin. I can unsubscribe at any time.', 'publishpress-welcome'), - 'privacy_label' => __('Privacy policy', 'publishpress-welcome'), - 'disclosure' => __('Your email address and your answer are sent to %s when you subscribe. Nothing is sent if you skip.', 'publishpress-welcome'), - 'submit' => __('Subscribe', 'publishpress-welcome'), - 'decline' => __('No thanks', 'publishpress-welcome'), - 'continue' => __('Continue', 'publishpress-welcome'), - 'success_title' => __('Check your inbox', 'publishpress-welcome'), - 'success_body' => __('Please confirm your email using the link from PublishPress before receiving tips and offers.', 'publishpress-welcome'), - 'confirmation_note' => __('Check your inbox for a confirmation link after subscribing. Confirm your email before receiving tips and offers.', 'publishpress-welcome'), - 'error_delivery' => __('We could not accept this subscription. Please try again later or choose "No thanks".', 'publishpress-welcome'), - 'error_email' => __('Please enter a valid email address.', 'publishpress-welcome'), - 'error_consent' => __('Please tick the box to confirm you want to hear from us, or choose "No thanks".', 'publishpress-welcome'), - ], - ]; - - $overrides = is_array($overrides) ? $overrides : []; - $merged = array_merge($defaults, $overrides); - $merged['copy'] = array_merge($defaults['copy'], isset($overrides['copy']) ? (array) $overrides['copy'] : []); - $merged['personas'] = isset($overrides['personas']) ? (array) $overrides['personas'] : $defaults['personas']; - - $statement = $merged['consent_statement']; - $customWording = $merged['copy']['consent_label'] !== $defaults['copy']['consent_label']; - if (! is_string($statement) || ! preg_match('/^[a-z0-9-]{1,32}$/D', $statement) - || ($customWording && 'welcome-email-v1' === $statement)) { - $merged['enabled'] = false; - $merged['configuration_error'] = 'invalid_consent_statement'; - } - - return $merged; - } - - /** @return string */ - private static function normalizeHex($color) - { - $color = is_string($color) ? strtolower(trim($color)) : ''; - - if (! preg_match('/^#([0-9a-f]{3}|[0-9a-f]{6})$/', $color)) { - return self::DEFAULT_ACCENT; - } - - if (4 === strlen($color)) { - $color = '#' . $color[1] . $color[1] . $color[2] . $color[2] . $color[3] . $color[3]; - } - - return $color; - } - - /** @return int[] */ - private static function hexToRgb($hex) - { - return [hexdec(substr($hex, 1, 2)), hexdec(substr($hex, 3, 2)), hexdec(substr($hex, 5, 2))]; - } - - /** @return array */ - public function all() - { - return $this->values; - } - - private function defaultLabels() - { - return [ - 'back' => __('Back', 'publishpress-welcome'), - 'next' => __('Next', 'publishpress-welcome'), - 'skip' => __('Skip', 'publishpress-welcome'), - 'exit' => __('Exit guide', 'publishpress-welcome'), - 'step_nav' => __('Guide steps', 'publishpress-welcome'), - 'guide_nav' => __('Guide navigation', 'publishpress-welcome'), - 'skip_content' => __('Skip to the guide content', 'publishpress-welcome'), - 'new_tab' => __('(opens in a new tab)', 'publishpress-welcome'), - 'task_done' => __('Completed:', 'publishpress-welcome'), - 'task_todo' => __('Not completed:', 'publishpress-welcome'), - 'changelog' => __('See the full changelog', 'publishpress-welcome'), - 'finish' => __('Finish', 'publishpress-welcome'), - 'start' => __('Start the tour', 'publishpress-welcome'), - 'continue' => __('Continue the tour', 'publishpress-welcome'), - 'dismiss' => __('Dismiss', 'publishpress-welcome'), - 'hide' => __('Hide', 'publishpress-welcome'), - 'do_it' => __('Do it', 'publishpress-welcome'), - 'view' => __('View', 'publishpress-welcome'), - 'replay' => __('Replay the guide', 'publishpress-welcome'), - 'step_format' => __('Step %1$s of %2$s', 'publishpress-welcome'), - 'reassurance' => __('This guide does not change any setting on your site.', 'publishpress-welcome'), - ]; - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/NoticePolicy.php b/lib/vendor/publishpress/publishpress-welcome/src/NoticePolicy.php deleted file mode 100644 index 0e338b18e..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/NoticePolicy.php +++ /dev/null @@ -1,46 +0,0 @@ -= (int) $days * 86400; - } - - /** - * Whether the current admin screen is one the notice may appear on. - * - * @param string $currentPage The ?page= slug, or an empty string. - * @param string[] $pages Allowed page slugs. - * @param bool $onDashboard Whether the main Dashboard is also allowed. - * @param bool $isDashboard Whether the current screen is the Dashboard. - * @return bool - */ - public static function allowsScreen($currentPage, array $pages, $onDashboard, $isDashboard) - { - if ($isDashboard) { - return (bool) $onDashboard; - } - - return '' !== $currentPage && in_array($currentPage, $pages, true); - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/Privacy.php b/lib/vendor/publishpress/publishpress-welcome/src/Privacy.php deleted file mode 100644 index 0e6cb4690..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/Privacy.php +++ /dev/null @@ -1,179 +0,0 @@ - Experience ID => user option prefix. */ - private static $experiences = []; - - /** @var bool */ - private static $hooked = false; - - /** - * Register an experience so its stored data is included in exports and erasures. - */ - public static function track($id, $userOptionPrefix) - { - self::$experiences[(string) $id] = (string) $userOptionPrefix; - - if (self::$hooked) { - return; - } - - self::$hooked = true; - - add_filter('wp_privacy_personal_data_exporters', [__CLASS__, 'registerExporter']); - add_filter('wp_privacy_personal_data_erasers', [__CLASS__, 'registerEraser']); - add_action('admin_init', [__CLASS__, 'addPolicyContent']); - } - - public static function registerExporter(array $exporters) - { - $exporters['publishpress-welcome'] = [ - 'exporter_friendly_name' => __('PublishPress welcome guide', 'publishpress-welcome'), - 'callback' => [__CLASS__, 'exporter'], - ]; - - return $exporters; - } - - public static function registerEraser(array $erasers) - { - $erasers['publishpress-welcome'] = [ - 'eraser_friendly_name' => __('PublishPress welcome guide', 'publishpress-welcome'), - 'callback' => [__CLASS__, 'eraser'], - ]; - - return $erasers; - } - - public static function exporter($emailAddress) - { - $user = get_user_by('email', $emailAddress); - $items = $user ? self::exportItems((int) $user->ID) : []; - - foreach (EmailQueue::itemsForEmail($emailAddress) as $index => $queued) { - $items[] = [ - 'group_id' => 'publishpress-welcome', - 'group_label' => __('PublishPress welcome guide', 'publishpress-welcome'), - 'item_id' => 'publishpress-welcome-pending-' . $index, - 'data' => [ - ['name' => __('Pending subscription (delivery will be retried)', 'publishpress-welcome'), 'value' => (string) $queued['payload']['email']], - ], - ]; - } - - return ['data' => $items, 'done' => true]; - } - - public static function eraser($emailAddress) - { - $user = get_user_by('email', $emailAddress); - $removed = $user ? self::erase((int) $user->ID) : false; - $removed = EmailQueue::eraseByEmail($emailAddress) || $removed; - - return ['items_removed' => $removed, 'items_retained' => false, 'messages' => [], 'done' => true]; - } - - /** - * @param int $userId - * @return array Export groups in the format WordPress expects. - */ - public static function exportItems($userId) - { - $items = []; - $shared = []; - - $status = SharedProfile::status($userId); - if ('' !== $status) { - $shared[] = ['name' => __('Email subscription choice', 'publishpress-welcome'), 'value' => $status]; - } - - $persona = SharedProfile::persona($userId); - if ('' !== $persona) { - $shared[] = ['name' => __('Answer to "what best describes you?"', 'publishpress-welcome'), 'value' => $persona]; - } - - if ($shared) { - $items[] = [ - 'group_id' => 'publishpress-welcome', - 'group_label' => __('PublishPress welcome guide', 'publishpress-welcome'), - 'item_id' => 'publishpress-welcome-profile-' . $userId, - 'data' => $shared, - ]; - } - - foreach (self::$experiences as $id => $prefix) { - $data = []; - - foreach (self::EXPERIENCE_SUFFIXES as $suffix) { - $value = get_user_option($prefix . '_' . $suffix, $userId); - - if (false !== $value && '' !== $value && null !== $value) { - $data[] = ['name' => $id . ' ' . $suffix, 'value' => (string) $value]; - } - } - - if ($data) { - $items[] = [ - 'group_id' => 'publishpress-welcome', - 'group_label' => __('PublishPress welcome guide', 'publishpress-welcome'), - 'item_id' => 'publishpress-welcome-' . $id . '-' . $userId, - 'data' => $data, - ]; - } - } - - return $items; - } - - /** - * Remove everything this library stores for the user. - * - * @param int $userId - * @return bool Whether anything was removed. - */ - public static function erase($userId) - { - $removed = false; - - foreach ([SharedProfile::STATUS_OPTION, SharedProfile::PERSONA_OPTION, SharedProfile::DECLINED_AT_OPTION] as $option) { - if (false !== get_user_option($option, $userId)) { - delete_user_option($userId, $option); - $removed = true; - } - } - - foreach (self::$experiences as $prefix) { - foreach (self::EXPERIENCE_SUFFIXES as $suffix) { - $option = $prefix . '_' . $suffix; - - if (false !== get_user_option($option, $userId)) { - delete_user_option($userId, $option); - $removed = true; - } - } - } - - return $removed; - } - - public static function addPolicyContent() - { - if (! function_exists('wp_add_privacy_policy_content')) { - return; - } - - $content = '

' . esc_html__('Suggested text for the optional PublishPress welcome guide:', 'publishpress-welcome') . '

' - . '

' . esc_html__('If you opt in on the welcome guide, we receive your email address, your answer to "what best describes you?" and an identifier for the consent statement shown to you. PublishPress records the receive time and asks you to confirm your email before sending tips and offers. Nothing is sent unless you choose Subscribe with the consent box ticked. The guide also stores your progress and your choice on this site; both are included in the personal data export and removed by the erasure tool, and you can unsubscribe at any time.', 'publishpress-welcome') . '

'; - - wp_add_privacy_policy_content(__('PublishPress welcome guide', 'publishpress-welcome'), $content); - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/RenderContext.php b/lib/vendor/publishpress/publishpress-welcome/src/RenderContext.php deleted file mode 100644 index 593ed8f51..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/RenderContext.php +++ /dev/null @@ -1,78 +0,0 @@ -controller = $controller; - $this->config = $config; - } - - public function config() - { - return $this->config; - } - - public function mode() - { - return $this->controller->mode(); - } - - public function emailCapture() - { - return $this->controller->emailCaptureState(); - } - - public function stepUrl($step) - { - return $this->controller->stepUrl($step); - } - - public function stepCount() - { - return $this->controller->stepCount(); - } - - public function stepUrlForId($id) - { - return $this->controller->stepUrlForId($id); - } - - public function figure(array $asset, $alt = '', $pinCount = 0, $slug = '') - { - $url = isset($asset['url']) ? $asset['url'] : ''; - $path = isset($asset['path']) ? $asset['path'] : ''; - - if ($url && $path && is_readable($path)) { - $version = md5_file($path); - - if ($version) { - $url = add_query_arg('ver', substr($version, 0, 12), $url); - } - } - - $classes = 'ppw-figure'; - if ($slug) { - $classes .= ' ppw-figure-' . sanitize_html_class($slug); - } - - printf('
', esc_attr($classes)); - - if ($url) { - printf('%s', esc_url($url), esc_attr($alt)); - } else { - printf('
%s
', esc_html($alt)); - } - - for ($index = 1; $index <= (int) $pinCount; $index++) { - printf('%1$d', (int) $index); - } - - echo '
'; - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/SharedProfile.php b/lib/vendor/publishpress/publishpress-welcome/src/SharedProfile.php deleted file mode 100644 index ff471734e..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/SharedProfile.php +++ /dev/null @@ -1,71 +0,0 @@ - 0 && ($now - $declinedAt) >= (int) $snoozeDays * 86400; - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/Stats.php b/lib/vendor/publishpress/publishpress-welcome/src/Stats.php deleted file mode 100644 index 1fe3b41bd..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/Stats.php +++ /dev/null @@ -1,91 +0,0 @@ - isset($stored[self::VIEWS]) ? (array) $stored[self::VIEWS] : [], - self::EXITS => isset($stored[self::EXITS]) ? (array) $stored[self::EXITS] : [], - self::COUNTERS => isset($stored[self::COUNTERS]) ? (array) $stored[self::COUNTERS] : [], - ]; - } - - /** - * Rows for the Site Health "Info" screen. - * - * @param string $experience - * @param string[] $stepIds Ordered step IDs, so the funnel reads in tour order. - * @return array - */ - public static function siteHealthFields($experience, array $stepIds) - { - $stats = self::get($experience); - $fields = []; - - foreach ($stepIds as $index => $stepId) { - $views = isset($stats[self::VIEWS][$stepId]) ? (int) $stats[self::VIEWS][$stepId] : 0; - $exits = isset($stats[self::EXITS][$stepId]) ? (int) $stats[self::EXITS][$stepId] : 0; - - $fields['step_' . $stepId] = [ - 'label' => sprintf( - /* translators: 1: step number, 2: step ID */ - __('Step %1$d (%2$s)', 'publishpress-welcome'), - $index + 1, - $stepId - ), - 'value' => sprintf( - /* translators: 1: number of views, 2: number of exits */ - __('%1$d views, %2$d exits', 'publishpress-welcome'), - $views, - $exits - ), - ]; - } - - foreach ([self::COMPLETED => __('Completed the guide', 'publishpress-welcome'), self::EMAIL_SUBSCRIBED => __('Subscribed by email', 'publishpress-welcome'), self::EMAIL_DECLINED => __('Declined the email step', 'publishpress-welcome')] as $counter => $label) { - $fields[$counter] = ['label' => $label, 'value' => (string) (isset($stats[self::COUNTERS][$counter]) ? (int) $stats[self::COUNTERS][$counter] : 0)]; - } - - return $fields; - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/Step/CallbackStep.php b/lib/vendor/publishpress/publishpress-welcome/src/Step/CallbackStep.php deleted file mode 100644 index ddaa8dec6..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/Step/CallbackStep.php +++ /dev/null @@ -1,36 +0,0 @@ -id = sanitize_key($id); - $this->renderer = $renderer; - $this->ownNavigation = (bool) $ownNavigation; - } - - public function getId() - { - return $this->id; - } - - public function hasOwnNavigation() - { - return $this->ownNavigation; - } - - public function render(RenderContext $context) - { - call_user_func($this->renderer, $context); - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/Step/ConceptsStep.php b/lib/vendor/publishpress/publishpress-welcome/src/Step/ConceptsStep.php deleted file mode 100644 index dd12403a1..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/Step/ConceptsStep.php +++ /dev/null @@ -1,47 +0,0 @@ -id = sanitize_key($id); - $this->data = $data; - } - - public function getId() - { - return $this->id; - } - - public function render(RenderContext $context) - { - ?> -
-

data['title']); ?>

- data['lede'])) : ?>

data['lede']); ?>

-
-
- data['concepts'] as $index => $concept) : ?> -
- - - - figure(isset($concept['asset']) ? $concept['asset'] : [], $concept['name'], 0, isset($concept['slug']) ? $concept['slug'] : ''); ?> - -

-

-
- -
- data['aside'])) : ?>

data['aside']); ?>

- id = 'email-capture'; - $this->settings = $settings; - } - - public function getId() - { - return $this->id; - } - - /** - * The form has its own submit and decline buttons. - * - * @return bool - */ - public function hasOwnNavigation() - { - return true; - } - - public function render(RenderContext $context) - { - $copy = $this->settings['copy']; - $state = $context->emailCapture(); - $recipient = isset($this->settings['recipient_name']) ? $this->settings['recipient_name'] : ''; - $hasArt = ! empty($this->settings['asset']); - ?> -
-
-
-

-

-
- -
- - - - - - - settings['personas'])) : ?> -
- -
- settings['personas'] as $key => $label) : ?> - - -
-
- - -
- - -
- - - -

- - settings['privacy_url'])) : ?> - config()->label('new_tab')); ?> - -

- -

- -
- - -
-
- settings['after_form_callback']) && is_callable($this->settings['after_form_callback'])) : ?> - settings['after_form_callback'], $context); ?> - -
- - - -
- id = sanitize_key($id); - $this->data = $data; - } - - public function getId() - { - return $this->id; - } - - public function render(RenderContext $context) - { - $columns = isset($this->data['columns']) && 2 === (int) $this->data['columns'] ? 2 : 3; - ?> -
-

data['title']); ?>

- data['lede'])) : ?>

data['lede']); ?>

-
-
    - data['features'] as $feature) : ?> -
  • - - - - - -
  • - -
- id = sanitize_key($id); - $this->data = $data; - } - - public function getId() - { - return $this->id; - } - - public function render(RenderContext $context) - { - ?> -
- -

data['title']); ?>

- data['lede'])) : ?>

data['lede']); ?>

-
- data['next_steps'])) : ?> - data['next_steps_title'])) : ?>

data['next_steps_title']); ?>

-
    - data['next_steps'] as $item) : ?> -
  1. - -
- - - data['promo'])) : ?> -
-
-

data['promo']['title']); ?>

- data['promo']['features'])) : ?> -
    data['promo']['features'] as $feature) : ?>
- -
- data['promo']['label']); ?> config()->label('new_tab')); ?> -
- - id = sanitize_key($id); - $this->data = $data; - } - - public function getId() - { - return $this->id; - } - - /** - * The hero renders its own primary call to action, so the shared footer omits "Next". - * - * @return bool - */ - public function hasOwnNavigation() - { - return true; - } - - public function render(RenderContext $context) - { - $action = isset($this->data['action']) ? $this->data['action'] : []; - $highlights = isset($this->data['highlights']) ? (array) $this->data['highlights'] : []; - $hasArt = ! empty($this->data['art_callback']) || ! empty($this->data['asset']); - ?> -
-
- data['eyebrow'])) : ?> - data['eyebrow']); ?> - -

data['title']); ?>

- data['lede'])) : ?> -

data['lede']); ?>

- - data['body'])) : ?> -

data['body']); ?>

- - -
    -
  • -
- - -
- - - -
- id = sanitize_key($id); - $this->data = $data; - } - - public function getId() - { - return $this->id; - } - - public function render(RenderContext $context) - { - $callouts = isset($this->data['callouts']) ? (array) $this->data['callouts'] : []; - ?> -
-
-

data['title']); ?>

- data['lede'])) : ?>

data['lede']); ?>

-
    -
  1. -
- data['action']['label'])) : ?> - - -
-
- figure(isset($this->data['asset']) ? $this->data['asset'] : [], isset($this->data['asset_alt']) ? $this->data['asset_alt'] : '', count($callouts), $this->id); ?> -
-
- versions[$versionString])) { - return false; - } - - $this->versions[$versionString] = $initializationCallback; - - return true; - } - - public function getVersions(): array - { - return $this->versions; - } - - public function latestVersion() - { - $keys = array_keys($this->versions); - if (empty($keys)) { - return false; - } - uasort($keys, 'version_compare'); - return end($keys); - } - - public function latestVersionCallback() - { - $latest = $this->latestVersion(); - if (empty($latest) || ! isset($this->versions[$latest])) { - return '__return_null'; - } - - return $this->versions[$latest]; - } - - /** - * @return VersionLoader - * @codeCoverageIgnore - */ - public static function getInstance(): ?VersionLoader - { - if (empty(self::$instance)) { - self::$instance = new self(); - } - - return self::$instance; - } - - /** - * @codeCoverageIgnore - */ - public static function initializeLatestVersion(): void - { - $self = self::getInstance(); - - call_user_func($self->latestVersionCallback()); - } - } -} - -if (! class_exists('PublishPress\\Welcome\\Versions', false)) { - class_alias(VersionLoader::class, 'PublishPress\\Welcome\\Versions'); -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/WelcomeController.php b/lib/vendor/publishpress/publishpress-welcome/src/WelcomeController.php deleted file mode 100644 index f432cda06..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/WelcomeController.php +++ /dev/null @@ -1,853 +0,0 @@ - */ - private static $registeredExperiences = []; - - /** @var ExperienceConfig */ - private $config; - - /** @var StepInterface[] */ - private $steps; - - /** @var ChecklistTaskInterface[] */ - private $tasks; - - /** @var array|null Resolved email step settings; null when the step is disabled. */ - private $emailSettings = null; - - /** @var array|null Last rejected email submission (error and the values to redisplay). */ - private $submission = null; - - public function __construct(ExperienceConfig $config, array $steps, array $tasks = []) - { - if (! $steps) { - throw new InvalidArgumentException('Welcome experience requires at least one step.'); - } - - foreach ($steps as $step) { - if (! $step instanceof StepInterface) { - throw new InvalidArgumentException('Every welcome step must implement StepInterface.'); - } - } - - foreach ($tasks as $task) { - if (! $task instanceof ChecklistTaskInterface) { - throw new InvalidArgumentException('Every checklist task must implement ChecklistTaskInterface.'); - } - } - - $this->config = $config; - $this->steps = array_values($steps); - $this->tasks = array_values($tasks); - } - - public function register() - { - $id = $this->config->get('id'); - - if (isset(self::$registeredExperiences[$id])) { - _doing_it_wrong(__METHOD__, esc_html(sprintf('Welcome experience "%s" is already registered.', $id)), esc_html(PUBLISHPRESS_WELCOME_VERSION)); - return false; - } - - $steps = apply_filters('publishpress_welcome_' . $id . '_steps', $this->steps); - if (empty($steps)) { - _doing_it_wrong(__METHOD__, esc_html__('A welcome experience must retain at least one step.', 'publishpress-welcome'), esc_html(PUBLISHPRESS_WELCOME_VERSION)); - return false; - } - - foreach ((array) $steps as $step) { - if (! $step instanceof StepInterface) { - _doing_it_wrong(__METHOD__, esc_html__('Filtered welcome steps must implement StepInterface.', 'publishpress-welcome'), esc_html(PUBLISHPRESS_WELCOME_VERSION)); - return false; - } - } - $this->steps = $this->withEmailCapture(array_values($steps)); - - self::$registeredExperiences[$id] = true; - - Privacy::track($id, $this->config->get('user_option_prefix')); - - add_action('admin_menu', [$this, 'registerAdminPage'], 99); - add_action('admin_init', [$this, 'handleExit'], 5); - add_action('admin_init', [$this, 'handleDismissal']); - add_action('admin_init', [$this, 'handleEmailCapture']); - add_action('admin_init', [$this, 'maybeRedirectAfterActivation'], 20); - add_action('admin_enqueue_scripts', [$this, 'enqueueAssets']); - add_filter('admin_body_class', [$this, 'filterBodyClass']); - add_filter('debug_information', [$this, 'addSiteHealthInfo']); - add_action('admin_notices', [$this, 'renderNotice']); - add_action('admin_notices', [$this, 'renderWhatsNew'], 15); - add_action('admin_notices', [$this, 'renderChecklist'], 20); - - return true; - } - - public function registerAdminPage() - { - // A null parent registers a page that WordPress can route to but never lists in the menu. - // Removing a listed submenu instead would make WordPress deny access to the page. - add_submenu_page( - $this->config->get('show_in_menu') ? $this->config->get('parent_slug') : null, - $this->config->get('page_title'), - $this->config->get('menu_title'), - $this->config->get('capability'), - $this->config->get('page_slug'), - [$this, 'render'] - ); - } - - public function enqueueAssets() - { - if (! $this->isWelcomePage() && ! $this->noticeIsDue() && ! $this->checklistIsDue() && ! $this->whatsNewIsDue()) { - return; - } - - wp_enqueue_style( - 'publishpress-welcome', - plugins_url('assets/css/welcome.css', PUBLISHPRESS_WELCOME_ROOT_FILE), - [], - PUBLISHPRESS_WELCOME_VERSION - ); - - foreach ($this->config->get('styles') as $style) { - if (empty($style['handle']) || empty($style['url'])) { - continue; - } - - wp_enqueue_style( - sanitize_key($style['handle']), - esc_url_raw($style['url']), - isset($style['dependencies']) ? (array) $style['dependencies'] : ['publishpress-welcome'], - isset($style['version']) ? $style['version'] : null - ); - } - } - - /** - * The layout in use for the current request: "screen" (inside the admin chrome) or "fullscreen". - * - * @return string - */ - public function mode() - { - $mode = ExperienceConfig::normalizeMode($this->config->get('mode')); - - if ($this->config->get('allow_mode_override') && isset($_GET['ppw_mode'])) { - $mode = ExperienceConfig::normalizeMode(sanitize_key(wp_unslash($_GET['ppw_mode']))); - } - - return $mode; - } - - public function filterBodyClass($classes) - { - if ($this->isWelcomePage()) { - $classes .= ' ppw-mode-' . $this->mode(); - $classes .= ExperienceConfig::MODE_FULLSCREEN === $this->mode() ? ' ppw-is-fullscreen' : ''; - } - - return $classes; - } - - public function render() - { - if (! current_user_can($this->config->get('capability'))) { - wp_die(esc_html__('You are not allowed to access this page.', 'publishpress-welcome')); - } - - $stepNumber = isset($_GET['ppw_step']) ? absint(wp_unslash($_GET['ppw_step'])) : 1; - $total = count($this->steps); - - if ($stepNumber < 1 || $stepNumber > $total) { - $stepNumber = 1; - } - - $this->rememberStep($stepNumber); - Stats::increment($this->config->get('id'), Stats::VIEWS, $this->steps[$stepNumber - 1]->getId()); - - $mode = $this->mode(); - $fullscreen = ExperienceConfig::MODE_FULLSCREEN === $mode; - $context = new RenderContext($this, $this->config); - $step = $this->steps[$stepNumber - 1]; - $percent = (int) round(($stepNumber / $total) * 100); - $isLast = $stepNumber === $total; - $ownNavigation = method_exists($step, 'hasOwnNavigation') && $step->hasOwnNavigation(); - ?> -
- config->label('skip_content')); ?> -
-
- renderBrand(); ?> - - - config->label('step_format')), esc_html(number_format_i18n($stepNumber)), esc_html(number_format_i18n($total))); ?> - - - config->label('exit')); ?> - - -
-
- -
-
-
- render($context); ?> -
-
- -
-
- config->get('logo'); - $name = (string) $this->config->get('brand_name'); - - echo '
'; - - if (! empty($logo['url'])) { - printf( - '', - esc_url($logo['url']), - esc_attr(isset($logo['alt']) ? $logo['alt'] : $name) - ); - } elseif (! empty($logo['dashicon'])) { - printf('', esc_attr(sanitize_html_class($logo['dashicon']))); - } - - if ($name && (empty($logo['url']) || ! empty($logo['show_name']))) { - printf('%s', esc_html($name)); - } - - echo '
'; - } - - public function stepUrl($step = 1) - { - $args = ['page' => $this->config->get('page_slug'), 'ppw_step' => (int) $step]; - - if ($this->config->get('allow_mode_override') && isset($_GET['ppw_mode'])) { - $args['ppw_mode'] = $this->mode(); - } - - return add_query_arg($args, admin_url('admin.php')); - } - - /** - * Insert the optional email step according to the (filterable) "email_capture" settings. - * - * @param StepInterface[] $steps - * @return StepInterface[] - */ - private function withEmailCapture(array $steps) - { - $settings = apply_filters('publishpress_welcome_' . $this->config->get('id') . '_email_capture', $this->config->get('email_capture')); - $settings = ExperienceConfig::mergeEmailCapture($settings); - - if (empty($settings['enabled'])) { - $this->emailSettings = null; - return $steps; - } - - // The choice is shared by every bundled plugin, so a user who already subscribed or declined - // anywhere on this site is never asked again. Register on "init" so the current user is known. - $userId = get_current_user_id(); - if ($userId && ! SharedProfile::shouldAsk($userId, (int) $settings['decline_snooze_days'])) { - $this->emailSettings = null; - return $steps; - } - - $this->emailSettings = $settings; - $this->registerQueueSender(); - $count = count($steps); - - if ('after_first' === $settings['position']) { - $index = min(1, $count); - } elseif (is_numeric($settings['position'])) { - $index = max(0, min($count, (int) $settings['position'] - 1)); - } else { - $index = $count > 1 ? $count - 1 : $count; - } - - array_splice($steps, $index, 0, [new EmailCaptureStep($settings)]); - - return $steps; - } - - private function registerQueueSender() - { - $settings = $this->emailSettings; - - EmailQueue::registerSender( - $this->config->get('id'), - function (array $payload) use ($settings) { - return (new EmailSubscriber($settings))->send($payload); - } - ); - } - - private function emailStepNumber() - { - if (! $this->emailSettings) { - return 0; - } - - foreach ($this->steps as $index => $step) { - if ($step instanceof EmailCaptureStep) { - return $index + 1; - } - } - - return 0; - } - - /** - * State the email step needs to render: saved status, any validation error and the form values. - * - * @return array - */ - public function emailCaptureState() - { - $number = $this->emailStepNumber(); - $user = wp_get_current_user(); - $values = $this->submission ? $this->submission['values'] : [ - 'email' => $user->user_email, - 'persona' => SharedProfile::persona($user->ID), - 'consent' => ! empty($this->emailSettings['consent_prechecked']), - ]; - - return [ - 'status' => SharedProfile::status($user->ID), - 'error' => $this->submission ? $this->submission['error'] : '', - 'values' => $values, - 'action_url' => $this->stepUrl($number), - 'next_url' => $this->stepUrl(min($number + 1, count($this->steps))), - 'nonce_action' => $this->emailNonceAction(), - ]; - } - - public function handleEmailCapture() - { - if (! $this->emailSettings || ! $this->isWelcomePage()) { - return; - } - - if (! isset($_SERVER['REQUEST_METHOD']) || 'POST' !== $_SERVER['REQUEST_METHOD']) { - return; - } - - $action = isset($_POST['ppw_email_action']) ? sanitize_key(wp_unslash($_POST['ppw_email_action'])) : ''; - - if (! in_array($action, ['subscribe', 'decline'], true) || ! current_user_can($this->config->get('capability'))) { - return; - } - - check_admin_referer($this->emailNonceAction()); - - $userId = get_current_user_id(); - - // Once the choice is saved the step is no longer offered, so the following step takes its number. - $nextUrl = $this->stepUrl(min($this->emailStepNumber(), count($this->steps) - 1)); - - if ('decline' === $action) { - SharedProfile::recordDeclined($userId); - Stats::increment($this->config->get('id'), Stats::COUNTERS, Stats::EMAIL_DECLINED); - wp_safe_redirect($nextUrl); - exit; - } - - $copy = $this->emailSettings['copy']; - $email = isset($_POST['ppw_email']) ? sanitize_email(wp_unslash($_POST['ppw_email'])) : ''; - $persona = isset($_POST['ppw_persona']) ? sanitize_key(wp_unslash($_POST['ppw_persona'])) : ''; - $consent = ! empty($_POST['ppw_consent']); - - if (! array_key_exists($persona, $this->emailSettings['personas'])) { - $persona = ''; - } - - $values = ['email' => $email, 'persona' => $persona, 'consent' => $consent]; - - if (! is_email($email)) { - $this->submission = ['error' => $copy['error_email'], 'values' => $values]; - return; - } - - if (! $consent) { - $this->submission = ['error' => $copy['error_consent'], 'values' => $values]; - return; - } - - $payload = apply_filters( - 'publishpress_welcome_' . $this->config->get('id') . '_subscribe_payload', - [ - 'plugin' => $this->config->get('id'), - 'email' => $email, - 'persona' => $persona, - 'consent' => true, - 'consent_statement' => $this->emailSettings['consent_statement'], - 'locale' => get_user_locale(), - ], - $this->emailSettings - ); - - $result = (new EmailSubscriber($this->emailSettings))->send((array) $payload); - - if (true !== $result && ! EmailQueue::retryable($result)) { - $this->submission = ['error' => $copy['error_delivery'], 'values' => $values]; - return; - } - - // The user has consented, so a failed delivery is queued and retried in the background rather than - // shown as an error they cannot fix. - if (true !== $result && EmailQueue::retryable($result)) { - EmailQueue::enqueue($this->config->get('id'), (array) $payload, null, $result); - } - - // The persona is only stored locally after the user has consented. - SharedProfile::recordSubscribed($userId, $persona); - Stats::increment($this->config->get('id'), Stats::COUNTERS, Stats::EMAIL_SUBSCRIBED); - - wp_safe_redirect($nextUrl); - exit; - } - - private function emailNonceAction() - { - return 'publishpress_welcome_email_' . $this->config->get('id'); - } - - public function stepCount() - { - return count($this->steps); - } - - /** Resolve after filters and optional steps have been applied. */ - public function stepUrlForId($id) - { - foreach ($this->steps as $index => $step) { - if ($step->getId() === $id) { - return $this->stepUrl($index + 1); - } - } - - return $this->stepUrl(1); - } - - public function maybeRedirectAfterActivation() - { - if (! $this->isEnrolled() || ! get_option($this->config->get('redirect_option'))) { - return; - } - - $requestAction = isset($_REQUEST['action']) ? sanitize_key(wp_unslash($_REQUEST['action'])) : ''; - - if (wp_doing_ajax()) { - return; - } - - if (is_network_admin() || 'activate-multi' === $requestAction) { - delete_option($this->config->get('redirect_option')); - return; - } - - if (! current_user_can($this->config->get('capability'))) { - return; - } - - delete_option($this->config->get('redirect_option')); - - if (! $this->isWelcomePage()) { - wp_safe_redirect($this->stepUrl(1)); - exit; - } - } - - /** - * Link that records where the user left the guide before sending them to the exit URL. - * - * @param int $fromStep - * @return string - */ - private function exitUrl($fromStep) - { - return add_query_arg( - ['page' => $this->config->get('page_slug'), 'ppw_exit' => (int) $fromStep], - admin_url('admin.php') - ); - } - - public function handleExit() - { - if (! $this->isWelcomePage() || ! isset($_GET['ppw_exit']) || ! current_user_can($this->config->get('capability'))) { - return; - } - - $number = absint(wp_unslash($_GET['ppw_exit'])); - - if ($number >= 1 && $number <= count($this->steps)) { - Stats::increment($this->config->get('id'), Stats::EXITS, $this->steps[$number - 1]->getId()); - } - - wp_safe_redirect($this->config->get('exit_url')); - exit; - } - - /** - * Show the local funnel under Site Health > Info. Nothing is transmitted. - */ - public function addSiteHealthInfo($info) - { - $stepIds = array_map(function (StepInterface $step) { - return $step->getId(); - }, $this->steps); - - $info['publishpress-welcome-' . $this->config->get('id')] = [ - 'label' => sprintf( - /* translators: %s: experience ID */ - __('PublishPress welcome guide: %s', 'publishpress-welcome'), - $this->config->get('id') - ), - 'fields' => Stats::siteHealthFields($this->config->get('id'), $stepIds), - ]; - - return $info; - } - - public function handleDismissal() - { - $value = isset($_GET['ppw_dismiss']) ? sanitize_text_field(wp_unslash($_GET['ppw_dismiss'])) : ''; - - if (! $value || 0 !== strpos($value, $this->config->get('id') . ':')) { - return; - } - - if (! current_user_can($this->config->get('capability'))) { - return; - } - - check_admin_referer($this->dismissNonceAction()); - - $target = substr($value, strlen($this->config->get('id')) + 1); - if (in_array($target, ['notice', 'card'], true)) { - update_user_option(get_current_user_id(), $this->userOption($target . '_dismissed'), 1); - $this->markWhatsNewSeen(); - } - - if ('whats_new' === $target) { - $this->markWhatsNewSeen(); - } - - wp_safe_redirect(remove_query_arg(['ppw_dismiss', '_wpnonce'])); - exit; - } - - public function renderNotice() - { - if (! $this->noticeIsDue()) { - return; - } - - // The notice's lifetime starts the first time the user actually sees it. - if (! $this->noticeFirstSeen()) { - update_user_option(get_current_user_id(), $this->userOption('notice_first_seen'), time()); - } - - $lastStep = $this->lastStep(); - $label = $lastStep > 1 ? $this->config->label('continue') : $this->config->label('start'); - $logo = $this->config->get('logo'); - $iconUrl = (string) $this->config->get('notice_icon_url'); - ?> -
-
- -
- config->get('notice_title')); ?> - config->get('notice_body')); ?> -
- -
-
- checklistIsDue()) { - return; - } - - $tasks = apply_filters('publishpress_welcome_' . $this->config->get('id') . '_tasks', $this->tasks); - $states = []; - $done = 0; - - foreach ($tasks as $task) { - if (! $task instanceof ChecklistTaskInterface) { - continue; - } - $complete = $task->isComplete(); - $states[] = [$task, $complete]; - $done += $complete ? 1 : 0; - } - - $total = count($states); - if (! $total) { - return; - } - - $percent = (int) round(($done / $total) * 100); - $allDone = $done === $total; - ?> -
-
- -
-

config->get('checklist_complete_title') : $this->config->get('checklist_title')); ?>

- - - -
- config->label('replay')); ?> - config->label($allDone ? 'dismiss' : 'hide')); ?> -
- - - -
- whatsNewIsDue()) { - return; - } - - $data = WhatsNew::normalize($this->config->get('whats_new')); - $label = $data['url_label'] ? $data['url_label'] : $this->config->label('changelog'); - - // The highlights' lifetime starts the first time this version's card is shown. - $firstSeen = $this->whatsNewFirstSeen($data['version']); - if (! $firstSeen) { - update_user_option(get_current_user_id(), $this->userOption('whats_new_first_seen'), $data['version'] . '|' . time()); - } - ?> -
-
- -
- -
    - -
  • - - -
  • - -
-
- -
-
- config->get('whats_new')); - - if (! $data || ! current_user_can($this->config->get('capability')) || $this->isWelcomePage()) { - return false; - } - - // One notice at a time: the welcome notice takes priority for people still on the guide. - if ($this->noticeIsDue()) { - return false; - } - - if (! NoticePolicy::allowsScreen($this->currentPage(), $this->config->get('notice_pages'), (bool) $this->config->get('notice_on_dashboard'), $this->isDashboard())) { - return false; - } - - $seen = (string) get_user_option($this->userOption('whats_new_seen'), get_current_user_id()); - - return WhatsNew::isNewerThan($seen, $data['version']) - && ! NoticePolicy::isExpired($this->whatsNewFirstSeen($data['version']), (int) $this->config->get('notice_days')); - } - - /** @return int Unix time the given version's card was first shown to this user, or 0. */ - private function whatsNewFirstSeen($version) - { - $stored = (string) get_user_option($this->userOption('whats_new_first_seen'), get_current_user_id()); - $parts = explode('|', $stored); - - return (2 === count($parts) && $parts[0] === $version) ? (int) $parts[1] : 0; - } - - private function markWhatsNewSeen() - { - $data = WhatsNew::normalize($this->config->get('whats_new')); - - if ($data) { - update_user_option(get_current_user_id(), $this->userOption('whats_new_seen'), $data['version']); - } - } - - private function noticeIsDue() - { - return $this->config->get('notice_enabled') - && $this->isEnrolled() - && current_user_can($this->config->get('capability')) - && ! $this->isWelcomePage() - && NoticePolicy::allowsScreen($this->currentPage(), $this->config->get('notice_pages'), (bool) $this->config->get('notice_on_dashboard'), $this->isDashboard()) - && ! $this->isComplete() - && ! NoticePolicy::isExpired($this->noticeFirstSeen(), (int) $this->config->get('notice_days')) - && ! get_user_option($this->userOption('notice_dismissed'), get_current_user_id()); - } - - private function noticeFirstSeen() - { - return (int) get_user_option($this->userOption('notice_first_seen'), get_current_user_id()); - } - - private function isDashboard() - { - global $pagenow; - - return 'index.php' === $pagenow; - } - - private function checklistIsDue() - { - return $this->config->get('checklist_enabled') - && $this->isEnrolled() - && current_user_can($this->config->get('capability')) - && in_array($this->currentPage(), $this->config->get('checklist_pages'), true) - && ! get_user_option($this->userOption('card_dismissed'), get_current_user_id()); - } - - private function isEnrolled() - { - return (bool) get_option($this->config->get('enrollment_option')); - } - - private function rememberStep($step) - { - update_user_option(get_current_user_id(), $this->userOption('step'), (int) $step); - - if ((int) $step === count($this->steps)) { - if (! $this->isComplete()) { - Stats::increment($this->config->get('id'), Stats::COUNTERS, Stats::COMPLETED); - } - - update_user_option(get_current_user_id(), $this->userOption('complete'), 1); - - // Someone who just finished the guide has no use for the release highlights of this version. - $this->markWhatsNewSeen(); - } - } - - private function lastStep() - { - $step = (int) get_user_option($this->userOption('step'), get_current_user_id()); - return ($step >= 1 && $step <= count($this->steps)) ? $step : 0; - } - - private function isComplete() - { - return (bool) get_user_option($this->userOption('complete'), get_current_user_id()); - } - - private function currentPage() - { - return isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; - } - - private function isWelcomePage() - { - return $this->currentPage() === $this->config->get('page_slug'); - } - - private function userOption($suffix) - { - return $this->config->get('user_option_prefix') . '_' . $suffix; - } - - private function dismissNonceAction() - { - return 'publishpress_welcome_dismiss_' . $this->config->get('id'); - } - - private function dismissUrl($target) - { - return wp_nonce_url( - add_query_arg('ppw_dismiss', $this->config->get('id') . ':' . $target), - $this->dismissNonceAction() - ); - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/WhatsNew.php b/lib/vendor/publishpress/publishpress-welcome/src/WhatsNew.php deleted file mode 100644 index db5b8ec25..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/WhatsNew.php +++ /dev/null @@ -1,59 +0,0 @@ - (string) $item['title'], 'body' => isset($item['body']) ? (string) $item['body'] : '']; - } - } - - $items = array_slice($items, 0, self::MAX_ITEMS); - - if ('' === $version || ! $items) { - return null; - } - - return [ - 'version' => $version, - 'title' => (string) $data['title'], - 'items' => $items, - 'url' => isset($data['url']) ? (string) $data['url'] : '', - 'url_label' => isset($data['url_label']) ? (string) $data['url_label'] : '', - ]; - } - - /** - * @param string $seenVersion Last version the user dismissed or finished the guide at; empty when none. - * @param string $version Version the highlights describe. - * @return bool - */ - public static function isNewerThan($seenVersion, $version) - { - return version_compare($seenVersion === '' ? '0' : (string) $seenVersion, (string) $version, '<'); - } -} diff --git a/lib/vendor/publishpress/publishpress-welcome/src/autoload.php b/lib/vendor/publishpress/publishpress-welcome/src/autoload.php deleted file mode 100644 index d206c24a5..000000000 --- a/lib/vendor/publishpress/publishpress-welcome/src/autoload.php +++ /dev/null @@ -1,36 +0,0 @@ -register('0.1.0', __NAMESPACE__ . '\initialize0Dot1Dot0'); - } - } - - function initialize0Dot1Dot0() - { - require_once __DIR__ . '/autoload.php'; - - if (! defined('PUBLISHPRESS_WELCOME_VERSION')) { - define('PUBLISHPRESS_WELCOME_VERSION', '0.1.0'); - } - - do_action('publishpress_welcome_0Dot1Dot0_initialized'); - } -} diff --git a/press-permit-core.php b/press-permit-core.php index 22e8f657c..0c6426f7f 100644 --- a/press-permit-core.php +++ b/press-permit-core.php @@ -4,7 +4,7 @@ * Plugin Name: PublishPress Permissions Free * Plugin URI: https://publishpress.com/presspermit * Description: PublishPress Permissions allows you to enable or deny access to posts, pages, categories, tags and more. - * Version: 4.9.1-alpha.1 + * Version: 4.9.1 * Author: PublishPress * Author URI: https://publishpress.com/ * Text Domain: press-permit-core diff --git a/tests/unit/welcome-post-types.php b/tests/unit/welcome-post-types.php deleted file mode 100644 index b7a20981c..000000000 --- a/tests/unit/welcome-post-types.php +++ /dev/null @@ -1,94 +0,0 @@ - '1', 'page' => '0', 'attachment' => '1'], - WelcomePostTypes::applyChoices(['post' => true, 'page' => true, 'attachment' => true], $offered, [], ['post', 'attachment']) -); - -// A fresh install has no stored value, so every offered type is written explicitly. -check( - 'fresh install', - ['post' => '1', 'page' => '1', 'attachment' => '0'], - WelcomePostTypes::applyChoices([], $offered, [], ['post', 'page']) -); - -// Types the guide did not offer keep whatever was stored, including custom types that are no longer registered. -check( - 'unoffered types are untouched', - ['post' => '1', 'page' => '0', 'attachment' => '0', 'product' => '1', 'legacy_type' => true], - WelcomePostTypes::applyChoices(['product' => '1', 'legacy_type' => true], $offered, [], ['post']) -); - -// A forged field name can neither create an entry nor switch on a type that was never offered. -check( - 'unknown names are ignored', - ['post' => '1', 'page' => '0', 'attachment' => '0'], - WelcomePostTypes::applyChoices([], $offered, [], ['post', 'wp_not_offered', '../../etc']) -); - -check( - 'a type the guide did not offer stays off', - ['shop_order' => '0', 'post' => '0', 'page' => '0', 'attachment' => '0'], - WelcomePostTypes::applyChoices(['shop_order' => '0'], $offered, [], ['shop_order']) -); - -// Locked types stay on even when the checkbox was disabled and therefore not submitted. -check( - 'locked types stay on', - ['post' => '0', 'page' => '1', 'attachment' => '0'], - WelcomePostTypes::applyChoices([], $offered, ['page'], []) -); - -// Clearing every box turns every offered type off, matching the Settings screen. -check( - 'nothing selected', - ['post' => '0', 'page' => '0', 'attachment' => '0'], - WelcomePostTypes::applyChoices(['post' => '1', 'page' => '1', 'attachment' => '1'], $offered, [], []) -); - -// Applying the same submission twice gives the same result. -$once = WelcomePostTypes::applyChoices(['post' => true], $offered, [], ['page']); -check('idempotent', $once, WelcomePostTypes::applyChoices($once, $offered, [], ['page'])); - -if ($failures) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Standalone CLI test output, not a filesystem write. - fwrite(STDERR, "FAILED\n" . implode("\n", $failures) . "\n"); - exit(1); -} - -echo "OK ($assertions welcome post type assertions)\n"; diff --git a/tests/unit/welcome-save.php b/tests/unit/welcome-save.php deleted file mode 100644 index 33b6aa3ba..000000000 --- a/tests/unit/welcome-save.php +++ /dev/null @@ -1,213 +0,0 @@ - '1', 'page' => '1', 'attachment' => '1', 'legacy_type' => '1']; - public $writes = 0; - - public function getOption($name) - { - if ('enabled_post_types' !== $name) { - throw new RuntimeException('Unexpected option read: ' . $name); - } - - return $this->options; - } - - public function updateOption($name, $value) - { - if ('enabled_post_types' !== $name) { - throw new RuntimeException('Unexpected option write: ' . $name); - } - - $this->options = $value; - $this->writes++; - } -} - -function presspermit() -{ - global $permissions; - return $permissions; -} - -function current_user_can($capability) -{ - global $canManageSettings; - return 'pp_manage_settings' === $capability && $canManageSettings; -} - -function check_admin_referer($action) -{ - global $validNonce; - - if (WelcomeExperience::POST_TYPES_NONCE !== $action || ! $validNonce) { - throw new WelcomeSaveRequestStopped('nonce', 403); - } -} - -function wp_die($message, $title, $args) -{ - throw new WelcomeSaveRequestStopped($message, $args['response']); -} - -function esc_html__($message, $domain) -{ - return $message; -} - -function wp_unslash($value) -{ - return $value; -} - -function is_multisite() -{ - global $networkOwned; - return $networkOwned; -} - -function apply_filters($hook, $value) -{ - global $lockedTypes; - return 'presspermit_locked_post_types' === $hook ? $lockedTypes : $value; -} - -function get_post_types($args, $output) -{ - return [ - 'post' => (object) ['name' => 'post'], - 'page' => (object) ['name' => 'page'], - 'attachment' => (object) ['name' => 'attachment'], - ]; -} - -function sanitize_key($value) -{ - return strtolower(preg_replace('/[^a-zA-Z0-9_\-]/', '', $value)); -} - -function wp_safe_redirect($url) -{ - if ('/welcome/teaser' !== $url) { - throw new RuntimeException('Unexpected redirect: ' . $url); - } - - // Stop before the production handler's exit, so each case can inspect the saved state. - throw new WelcomeSaveRequestStopped('redirect', 302); -} - -$reflection = new ReflectionClass(WelcomeExperience::class); -$experience = $reflection->newInstanceWithoutConstructor(); -$controller = $reflection->getProperty('controller'); -$controller->setAccessible(true); -$controller->setValue($experience, new class { - public function stepUrlForId($id) - { - return '/welcome/' . $id; - } -}); - -$failures = []; -$assertions = 0; - -function check($label, $expected, $actual) -{ - global $failures, $assertions; - $assertions++; - - if ($expected !== $actual) { - $failures[] = $label . ': expected ' . json_encode($expected) . ', got ' . json_encode($actual); - } -} - -$unchanged = ['post' => '1', 'page' => '1', 'attachment' => '1', 'legacy_type' => '1']; -$cleared = ['post' => '0', 'page' => '0', 'attachment' => '0', 'legacy_type' => '1']; -$cases = [ - 'GET with valid nonce' => ['method' => 'GET', 'status' => 405], - 'missing request method' => ['method' => null, 'status' => 405], - 'invalid nonce' => ['nonce' => false, 'status' => 403], - 'insufficient capability' => ['allowed' => false, 'status' => 403], - 'network-owned setting' => ['network' => true, 'status' => 302], - 'intentional empty POST' => ['writes' => 1, 'saved' => $cleared], - 'selected and forged types' => [ - 'posted' => ['page' => '1', 'unknown_type' => '1'], - 'writes' => 1, - 'saved' => array_merge($cleared, ['page' => '1']), - ], - 'disabled locked checkbox omitted from POST' => [ - 'locked' => ['page' => 'page'], - 'writes' => 1, - 'saved' => array_merge($cleared, ['page' => '1']), - ], - 'unchanged submission avoids a write' => [ - 'posted' => ['post' => '1', 'page' => '1', 'attachment' => '1'], - ], -]; - -foreach ($cases as $label => $case) { - $case = array_merge([ - 'method' => 'POST', - 'nonce' => true, - 'allowed' => true, - 'network' => false, - 'locked' => [], - 'posted' => [], - 'status' => 302, - 'writes' => 0, - 'saved' => $unchanged, - ], $case); - - $permissions = new WelcomeSavePermissions(); - $validNonce = $case['nonce']; - $canManageSettings = $case['allowed']; - $networkOwned = $case['network']; - $lockedTypes = $case['locked']; - if ($networkOwned) { - $permissions->netwide_options = ['enabled_post_types']; - } - - unset($_SERVER['REQUEST_METHOD']); - if (null !== $case['method']) { - $_SERVER['REQUEST_METHOD'] = $case['method']; - } - $_POST = $case['posted'] ? ['ppw_post_types' => $case['posted']] : []; - - $status = 0; - try { - $experience->savePostTypes(); - } catch (WelcomeSaveRequestStopped $stop) { - $status = $stop->getCode(); - } - - check($label . ' response', $case['status'], $status); - check($label . ' writes', $case['writes'], $permissions->writes); - check($label . ' saved options', $case['saved'], $permissions->options); -} - -if ($failures) { - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- Standalone CLI test output, not a filesystem write. - fwrite(STDERR, "FAILED\n" . implode("\n", $failures) . "\n"); - exit(1); -} - -echo "OK ($assertions welcome save handler assertions)\n";