get_blog_prefix() . $name; } public static function suggestions_hidden() { return (int) get_user_meta(get_current_user_id(), self::preference_key(), true) === 1; } public static function menu() { add_submenu_page('customfields', __('Explore CustomFields.com Pro', 'customfields-com'), __('Explore Pro', 'customfields-com'), 'manage_options', 'customfields-pro-preview', array(__CLASS__, 'page')); } public static function assets() { if (!current_user_can('manage_options') || !in_array(self::current_page(), array('customfields', 'customfields-start', 'customfields-csv-check', 'customfields-health', 'customfields-pro-preview', 'customfields-pro-license', 'customfields-publishing', 'customfields-welcome'), true)) { return; } wp_enqueue_style('customfields-pro-discovery', CUSTOMFIELDS_URL . 'assets/pro-discovery.css', array('customfields-admin'), CUSTOMFIELDS_VERSION); wp_enqueue_script('customfields-pro-discovery', CUSTOMFIELDS_URL . 'assets/pro-discovery.js', array(), CUSTOMFIELDS_VERSION, true); wp_localize_script('customfields-pro-discovery', 'CustomFieldsProDiscovery', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'failure' => __('The preference could not be saved. Please try again.', 'customfields-com'), 'mappingInitial' => __('Example mapping: ready to save', 'customfields-com'), 'mappingSaved' => __('Example mapping: saved for both uploads', 'customfields-com'), 'firstUpload' => __('First fictional CSV: workshop capacities', 'customfields-com'), 'nextUpload' => __('Next fictional CSV: same columns, new values', 'customfields-com'), 'initial' => __('Start by saving this example mapping. Nothing is uploaded or saved to WordPress.', 'customfields-com'), 'review' => __('Example mapping saved in this page only. Review 12 → 16 and 8 → 10, then apply these fictional changes.', 'customfields-com'), 'applied' => __('First example update applied. Try the next fictional CSV with the same mapping, or undo this example update. Your WordPress content is unchanged.', 'customfields-com'), 'nextReview' => __('The same example mapping is reused. This new input needs a fresh review: 16 → 18 and 10 → 12. It is not applied automatically.', 'customfields-com'), 'nextApplied' => __('Second example update applied after a new review. Only the sample numbers on this page changed; no file, content or schedule was saved.', 'customfields-com'), 'undone' => __('This example update was restored to its before values. Real Pro undo checks for later edits and stops on a conflict; it does not promise unconditional rollback.', 'customfields-com'), 'submission' => __('Example approved as a draft. In Pro, a permitted reviewer can open the new WordPress draft before publication.', 'customfields-com'), 'reset' => __('Example reset. No submission or content was saved.', 'customfields-com'), )); } public static function save_preference($choice) { if (!current_user_can('manage_options')) { return new \WP_Error('forbidden', __('You cannot change this preference.', 'customfields-com')); } if (!is_string($choice) || !in_array($choice, array('hide', 'show'), true)) { return new \WP_Error('invalid', __('Choose whether to hide or show Pro suggestions.', 'customfields-com')); } if ('hide' === $choice) { update_user_option(get_current_user_id(), self::PREFERENCE, 1, false); } else { delete_user_option(get_current_user_id(), self::PREFERENCE, false); } wp_cache_delete(get_current_user_id(), 'user_meta'); $stored = get_user_meta(get_current_user_id(), self::preference_key(), false); if (('hide' === $choice && (count($stored) !== 1 || (int) $stored[0] !== 1)) || ('show' === $choice && $stored)) { return new \WP_Error('storage', __('The preference could not be saved. Please try again.', 'customfields-com')); } return true; } public static function handle() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You cannot change this preference.', 'customfields-com'), '', array('response' => 403)); } $nonce = isset($_POST['_wpnonce']) && is_string($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; if (!wp_verify_nonce($nonce, 'customfields_pro_suggestions')) { wp_die(esc_html__('Your session expired. Reload the Pro guide.', 'customfields-com'), '', array('response' => 403)); } $result = self::save_preference(isset($_POST['choice']) && is_string($_POST['choice']) ? sanitize_text_field(wp_unslash($_POST['choice'])) : null); if (is_wp_error($result)) { wp_die(esc_html($result->get_error_message()), '', array('response' => 400)); } wp_safe_redirect(admin_url('admin.php?page=customfields-pro-preview&preference=saved')); exit; } public static function ajax() { if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => __('You cannot change this preference.', 'customfields-com')), 403); } $nonce = isset($_POST['_wpnonce']) && is_string($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; if (!wp_verify_nonce($nonce, 'customfields_pro_suggestions')) { wp_send_json_error(array('message' => __('Your session expired. Reload the Pro guide.', 'customfields-com')), 403); } $result = self::save_preference(isset($_POST['choice']) && is_string($_POST['choice']) ? sanitize_text_field(wp_unslash($_POST['choice'])) : null); if (is_wp_error($result)) { wp_send_json_error(array('message' => $result->get_error_message()), 400); } wp_send_json_success(); } /** One offer based on a completed local task or an explicit compatible goal. */ public static function contextual_offer() { if (!current_user_can('manage_options') || defined('CUSTOMFIELDS_PRO_VERSION') || self::suggestions_hidden() || !empty($_GET['edit'])) { return null; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing, filtering or pagination; no mutation is authorized by this query parameter. Object access remains checked by the renderer. $page = self::current_page(); if (!in_array($page, array('customfields', 'customfields-start', 'customfields-csv-check', 'customfields-health'), true)) { return null; } $summaries = Outcomes::summaries(); $csv = $summaries['csv'] ?? array(); if (empty(self::dismissed()['csv']) && in_array($page, array('customfields', 'customfields-csv-check'), true) && empty($csv['stale']) && ($csv['state'] ?? '') !== 'unavailable' && !empty($csv['eligible_tasks']) && (int) ($csv['changes'] ?? 0) > 0) { return array(sprintf(/* translators: %d: Proposed change count. */ _n('%d proposed field change was found in your saved CSV checks.', '%d proposed field changes were found in your saved CSV checks.', (int) $csv['changes'], 'customfields-com'), (int) $csv['changes']), __('Saved checks can overlap or become stale. Refresh a compatible comparison to see its current scope. Every difference and manual edit remains available in Free; Pro adds saved mappings and separately approved updates.', 'customfields-com'), 'csv', __('See Pro CSV updates', 'customfields-com')); } $health = $summaries['health'] ?? array(); if (empty(self::dismissed()['maintenance']) && in_array($page, array('customfields', 'customfields-health'), true) && empty($health['stale']) && ($health['state'] ?? '') === 'complete' && (int) ($health['findings'] ?? 0) > 0 && (int) ($health['checked'] ?? 0) > 0 && empty($health['coverage']['unsupported']) && empty($health['coverage']['excluded']) && empty($health['coverage']['more'])) { return array(sprintf(/* translators: %d: Finding count. */ __('%d findings are available in your saved content check.', 'customfields-com'), (int) $health['findings']), __('Keep every finding and manual fix in Free. Pro can save supported business rules and repeat checks locally; review the supported correction scope before planning a batch update.', 'customfields-com'), 'maintenance', __('See Pro maintenance', 'customfields-com')); } $pref = Outcomes::preferences(); if (empty(self::dismissed()['submissions']) && $page === 'customfields-start' && $pref['goal'] === 'submissions' && $pref['group'] && class_exists(SubmissionEligibility::class)) { $group = Outcomes::group($pref['group']); if ($group) { $report = SubmissionEligibility::report($group, $group['post_types'][0] ?? 'post'); if (!empty($report['eligible'])) { return array(__('This group supports the current submission field scope.', 'customfields-com'), __('Pro member forms remain available. Pro adds a private intake queue and assigned reviews for supported new entries or changes.', 'customfields-com'), 'submissions', __('See Pro submission reviews', 'customfields-com')); } } } return null; } public static function prompt() { $prompt = self::contextual_offer(); if (!$prompt || !empty(self::dismissed()[$prompt[2]])) { return; } $inspection = isset($_GET['task']) && is_string($_GET['task']) ? sanitize_text_field(wp_unslash($_GET['task'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only context checked against the current user by preview_url. $url = self::preview_url($prompt[2], $inspection); echo ''; } /** Fixed local destinations. No input becomes a URL or an operational command. */ public static function workflows() { return array( 'csv' => array('title' => __('Update existing content from your next spreadsheet', 'customfields-com'), 'name' => __('CSV workflows', 'customfields-com'), 'page' => 'customfields-csv', 'benefit' => __('Reuse your column mapping, review what changed and approve the next update.', 'customfields-com'), 'steps' => array(__('Choose a saved mapping or continue a compatible Free comparison.', 'customfields-com'), __('Prepare a fresh review of every supported row and resolve any issues.', 'customfields-com'), __('Approve the update separately; keep its review and guarded undo record.', 'customfields-com')), 'free' => __('Free compares your CSV and keeps every difference and manual edit available.', 'customfields-com'), 'limits' => __('Updates target existing posts with 1–10 supported scalar or choice fields. Every mapped column must occur exactly once. Background work follows explicit approval; conflicts stop affected work and undo requires unchanged values and retained records.', 'customfields-com')), 'submissions' => array('title' => __('Review contributions before they become content', 'customfields-com'), 'name' => __('Submission forms', 'customfields-com'), 'page' => 'customfields-submissions', 'benefit' => __('Collect supported entries and attachments in a private queue, assign a reviewer and approve new drafts.', 'customfields-com'), 'steps' => array(__('Choose the supported fields and who may submit.', 'customfields-com'), __('Review the private entry and its attachments with an assigned reviewer.', 'customfields-com'), __('Approve a draft, then make a separate publishing decision in WordPress.', 'customfields-com')), 'free' => __('Free keeps your field builder and WordPress content editing with existing permissions.', 'customfields-com'), 'limits' => __('New-entry and change-request forms have separate scopes. Supported uploads are JPEG, PNG, WebP and PDF: up to four files, 2 MiB each and 4 MiB total. Approved files have public Media Library URLs even when the post remains a draft. Visibility and relationship restrictions still apply.', 'customfields-com')), 'maintenance' => array('title' => __('Turn a useful content check into a routine', 'customfields-com'), 'name' => __('Maintenance', 'customfields-com'), 'page' => 'customfields-maintenance', 'benefit' => __('Save supported rules, repeat checks locally and keep report history for your team.', 'customfields-com'), 'steps' => array(__('Choose the content scope and supported rules.', 'customfields-com'), __('Run the checks and inspect their coverage and findings.', 'customfields-com'), __('Repeat the saved check; review any proposed correction separately.', 'customfields-com')), 'free' => __('Free keeps your saved content checks, findings and manual fixes.', 'customfields-com'), 'limits' => __('Scheduled checks depend on WordPress Cron or a configured runner. Reports identify unsupported checks and omitted detail. Checks never approve corrections. External templates and unsupported storage are outside coverage.', 'customfields-com')), 'bulk' => array('title' => __('Review a field change across several records', 'customfields-com'), 'name' => __('Bulk editor', 'customfields-com'), 'page' => 'customfields-bulk', 'benefit' => __('Edit supported values together and compare before and after values before applying them.', 'customfields-com'), 'steps' => array(__('Choose an editable field and the records in scope.', 'customfields-com'), __('Prepare values and inspect the before-and-after review.', 'customfields-com'), __('Approve the reviewed change and retain the guarded undo record.', 'customfields-com')), 'free' => __('Free keeps individual content editing, history and supported migrations.', 'customfields-com'), 'limits' => __('Individual-value editing supports 1–50 posts. Same-value jobs capture up to 2,000 posts and run in 25-post chunks. A conflict stops further chunks; earlier changes remain. Undo stops on later edits and needs retained records. Reciprocal relationships use the normal editor.', 'customfields-com')), 'deploy' => array('title' => __('Bring your field setup to another site', 'customfields-com'), 'name' => __('Move field setup', 'customfields-com'), 'page' => 'customfields-agency', 'benefit' => __('Package selected definitions and inspect the destination before adopting them.', 'customfields-com'), 'steps' => array(__('Choose field groups and their supported content definitions.', 'customfields-com'), __('Open the package at the destination and review differences and dependencies.', 'customfields-com'), __('Confirm the reviewed setup; move content separately when needed.', 'customfields-com')), 'free' => __('Free keeps local definition files and supported field migrations.', 'customfields-com'), 'limits' => __('Packages carry selected definitions, with destination and dependency review. Content and media use a separate package. This is not a full-site backup or a remote deployment service.', 'customfields-com')), 'transfer' => array('title' => __('Move selected content with its local files', 'customfields-com'), 'name' => __('Content & media', 'customfields-com'), 'page' => 'customfields-transfer', 'benefit' => __('Review a local package and create new draft copies with explicit destination mappings.', 'customfields-com'), 'steps' => array(__('Export selected posts and supported local media.', 'customfields-com'), __('Review the destination and match users, terms and field definitions.', 'customfields-com'), __('Approve new draft copies, then review them before publishing.', 'customfields-com')), 'free' => __('Free keeps field definitions, content editing and supported local migrations.', 'customfields-com'), 'limits' => __('Packages support up to 50 posts, 100 attachments, 8 MB per file and 32 MB of media. Dynamic blocks, shortcodes and reciprocal relationships require an adapter. No remote files are downloaded. Guarded undo stops on later changes or shared use; this is not a full-site backup.', 'customfields-com')), 'listing-search' => array('title' => __('Build a directory and help visitors find an item', 'customfields-com'), 'name' => __('Directory & listing builder', 'customfields-com'), 'page' => 'customfields-publishing', 'benefit' => __('Create and configure public listings with layouts, queries, sorting and reader search.', 'customfields-com'), 'steps' => array(__('Create a listing with its allowed content and display.', 'customfields-com'), __('Enable the Pro reader search box and save the listing.', 'customfields-com'), __('Preview the listing and try a visitor search.', 'customfields-com')), 'free' => __('Free creates fields, repeaters and basic display; existing saved listings keep rendering. The visual listing builder and configurable listing layouts and queries require Pro.', 'customfields-com'), 'limits' => __('This is a listing text search box, not custom-field faceted filtering. Only published, non-password-protected content in the listing scope is returned.', 'customfields-com')), 'member-forms' => array('title' => __('Let permitted members edit from a site page', 'customfields-com'), 'name' => __('Member forms', 'customfields-com'), 'page' => 'customfields-start', 'benefit' => __('Use supported fields in a frontend form for signed-in users with the required WordPress permissions.', 'customfields-com'), 'steps' => array(__('Choose a registered post field group.', 'customfields-com'), __('Place the supported form shortcode on a page.', 'customfields-com'), __('Test creating a draft or editing with an appropriately permitted member.', 'customfields-com')), 'free' => __('Free keeps the field builder and editing through WordPress.', 'customfields-com'), 'limits' => __('Members still need permission for the requested record or draft. Object-specific rules may exclude creation. Use the separate submission workflow when you need private intake and reviewer approval.', 'customfields-com')), ); } public static function preview_tabs_hidden() { return (int) get_user_meta(get_current_user_id(), self::preference_key(self::NAVIGATION), true) === 1; } private static function dismissed() { $raw = get_user_meta(get_current_user_id(), self::preference_key(self::DISMISSED), true); return is_array($raw) ? array_intersect_key($raw, self::workflows()) : array(); } public static function preview_url($workflow = '', $inspection = '') { $args = array('page' => 'customfields-pro-preview'); if (isset(self::workflows()[$workflow])) { $args['feature'] = $workflow; } if ($workflow === 'csv' && self::inspection($inspection)) { $args['inspection'] = $inspection; } return add_query_arg($args, admin_url('admin.php')); } /** Only a currently accessible local CSV task may be carried across activation. */ private static function inspection($id) { if (!is_string($id) || !preg_match('/^[a-f0-9-]{36}$/D', $id) || !class_exists(CsvInspector::class)) { return false; } return !is_wp_error(CsvInspector::get_task($id)); } public static function intent() { if (!current_user_can('manage_options')) { return null; } $raw = get_user_meta(get_current_user_id(), self::preference_key(self::INTENT), true); if (!is_array($raw) || !is_string($raw['workflow'] ?? null) || !isset(self::workflows()[$raw['workflow']]) || !is_int($raw['created'] ?? null) || !is_int($raw['expires'] ?? null) || $raw['created'] > time() || $raw['expires'] !== $raw['created'] + 7 * DAY_IN_SECONDS || $raw['expires'] <= time()) { return null; } return array('workflow' => $raw['workflow'], 'created' => $raw['created'], 'expires' => $raw['expires'], 'inspection' => $raw['workflow'] === 'csv' && self::inspection($raw['inspection'] ?? '') ? $raw['inspection'] : ''); } /** Local preferences only. A chosen task never authorizes a content operation. */ public static function change($operation, $workflow = '', $inspection = '') { if (!current_user_can('manage_options')) { return new \WP_Error('forbidden', __('Administrator access is required.', 'customfields-com')); } if (!is_string($operation) || !is_string($workflow) || !is_string($inspection)) { return new \WP_Error('invalid', __('Choose a supported preference.', 'customfields-com')); } if (in_array($operation, array('intent', 'dismiss'), true) && !isset(self::workflows()[$workflow])) { return new \WP_Error('invalid', __('Choose a supported workflow.', 'customfields-com')); } $previous = get_user_meta(get_current_user_id(), self::preference_key(self::INTENT), true); if ($operation === 'intent') { if ($inspection !== '' && ($workflow !== 'csv' || !self::inspection($inspection))) { return new \WP_Error('invalid', __('That saved comparison is no longer available. Open your CSV checks to choose another.', 'customfields-com')); } $now = time(); $value = array('workflow' => $workflow, 'created' => $now, 'expires' => $now + 7 * DAY_IN_SECONDS, 'inspection' => $inspection); $key = self::INTENT; } elseif ($operation === 'clear') { $key = self::INTENT; $value = null; } elseif ($operation === 'hide-tabs' || $operation === 'show-tabs') { $key = self::NAVIGATION; $value = $operation === 'hide-tabs' ? 1 : null; } elseif ($operation === 'dismiss') { $key = self::DISMISSED; $value = self::dismissed(); $value[$workflow] = true; } elseif ($operation === 'restore') { $key = self::DISMISSED; $value = null; } else { return new \WP_Error('invalid', __('Choose a supported preference.', 'customfields-com')); } if ($value === null) { delete_user_option(get_current_user_id(), $key, false); } else { update_user_option(get_current_user_id(), $key, $value, false); } wp_cache_delete(get_current_user_id(), 'user_meta'); $stored = get_user_meta(get_current_user_id(), self::preference_key($key), false); $matches = count($stored) === 1 && (is_int($value) ? (string) $stored[0] === (string) $value : $stored[0] === $value); if (($value === null && $stored) || ($value !== null && !$matches)) { return new \WP_Error('storage', __('Your choice could not be saved. Please try again.', 'customfields-com')); } if (in_array($operation, array('intent', 'clear'), true) && is_array($previous) && is_int($previous['created'] ?? null)) { wp_clear_scheduled_hook('customfields_pro_intent_expire', array(get_current_user_id(), $previous['created'])); } if ($operation === 'intent') { wp_schedule_single_event($value['expires'], 'customfields_pro_intent_expire', array(get_current_user_id(), $value['created'])); } return true; } public static function expire_intent($user, $created) { $raw = get_user_meta((int) $user, self::preference_key(self::INTENT), true); if (!is_array($raw) || ($raw['created'] ?? null) !== (int) $created || (int) ($raw['expires'] ?? 0) > time()) { return; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Exact-value deletion fences an expired cleanup against a newer task saved after the read. Only this user's local, blog-scoped intent is addressed. $wpdb->query($wpdb->prepare('DELETE FROM %i WHERE user_id = %d AND meta_key = %s AND BINARY meta_value = BINARY %s', $wpdb->usermeta, (int) $user, self::preference_key(self::INTENT), maybe_serialize($raw))); wp_cache_delete((int) $user, 'user_meta'); } public static function resume_url() { $intent = self::intent(); if (!$intent) { return self::preview_url(); } if (!Workspace::pro_available()) { return defined('CUSTOMFIELDS_PRO_VERSION') ? admin_url('admin.php?page=customfields-pro-license') : self::preview_url($intent['workflow'], $intent['inspection']); } if ($intent['workflow'] === 'csv' && $intent['inspection']) { return CsvInspector::url($intent['inspection']); } $args = array('page' => self::workflows()[$intent['workflow']]['page']); if ($intent['workflow'] === 'listing-search') { $args['tab'] = 'listings'; } return add_query_arg($args, admin_url('admin.php')) . ($intent['workflow'] === 'member-forms' ? '#cf-member-form-setup' : ''); } public static function allow_pricing_host($hosts) { $hosts[] = 'customfields.com'; return array_unique($hosts); } public static function act() { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Exact HTTP method comparison; only POST can change local choices. if ((isset($_SERVER['REQUEST_METHOD']) && is_string($_SERVER['REQUEST_METHOD']) ? wp_unslash($_SERVER['REQUEST_METHOD']) : '') !== 'POST') { wp_die(esc_html__('Use the form to save your choice.', 'customfields-com'), '', array('response' => 403)); } if (!current_user_can('manage_options')) { wp_die(esc_html__('Administrator access is required.', 'customfields-com'), '', array('response' => 403)); } $nonce = isset($_POST['_wpnonce']) && is_string($_POST['_wpnonce']) ? sanitize_text_field(wp_unslash($_POST['_wpnonce'])) : ''; if (!wp_verify_nonce($nonce, 'customfields_pro_discovery')) { wp_die(esc_html__('Your session expired. Reload the page.', 'customfields-com'), '', array('response' => 403)); } $operation = isset($_POST['operation']) && is_string($_POST['operation']) ? sanitize_key(wp_unslash($_POST['operation'])) : ''; $workflow = isset($_POST['workflow']) && is_string($_POST['workflow']) ? sanitize_key(wp_unslash($_POST['workflow'])) : ''; $inspection = isset($_POST['inspection']) && is_string($_POST['inspection']) ? sanitize_text_field(wp_unslash($_POST['inspection'])) : ''; $next = isset($_POST['next']) && is_string($_POST['next']) ? sanitize_key(wp_unslash($_POST['next'])) : ''; if (!in_array($next, array('', 'save', 'plans', 'activate', 'continue'), true)) { wp_die(esc_html__('Choose a supported next step.', 'customfields-com'), '', array('response' => 400)); } $result = self::change($operation, $workflow, $inspection); if (is_wp_error($result)) { wp_die(esc_html($result->get_error_message()), '', array('response' => 400)); } $url = self::preview_url($workflow, $inspection); $return_page = isset($_POST['return_page']) && is_string($_POST['return_page']) ? sanitize_key(wp_unslash($_POST['return_page'])) : ''; if ($operation !== 'intent' && isset(Workspace::routes()[$return_page])) { $url = admin_url('admin.php?page=' . $return_page); } if ($operation === 'dismiss' && $return_page === 'customfields-csv-check' && self::inspection($inspection)) { $url = CsvInspector::url($inspection); } if ($operation === 'intent' && in_array($next, array('plans', 'activate', 'continue'), true)) { if (Workspace::pro_available() || defined('CUSTOMFIELDS_PRO_VERSION')) { $url = self::resume_url(); } else { add_filter('allowed_redirect_hosts', array(__CLASS__, 'allow_pricing_host')); $url = 'https://customfields.com/pricing/'; } } wp_safe_redirect($url); exit; } private static function form($operation, $workflow = '', $inspection = '', $new_window = false) { echo '
'; echo ''; wp_nonce_field('customfields_pro_discovery'); echo ''; } private static function next_action($workflow, $inspection = '') { $active = Workspace::pro_available(); $installed = defined('CUSTOMFIELDS_PRO_VERSION'); self::form('intent', $workflow, $inspection, !$active && !$installed); echo '
'; } public static function resume_panel() { $intent = self::intent(); if (!$intent) { return; } $workflow = self::workflows()[$intent['workflow']]; echo '

' . esc_html__('YOUR CHOSEN WORKFLOW', 'customfields-com') . '

' . esc_html($workflow['title']) . '

' . esc_html__('Saved on this site for seven days. Current data and permissions will be checked again; choosing a task approves no changes or background work.', 'customfields-com') . '

'; self::next_action($intent['workflow'], $intent['inspection']); self::form('clear'); echo '
'; } private static function preferences() { echo '
' . esc_html__('Preview and suggestion preferences', 'customfields-com') . '

' . esc_html__('These choices apply only to your WordPress account on this site. Explore Pro stays available.', 'customfields-com') . '

'; self::form(self::preview_tabs_hidden() ? 'show-tabs' : 'hide-tabs'); echo ''; self::form('restore'); echo ''; echo '
'; wp_nonce_field('customfields_pro_suggestions'); echo '
'; } public static function page() { if (!current_user_can('manage_options')) { return; } $feature = self::requested_feature(); $workflows = self::workflows(); $inspection = isset($_GET['inspection']) && is_string($_GET['inspection']) ? sanitize_text_field(wp_unslash($_GET['inspection'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only task context; current ownership is checked before presentation and again on the explicit POST. if ($feature !== 'csv' || !self::inspection($inspection)) { $inspection = ''; } echo '
'; if (isset($workflows[$feature])) { $w = $workflows[$feature]; echo '← ' . esc_html__('All Pro workflows', 'customfields-com') . '

' . esc_html__('PRO PREVIEW · FICTIONAL DEMO DATA', 'customfields-com') . '

' . esc_html($w['title']) . '

' . esc_html($w['benefit']) . '

    '; foreach ($w['steps'] as $step) { echo '
  1. ' . esc_html($step) . '
  2. '; } echo '

' . esc_html(Workspace::pro_available() ? __('Ready in your installed Pro add-on', 'customfields-com') : (defined('CUSTOMFIELDS_PRO_VERSION') ? __('Your Pro add-on is installed', 'customfields-com') : __('Included with CustomFields.com Pro', 'customfields-com'))) . '

' . esc_html($w['free']) . '

'; self::next_action($feature, $inspection); echo '' . esc_html__('Continue with your Free tools', 'customfields-com') . '

' . esc_html__('Choosing the next step remembers this workflow locally for seven days. Viewing this example saves no task and changes no content.', 'customfields-com') . '

'; if ($inspection) { echo '

' . esc_html__('Your selected Free comparison is retained. After activation, return to that check and explicitly continue to a freshly validated review. Nothing is applied automatically.', 'customfields-com') . '

'; } $file = 'assets/pro-previews/' . $feature . '.png'; if (is_readable(CUSTOMFIELDS_PATH . $file)) { $url = CUSTOMFIELDS_URL . $file; echo '
' . esc_html__('Actual Pro screen · fictional demo data', 'customfields-com') . '

' . esc_html__('This is a screenshot of the installed tool. The example values are not your site’s content.', 'customfields-com') . '

' . esc_attr(sprintf(/* translators: %s: Workflow name. */ __('Actual %s screen with fictional example content.', 'customfields-com'), $w['name'])) . '

' . esc_html__('Open full-size screenshot (new tab)', 'customfields-com') . '

'; } if ($feature === 'csv') { echo '
' . esc_html__('Try a local illustrative example', 'customfields-com') . ''; self::csv_example(); echo '
'; } echo '
' . esc_html__('Supported scope and limits', 'customfields-com') . '

' . esc_html($w['limits']) . '

'; } else { echo '

' . esc_html__('CUSTOMFIELDS.COM PRO · PRIVATE BETA', 'customfields-com') . '

' . esc_html__('Make recurring content work easier', 'customfields-com') . '

' . esc_html__('Choose the task you want to finish. See the actual tool and what Pro adds before deciding.', 'customfields-com') . '

'; self::resume_panel(); echo '
'; foreach ($workflows as $id => $w) { echo ''; } echo '
'; self::comparison(); echo '
' . esc_html__('Licence activation and updates', 'customfields-com') . '

' . esc_html__('Install Pro alongside Free and activate your licence once on this site. Earned local access survives expiry or a temporary service outage. Removing the connection locks new Pro operations and pauses background work; saved content remains. Update checks are a separate optional service.', 'customfields-com') . '

'; } self::preferences(); echo '
'; } public static function exporters($exporters) { $exporters['customfields-pro-guidance'] = array('exporter_friendly_name' => __('CustomFields workflow preferences', 'customfields-com'), 'callback' => array(__CLASS__, 'export')); return $exporters; } public static function erasers($erasers) { $erasers['customfields-pro-guidance'] = array('eraser_friendly_name' => __('CustomFields workflow preferences', 'customfields-com'), 'callback' => array(__CLASS__, 'erase')); return $erasers; } public static function export($email, $page = 1) { $user = get_user_by('email', $email); $data = array(); if ($user && (int) $page === 1) { foreach (array(self::PREFERENCE, self::NAVIGATION, self::INTENT, self::DISMISSED) as $key) { $value = get_user_meta($user->ID, self::preference_key($key), true); if ($value !== '' && !($key === self::INTENT && (!is_array($value) || (int) ($value['expires'] ?? 0) <= time()))) { $data[] = array('name' => $key, 'value' => is_array($value) ? wp_json_encode($value) : (string) $value); } } } return array('data' => $data ? array(array('group_id' => 'customfields-workflow-preferences', 'group_label' => __('CustomFields workflow preferences', 'customfields-com'), 'item_id' => 'customfields-workflow-preferences-' . get_current_blog_id(), 'data' => $data)) : array(), 'done' => true); } public static function erase($email, $page = 1) { $user = get_user_by('email', $email); $removed = false; $retained = false; if ($user && (int) $page === 1) { $intent = get_user_meta($user->ID, self::preference_key(self::INTENT), true); foreach (array(self::PREFERENCE, self::NAVIGATION, self::INTENT, self::DISMISSED) as $key) { $removed = delete_user_option($user->ID, $key, false) || $removed; $retained = (bool) get_user_meta($user->ID, self::preference_key($key), false) || $retained; } if (is_array($intent) && is_int($intent['created'] ?? null)) { wp_clear_scheduled_hook('customfields_pro_intent_expire', array((int) $user->ID, $intent['created'])); } } return array('items_removed' => $removed, 'items_retained' => $retained, 'messages' => $retained ? array(__('Some workflow preferences could not be removed. Please retry.', 'customfields-com')) : array(), 'done' => true); } private static function comparison() { $rows = array( array(__('Publish a directory', 'customfields-com'), __('Fields, repeaters, basic display and existing saved listing rendering.', 'customfields-com'), __('Visual directory/listing builder, configurable listing layouts and queries, and reader search.', 'customfields-com')), array(__('Update from a spreadsheet', 'customfields-com'), __('Compare a CSV and make manual edits.', 'customfields-com'), __('Save mappings and review each batch before applying updates.', 'customfields-com')), array(__('Collect contributions', 'customfields-com'), __('WordPress-admin editing with existing permissions.', 'customfields-com'), __('Logged-in edit/create forms, interactive listing controls and existing private approval workflows.', 'customfields-com')), array(__('Check content regularly', 'customfields-com'), __('Run content checks on demand and fix findings manually.', 'customfields-com'), __('Save supported rules, schedule checks and retain local report history.', 'customfields-com')), ); echo '
' . esc_html__('Compare Free and Pro', 'customfields-com') . '
'; foreach ($rows as $row) { echo ''; } echo '
' . esc_html__('What stays in Free and what Pro adds', 'customfields-com') . '
' . esc_html__('When you need to…', 'customfields-com') . '' . esc_html__('Included in Free', 'customfields-com') . '' . esc_html__('Pro adds', 'customfields-com') . '
' . esc_html($row[0]) . '' . esc_html($row[1]) . '' . esc_html($row[2]) . '

' . esc_html__('Pro also adds reviewed deployment and supported content-transfer packages. See each workflow above for its supported scope.', 'customfields-com') . '

'; } private static function csv_example() { echo '

' . esc_html__('Fictional example · stays on this page. Nothing is uploaded, sent or saved.', 'customfields-com') . '

' . esc_html__('Example mapping: ready to save', 'customfields-com') . '

' . esc_html__('First fictional CSV: workshop capacities', 'customfields-com') . '
' . esc_html__('Workshop', 'customfields-com') . '' . esc_html__('Sample capacity now', 'customfields-com') . '' . esc_html__('Proposed CSV value', 'customfields-com') . '
' . esc_html__('Friday pottery · ID 101', 'customfields-com') . '1216
' . esc_html__('Saturday drawing · ID 102', 'customfields-com') . '810

' . esc_html__('Start by saving this example mapping. Nothing is uploaded or saved to WordPress.', 'customfields-com') . '

' . esc_html__('Read the example step by step', 'customfields-com') . '
  1. ' . esc_html__('Save the mapping for post_id and capacity. Review the first fictional upload: Friday changes from 12 to 16 and Saturday from 8 to 10.', 'customfields-com') . '
  2. ' . esc_html__('Approve that example update. A later fictional CSV uses the same columns and mapping, with new capacities of 18 and 12. It needs a fresh review before applying.', 'customfields-com') . '
  3. ' . esc_html__('Undo the latest example update to restore its before values. Real Pro undo checks for later edits and stops on a conflict; retained undo records are required.', 'customfields-com') . '

' . esc_html__('The buttons only change fictional numbers on this page. Reloading resets the example. No CSV, saved workflow, content change or automatic task is created.', 'customfields-com') . '

'; } }